Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

RenderLog

[번역] How to read shader assembly – Interplay of Light 본문

Graphics/참고자료

[번역] How to read shader assembly – Interplay of Light

scahp 2021. 4. 24. 10:15

개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.
또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.
출처 : interplayoflight.wordpress.com/2021/04/18/how-to-read-shader-assembly/

 
Upadated 2021.05.01 : 잘못해서 번역 단락 바꿔져서 들어간 부분 수정


HOW TO READ SHADER ASSEMBLY

April 18, 2021 | Kostas Anagnostou

 

내가 그래픽스 프로그래밍 쉐이딩 언어를 시작했을때, HLSL과 GLSL 같은 언어는 아직 게임 개발에 대중적이지 않았고 쉐이더들은 어셈블리에서 직접 개발되었습니다. HLSL이 나왔을 때, 우리는 더 짧고 더 압축된 어셈블리 코드를 손으로 만들어 컴파일러를 이기려 했던 것으로 기억합니다, 어렵지는 않았습니다. 그 후 쉐이더 컴파일러 기술이 굉장히 발전했고, 요즘은 왠만하면 직접 어셈블리 코드를 만드는게 어렵습니다(쉐이더가 너무 커지고 복잡해서 더이상 효율적이지 않음)

요즘은 쉐이더를 직접 어셈블리로 작성하지 않더라도, 컴파일러로 부터 생성된 어셈블리 쉐이더(ISA) 코드를 읽고 이해할 수 있는 것은 그래픽스 프로르래머에 여전히 유용합니다. 첫째로, 컴파일러가 high level shader 번역을 어떻게 하는지 이해하는 것이 도움이 됩니다. 몇몇 명령어는, 예를들어 tan() 이나 integer 나누기는 하드웨어와 바로 연결되지 않고 여러 어셈블리 명령어로 확장됩니다.

둘째로, GPU의 작동방식을 이해하는 것이 도움이 됩니다, 어떻게 데이터를 요청하는지, 분기를 수행하는지, 출력을 기록하는지 등등. 셋째로, 실제 쉐이더 코드를 사용할 수 없을때, 쉐이더 디버깅에 도움을 줍니다. 그리고 비록 우리가 더이상 쉐이더를 손으로 튜닝하지 않지만, 그것을 이해하면 우리가 high level 쉐이더를 작성할 때, 더 나른 성능의 어셈블리 코드를 만들 수 있게 해줍니다. 마지막으로, 개인적인 생각으로는, 무엇을 하려는지 깔끔하게 이해하게 해주고 가능한한 하드웨어에 가까워져서 추상 레이어 없이 코드를 읽을 수 있어서 꽤 즐겁습니다.

이 블로그 글에서 우리는 약간의 쉐이더 어셈블리에 대해 토론하고 읽는 방법에 대한 지침을 제공할 것입니다. 이 토론은 DirectX와 HLSL에 중점을 둡니다, 비슷한 방식으로 다른 APIs/Shading language에 적용할 수 있을 것입니다. 또한 예제에서 나는 AMD GPU에 접근 권한이 없더라도 잘 정리된 문서와 훌륭한 Shader Playground에 접근하기 쉽기 때문에 AMD의 쉐이더 어셈블리 (ISA)를 사용하고 있습니다.

시작하기 전에 2 스테이지안에 끝나는 쉐이더 컴파일에 대해서 언급하는 것은 중요합니다. 먼저 fxc나 dxc 같은 툴은 HLSL 코드를 Intermediate language(IL)인 GPU agnostic format 으로 변환합니다. 그리고나서 GPU 드라이버는 IL을 특정 GPU에서 실행될 수 있는 최종 쉐이더 어셈블리(ISA)로 변환합니다. 우리는 IL이 아니라 ISA에 중점을 둘 것입니다, 그것이 실제로 실행되는 코드를 더 잘 나타내기 때문입니다. 뒤따른 예제에서, 두개의 수를 곱하는 HLSL shader 는 IL 코드를 왼쪽에 생성하고 ISA 코드를 오른쪽에 생성합니다. IL 코드는 여전히 상대적으로 high level 이고 수많은 구현의 세부사항을 숨깁니다.

 

