要旨
Intel® HD Graphics用プログラムの開発環境は人間がアセンブリコードを記述するには不向きなようです。読む場合も大雑把に雰囲気をとらえる程度にとどめておく方がよいかもしれません。
準備
無料の開発者ソフトウェアとサービス - Visual StudioからVisual Studio Communityを、
Intel® ToolkitsからIntel® oneAPI Base Toolkitを適当にインストールします。
次に挙げる参考文献をダウンロードします。
- The Compute Architecture of Intel® Processor Graphics Gen9
- GEN アセンブリーの概要
- INTEL® GRAPHICS ISA, INTEL® GRAPHICS COMPILER
- 2016 Intel® Processors (Formerly Kaby Lake)
GCNを読むで扱った、コンパイルしバイナリ列を保存するプログラムをビルドします。ファイル名はcl2bin.exe辺りでよいでしょう。
次のようなカーネルを書きます。ファイル名はexample.cl辺りでよいでしょう。
kernel void K0()
{
}
kernel void K1(global uchar *d)
{
d[31] = 42;
}
cl2bin
Platform-Index, Device-Index: Device-Name
0, 0: Intel(R) FPGA Emulation Device
1, 0: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz
2, 0: gfx900
3, 0: Intel(R) UHD Graphics 630
cl2bin Platform-Index Device-Index Source-File-Path
ファイルに書き出します。
cl2bin 3 0 example.cl > example.bin
ディスアセンブルします。怪しい雰囲気ですが気にしません。./dump以下に*.asmが三つ出来ているはずです。
ocloc disasm -file example.bin
Warning : missing or invalid -device parameter - results may be inaccurate
Warning : Path to dump folder not specificed - using ./dump as default.
Path to patch list not provided - using defaults, skipping patchtokens as undefined.
Trying to disassemble K0.krn
Trying to disassemble K1.krn
Trying to disassemble K2.krn
読む
カーネルK0のディスアセンブル結果は次のようなものでした。
L0:
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
L56:
読んでいきます。
L0:
とL56:
はラベルで、0バイト目と56バイト目を意味しているようです。{Compacted}
と付いている命令の長さは8バイトで、付いていない命令は16バイトです。基本的に命令長は16バイトなのですが条件を満たせば8バイトにできます。
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
レジスタのサイズは32バイト、つまり32bitを8個ぶんです。
uint32_t *d = ((uint32_t *)r2);
uint32_t *s = ((uint32_t *)r0);
d[0] = s[0];
d[1] = s[0];
d[2] = s[0];
d[3] = s[0];
d[4] = s[0];
d[5] = s[0];
d[6] = s[0];
d[7] = s[0];
といった感じです。
(W)
predicate(選択的に結果を反映するあれ)とのことですがよくわかりません。
(8|M0)
8要素をSIMDのチャネル0から順に詰めるといった感じです。左側の数字は1, 2, 4, 8, 16, 32のいずれかで、右側の数字は4の非負整数倍です。
ソースオペランドr0.0<1;1,0>:ud
のフォーマットは regnum.subreg<vertStride;width,horzStride>:type です。udはunsigned dword、(8|M0)なので同じものを8要素となります。
デスティネーションオペランドr2.0<1>:ud
の山括弧内の1はhorzStrideです。
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
half float, float, double floatの非正規数を許可します。コントロールレジスタを操作する際は{Switch}
指定で強制的にスレッドを切り替えて命令キューをフラッシュします。
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
r2の値をr127にコピーしています。
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
スレッドの終了のようです。(8|M0)な点が少々不思議です。
dest: null
src: r127
ex_desc: 0x27 (0b00100111)
- Target Function ID: 7(SFID_SPAWNER)
- EOT: 1
- Extended Message Length: 0
- Extended Function Control: 0
desc: 0x02000010 (0b00000010000000000000000000010000)
- Function Control: 0b0000000000000010000
- Header Present: 0
- Response Length: 0
- Message Length: 1
カーネルK1のディスアセンブル結果は次のようなものでした。
L0:
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
(W) mov (1|M0) r4.0<2>:b 42:w
(W) mov (1|M0) r3.0<1>:ud 0x1F:uw
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
(W) mov (1|M0) r5.0<1>:ud r4.0<0;1,0>:ub
(W) sends (1|M0) null:ud r3 r5 0x4A 0x02030000 // wr:1+1, rd:0; hdc.dc0; byte scattering write 8b
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
L120:
(W) sends (1|M0) null:ud r3 r5 0x4A 0x02030000 // wr:1+1, rd:0; hdc.dc0; byte scattering write 8b
グローバルメモリに1バイト書き込むようです。r3はオフセットです。
dest: null:ud
src0: r3
src1: r5
ex_desc: 0x4A (0b01001010)
- Target Function ID: 10(SFID_DP_DC0)
- EOT: 0
- Extended Message Length: 1
- Extended Function Control: 0
desc: 0x02030000 (0b00000010000000110000000000000000)
- Function Control: 0b0110000000000000000
- Header Present: 0
- Response Length: 0
- Message Length: 1
さて、ABIがどうなっているのか調べても容易には見つからなかったので雑に探ってみたわけですが...。次のようなカーネルを書いてみます。
kernel void K2(global uint *d)
{
*d = get_work_dim();
}
kernel void K3(global uint *d)
{
*d = get_local_size(0);
}
カーネルK2のディスアセンブル結果は次のようなものでした。
L0:
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
(W) mov (1|M0) r4.0<1>:uq r3.0<0;1,0>:uq
(W) mov (1|M0) r6.0<1>:d r3.2<0;1,0>:d {Compacted}
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
(W) sends (1|M0) null:ud r4 r6 0x4C 0x040681FF // wr:2+1, rd:0; hdc.dc1; a64 dword scattering write
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
L96:
最初の引数のアドレスはr3.0<1>:uq、次元はr3.2<1>:dに入っているようです。引数が増えると次元を入れてある場所が変わるかもしれません。
(W) sends (1|M0) null:ud r4 r6 0x4C 0x040681FF // wr:2+1, rd:0; hdc.dc1; a64 dword scattering write
dest: null:ud
src0: r4
src1: r6
ex_desc: 0x4C (0b01001100)
- Target Function ID: 12 (SFID_DP_DC1)
- EOT: 0
- Extended Message Length: 1
- Extended Function Control: 0
desc: 0x040681FF (0b00000100000001101000000111111111)
- Function Control: 0b1101000000111111111
- Header Present: 0
- Response Length: 0
- Message Length: 2
カーネルK1の場合とはTarget Function IDとFunction Controlが異なります。r4はオフセットではなくアドレスだからなのでしょう。
カーネルK3のディスアセンブル結果は次のようなものでした。
L0:
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
(W) mov (1|M0) r4.0<1>:uq r3.0<0;1,0>:uq
(W) mov (1|M0) r6.0<1>:d r3.2<0;1,0>:d {Compacted}
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
(W) sends (1|M0) null:ud r4 r6 0x4C 0x040681FF // wr:2+1, rd:0; hdc.dc1; a64 dword scattering write
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
L96:
ワークグループのx次元のサイズはr3.2<1>:dに入っているようです。
「ワークグループのx次元のサイズ」と「次元」は同じ場所に書かれているようです。
気を取り直して、次のようなカーネルを書いてディスアセンブルします。
kernel void K4(global uint *d)
{
d[0] = get_work_dim();
d[1] = get_local_size(0);
}
L0:
(W) mov (8|M0) r2.0<1>:ud r0.0<1;1,0>:ud
(W) or (1|M0) cr0.0<1>:ud cr0.0<0;1,0>:ud 0x4C0:uw {Switch}
(W) mov (2|M0) r3.4<1>:d r3.2<2;2,1>:d
(W) mov (8|M0) r7.0<1>:w 0x40:uv
(W) mov (8|M0) r127.0<1>:ud r2.0<8;8,1>:ud {Compacted}
(W) mov (8|M0) r6.0<1>:d r3.4<0;1,0>:d {Compacted}
(W) add (8|M0) r4.0<1>:uq r3.0<0;1,0>:uq r7.0<8;8,1>:w
(W) mov (2|M0) r6.0<1>:d r3.4<2;2,1>:d
(W) sends (8|M0) null:ud r4 r6 0x4C 0x040681FF // wr:2+1, rd:0; hdc.dc1; a64 dword scattering write
(W) send (8|M0) null r127 0x27 0x02000010 {EOT} // wr:1+0, rd:0; spawner; end of thread
L144:
次元はr3.2<1>:d、ワークグループのx次元のサイズはr3.3<1>:dに入っているようです。
任意のカーネルを走らせるプログラムをビルドしK2, K3, およびK4をIntel® VTune™ Profilerで観測してみましたが、同様のディスアセンブル結果でした。使用するデバイスの名前はIntel(R) UHD Graphics 630であることをcl::Device::getInfo<CL_DEVICE_NAME>()で取得し確認しています。