本当は怖いHPC

HPC屋の趣味&実益ブログ

x86-64のCalling Convention

x86-64(AMD64)のCalling Conventionについてメモ.ネット上に資料はたくさんありますが,断片的だったり,一覧性が低かったり,互いに矛盾しているように見えたりしたので自分でまとめることにしました.

(と言っておきながら,Agner Fogさんの資料*1のp.10 Register Usage に全部書いてあったので,それを読めば良いと思います)

Calling Conventionとは

関数を呼び出すとき,機械語レベルでは,レジスタをどう使うかが重要になります.スタックは関数ごとに独立してメモリを利用できますが,レジスタは共有資源なので関数の呼び出し元(caller)と呼び出し先(callee)で協調して利用する必要があります.

ポイントとしては,

  • 引数をどうやって渡すか
  • 戻り値をどうやって返すか
  • それ以外のレジスタをどう使うか(レジスタがVolatileかどうか)

があります.

前述のようにレジスタは共有資源なので,利用方法について合意する必要があります.「Volatileかどうか」とは,その取り決めのことです. 以下の2通りのやり方があります(C言語の volatile とは関係ない).

  • Caller-save (= volatile or scratch) :callee が自由に使って良い(破壊して良い).よって,必要であればCallerが自分で内容を保存しておく責任がある
  • Callee-save (=non-volatile):callee側が使って良いが,callerから見て呼び出し前と呼び出し後で値が同じである必要がある.言い換えると,calleeが現状復帰する責任がある

呼び出し規約の種類

x86-64で用いられる呼び出し規約は大きく分けて2種類あります.

  • System V AMD64 ABI - 主に Solaris, Linux, FreeBSD, macOS で用いられる.
  • Microsoft x64 calling convention - Windowsで用いられる.(vectorcall というのもあるが,これはMS-x64の拡張)

呼び出し規約の詳細

MS-x86とSysVの共通事項

  • ある個数の引数まではレジスタで渡す,レジスタに載らない引数はスタックで渡す.スタックに詰む順番は右から左 (RTL)

System V AMD64 ABI

  • 関数呼び出し
    • 整数 or ポインタ RDI, RSI, RDX, RCX, R8, R9
    • 不動小数点 XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7
  • 戻り値
    • 整数64ビットまでRAX
    • 整数128ビットまでRAX & RDX
    • 不動小数点 XMM0 and XMM1
  • caller/callee-save
    • RBX, RSP, RBP, and R12–R15 : non-volatile (= callee save)
    • RAX, RCX, RDX, RSI, RDI, R8-R11 XMM0-XMM15, YMM0-YMM15, ZMM0-ZMM31: volatile (= caller save)
  • システムコールでは RCX の代わりに R10 を使用

Microsoft x64 calling convention

  • 関数呼び出
    • 整数 RCX, RDX, R8, R9
    • 不動小数点 XMM0, XMM1, XMM2, XMM3
  • 戻り値 = RAX or XMM0
  • caller/callee-save
    • RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, XMM6, XMM7 : non-volatile (= callee save)
    • RAX, RCX, RDX, R8-R11, XMM0-XMM5, (YMM, ZMMレジスタ,ただしXMM6-XMM15の下位128ビット以外) volatile (=caller-save)

参考文献

  • Wikipedia X86 Calling Conventions*2
  • Calling conventions for different C++ compilers and operating systems *3

UCX1.8リリース

UCX1.8がリリースされました。

全体的には堅実で地味なリリースですが、個人的に興味あるものをいくつか取り上げるとすると

  • ROCm環境サポートの充実
  • CUDA向けのさらなる最適化
  • RDMA memory registration

でしょうか。それぞれを簡単に紹介します:

ROCmは、AMD製GPUにおけるGPGPUプログラミング環境です。要するにAMD版CUDAです(厳密には、ROCmがCUDA Runtimeとドライバ群相当、HIPがCUDAプログラミングモデル+コンパイラ相当だと思います)。NVIDIAに比べるとソフトウェア環境で大幅な遅れを取っているAMDですが、ソフトウェアエコシステム一式をオープンソースで開発中です。RDMAを中心としたネットワークライブラリは、UCXを基本としてサポートされていくようです。

RDMAのMemory Registrationの高速化は、地味に嬉しいケースがあると思います。そもそもRDMA通信におけるRegistrationとは、CPUのホストメモリをRDMAように用いるための事前処理のことです。これは地味に時間がかかる処理で、以前に私が関わったPFNにおける試作Allreduceライブラリでは、事前にメモリプールを作ってRegistrationを前処理しておくことでオーバーヘッドを回避しました。この処理がマルチスレッドで実装できれば嬉しいかもしれません。

DockerfileのARGとFROMの順序と変数のスコープ

Dockerfileの中のFROMARGコマンドの位置関係について、ちょっとハマることがあったのでまとめておきます。

このあたりは、英語でも日本語でも直接的にはっきり書いてあるドキュメントがほとんどないので調べるのに時間がかかりました。

なお、実験は MacOS上の Docker version 19.03.8, build afacb8b で行っています

問題