Intermediate LanguageGCN ISA
il_ps_2_55
dcl_global_flags refactoringAllowed
dcl_cb cb0[1]
dcl_input_generic_interp(linear) v0
dcl_output_generic o0
mul_ieee r4096, v0, cb0[0]
mov o0, r4096
ret_dyn
end
s_mov_b32 m0, s8
s_buffer_load_dwordx4 s[0:3], s[4:7], 0x00
v_interp_p1_f32 v2, v0, attr0.x
v_interp_p2_f32 v2, v1, attr0.x
v_interp_p1_f32 v3, v0, attr0.y
v_interp_p2_f32 v3, v1, attr0.y
v_interp_p1_f32 v4, v0, attr0.z
v_interp_p2_f32 v4, v1, attr0.z
v_interp_p1_f32 v0, v0, attr0.w
v_interp_p2_f32 v0, v1, attr0.w
s_waitcnt lgkmcnt(0)
v_mul_f32 v1, s0, v2
v_mul_f32 v2, s1, v3
v_mul_f32 v3, s2, v4
v_mul_f32 v0, s3, v0
v_cvt_pkrtz_f16_f32 v1, v1, v2
v_cvt_pkrtz_f16_f32 v0, v3, v0
exp mrt0, v1, v1, v0, v0 done compr vm
s_endpgm
end

fictional HLSL shader 를 봅시다. 비록 이것이 유용한 무엇인가를 하진 않지만, 현실적인 시나리오에서 사용할 수 있는 수많은 언어의 특징을 사용합니다, attribute interpolation, constant buffer, texture reads, 수학 연산 그리고 분기 같은:

struct PSInput
{
    float2 uv : TEXCOORD;
};
 
cbuffer cbData
{
    float4 data;
}
 
Texture2D<float4> tex;
SamplerState samplerLinear;
 
float4 PSMain(PSInput input) : SV_TARGET
{
    float4 result = tex.Sample(samplerLinear, input.uv); 
     
    float factor = data.x * data.y;
     
    if( factor > 0 )
        return data.z * result; 
    else
        return data.w * result;   
}

이것은 AMD의 GCN GPU architecture를 타겟으로 하는 Radeon GPU Analyser를 사용하여 생성된 쉐이더 어셈블리입니다:

  s_mov_b32     m0, s20             
  s_mov_b64     s[22:23], exec      
  s_wqm_b64     exec, exec          
  v_interp_p1_f32  v2, v0, attr0.x  
  v_interp_p2_f32  v2, v1, attr0.x  
  v_interp_p1_f32  v3, v0, attr0.y  
  v_interp_p2_f32  v3, v1, attr0.y  
  s_and_b64     exec, exec, s[22:23]
  image_sample  v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf 
  s_buffer_load_dwordx4  s[0:3], s[16:19], 0x00   
  s_waitcnt     lgkmcnt(0)                        
  v_mov_b32     v4, s1                            
  v_mul_f32     v4, s0, v4                        
  v_cmp_lt_f32  vcc, 0, v4                        
  s_cbranch_vccz  label_0017                      
  s_waitcnt     vmcnt(0)                          
  v_mul_f32     v0, s2, v0                        
  v_mul_f32     v1, s2, v1                        
  v_mul_f32     v2, s2, v2                        
  v_mul_f32     v3, s2, v3                        
  s_branch      label_001C                        
label_0017:
  s_waitcnt     vmcnt(0)                          
  v_mul_f32     v0, s3, v0                        
  v_mul_f32     v1, s3, v1                        
  v_mul_f32     v2, s3, v2                        
  v_mul_f32     v3, s3, v3                        
label_001C:
  s_mov_b64     exec, s[22:23]                    
  v_cvt_pkrtz_f16_f32  v0, v0, v1                 
  v_cvt_pkrtz_f16_f32  v1, v2, v3                 
  exp           mrt0, v0, v0, v1, v1 done compr vm
  s_endpgm                                        
end

처음 봤을때, 이것은 엉망진창인 명령어와 숫자처럼 보입니다. HLSL이 어셈블리로 어떻게 변환되는지 대략적인 느낌을 얻기 위해서 먼저 두 쉐이더 코드 사이 영역에 컬러링을 해봅시다.

 

 

색상이 없는 부분은 하이라이트된 주요 명령어 설정하기위해 GPU가 실행해야만 하는 코드입니다.

