1737878743
2025-01-24 13:50:00
私は数日前、単一の atan2() 命令に対して RGA コンパイラーによって生成された長いシェーダー ISA コードのスクリーンショットを投稿しました。この投稿にはかなり多くの反響があり、多くの人がその事実に驚いているように感じたので、シェーダー命令の「隠れた」コストについてもう少し詳しく議論するために投稿を書くことにしました。
以下では GCN/RDNA アーキテクチャを参照しており、ほとんどの ISA は以下を使用して作成されています。 https://godbolt.org/。議論を助けるために、私はまったく非科学的ですが、シェーダ命令のコストが「隠れた」原因を大きく 3 つのカテゴリに割り当てました。
- 命令に対するハードウェアサポートはありません
- 命令のハードウェア実装
- 命令にはリソースへの依存関係があります
最初のカテゴリから始めましょう。命令にはハードウェア (ネイティブ) 実装がなく、場合によっては多数のネイティブ命令を使用して実装する必要があります。これは「隠れた」コストの非常に一般的な原因であり、人々を驚かせる可能性があります。逆三角関数 (acos、asin、atan、atan2) にはネイティブ実装がありません。これは、たとえば、単一の atan2 に対して生成された RDNA ISA コードです。
v_cmp_gt_f32 s[0:1], v2, 0 // 00000000001C: D4040000 00010102
s_mov_b64 s[2:3], exec // 000000000024: BE82047E
v_cmpx_eq_f32 exec, v0, 0 // 000000000028: D412007E 00010100
v_mov_b32 v0, lit(0x3fc90fda) // 000000000030: 7E0002FF 3FC90FDA
v_cndmask_b32 v0, lit(0xbfc90fda), v0, s[0:1] // 000000000038: D5010000 000200FF BFC90FDA
s_andn2_b64 exec, s[2:3], exec // 000000000044: 8AFE7E02
s_cbranch_execz label_0308 // 000000000048: BF8800AF
v_cmp_gt_f32 s[4:5], v0, 0 // 00000000004C: D4040004 00010100
s_mov_b64 s[6:7], exec // 000000000054: BE86047E
v_cmpx_eq_f32 exec, v2, 0 // 000000000058: D412007E 00010102
v_cndmask_b32 v0, lit(0x40490fda), 0, s[4:5] // 000000000060: D5010000 001100FF 40490FDA
s_andn2_b64 exec, s[6:7], exec // 00000000006C: 8AFE7E06
s_cbranch_execz label_0308 // 000000000070: BF8800A5
s_mov_b64 s[8:9], exec // 000000000074: BE88047E
v_cmpx_gt_f32 exec, abs(v0), abs(v2) // 000000000078: D414037E 00020500
v_div_scale_f32 v1, vcc, v0, v0, v2 // 000000000080: D56D6A01 040A0100
s_cbranch_execz label_01A4 // 000000000088: BF880046
v_div_scale_f32 v3, vcc, v2, v0, v2 // 00000000008C: D56D6A03 040A0102
s_denorm_mode 0x000f // 000000000094: BFA5000F
v_rcp_f32 v6, v1 // 000000000098: 7E0C5501
v_fma_f32 v5, -v1, v6, 1.0 // 00000000009C: D54B0005 23CA0D01
v_fmac_f32 v6, v5, v6 // 0000000000A4: 560C0D05
v_mul_f32 v4, v3, v6 // 0000000000A8: 10080D03
v_fma_f32 v5, -v1, v4, v3 // 0000000000AC: D54B0005 240E0901
v_fmac_f32 v4, v5, v6 // 0000000000B4: 56080D05
v_fma_f32 v3, -v1, v4, v3 // 0000000000B8: D54B0003 240E0901
s_denorm_mode 0x000c // 0000000000C0: BFA5000C
v_div_fmas_f32 v1, v3, v6, v4 // 0000000000C4: D56F0001 04120D03
v_mov_b32 v4, lit(0xbc8bf91a) // 0000000000CC: 7E0802FF BC8BF91A
v_div_fixup_f32 v0, v1, v0, v2 // 0000000000D4: D55F0000 040A0101
v_cmp_eq_f32 s[10:11], abs(v0), 0 // 0000000000DC: D402010A 00010100
v_rcp_f32 v1, abs(v0) // 0000000000E4: D5AA0101 00000100
v_and_b32 v2, lit(0x7fffffff), v0 // 0000000000EC: 360400FF 7FFFFFFF
v_cmp_gt_f32 vcc, abs(v0), 1.0 // 0000000000F4: D404016A 0001E500
v_and_b32 v0, lit(0x80000000), v0 // 0000000000FC: 360000FF 80000000
v_cndmask_b32 v1, v1, 0, s[10:11] // 000000000104: D5010001 00290101
v_cndmask_b32 v3, v2, v1, vcc // 00000000010C: 02060302
v_mul_legacy_f32 v1, v3, v3 // 000000000110: 0E020703
v_fmac_legacy_f32 v4, lit(0x3b47bf1d), v1 // 000000000114: 0C0802FF 3B47BF1D
v_fma_legacy_f32 v4, v4, v1, lit(0x3d3751b7) // 00000000011C: D5400004 03FE0304 3D3751B7
v_fma_legacy_f32 v4, v4, v1, lit(0xbd9e0bf8) // 000000000128: D5400004 03FE0304 BD9E0BF8
v_fma_legacy_f32 v4, v4, v1, lit(0x3ddc5c26) // 000000000134: D5400004 03FE0304 3DDC5C26
v_fma_legacy_f32 v4, v4, v1, lit(0xbe11cde3) // 000000000140: D5400004 03FE0304 BE11CDE3
v_fma_legacy_f32 v4, v4, v1, lit(0x3e4cc636) // 00000000014C: D5400004 03FE0304 3E4CC636
v_fma_legacy_f32 v4, v4, v1, lit(0xbeaaaaa3) // 000000000158: D5400004 03FE0304 BEAAAAA3
v_fma_legacy_f32 v2, v4, v1, 1.0 // 000000000164: D5400002 03CA0304
v_mul_legacy_f32 v4, v3, v2 // 00000000016C: 0E080503
v_fma_legacy_f32 v1, -v3, v2, lit(0x3fc90fdb) // 000000000170: D5400001 23FE0503 3FC90FDB
v_cndmask_b32 v1, v4, v1, vcc // 00000000017C: 02020304
v_xor_b32 v0, v0, v1 // 000000000180: 3A000300
v_mov_b32 v1, lit(0x40490fda) // 000000000184: 7E0202FF 40490FDA
v_cndmask_b32 v1, lit(0xc0490fda), v1, s[0:1] // 00000000018C: D5010001 000202FF C0490FDA
v_add_f32 v1, v0, v1 // 000000000198: 06020300
v_cndmask_b32 v0, v1, v0, s[4:5] // 00000000019C: D5010000 00120101
label_01A4:
s_andn2_b64 exec, s[8:9], exec // 0000000001A4: 8AFE7E08
s_cbranch_execz label_0308 // 0000000001A8: BF880057
s_mov_b64 s[10:11], exec // 0000000001AC: BE8A047E
v_cmpx_eq_f32 exec, abs(v0), abs(v2) // 0000000001B0: D412037E 00020500
v_mov_b32 v0, lit(0x3f490fda) // 0000000001B8: 7E0002FF 3F490FDA
v_mov_b32 v1, lit(0xbf490fda) // 0000000001C0: 7E0202FF BF490FDA
v_cndmask_b32 v0, lit(0x4016cbe4), v0, s[4:5] // 0000000001C8: D5010000 001200FF 4016CBE4
v_cndmask_b32 v1, lit(0xc016cbe4), v1, s[4:5] // 0000000001D4: D5010001 001202FF C016CBE4
v_cndmask_b32 v0, v1, v0, s[0:1] // 0000000001E0: D5010000 00020101
s_andn2_b64 exec, s[10:11], exec // 0000000001E8: 8AFE7E0A
v_div_scale_f32 v1, vcc, v2, v2, v0 // 0000000001EC: D56D6A01 04020502
s_cbranch_execz label_0308 // 0000000001F4: BF880044
v_div_scale_f32 v3, vcc, v0, v2, v0 // 0000000001F8: D56D6A03 04020500
s_denorm_mode 0x000f // 000000000200: BFA5000F
v_rcp_f32 v6, v1 // 000000000204: 7E0C5501
v_fma_f32 v5, -v1, v6, 1.0 // 000000000208: D54B0005 23CA0D01
v_fmac_f32 v6, v5, v6 // 000000000210: 560C0D05
v_mul_f32 v4, v3, v6 // 000000000214: 10080D03
v_fma_f32 v5, -v1, v4, v3 // 000000000218: D54B0005 240E0901
v_fmac_f32 v4, v5, v6 // 000000000220: 56080D05
v_fma_f32 v3, -v1, v4, v3 // 000000000224: D54B0003 240E0901
s_denorm_mode 0x000c // 00000000022C: BFA5000C
v_div_fmas_f32 v1, v3, v6, v4 // 000000000230: D56F0001 04120D03
v_mov_b32 v4, lit(0xbc8bf91a) // 000000000238: 7E0802FF BC8BF91A
v_div_fixup_f32 v0, v1, v2, v0 // 000000000240: D55F0000 04020501
v_cmp_eq_f32 s[4:5], abs(v0), 0 // 000000000248: D4020104 00010100
v_rcp_f32 v1, abs(v0) // 000000000250: D5AA0101 00000100
v_and_b32 v2, lit(0x7fffffff), v0 // 000000000258: 360400FF 7FFFFFFF
v_cmp_gt_f32 vcc, abs(v0), 1.0 // 000000000260: D404016A 0001E500
v_and_b32 v0, lit(0x80000000), v0 // 000000000268: 360000FF 80000000
v_cndmask_b32 v1, v1, 0, s[4:5] // 000000000270: D5010001 00110101
v_cndmask_b32 v3, v2, v1, vcc // 000000000278: 02060302
v_mul_legacy_f32 v1, v3, v3 // 00000000027C: 0E020703
v_fmac_legacy_f32 v4, lit(0x3b47bf1d), v1 // 000000000280: 0C0802FF 3B47BF1D
v_fma_legacy_f32 v4, v4, v1, lit(0x3d3751b7) // 000000000288: D5400004 03FE0304 3D3751B7
v_fma_legacy_f32 v4, v4, v1, lit(0xbd9e0bf8) // 000000000294: D5400004 03FE0304 BD9E0BF8
v_fma_legacy_f32 v4, v4, v1, lit(0x3ddc5c26) // 0000000002A0: D5400004 03FE0304 3DDC5C26
v_fma_legacy_f32 v4, v4, v1, lit(0xbe11cde3) // 0000000002AC: D5400004 03FE0304 BE11CDE3
v_fma_legacy_f32 v4, v4, v1, lit(0x3e4cc636) // 0000000002B8: D5400004 03FE0304 3E4CC636
v_fma_legacy_f32 v4, v4, v1, lit(0xbeaaaaa3) // 0000000002C4: D5400004 03FE0304 BEAAAAA3
v_fma_legacy_f32 v2, v4, v1, 1.0 // 0000000002D0: D5400002 03CA0304
v_mul_legacy_f32 v4, v3, v2 // 0000000002D8: 0E080503
v_fma_legacy_f32 v1, -v3, v2, lit(0x3fc90fdb) // 0000000002DC: D5400001 23FE0503 3FC90FDB
v_cndmask_b32 v1, v4, v1, vcc // 0000000002E8: 02020304
v_xor_b32 v0, v0, v1 // 0000000002EC: 3A000300
v_sub_f32 v1, lit(0x3fc90fda), v0 // 0000000002F0: 080200FF 3FC90FDA
v_sub_f32 v0, lit(0xbfc90fda), v0 // 0000000002F8: 080000FF BFC90FDA
v_cndmask_b32 v0, v0, v1, s[0:1] // 000000000300: D5010000 00020300
label_0308:
s_mov_b64 exec, s[2:3] // 000000000308: BEFE0402
v_cvt_pkrtz_f16_f32 v2, v0, 0 // 00000000030C: D52F0002 00010100
v_mov_b32 v3, 0 // 000000000314: 7E060280
exp mrt0, v2, v2, v3, v3 done compr vm // 000000000318: F8001C0F 00000302
s_endpgm // 000000000320: BF810000
確かに、これは最も極端な例の 1 つであり、すべての逆三角関数がそれほど多くの命令に拡張されるわけではありません。逆三角関数命令だけが多くのネイティブ命令に拡張されているだけではなく、tan() にもネイティブ実装はなく、次のような cos 命令と sin 命令を使用して計算されます。
v_mul_f32 v0, 0.15915494, v0 // 000000000014: 100000F8
v_cos_f32 v1, v0 // 000000000018: 7E026D00
v_sin_f32 v0, v0 // 00000000001C: 7E006B00
v_rcp_f32 v1, v1 // 000000000020: 7E025501
v_mul_legacy_f32 v0, v0, v1 // 000000000024: 0E000300
ネイティブ実装を持たない、より広く使用されている命令は、normalize() と length() です。たとえば、これは Normalize() です。
v_mul_legacy_f32 v1, v2, v2 // 000000000024: 0E020502
v_fmac_f32 v1, v3, v3 // 000000000028: 56020703
v_fmac_f32 v1, v0, v0 // 00000000002C: 56020100
v_rsq_f32 v1, v1 // 000000000030: 7E025D01
v_mul_legacy_f32 v2, v2, v1 // 000000000034: 0E040302
v_mul_legacy_f32 v3, v3, v1 // 000000000038: 0E060303
v_mul_legacy_f32 v0, v0, v1 // 00000000003C: 0E000300
ベクトル レジスタを使用した整数の除算は、大規模な命令拡張のもう 1 つの領域です。これを実装するためのネイティブ ベクトル命令がないため、整数オペランドを使用した 1 つの x/y 除算により、RDNA ISA では約 35 の命令が生成されます。スカラー レジスタを使用した整数除算はさらに悪く、約 42 個のスカラー命令とベクトル命令が混在することになります (ここでは、終わりのない命令ストリームを貼り付けるのはやめておきます。実験してみてください) ゴッドボールド.org 実際の動作を見たい場合は)。
キューブマップ サンプリングは、コンパイラが使用する面を計算しようとするときに、おそらく予期せず複数の命令に拡張されるもう 1 つの命令です。
// cubemap.Sample(samplerLinear, direction.xyz);
v_cubema_f32 v1, v2, v3, v0 // 000000000034: D5470001 04020702
s_load_dwordx8 s[4:11], s[0:1], null // 00000000003C: F40C0100 FA000000
s_load_dwordx4 s[0:3], s[0:1], 0x000020 // 000000000044: F4080000 FA000020
v_cubetc_f32 v4, v2, v3, v0 // 00000000004C: D5460004 04020702
v_rcp_f32 v1, abs(v1) // 000000000054: D5AA0101 00000101
v_cubesc_f32 v5, v2, v3, v0 // 00000000005C: D5450005 04020702
v_cubeid_f32 v0, v2, v3, v0 // 000000000064: D5440000 04020702
v_fmaak_f32 v2, v4, v1, lit(0x3fc00000) // 00000000006C: 5A040304 3FC00000
v_fmaak_f32 v1, v5, v1, lit(0x3fc00000) // 000000000074: 5A020305 3FC00000
s_and_b64 exec, exec, s[12:13] // 00000000007C: 87FE0C7E
s_waitcnt lgkmcnt(0) // 000000000080: BF8CC07F
image_sample v[0:2], [v1,v2,v0], s[4:11], s[0:3] dmask:0x7 dim:SQ_RSRC_IMG_CUBE // 000000000084: F080071A 00010001 00000002
生成される命令の数に大きな影響を与える可能性がある HLSL 操作のもう 1 つの例は、レジスタ (VGPR) 配列のインデックス付けです。均一 (すべてのスレッドで同じ) インデックスを使用してレジスタにアクセスしようとするとします。
// float data[4] = {....} // store some values in a VGPR array
// float result = data[index]; // index is the same for all threads
v_mov_b32 v4, 0 // 00000000004C: 7E080280
s_cmp_lt_u32 s0, 4 // 000000000054: BF0A8400
コンパイラは範囲外チェックを追加し、インデックスが範囲内にある場合は、相対オフセット (v_movrels_b32 v4、v5) としてインデックスを使用して配列内のレジスタにアクセスします。
ただし、インデックスがスレッドごとに異なる場合 (スレッド バリアント)、コンパイラーはそれを相対オフセットとして使用できず、インデックス値を範囲内のすべての可能な値と比較します。
// float data[4] = {....} // store some values in a VGPR array
// float result = data[index]; // index is thread variant
v_mov_b32 v4, 0 // 000000000034: 7E080280
v_cmp_eq_i32 vcc, 0, v5 // 000000000038: 7D040A80
v_cndmask_b32 v0, v4, v6, vcc // 00000000003C: 02000D04
v_cmp_eq_i32 vcc, 1, v5 // 000000000040: 7D040A81
v_cndmask_b32 v1, v0, v7, vcc // 000000000044: 02020F00
v_cmp_eq_i32 vcc, 2, v5 // 000000000048: 7D040A82
v_cndmask_b32 v1, v1, v8, vcc // 00000000004C: 02021101
v_cmp_eq_i32 vcc, 3, v5 // 000000000050: 7D040A83
v_cndmask_b32 v1, v1, v9, vcc // 000000000054: 02021301
レジスタ配列が大きくなるほど、比較する必要がある値が多くなり、生成されるコードも長くなります。
次に、2 番目のカテゴリである、命令の特定のハードウェア実装または操作のコストに影響を与える可能性のあるハードウェア制限から生じる追加コストについて考えてみましょう。
ネイティブ命令 (ハードウェア実装のある命令) の場合でも、すべての命令のコストが同じであるわけではありません。超越命令 (cos、sin、exp、log、rsq、sqrt) は多くのアーキテクチャでネイティブ実装されていますが、たとえば AMD GPU では浮動小数点の乗算または加算の 4 倍のコストがかかります。また、AMD では、整数の乗算にも 4 倍のコストがかかります。これを説明するために、これは、RGA でコンパイルされたシェーダーの ISA の内訳から抽出したいくつかのネイティブ命令のレイテンシーです。 シェーダープレイグラウンド:
s_mov_b32 m0, s2 Scalar ALU 4
v_rcp_f32 v2, v2 Vector ALU 16
v_mul_f32 v2, v2, v3 Vector ALU 4
v_mul_lo_u32 v3, v3, v4 Vector ALU 16
v_cvt_f32_i32 v3, v3 Vector ALU 4
v_cos_f32 v0, v0 Vector ALU 16
v_mac_f32 v3, v2, v0 Vector ALU 4
v_mov_b32 v0, 0 Vector ALU 4
GCN GPU では、浮動小数点乗算のレイテンシは 4 クロック サイクルですが、cos、rcp、および整数乗算 (v_mul_lo_u32) のレイテンシはすべて 16 クロック サイクルです。レイテンシは、すべての Wave スレッドでの命令の発行から命令の終了までのクロック サイクル数です。
ハードウェアの制限により、コンパイラーが最終的に生成するコードが予想される動作と正確に一致しない場合もあります。たとえば、スカラー レジスタを使用して浮動小数点演算を実行する場合、GCN/RDNA アーキテクチャはそれをサポートしていないため、コンパイラは命令の量を増やすことはありませんが、すべての命令をベクトル演算に変換します。
// float3x3 m; // both m and v are stored in scalar registers
// float3 v;
// float3 result = mul(v,m)
v_mul_f32 v0, s0, s8 // 000000000060: D5080000 00001000
v_mul_f32 v1, s0, s10 // 000000000068: D5080001 00001400
v_mul_f32 v2, s0, s12 // 000000000070: D5080002 00001800
v_fma_f32 v0, s9, s1, v0 // 000000000078: D54B0000 04000209
v_fma_f32 v1, s11, s1, v1 // 000000000080: D54B0001 0404020B
v_fma_f32 v2, s13, s1, v2 // 000000000088: D54B0002 0408020D
v_fma_f32 v0, s3, s2, v0 // 000000000094: D54B0000 04000403
v_fma_f32 v1, s14, s2, v1 // 00000000009C: D54B0001 0404040E
これは、ベクトル レジスタ (VGPR) の割り当てと、場合によってはシェーダの占有に影響を与える可能性があります。
認識のために簡単に説明したい隠れたコストの最後の原因には、テクスチャ読み取り命令、グループ共有メモリ命令など、何らかのリソースへのアクセスが必要な命令が含まれます。このタイプのコストは、多くの場合、他のコストに比べて予測可能ではありません。これまで説明してきたように、たとえば、特定のアーキテクチャをターゲットとする場合、cos() は乗算と比較して常に 4 分の 1 レートになります。テクスチャ読み取りのコストは、メモリがキャッシュ内にあるか (数サイクル)、RAM にアクセスする必要があるか (数百サイクル) によって大きく異なります。さらに、そのメモリ レイテンシの影響は、コンパイラが命令の並べ替えによってメモリ レイテンシを隠すことができるか、またはコンピューティング ユニットがスワップして GPU の停止を回避するのに十分なウェーブを飛行中に持っているかどうかによって異なります (詳細はこちら)。
メモリ読み取りには、何を読み取るか、どのように読み取りを要求するかによって、隠れたコストがかかる場合もあります。たとえば、GCN では、ポイント サンプラーを使用して単一チャネル テクスチャを読み取ります。 ない場合よりも高価になります (以下では、要求されたデータがキャッシュ内にあることを前提としています):
float result = tex.Sample(pointSampler, uv); // 16 clocks, assuming cache hit
float result = tex[coord]; // 4 clocks, assuming cache hit
テクスチャの配列 (リソース一般) を使用する場合、VGPR インデックス作成で上で説明したものと同様の応答が発生する可能性があります。インデックスがすべてのスレッドで同じではない場合、コンパイラはインデックスによるバッチ リソース アクセスに追加のコードを追加します。これは「ウォーターフォール ループ」と呼ばれるプロセスです (少なくとも GCN/RDNA GPU では)。
// StructuredBuffer inputBuffer[];
// float result = inputBuffer[NonUniformResourceIndex(index)][j]; // index is thread variant
v_lshlrev_b32 v1, 2, v1
v_lshlrev_b32 v0, 5, v0
s_mov_b32 s0, s2
s_mov_b64 s[2:3], exec
s_mov_b64 s[4:5], exec
label_0009:
v_readfirstlane_b32 s6, v0 //
別の「隠れた」コストは、グループ共有メモリ (LDS) のアクセス パターンから発生する可能性があります。たとえば、RDNA だけでなく NVidia GPU も同様です LDS は 32 のバンクに分割されています 最適なアクセス パターンは、各スレッドが異なるバンクにアクセスすることです。
float groupshared data[128]; // memory is divided into 32 banks, accessed as index % 32
[numthreads(8, 8, 1)]
void main( uint2 GTid : SV_GroupThreadID )
{
float result = data[GTid.y*32 + GTid.x] // each thread accesses a different bank, no conflict
}
このアクセス パターンからの逸脱により、連続したスレッドが同じメモリ バンクにアクセスしようとする次のような極端なケースでは、競合が発生し、命令レイテンシが増加する可能性があります。
float groupshared data[128]; // memory is divided into 32 banks, accessed as index % 32
[numthreads(8, 8, 1)]
void main( uint2 GTid : SV_GroupThreadID )
{
float result = data[GTid.x*32 + GTid.y] // consecutive threads access the same bank, conflicts
}
これによりアクセスがシリアル化され、命令の待ち時間が 32 倍に増加する可能性があります。
上記は、コンパイラーが高レベルの命令を予期しない方法で解釈する例のほんの一部ですが、命令の特定のハードウェア実装によりコストが増加することもあります。
問題は、そのような場合に何ができるかということです。少なくとも逆三角関数の場合には、できる近似はたくさんあります。 考慮する、しかしまた、 それらを避ける 全く。ただし、最終的には、「隠れたコスト」の実際の影響は、状況、実行コストと対象の GPU のボトルネック、VGPR 割り当ての潜在的な増加が占有率にどのように影響するか、命令数の増加が影響するかどうかによって異なります。命令キャッシュへの負荷、シェーダーがメモリ レイテンシに制限されており、より多くの ALU を追加できる余地があるかどうか、未使用のリソースを吸収するために補完的な非同期コンピューティング タスクを並列実行しているかどうか。
ただし、それに対して何かをする必要があるかどうかに関係なく、常に「隠れたコスト」を認識し、可能であればターゲット プラットフォーム用にコンパイラーが生成するものを検査することは価値があります (DXIL のような中間コードではなく、実際の ISA)。とプロファイルを作成し、その影響がどのようなものになるかを決して想定しないでください。
#シェーダー命令の隠れたコスト #Interplay #Light