まず、以下の例を見てみます。

# Dockerfile
ARG UBUNTU_VER
FROM ubuntu:${UBUNTU_VER}
RUN echo UBUNTU_VER=$UBUNTU_VER
$ docker build . --build-arg UBUNTU_VER=18.04 --no-cache
Sending build context to Docker daemon  2.048kB
Step 1/3 : ARG UBUNTU_VER
Step 2/3 : FROM ubuntu:${UBUNTU_VER}
 ---> 4e5021d210f6
Step 3/3 : RUN echo UBUNTU_VER=$UBUNTU_VER
 ---> Running in b1db86ff5314
UBUNTU_VER=
Removing intermediate container b1db86ff5314
 ---> 212d057cc765
Successfully built 212d057cc765

冒頭に ARG で定義した UBUNTU_VER という変数を、コマンドラインから --build-arg を使って代入しています。ここで、FROM の行では正しく値が設定されて ubuntu:18.04 というイメージが使われているのに対し、3行目の RUN コマンドの行では値が代入されていません。

どういうことでしょうか? ARG は、FROMより前に書いてはいけないのでしょうか?しかし、FROMコマンドで変数を使うためには、FROMより前にARGを書く必要があります。そもそも、FROMでは変数を使うことができているのに、その後のRUNでは空(未定義)なのはなぜでしょうか?

理由の説明

これには理由があります。まず、 FROM コマンドは、新しいステージ(build stage)を作成し、その次の行から スコープ を形成します。普通のプログラムで言う変数のスコープと同じです。また FROM より前に出現する ARG は、グローバルスコープ(globally-scoped ARG)となります。同様に、普通の関数でいうグローバル変数のようなものです。

次に、グローバルスコープの ARG は、それぞれの FROM スコープへは自動的には引き継がれません。スコープ毎に ARG コマンドを使って宣言する必要があります。また、その際にデフォルト値を変更することもできます。

ということで、それを実験するために以下のようなDockerfileをビルドしてみます。

# Dockerfile

# グローバルスコープのARG
ARG VER=18.04

FROM ubuntu:${VER} as base1
# (1) ローカルスコープARG宣言無し
RUN echo 1 VER=${VER}

FROM ubuntu:${VER} as base2
# (2) ローカルスコープのARG宣言(デフォルト値なし)
ARG VER
RUN echo 2 VER=${VER}

FROM ubuntu:${VER} as base3
# (3) ローカルスコープのARG宣言(デフォルト値あり)
ARG VER="In 3rd FROM"
RUN echo 3 VER=${VER}

FROM ubuntu:${VER} as base4
# (4) ローカルスコープのARG宣言(再びデフォルト値なし)
ARG VER
RUN echo 4 VER=${VER}

このDockerfileをビルドすると、下のようになります。(不要な部分を省略済)

Sending build context to Docker daemon  3.584kB
Step 1/12 : ARG VER=18.04
Step 2/12 : FROM ubuntu:${VER} as base1       # <-- (0)
 ---> 4e5021d210f6
Step 3/12 : RUN echo 1 VER=${VER}
 ---> Running in a954660d91fd
1 VER=                                        # <-- (1)

Step 6/12 : RUN echo 2 VER=${VER}
 ---> Running in a8ac09e221dc
2 VER=18.04                                   # <-- (2)

Step 9/12 : RUN echo 3 VER=${VER}
 ---> Running in 79466895adae
3 VER=In 3rd FROM                            # <-- (3)

Step 12/12 : RUN echo 4 VER=${VER}
 ---> Running in 5ebfe419e434
4 VER=18.04                                  # <-- (4)

Successfully built c32c9a07c503

ここで起こっていることを整理してみます。

  • まず、Dockerfileの冒頭でグローバルスコープの VER を定義します。これにより、 FROM コマンドで変数 VER が使えるようになります(出力の (0))。以降のすべての FROM コマンドで VER が使えます。
  • 次に、FROMで作成されたスコープの中で変数宣言なしで VER を使ってみると、中身は空です(出力の (1))。
  • 新しく作った別のスコープで、デフォルト値無しで VER を宣言します。すると、VER 変数が有効になり、値はグローバルスコープの定義を引き継ぎます(出力の (2))。
  • さらに新しく作った別のスコープでデフォルト値付きで変数宣言をすると、VER は宣言された値に設定されます(出力の `(3))。
  • また新しくスコープを作り、再び「デフォルト値なしのARG宣言」をしてみると、値はグローバルスコープのものになります。つまり、直前のスコープで設定した値は破棄されます。

まとめ

Dockerfileの中で FROM コマンドは新しいスコープを作り、その中で宣言された ARG は、そのローカルスコープの中のみで有効です。最初の FROM より前に宣言した ARG はグローバルスコープとなり、 FROMコマンド自体の中で使うことができますが、ローカルスコープの中では継承されません。ローカルスコープの中で変数を使いたい場合は、グローバルスコープの宣言があったとしても、再度 ARG で宣言する必要があります。また、ローカルスコープで設定した値は、他のローカルスコープには引き継がれません。

参考文献

【広告】