우리가 코드로 더 깊게 들어가기전에 몇가지를 논의할 중요한 것이 있습니다. 먼저 우리는 거의 모든 명령어가 접두사 v_ 나 접두사 s_ 로 시작하는 것을 알챘습니다. 즉, v_mulf32 그리고 s_mov_b32. 이것은 우리에게 하드웨어에 대한 몇몇 정보를 줍니다: GCN architecture는 wavefront라 불리는 64개의 배치(batch)들로 work item(픽셀, 버택스 등등)을 일괄처리 합니다. 그리고 각각의 쉐이더 명령어를 병렬로 실행합니다, 각 스레드에 고유한 데이터 (벡터로 부터 v_ 접두사를 사용) 또는 모든 스레드에 공통인 (스칼라로 부터 s_ 접두사). 언급할 가치가 있는 것은 work item은 종종 "thread"라고 불립니다. 픽셀의 컬러에 특정 값을 곱하는 것은 각 스레드에 고유한 데이터를 사용하는 연산입니다, 그래서 GPU는 벡터 명령어를 사용합니다. 상수 버퍼의 특정 값을 읽는 것은 모든 스레드에 공통 데이터를 사용합니다. 그래서 GPU는 스칼라 명령어를 사용할 것입니다.

이것은 벡터와 스칼라 명령어를 수행하는 레지스터의 타입에 대해서 짧게 논의하기 좋은 기회입니다. 위에 나온 쉐이더 코드는 벡터(vXX)와 스칼라 (sXX) 레지스터로 뒤덮혀 있습니다, 즉 v_mul_f32 v1, s3, v1. 레지스터는 데이터를 지역적으로 저장합니다. 레지스터는 쉐이더 명령어가 연산에 사용하는 데이터를 지역적으로 저장합니다. 벡터 레지스터는 Wavefront thread 당 (총 64개) 하나에 32 bit 를 저장합니다. 나는 이것을 명확하게 하기 위해서 64 thread wavefront 그리고 벡터와 스칼라 레지스터가 이것에 매핑되는 방식을 보여주는 다이어그램을 준비했습니다.

 

 

mul 명령어는 위에서 언급한 스레드당 v1 벡터 레즈스터 값과, 모든 스레드에 공통인, s3 스칼라 레지스터의 값을 곱할 것이며 결과를 v1 벡터 레지스터에 저장할 것입니다. 가끔 레지스터 인덱스들이 괄호안에 주어지는데, 예를들면 s[22:23]. 이것은 레지스터의 범위를 가리킵니다(예제에서 스칼라 레지스터는 22와 23) 이 범위는 32비트 보다 큰 양을 저장하는데 사용될 수 있습니다.

마지막으로, 각 명령어는 데이터 타입에 따라 작동하는 것을 알아두는 것이 좋습니다. 예를들어 v_mul_f32 연산은 32비트 floating point 로 수행합니다, v_move_b32 는 32비트 (타입이 지정되지 않은) 크기를 레지스터끼리 "복사"합니다, v_cvt_pkrtz_f16_f32 는 32비트 floating point 를 16비트로 변환합니다. 이것은 각 명령어에서 사용하는 데이터 타입과 크기를 이해하는데 유용합니다.

이 정보를 갖고, 어셈블리 코드를 한번에 하나씩 해독해봅시다.

s_mov_b32     m0, s20             
s_mov_b64     s[22:23], exec      
s_wqm_b64     exec, exec  

쉐이더는 몇몇 설정 코드로 시작합니다. 우리는 어떤 보간을 이후에 할 것이기 때문에 컴파일러는 보간 데이터(버택스당 uv 좌표의 경우)에 대한 M0 레지스터(wavefront 당 한개, 32bit)를 로컬 데이터 스토리지(LDS) 오프셋으로 채웁니다. 다음으로, exec 레지스터를 스칼라 레지스터 22와 23으로 복사합니다. exec (Execute Mask) 레지스터는 64 bit mask를 저장합니다, wavefront thread 당 한개의 비트, 이것은 어떤 스레드가 그 순간에 활성 그리고 비활성되었는지를 결정합니다. 이 레지스터는 모든 wavefront thread에 공통이기 때문에 스칼라 (s_) 명령어를 사용합니다. 또한, 이것은 32bit 보다 더 큰 값을 저장하기 위해서 레지스터를 조합하는 좋은 예제입니다. 마지막으로 wavefront의 어떤 thread가 active pixel quad에 속하는지 결정하기 위해서 s_wqm_b64 명령어를 실행합니다. 픽셀 쉐이더는 항상 2x2 픽셀 그룹 단위로 수행합니다. 만약 활성화된 quad 안에 픽셀이 있다면, 이것은 삼각형을 덮고 있다는 의미, quad의 모든 픽셀이 활성으로 mark 될 것입니다. 이것은 GPU가 derivative 계산을 위해서 어떤 quad가 활성화 되어할지 결정하게 해줍니다.

