【日本語訳】"Low-level Thinking in High-level Shading Languages"1. Low-level Thinking in High-level
Shading Languages
High-level Shading Languages(HLSL) におけるローレベル思考
Emil Persson
Head of Research, Avalanche Studios
翻訳: @Reputeless
3. この講演の目的
“ローレベル思考が
今日でもまだ有効であること明らかにする”
4. 背景
● 古き良き時代、おじいちゃんがまだ子供だった頃…
● シェーダは短かった
● SM1: 最大 8 命令, SM2: 最大 64 命令
● シェーダはアセンブリで書かれていた
● すでに SM2 の時代になって消えた
● D3D の命令は実際のハードウェアに適切にマッピングされていた
● 手動での最適化は当たり前のことだった
def c0, 0.3f, 2.5f, 0, 0 def c0, -0.75f, 2.5f, 0, 0
texld
sub
r0, t0
r0, r0, c0.x ⇨ texld
mad
r0, t0
r0, r0, c0.y, c0.x
mul r0, r0, c0.y
5. 背景
● Low-level shading languages は死んだ
● シェーダを書くには非生産的
● DX10 以降はアセンブリに非対応
● とにもかくにも誰も使っていない
● コンパイラとドライバの最適化が良くはたらいてくれる (時々…)
● なんてこった、最近はアーティストがシェーダを作りやがる!
● ビジュアルエディタを使って
● ボックスや矢印を使って
● サイクル数も数えず、アセンブリも調べずに
● テクニカルドキュメントさえ読まずに
● うわぁぁ、あの子供時代、戻ってこい、戻ってこい ・・・
● 要するに:
● シェーダの書き手がハードウェアに疎くなった
6. どうしてわざわざ気を付けないといけないの?
● どうシェーダを書くかは重要!
// float3 float float float3 float float // float float float float float3 float3
return Diffuse * n_dot_l * atten * LightColor * shadow * ao; return (n_dot_l * atten) * (shadow * ao) * (Diffuse * LightColor);
0 x: MUL_e ____, R0.z, R0.w 0 x: MUL_e ____, R2.x, R2.y
y: MUL_e ____, R0.y, R0.w y: MUL_e R0.y, R0.y, R1.y VEC_021
z: MUL_e ____, R0.x, R0.w z: MUL_e R0.z, R0.x, R1.x VEC_120
1 y: MUL_e ____, R1.w, PV0.x w: MUL_e ____, R0.w, R1.w
z: MUL_e ____, R1.w, PV0.y t: MUL_e R0.x, R0.z, R1.z
w: MUL_e ____, R1.w, PV0.z 1 w: MUL_e ____, PV0.x, PV0.w
2 x: MUL_e ____, R1.x, PV1.w 2 x: MUL_e R0.x, R0.z, PV1.w
z: MUL_e ____, R1.z, PV1.y y: MUL_e R0.y, R0.y, PV1.w
w: MUL_e ____, R1.y, PV1.z z: MUL_e R0.z, R0.x, PV1.w
3 x: MUL_e ____, R2.x, PV2.w
y: MUL_e ____, R2.x, PV2.x
w: MUL_e ____, R2.x, PV2.z
4 x: MUL_e R2.x, R2.y, PV3.y
y: MUL_e R2.y, R2.y, PV3.x
z: MUL_e R2.z, R2.y, PV3.w
7. どうしてわざわざ気を付けないといけないの?
● より良いパフォーマンスが得られる
● 「うちは ALU がボトルネックじゃないんだけど・・・」
● 消費電力を節約しよう
● Texture やメモリ帯域の使用をまだ改善できるはず
● 追加機能のために余力を残しておこう
● 「プロジェクトの最後で最適化するつもりなんだけど・・・」
● 家に帰れなくならないようお祈りします・・・
● 一貫性が得られる
● 物事にはしばしば最良の方法がある
● 読みやすさを改善しよう
● 楽しい!
9. ”コンパイラが最適化してくれるでしょ!”
● コンパイラは狡猾だ!
● もう賢すぎてコンパイラ自身をだませちゃう!
● しかし:
● コンパイラはあなたの心を読めない
● コンパイラは全容が見えていない
● コンパイラは限られた情報しか使えない
● コンパイラはルールを破れない
11. ”コンパイラが最適化してくれるでしょ!”
これは MAD (発狂)しますか? (なんちゃって)
float main(float x : TEXCOORD) : SV_Target
{
return (x + 1.0f) * 0.5f;
}
しなかった! ドライバがしてくれるでしょ?
add r0.x, v0.x, l(1.000000)
mul o0.x, r0.x, l(0.500000)
12. ”コンパイラが最適化してくれるでしょ!”
これは MAD (発狂)しますか? (なんちゃって)
float main(float x : TEXCOORD) : SV_Target
{
return (x + 1.0f) * 0.5f;
}
しなかった! こいつもダメだ!
00 ALU: ADDR(32) CNT(2)
add r0.x, v0.x, l(1.000000) 0 y: ADD ____, R0.x, 1.0f
mul o0.x, r0.x, l(0.500000) 1 x: MUL_e R0.x, PV0.y, 0.5
01 EXP_DONE: PIX0, R0.x___
13. どうしてダメだった?
● 結果が一致するとは限らないため
● INF や NAN を引き起こす場合がある
● 一般的にコンパイラが得意なのは:
● 使われていないコードの除去
● 使われていないリソースの除去
● 定数の組み立て
● レジスタの割り当て
● コードのスケジューリング
● 苦手なことは:
● コードの意味を変えること
● 依存関係を壊すこと
● ルールを破ること
15. ルール
● D3D10 以降は基本的に IEEE-754-2008 [1] に従う
● 例外[2]:
● 演算精度は 0.5 ULP ではなく 1 ULP
● 非正規化数は演算でフラッシュされて 0 になる
● MOV 系の命令を除いて
● min/max は入力をフラッシュするが、出力については決まっていない
● HLSL コンパイラが無視すること:
● 特定の条件で NaN や INF になる可能性
● 例)本当は NaN * 0 = NaN だが、x * 0 = 0 とする
● → precise キーワードか IEEE Strictness を有効にしていない場合
● 注意: コンパイラは isnan() と isfinite() の呼び出しを最適化で消してしま
うかもしれない!
16. ハードウェアに関する普遍的*事実
● 乗算→加算 は 1 つの命令。加算→乗算 は 2 つの命令
● 絶対値(abs), 符号反転(-) , saturate はコストがかからない
● MOV が発生する場合を除いて
● スカラー演算はベクトル演算より使用するリソースが少ない
● 定数だけを使う数学関数はバカげてる
● 何もしないことは何かをすることより早い
* 我々が知る限りの宇宙において
17. MAD
● 一次関数 → mad
● さらに clamp する場合 → mad_sat
● clamp が [0, 1] の範囲でない場合 → mad_sat + mad
● 範囲を変える操作 == 一次関数
● MAD はいつも直感的な形とは限らない
● MAD = x * slope + offset_at_zero
● 簡単なパラメータから slope と offset を作ってみよう【訳注: x 以外は定数】
(x – start) * slope → x * slope + (-start * slope)
(x – start) / (end – start) → x * (1.0f / (end - start)) + (-start / (end - start))
(x – mid_point) / range + 0.5f → x * (1.0f / range) + (0.5f - mid_point / range)
clamp(s1 + (x-s0)*(e1-s1)/(e0-s0), s1, e1) → saturate(x * (1.0f/(e0-s0)) + (-s0/(e0-s0))) * (e1-s1) + s1
18. MAD
● その他の式変形
x * (1.0f – x) → x–x*x
x * (y + 1.0f) → x*y+x
(x + c) * (x - c) → x * x + (-c * c)
(x + a) / b → x * (1.0f / b) + (a / b)
x += a * b + c * d; → x += a * b;
x += c * d;
19. 除算
● a / b は一般的に a * rcp(b) と実装される
● ただし D3D アセンブリは DIV 命令を使うことがある
● 明示的な rcp() はときどき良いコードを生成する
● 式変形
a / (x + b) → rcp(x * (1.0f / a) + (b / a))
a / (x * b) → rcp(x) * (a / b)
rcp(x * (b / a))
a / (x * b + c) → rcp(x * (b / a) + (c / a))
(x + a) / x → 1.0f + a * rcp(x)
(x * a + b) / x → a + b * rcp(x)
● どれも中学生の数学レベル!
● すべて究極形まで式を導出をしている! [3]
20. マッドネス
● 最初はこんなコード:
float AlphaThreshold(float alpha, float threshold, float blendRange)
{
float halfBlendRange = 0.5f*blendRange;
threshold = threshold*(1.0f + blendRange) - halfBlendRange;
float opacity = saturate( (alpha - threshold + halfBlendRange)/blendRange );
return opacity;
}
mul r0.x, cb0[0].y, l(0.500000) 0 y: ADD ____, KC0[0].y, 1.0f
add r0.y, cb0[0].y, l(1.000000) z: MUL_e ____, KC0[0].y, 0.5
mad r0.x, cb0[0].x, r0.y, -r0.x t: RCP_e R0.y, KC0[0].y
add r0.x, -r0.x, v0.x 1 x: MULADD_e ____, KC0[0].x, PV0.y, -PV0.z
mad r0.x, cb0[0].y, l(0.500000), r0.x 2 w: ADD ____, R0.x, -PV1.x
div_sat o0.x, r0.x, cb0[0].y 3 z: MULADD_e ____, KC0[0].y, 0.5, PV2.w
4 x: MUL_e R0.x, PV3.z, R0.y CLAMP
21. マッドネス
● AlphaThreshold() はこうできる!
// scale = 1.0f / blendRange
// offset = 1.0f - (threshold/blendRange + threshold)
float AlphaThreshold(float alpha, float scale, float offset)
{
return saturate( alpha * scale + offset );
}
mad_sat o0.x, v0.x, cb0[0].x, cb0[0].y 0 x: MULADD_e R0.x, R0.x, KC0[0].x, KC0[0].y CLAMP
22. 修飾子
● MOV が発生しなければタダ
● 入力に対する abs / neg
● 出力に対する saturate
float main(float2 a : TEXCOORD) : SV_Target float main(float2 a : TEXCOORD) : SV_Target
{ {
return abs(a.x) * abs(a.y); return abs(a.x * a.y);
} }
0 x: MUL_e R0.x, |R0.x|, |R0.y| 0 y: MUL_e ____, R0.x, R0.y
1 x: MOV R0.x, |PV0.y|
23. 修飾子
● MOV が発生しなければタダ
● 入力に対する abs / neg
● 出力に対する saturate
float main(float2 a : TEXCOORD) : SV_Target float main(float2 a : TEXCOORD) : SV_Target
{ {
return -a.x * a.y; return -(a.x * a.y);
} }
0 x: MUL_e R0.x, -R0.x, R0.y 0 y: MUL_e ____, R0.x, R0.y
1 x: MOV R0.x, -PV0.y
24. 修飾子
● MOV が発生しなければタダ
● 入力に対する abs / neg
● 出力に対する saturate
float main(float a : TEXCOORD) : SV_Target float main(float a : TEXCOORD) : SV_Target
{ {
return 1.0f - saturate(a); return saturate(1.0f - a);
} }
0 y: MOV ____, R0.x CLAMP 0 x: ADD R0.x, -R0.x, 1.0f CLAMP
1 x: ADD R0.x, -PV0.y, 1.0f
25. 修飾子
● saturate() はタダ, min() と max() はタダでない
● max(x, 0.0f) や min(x, 1.0f) で足りる場合でも saturate(x) を使う
● (x > 1.0f) や (x < 0.0f) にそれぞれ意味がある場合を除いて
● 不幸なことに, HLSL コンパイラは時々逆のことをしてしまう…
● saturate(dot(a, a)) → “わーい、dot(a, a) は常に正だぞ” →
min(dot(a, a), 1.0f)
● 回避方法:
● 実際の範囲をコンパイラにわかりづらくさせる
● 例) リテラル値を constants に移動させる
● precise キーワードを使う
● IEEE Strictness を強制できる
● 回避法が回避されていないかチェックしよう
● mad(x, slope, offset) 関数は消えた MAD を復活させられる
26. HLSL コンパイラへの対策
● precise キーワードを使う
● コンパイラは NaN を無視しない
● saturate(NaN) == 0
float main(float3 a : TEXCOORD0) : SV_Target float main(float3 a : TEXCOORD0) : SV_Target
{ {
return saturate(dot(a, a)); return (precise float) saturate(dot(a, a));
} }
dp3 r0.x, v0.xyzx, v0.xyzx dp3_sat o0.x, v0.xyzx, v0.xyzx
min o0.x, r0.x, l(1.000000)
0 x: DOT4_e ____, R0.x, R0.x 0 x: DOT4_e R0.x, R0.x, R0.x CLAMP
y: DOT4_e ____, R0.y, R0.y y: DOT4_e ____, R0.y, R0.y CLAMP
z: DOT4_e ____, R0.z, R0.z z: DOT4_e ____, R0.z, R0.z CLAMP
w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f CLAMP
1 x: MIN_DX10 R0.x, PV0.x, 1.0f
27. 組み込み関数
● rcp(), rsqrt(), sqrt()* は直接ハードウェアの命令になる
● 同等の計算が最適化されるとは限らない…
● 1.0f / x は rcp(x) になりやすい
● 1.0f / sqrt(x) は rsqrt(x) でなく rcp(sqrt(x)) になる!
● exp2() と log2() はハードウェア命令, exp() と log() は違う
● exp2(x * 1.442695f) や log2(x * 0.693147f) として実装される
● pow(x, y) は exp2(log2(x) * y) として実装される
● リテラル値 y の特殊なケース
● z * pow(x, y) = exp2(log2(x) * y + log2(z))
● もし log2(z) がコンパイル時に計算できる場合、乗算のコストは 0
● 例) specular_normalization * pow(n_dot_h, specular_power)
28. 組み込み関数
● sign()
● 0 ケースに気を付ける
● 0 を気にしない? それなら (x >= 0)? 1 : -1 を使おう
● sign(x) * y → (x >= 0)? y : -y
● sin(), cos(), sincos() もハードウェア命令
● ただし一部のハードウェアでは少し時間が必要
● asin(), acos(), atan(), atan2(), degrees(), radians()
● そんなのを使うなんて間違ってる!
● とてつもなく長い命令を生成する 【訳注: degrees() と radians() は単に乗算になる】
● cosh(), sinh(), log10()
● キミたち誰? シェーダでどんな仕事ができるっていうの?
29. 組み込み関数
● mul(v, m)
● v.x * m[0] + v.y * m[1] + v.z * m[2] + v.w * m[3]
● MUL – MAD – MAD – MAD
● mul(float4(v.xyz, 1), m)
● v.x * m[0] + v.y * m[1] + v.z * m[2] + m[3]
● MUL – MAD – MAD – ADD
● v.x * m[0] + (v.y * m[1] + (v.z * m[2] + m[3]))
● MAD – MAD – MAD
30. 組み込み関数
float4 main(float4 v : TEXCOORD0) : SV_Position float4 main(float4 v : TEXCOORD0) : POSITION
{ {
return mul(float4(v.xyz, 1.0f), m); return v.x*m[0] + (v.y*m[1] + (v.z*m[2] + m[3]));
} }
0 x: MUL_e ____, R1.y, KC0[1].w 0 z: MULADD_e R0.z, R1.z, KC0[2].y, KC0[3].y
y: MUL_e ____, R1.y, KC0[1].z w: MULADD_e R0.w, R1.z, KC0[2].x, KC0[3].x
z: MUL_e ____, R1.y, KC0[1].y 1 x: MULADD_e ____, R1.z, KC0[2].w, KC0[3].w
w: MUL_e ____, R1.y, KC0[1].x y: MULADD_e ____, R1.z, KC0[2].z, KC0[3].z
1 x: MULADD_e ____, R1.x, KC0[0].w, PV0.x 2 x: MULADD_e ____, R1.y, KC0[1].w, PV1.x
y: MULADD_e ____, R1.x, KC0[0].z, PV0.y y: MULADD_e ____, R1.y, KC0[1].z, PV1.y
z: MULADD_e ____, R1.x, KC0[0].y, PV0.z z: MULADD_e ____, R1.y, KC0[1].y, R0.z
w: MULADD_e ____, R1.x, KC0[0].x, PV0.w w: MULADD_e ____, R1.y, KC0[1].x, R0.w
2 x: MULADD_e ____, R1.z, KC0[2].w, PV1.x 3 x: MULADD_e R1.x, R1.x, KC0[0].x, PV2.w
y: MULADD_e ____, R1.z, KC0[2].z, PV1.y y: MULADD_e R1.y, R1.x, KC0[0].y, PV2.z
z: MULADD_e ____, R1.z, KC0[2].y, PV1.z z: MULADD_e R1.z, R1.x, KC0[0].z, PV2.y
w: MULADD_e ____, R1.z, KC0[2].x, PV1.w w: MULADD_e R1.w, R1.x, KC0[0].w, PV2.x
3 x: ADD R1.x, PV2.w, KC0[3].x
y: ADD R1.y, PV2.z, KC0[3].y
z: ADD R1.z, PV2.y, KC0[3].z
w: ADD R1.w, PV2.x, KC0[3].w
31. 行列計算
● 行列は任意の線形変換を一飲みにできる
● CPU 側と GPU 側で!
float4 pos = // tex_coord pre-transforms merged into matrix
{ float4 pos = { tex_coord.xy, depth, 1.0f };
tex_coord.x * 2.0f - 1.0f,
1.0f - 2.0f * tex_coord.y,
depth, 1.0f
⇨ float4 l_pos = mul(pos, new_mat);
}; // LightPos translation merged into matrix
float3 light_vec = l_pos.xyz / l_pos.w;
float4 w_pos = mul(cs, mat);
float3 world_pos = w_pos.xyz / w_pos.w;
float3 light_vec = world_pos - LightPos; // CPU-side code
float4x4 pre_mat = Scale(2, -2, 1) * Translate(-1, 1, 0);
float4x4 post_mat = Translate(-LightPos);
float4x4 new_mat = pre_mat * mat * post_mat;
32. スカラー演算
● 現代のハードウェアにはスカラー ALU がある
● スカラー計算は常にベクトル計算より早い
● 昔の VLIW やベクトル ALU アーキテクチャでもメリットがある
● シェーダが短くなることがある
● あるいは、ほかの処理のためにレーンが空く
● スカラーからベクトルへの展開は気付きにくい
● 式の評価順とかっこに依存している
● 時には関数や抽象化で隠される
● 時には関数内で隠される
33. スカラー演算とベクトル演算の混在
● ローレベルの計算を考える
● ベクトル部分とスカラー部分を分離する
● 共通の部分式を探す
● コンパイラはつねに共通の部分式を再利用できるわけではない!
● コンパイラはスカラーを取り出せないこともある!
● dot(), normalize(), reflect(), length(), distance()
● スカラー計算とベクトル計算を分けて管理する
● 評価順に気を付けよう
● 式は左から右に評価される
● かっこを使おう
34. 隠れたスカラー演算
● normalize(vec)
● 入力も出力もベクトルだが、中間式でスカラー値が出現する
● normalize(vec) = vec * rsqrt(dot(vec, vec))
● dot() はスカラー値を返す。 rsqrt() もまだスカラー
● ベクトルと正規化係数を分けて管理する
● 一部のハードウェア (とりわけ PS3) は組み込みの normalize() がある
● その場合 normalize() を使った方が良い
● reflect(i, n) = i – 2.0f * dot(i, n) * n
● lerp(a, b, c) は (b-a) * c + a と実装されている
● c がスカラー値で、a または b もスカラー値なら, b * c + a * (1-c)
が少ない演算でできる
35. 隠れたスカラー演算
● 50.0f * normalize(vec) = 50.0f * (vec * rsqrt(dot(vec, vec)))
● 不必要にベクトル演算を行っている
float3 main(float3 vec : TEXCOORD0) : SV_Target float3 main(float3 vec: TEXCOORD) : SV_Target
{ {
return 50.0f * normalize(vec); return vec * (50.0f * rsqrt(dot(vec, vec)));
} }
0 x: DOT4_e ____, R0.x, R0.x 0 x: DOT4_e ____, R0.x, R0.x
y: DOT4_e ____, R0.y, R0.y y: DOT4_e ____, R0.y, R0.y
z: DOT4_e ____, R0.z, R0.z z: DOT4_e ____, R0.z, R0.z
w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f
1 t: RSQ_e ____, PV0.x 1 t: RSQ_e ____, PV0.x
2 x: MUL_e ____, R0.y, PS1 2 w: MUL_e ____, PS1, (0x42480000, 50.0f).x
y: MUL_e ____, R0.x, PS1 3 x: MUL_e R0.x, R0.x, PV2.w
w: MUL_e ____, R0.z, PS1 y: MUL_e R0.y, R0.y, PV2.w
3 x: MUL_e R0.x, PV2.y, (0x42480000, 50.0f).x z: MUL_e R0.z, R0.z, PV2.w
y: MUL_e R0.y, PV2.x, (0x42480000, 50.0f).x
z: MUL_e R0.z, PV2.w, (0x42480000, 50.0f).x
36. 隠れた共通の部分式
● normalize(vec) and length(vec) contain dot(vec, vec)
● コンパイラは完全に一致したら再利用する
● コンパイラは異なった使い方には再利用をしない
● 例)ベクトルの長さを 1 にクランプする
float3 main(float3 v : TEXCOORD0) : SV_Target 0 x: DOT4_e ____, R0.x, R0.x
{ y: DOT4_e R1.y, R0.y, R0.y
if (length(v) > 1.0f) z: DOT4_e ____, R0.z, R0.z
v = normalize(v); w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f
return v; 1 t: SQRT_e ____, PV0.x
} 2 w: SETGT_DX10 R0.w, PS1, 1.0f
t: RSQ_e ____, R1.y
dp3 r0.x, v0.xyzx, v0.xyzx 3 x: MUL_e ____, R0.z, PS2
sqrt r0.y, r0.x y: MUL_e ____, R0.y, PS2
rsq r0.x, r0.x z: MUL_e ____, R0.x, PS2
mul r0.xzw, r0.xxxx, v0.xxyz 4 x: CNDE_INT R0.x, R0.w, R0.x, PV3.z
lt r0.y, l(1.000000), r0.y y: CNDE_INT R0.y, R0.w, R0.y, PV3.y
movc o0.xyz, r0.yyyy, r0.xzwx, v0.xyzx z: CNDE_INT R0.z, R0.w, R0.z, PV3.x
37. 隠れた共通の部分式
● 最適化:ベクトルの長さを 1 にクランプする
最初の形 if (length(v) > 1.0f)
v = normalize(v);
float norm_factor =
min(rsqrt(dot(v, v)), 1.0f); 部分式を取り出
return v; v *= norm_factor; す
return v;
式を展開 if (sqrt(dot(v, v)) > 1.0f)
v *= rsqrt(dot(v, v));
float norm_factor =
saturate(rsqrt(dot(v, v)));
saturate に置換
return v; return v * norm_factor;
式を統合 if (rsqrt(dot(v, v)) < 1.0f)
v *= rsqrt(dot(v, v));
precise float norm_factor =
saturate(rsqrt(dot(v, v)));
HLSL
return v; return v * norm_factor; コンパイラ対策
38. 隠れた共通の部分式
● 最適化:ベクトルの長さを 1 にクランプする
float3 main(float3 v : TEXCOORD0) : SV_Target float3 main(float3 v : TEXCOORD0) : SV_Target
{ {
if (length(v) > 1.0f) if (rsqrt(dot(v, v)) < 1.0f)
v = normalize(v); v *= rsqrt(dot(v, v));
return v; return v;
} }
0 x: DOT4_e ____, R0.x, R0.x 0 x: DOT4_e ____, R0.x, R0.x
y: DOT4_e R1.y, R0.y, R0.y y: DOT4_e ____, R0.y, R0.y
z: DOT4_e ____, R0.z, R0.z z: DOT4_e ____, R0.z, R0.z
w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f
1 t: SQRT_e ____, PV0.x 1 t: RSQ_e ____, PV0.x
2 w: SETGT_DX10 R0.w, PS1, 1.0f 2 x: MUL_e ____, R0.y, PS1
t: RSQ_e ____, R1.y y: MUL_e ____, R0.x, PS1
3 x: MUL_e ____, R0.z, PS2 z: SETGT_DX10 ____, 1.0f, PS1
y: MUL_e ____, R0.y, PS2 w: MUL_e ____, R0.z, PS1
z: MUL_e ____, R0.x, PS2 3 x: CNDE_INT R0.x, PV2.z, R0.x, PV2.y
4 x: CNDE_INT R0.x, R0.w, R0.x, PV3.z y: CNDE_INT R0.y, PV2.z, R0.y, PV2.x
y: CNDE_INT R0.y, R0.w, R0.y, PV3.y z: CNDE_INT R0.z, PV2.z, R0.z, PV2.w
z: CNDE_INT R0.z, R0.w, R0.z, PV3.x
39. 隠れた共通の部分式
● 最適化:ベクトルの長さを 1 にクランプする
float3 main(float3 v : TEXCOORD0) : SV_Target
{
precise float norm_factor =
saturate(rsqrt(dot(v, v)));
return v * norm_factor;
}
0 x: DOT4_e ____, R0.x, R0.x
y: DOT4_e ____, R0.y, R0.y
z: DOT4_e ____, R0.z, R0.z
w: DOT4_e ____, (0x80000000, -0.0f).x, 0.0f
1 t: RSQ_e ____, PV0.x CLAMP
2 x: MUL_e R0.x, R0.x, PS1
y: MUL_e R0.y, R0.y, PS1
z: MUL_e R0.z, R0.z, PS1
● 汎用的なケースに拡張
● 長さ 5.0f にクランプ → norm_factor = saturate(5.0f * rsqrt(dot(v, v)));
40. 評価の順序
● 式は左から右に評価される
● かっこや演算子の優先順位がある場合を除いて
● スカラー計算を式の左に置いたりかっこを使う
// float3 float float float3 float float // float3 float3 (float float float float)
return Diffuse * n_dot_l * atten * LightColor * shadow * ao; return Diffuse * LightCol * (n_dot_l * atten * shadow * ao);
0 x: MUL_e ____, R0.z, R0.w 0 x: MUL_e R0.x, R0.x, R1.x
y: MUL_e ____, R0.y, R0.w y: MUL_e ____, R0.w, R1.w
z: MUL_e ____, R0.x, R0.w z: MUL_e R0.z, R0.z, R1.z
1 y: MUL_e ____, R1.w, PV0.x w: MUL_e R0.w, R0.y, R1.y
z: MUL_e ____, R1.w, PV0.y 1 x: MUL_e ____, R2.x, PV0.y
w: MUL_e ____, R1.w, PV0.z 2 w: MUL_e ____, R2.y, PV1.x
2 x: MUL_e ____, R1.x, PV1.w 3 x: MUL_e R0.x, R0.x, PV2.w
z: MUL_e ____, R1.z, PV1.y y: MUL_e R0.y, R0.w, PV2.w
w: MUL_e ____, R1.y, PV1.z z: MUL_e R0.z, R0.z, PV2.w
3 x: MUL_e ____, R2.x, PV2.w
y: MUL_e ____, R2.x, PV2.x
w: MUL_e ____, R2.x, PV2.z
4 x: MUL_e R2.x, R2.y, PV3.y
y: MUL_e R2.y, R2.y, PV3.x
z: MUL_e R2.z, R2.y, PV3.w
41. 評価の順序
● VLIW とベクトルアーキテクチャは依存関係に注意しないとい
けない
● 特にスコープの始まりと終わりで
● a * b * c * d = ((a * b) * c) * d;
// float
● Break dependency chains with parentheses: (a*b) * (c*d)
float float float float3 //float3float
return n_dot_l * atten * shadow * ao * Diffuse * LightColor;
float float float float3
return (n_dot_l * atten) * (shadow * ao) * (Diffuse
float3
* LightColor);
0 x: MUL_e ____, R0.w, R1.w 0 x: MUL_e ____, R2.x, R2.y
1 w: MUL_e ____, R2.x, PV0.x y: MUL_e R0.y, R0.y, R1.y VEC_021
2 z: MUL_e ____, R2.y, PV1.w z: MUL_e R0.z, R0.x, R1.x VEC_120
3 x: MUL_e ____, R0.y, PV2.z w: MUL_e ____, R0.w, R1.w
y: MUL_e ____, R0.x, PV2.z t: MUL_e R0.x, R0.z, R1.z
w: MUL_e ____, R0.z, PV2.z 1 w: MUL_e ____, PV0.x, PV0.w
4 x: MUL_e R1.x, R1.x, PV3.y 2 x: MUL_e R0.x, R0.z, PV1.w
y: MUL_e R1.y, R1.y, PV3.x y: MUL_e R0.y, R0.y, PV1.w
z: MUL_e R1.z, R1.z, PV3.w z: MUL_e R0.z, R0.x, PV1.w
42. 実際のテスト
● ケース・スタディ: Clustered deferred shading
● 品質が混在するコード
● オリジナルのライティングコードは完全に最適化
● さまざまなプロトタイプ品質のコードをあとで追加
● ローレベルの最適化
● 1-2 時間ほどの作業
● シェーダは約 7% 短くなった
● 太陽光光源のみ: 0.40ms → 0.38ms (5% 高速化)
● 大量の点光源: 3.56ms → 3.22ms (10% 高速化)
● ハイレベルの最適化
● 数週間の作業
● 古典的な deferred shading に比べ -15% ~ +100% の高速化
● 両方しよう!
43. その他の推奨事項
● [branch], [flatten], [loop], [unroll] で意図を伝える
● [branch] は “勾配関数” の警告をエラーにする
【訳注: http://msdn.microsoft.com/ja-jp/library/bb509610%28v=vs.85%29.aspx 参照】
● これは素晴らしい!
● さもないと、条件外の時のコードのかたまりを引きずることになる
● シェーダの外でできることをシェーダ内に書かない
線形の演算は頂点シェーダに移す
float2 ClipSpaceToTexcoord(float3 Cs)
● {
Cs.xy = Cs.xy / Cs.z;
● もちろん頂点がボトルネックでなければ Cs.xy = Cs.xy * 0.5h + 0.5h;
Cs.y = ( 1.h - Cs.y );
return Cs.xy;
● 必要以上の出力をしない }
● SM4 以降は float4 の SV_Target が必須でない
● 使われないアルファは書き込まない! float2 tex_coord = Cs.xy / Cs.z;
44. 良いローレベルコーダーになるには?
● GPUハードの命令をよく理解する
● そして PC の D3D アセンブリを学ぼう
● HLSL からハードウェアコードへの変換を理解する
● GPUShaderAnalyzer, NVShaderPerf, fxc.exe 等のツール
● あらゆるハードウェアとプラットフォームで結果を比較する
● シェーダの編集がコードの長さに与えた影響をチェックする
● 異常な結果だったら? → アセンブリを精査し、原因と結果を調べる
● 実際のベンチマークを行う
47. Questions?
@_Humus_
emil.persson@avalanchestudios.se
We are hiring!
New York, Stockholm
48. 翻訳: @Reputeless
訳文改善の指摘は reputeless@gmail.com までお送りください。