v_interp_p1_f32  v2, v0, attr0.x  
v_interp_p2_f32  v2, v1, attr0.x  
v_interp_p1_f32  v3, v0, attr0.y  
v_interp_p2_f32  v3, v1, attr0.y  

다음 것은 로컬 데이터 스토리지(LDS) 메모리를 통해 버택스 쉐이더가 제공하는 float2 uv 좌표를 보간(interpolates)합니다. 그것은 위의 M0 레지스터에 저장된 오프셋을 사용합니다. 우리는 이 경우에 벡터 명령어(접두사 v_)를 사용합니다. 왜냐하면 각각 thread(pixel)는 자신의 uv 값을 가질 것이기 때문입니다. 우리는 각각의 uv.x와 uv.y 컴포넌트를 보간하는 것은 p1, p2 두 단계로 완료된 것을 알 수 있습니다, 컴포넌트당 두개의 명령어. 이 두 단계 동안, GPU는 2개의 무개중심 좌표에 따라 3개의 uv 컴포넌트를 읽고, 버택스당 한번씩, 최종 값을 보간합니다.(During those two steps the GPU reads 3 uv component values, one per vertex, along with the 2 barycentric coordinates and interpolates the final value) 이것은 우리에게 GPU 하드웨어에 관한 또다른 지식을 줍니다. 보간은 쉐이더에서 수행되며 그 연산을 위한 전용 하드웨어가 없다는 것입니다. uv 좌표계는 이제 벡터 레지스터 v2와 v3에 저장되었습니다.

s_and_b64     exec, exec, s[22:23]

이 명령어는 모든 wavefront thread를 위한 excution mask를 갱신합니다. 만약 thread가 active pixel quad에 속하지 않는다면, 그것은 비활성화 됩니다.

image_sample  v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf 

이것은 텍스쳐 샘플 명령어 입니다. sample 접미사는 이 연산이 SampleState object를 통하여 필터 될 수 있다는 것을 암시합니다. 그것은 예제의 HLSL 코드가 사용하는 것입니다. 먼저 벡터 레지스터의 범위는, v[0:3], 4개의 레지스터를 가리킵니다. 그 레지스터는 결과 (v0-v3, 4개 floating point 값)을 저장하는데 사용할 것입니다, v[2:4] 범위는 위에나온 (v2 와 v3)가 보간된 uv 좌표계를 포함합니다, 스칼라 레지스터의 범위 [4:11]은 texture descriptor(텍스쳐의 메모리 주소를 가리키는 것)를 저장하기 위해서 8개의 레지스터를 포함합니다. 그리고 다른 스칼라 범위 [12:15] 는 4개의 레지스터를 텍스쳐 샘플 필터링을 위해 사용되는 sampler object의 descriptor (메모리 주소)를 저장하는데 사용합니다(SampleState object는 HLSL 쉐이더에서 정의됩니다). 최종 dmask (4-bit data mask) 피연산자(operand)는 텍스쳐 읽기가 얼마나 많은 컴포넌트를 처리해야하는지 나타냅니다. 0xf 값은 4개의 컴포넌트를 나타냅니다. 비록 v_ 접두어가 없더라도, image_sample 은 벡터 명령어입니다.

s_buffer_load_dwordx4  s[0:3], s[16:19], 0x00   
s_waitcnt     lgkmcnt(0)                        
v_mov_b32     v4, s1                            
v_mul_f32     v4, s0, v4  

다음으로, 우리는 텍스쳐 컬러에 곱하기 위해서 상수 데이터를 상수 버퍼로 부터 읽어야 합니다. 상수 버퍼로 부터 읽는 것은 보통 모든 wavefront thread에 스칼라 연산입니다, 그래서 s_buffer_load_dwordx4 가 제출됩니다. 먼저 스칼라 범위는 로드의 결과(s0-s3, 4 fp32 값)를 저장하기 위해서 스칼라 레지스터를 명세합니다. 그리고 두번째 범위는 상수 버퍼의 descriptor(저장 되어있는 메모리 주소를 가리킴)를 명세합니다. 모든 메모리 로드는 지연을 가집니다, 그것은 수많은 클럭 사이클(잠재적으로 큼)을 로드 명령어가 s_buffer_load 제출되었을 때 수행한다는 의미입니다. 그리고 반환값은 이어 나오는 v_move_b32 에서 사용될 수 있습니다, 쉐이더 컴파일러는 s_waitcnt 명령어를 두 명령어 사이에 추가하여 data가 준비 될 때까지 기다릴(stall) 것입니다. wait 명령어의 lgkmcnt 인자는 명령어가 상수 버퍼(또는 로컬/글로벌 데이터 저장소)를 읽어 반환하기를 기다리는 것을 나타냅니다. 데이터가 여기에 오면, 쉐이더는 move 명령어를 s1 의 값에서 v4 레지스터로 복사하기 위해서 제출하고 연이어 그것을 s0 레지스터와 곱하기 위해서 v_mul 명령어를 제출합니다(사실상 float factor = data.x * data.y HLSL 명령어를 구현함).

이 코드는 하드웨어에 대한 다른 정보를 보여줍니다, 그것은 스칼라 값의 곱을 직접 할 수 없다는 것입니다, 먼저 두개중 하나를 벡터 레지스터에 곱해야 합니다. 모든 데이터에 대해서 그럴까요? 명백히 아닙니다, 만약 상수 버퍼 데이터를 float4 대신 uint4로 변경하면, 쉐이더는 정수 곱 명령어 s_mul_i32로 바로 두 스칼라 값을 곱합니다:

  s_buffer_load_dwordx4  s[0:3], s[16:19], 0x00         
  s_waitcnt     lgkmcnt(0)                              
  s_mul_i32     s0, s1, s0  

메인 쉐이더로 돌아가서

v_cmp_lt_f32  vcc, 0, v4                        
s_cbranch_vccz  label_0017  

이제 쉐이더는 v4에서 곱셈 결과를 가지고 v_cmp_lt_f32 명령어를 수행하여 그것이 0보다 작은지 판별 할 수 있습니다. 이 명령어는 Vector Condition Code 레지스터 (VCC, 비트 값 1은 스레드가 비교에 통과 했다는 의미고, 0은 실패했다는 의미)를 설정합니다. 비록 벡터 비교이지만 (즉, 각스레드마다 다른), v4 는 모든 스레드에서 같은 값을 가져서 모든 스레드에서 결과가 동일하다는 점을 기억하세요. 만약 비교의 결과가 VCC에 저장되면 0 입니다(c_cbranch_vccz), 이것은 "less than" 비교가 실패했다는 의미 입니다, 쉐이더는 뒤이은 코드의 분기를 건너 뛰고 계속해서 label_0017에서 실행할 것입니다.

하드웨어에 대한 또다른 통찰력도 여기에 있습니다. 비록 "비교" 명령어는 벡터 이지만 (wavefront에 각 thread가 값을 서로 다르게 다룸), 실제 분기 명령은 스칼라입니다, 즉, 모든 스레드에서 같습니다. GCN에서 모든 타입의 분기에 대해서 이것은 사실이며, 그들은 스칼라 유닛에서 처리됩니다. 또한, 이경우 분기는 전부 또는 전혀없음 입니다, 모든 thread의 factor.x 값이 0보다 작거나 모두 0보다 더 크거나 중하나 입니다. 왜냐하면 컴파일러는 그 값이 스칼라로 부터 왔다는 것을 알기 때문입니다 그래서 모든 스레드에서 같습니다. 즉, 여기에 발산(divergence - 역주 : 스레드 들이 서로 다른 값을 가진다는 의미로 보면 될거 같습니다.)은 없습니다. 만약 내가 비교 변수를 thread에 따라 다양하게 했다면:

float4 PSMain(PSInput input) : SV_TARGET
{
    float4 result = tex.Sample(samplerLinear, input.uv); 
    	
    if( result.x > 0 )
    	return data.z * result; 
    else
    	return data.w * result; 
  
}

ISA 결과가 발산을 고려하도록 바뀝니다.

  image_sample  v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf  
  s_mov_b64     s[0:1], exec                            
  s_waitcnt     vmcnt(0)                                
  v_cmpx_gt_f32  s[2:3], v0, 0                           
  s_cbranch_execz  label_0017   

이제 쉐이더 컴파일러는 스레드당 비교 결과를 직접 저장하기 위해 v_cmpx_gt_f32 명령어를 사용하여 execution mask를 사용합니다(그것은 어떤 스레드가 활성 or 비활성화 되었는지 제어). 비교에 실패한 thread는 s_cbranch_execz 명령어가 실행될 때 "건너뛸것"입니다(VCC 레지스터가 아닌 execution mask를 사용한 분기).

메인 쉐이더로 다시가서

s_waitcnt     vmcnt(0)                          
v_mul_f32     v0, s2, v0                        
v_mul_f32     v1, s2, v1                        
v_mul_f32     v2, s2, v2                        
v_mul_f32     v3, s2, v3                        
s_branch      label_001C 

이것은 텍스쳐 읽기 명령어의 결과에 상수 값 (data.z)를 곱할 첫번째 if-statement 분기입니다. 위의 상수 버퍼 읽기와 비슷하게, 이제 텍스쳐 읽기 결과는 GPU가 그들을 로드완료 했다는 것을 보장해줘야 합니다. 그렇지 않으면 정의되지 않은 결과가 나옵니다. 이것을 위해, 쉐이더 컴파일러는 또다른 wait 명령어 s_waitcnt를 추가합니다. 이번의 vmcnt 피연산자는 벡터 메모리 반환을 기다리고 있다는 것을 의미합니다(상수 버퍼 읽기에서 사용하는 스칼라 메모리 읽기와 반대). 코드의 마지막 조각은 두번째 분기를 피하기 위해서 무조건 label_001C 로 점프할 것입니다.

label_0017:
  s_waitcnt     vmcnt(0)                          
  v_mul_f32     v0, s3, v0                        
  v_mul_f32     v1, s3, v1                        
  v_mul_f32     v2, s3, v2                        
  v_mul_f32     v3, s3, v3 

이것은 텍스쳐 읽기의 결과에 다른 상수 값(data.w)를 곱하기 위한 코드의 두번째 분기입니다. v_mul_f32 명령어를 보면, 그것은 스칼라와 벡터를 직접 곱할 수 있습니다, 기반 하드웨어에 대한 또다른 정보입니다. 비록 float4 곱셈이 HLSL 과 중간 언어(Intermediate Language)에서 한개의 명령어로 보이지만, GCN의 디자인에 따라서 실제로는 쉐이더 어셈블리에서 4개의 명령어 입니다(스칼라 아키텍스쳐와 관련있습니다만 나는 스칼라 명령어와 레지스터와 혼동하고 싶지 않습니다, 수많은 온라인 자료가 온라인에 있습니다. 그것들은 어떻게 그것들이 작동하는지 더 자세히 설명합니다.)

label_001C:
  s_mov_b64     exec, s[22:23]                    
  v_cvt_pkrtz_f16_f32  v0, v0, v1                 
  v_cvt_pkrtz_f16_f32  v1, v2, v3                 
  exp           mrt0, v0, v0, v1, v1 done compr vm
s_endpgm 

쉐이더의 실행의 분석 끝나가고 있습니다, 남은 것은 결과를 기록하는 것입니다. 모든 요청된 thread 가 결과를 기록하게 하기 위해서, 코드에서는 execution mask의 값을 다시 저장하는 것에서 시작합니다, 프로그램 시작에서 스칼라 s22와 s23에 저장된것. 출력(float4 한개)은 현재 벡터 레지스터 v0-v3에 저장됨. v_cvt_pkrtz_f16_f32 명령어는 2개의 float32 (즉, v0과 v1)를 한개의 float32 벡터 레지스터로 압축합니다. 이것은 두쌍 (v0, v1 과 v2, v3)에 대해서 2번 수행됩니다. 결국 우리는 메모리 대역폭을 줄이기 위해 원본 float4 출력을 압축된 형태로 된 두 백터 레지스터 v0과 v1을 갖습니다. 최종적으로, exp 명령어는 출력을 바인딩 된 렌더타겟에 복사하게 합니다. mrt0 인자의 의미는 여러 렌더타겟인 경우(최대 8개의 렌더타겟이 픽셀쉐이더의 출력에 바인딩 될 수 있음) 이 명령어의 타겟을 첫번째 렌더타겟으로 한다는 것입니다, 다음은 압축된 출력을 가진 벡터 레지스터들 입니다, done 의미는 쉐이더에서 마지막 출력입니다, compr는 데이터가 압축된 형태로 있다는 의미이고 execution mask를 가리키은 vm 플래그는 컬러 버퍼에 어떤 픽셀이 유효하고 무시(discarded)될지 알립니다. 이것으로, 쉐이더의 실행은 중단됩니다.

 

나는 일찍이 어떻게 쉐이더 어셈블리를 읽고 이해하는지가 쉐이더를 작성할때 더 나은 결정을 만드는데 도움을 줄 수 있다고 언급했습니다. 비록 내가 제공한 쉐이더 예제가 아주 간단하고 유용하진 않지만, 제공된 코드로 컴파일러가 무엇을 하는지 내부 지식 없이는 개선하기 어렵습니다. 예제의 쉐이더 코드를 약간 바꿔 if-statement 분기 밖으로 return;을 빼보면 컴파일러가 분기를 어셈블리 코드에서 완전히 제거합니다, 4개의 v_cndmask_b32를 사용하여 v_cmp_lt_f32 명령어 비교의 결과로 만들어진 출력 값을 선택합니다.

float4 PSMain(PSInput input) : SV_TARGET
{
    float4 result = tex.Sample(samplerLinear, input.uv); 
     
    float factor = data.x * data.y;
     
    if( factor > 0 )
        result *= data.z; 
    else
        result *= data.w;
     
    return result;  
}

쉐이더 어셈블리:

s_mov_b32     m0, s20                    
  s_mov_b64     s[22:23], exec           
  s_wqm_b64     exec, exec               
  v_interp_p1_f32  v2, v0, attr0.x       
  v_interp_p1_f32  v3, v0, attr0.y       
  v_interp_p2_f32  v2, v1, attr0.x       
  v_interp_p2_f32  v3, v1, attr0.y       
  s_and_b64     exec, exec, s[22:23]     
  image_sample  v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf 
  s_buffer_load_dwordx4  s[0:3], s[16:19], 0x00        
  s_waitcnt     lgkmcnt(0)                             
  v_mov_b32     v4, s1                                 
  v_mul_f32     v4, s0, v4                             
  v_cmp_lt_f32  vcc, 0, v4                             
  s_waitcnt     vmcnt(0)                               
  v_mul_f32     v4, s2, v0                             
  v_mul_f32     v5, s2, v1                             
  v_mul_f32     v6, s2, v2                             
  v_mul_f32     v7, s2, v3                             
  v_mul_f32     v0, s3, v0                             
  v_mul_f32     v1, s3, v1                             
  v_mul_f32     v2, s3, v2                             
  v_mul_f32     v3, s3, v3                             
  v_cndmask_b32  v0, v0, v4, vcc                       
  v_cndmask_b32  v1, v1, v5, vcc                       
  v_cndmask_b32  v2, v2, v6, vcc                       
  v_cndmask_b32  v3, v3, v7, vcc                       
  s_mov_b64     exec, s[22:23]                         
  v_cvt_pkrtz_f16_f32  v0, v0, v1                      
  v_cvt_pkrtz_f16_f32  v1, v2, v3                      
  exp           mrt0, v0, v0, v1, v1 done compr vm     
  s_endpgm  

비록 이 경우에 많은 성능 차이는 없을 것입니다. 왜냐하면 모든 스레드가 항상 한개의 if-statement 분기를 따르기 때문입니다, 그리고 만약 어떤 것이든 벡터 레지스터의 사용을 높이는 것은 좋지 않을 것입니다, 이것은 다른 시나리오에 적용하기에 좋은 지식입니다. 결론은 HLSL의 변경이 최종 쉐이더 어셈블리에 어떻게 영향을 미치는지 검사해보지 않고는 쉽게 예측할 수 없다는 것입니다.

 

이 쉐이더 어셈블리 분석에 사용한 대부분의 정보는 Vega’s ISA reference documentation 에 있습니다. 만약 당신이 더 배우는데 관심이 있다면, 다른 GPU 또한, low-level GPU guides 의 많은 자료들이 연구할만한 가치가 있습니다. 그리고 Emil Persson의 excellent presentations가 low level shader 최적화도 있습니다.

반응형