본문 바로가기

GPU

Shader Programs 최적화

https://developer.nvidia.com/docs/drive/drive-os/6.0.6/public/drive-os-linux-sdk/common/topics/graphics_content/ShaderPrograms72.html?hl=shader 
 

Shader Programs | NVIDIA Docs

Writing efficient shaders is critical to achieving good performance. One should treat shaders like pieces of code that run in the inner-most loops on a CPU. There is a very high cost to littering these with conditionals or recomputing loop invariants. Befo

developer.nvidia.com

 

  • 셰이더를 cpu에서 가장 최대한으로 돌아가는 loop처럼 다루어야 한다. 조건부 또는 루프 불변량을 재계산하여 이들을 어지럽히는 데는 매우 높은 비용이 든다.
  • 값비싼 버텍스 셰이더를 최적화하기 전에 완전히 view frustum 외부에 있는 지오메트리가 CPU에서 추려지고 있는지 확인하라 - 값비싼 프래그먼트 셰이더를 최적화하기 전에 애플리케이션이 과도한 수의 프래그먼트를 생성하지 않는지 확인해야 한다.

셰이더를 최적화 할 때, 아웃풋에 기여하지 않는 소스 코드들은 컴파일러가 자동으로 제외시킨다. 이 기능을 이용하면, 출력 변수에 null vector를 곱해서 셰이더가 현재 병목 현상의 일부인지 여부에 대한 지식을 얻은 다음, 프레임 속도가 개선되는 지 측정할 수 있다. 반대로, 최적화의 마지막 단계에서, 셰이더 유닛 쪽으로 계산을 나누어 줄 수 있는 여지가 있는지, 또는 의미 없지만 비싼 ALU 인스트럭션을 추가거나 텍스처 샘플링을 추가해서 이미지 품질을 높일 수 있는지 측정할 수 있다.

 

계산을 파이프라인 위쪽으로 이동하자 [P1]

렌더링 파이프라인이 CPU 에서 Vertex Processor로, 그리고 Fragment processor로 진행됨에 따라, 필요한 workload는 정새진 규칙에 따라 증가하는 경향이 있다. 각 모델, 각 프리미티브, 또는 각 버텍스에 대해 일정한(constant) 연산은 fragnemt processsor에 포함되지 않고, vertex shader 또는 그보다 윗 단계로 옮겨져야 한다. 드로우 콜 당 연산은 vertex processor 대신 CPU로 옮겨져야 한다. 예를 들어, 라이팅이 eye-space로 변환되어야 한다면, 각 fragment나 vertex 단위로 반복하는 게 아니라 uniform에 저장되어야 한다. light vector는 원래 normalized 되기 전에 저장된다. 대부분 light vector 연산은 드로우 콜마다 일정하기 때문에, 어느 셰이더에도 포함되지 않는다.

 

크거나 일반화된(generalized) 셰이더를 작성하지 말라 [P2]

constant 변수 값에 따라 다른 분기를 타는 셰이더를 작성하려는 유혹이 있을 것이다. Uniform은 하나의 (잘하면 여러 개의) 프리미티브에 걸쳐서 상수 값으로 사용된다. Uniform은 UseProgram 의 대체재가 아니다. 셰이더는 그들이 수행하는 태스크에 대해서 특화되고, 미니멀해야 한다. 빠르게 실행되는 작은 셰이더를 여러 개 사용하는 것이 더 적은 수의 무거운 셰이더를 돌리는 것 보다 낫다. (Source shader가 지원된다면) 코드를 재사용하는 것은 어플리케이션 레벨에서 ShaderResource 함수로 다루어야 한다. 만약 범용적인 셰이더를 작성하지 말라는 이 조언이 셰이더와 state 변환을 간소화하는 것과 상충된다면, 더 작고 더 특수화된 셰이더를 사용하는 것이 일반적으로 우선된다. 추가로, 최종 셰이더 소스 코드로 연결되도록 의도한 shader function을 작성할 때는 주의하여야 한다 -

 

어플리케이션 특화된 지식을 잘 활용하라 [P3]

연산을 단순화하거나 피하는 데에 어플리케이션 지식을 활용할 수 있다. 빠른 계산(Math Shortcut)은 어느 곳에서든 추구해야 한다 - 셰이더 컴파일러나 GPU가 할 수 없는 최적화가 있기 때문이다. 예를 들어, screen에 정렬된 프리미티브를 렌더링 하는 것은 2D 유저 인터페이스와 포스트 프로세스 이펙트에서는 일반적이다. 이 경우, 모든 버텍스를 NDC(Normalized Device Coordinates)에서 정의하면, modelView 변환 전체를 생략할 수 있다. Full-screen quad는 [-1.0, 1.0] 범우에 버텍스 좌표를 가지기 때문에, 이것을 버텍스 어트리뷰트로부터 gl_Position 으로 바로 넘겨줄 수 있다. 어플리케이션 안에서 modelView 매트릭스를 만들 때 적용된 매트릭스 변환 종류들을 추적해서 활용하면 좋다. 예를 들어, non-uniform scaling이 없는 직교 정규화 행렬은 inverse-transpose sub매트릭스로 법선을 변환할 때 계산을 피할 수 있는 기회를 제공한다.

 

깊이, 스텐실 culling을 최적화하라 [P4]

GPU는 깊이 혹은 스텐실 테스팅에 기반해, 픽셀 셰이더가 실행되기 전에 fragment를 빠르게 reject 할 수 있다. 장면의 깊이 복잡도는 각 fragment가 기록되는(쓰여지는) 횟수이다. 깊이 복잡도는 stencil buffer 값을 늘려서 측정할 수 있다. 3D 장면의 깊이 복잡도가 너무 높으면, 불투명한 오부젝트를 최적의 순서가 아닌 순서로 렌더링 하게될 수 있다. 가장 안좋은 상황은 뒤에서 앞 (painter’s algorithm으로 알려진) 순서로 렌더링할 때인데, 많은 양의 fragment가 오버드로우 되기 때문이다. 깊이 복잡도가 높은 어플리케이션은 불투명 오브젝트가 깊이 테스트가 활성화 된 채로 앞에서 뒤로 가는 순서로 렌더링되는 것이 확실시되어야 한다. 2D 유저 인터페이스를 직접 렌더링하는 것 또한 깊이 복잡도를 증가시키지만, 역시 같은 방식으로 다시 감소시킬 수도 있고, 스텐실 버퍼를 사용해서 fragment들을 마스킹 할 수도 있다. fragment가 아주 제한된 어플리케이션은 이러한 방식들을 잘 활용하면 속도를 10배 또는 그 이상으로 크게 향상시킬 수 있다.

만약 버텍스 처리가 병목이 아니라면, 첫 번째 패스에서 뎁스 버퍼를 미리 준비하는 실험을 실행하는 것은 가치가 있다. 모든 color 쓰기를 첫 패스에서 ColorMask로 비활성화 한다. depth buffer의 fragmet들은 두 번째 패스에서 color write가 활성화 되어 값비싼 fragment 셰이더가 실행될 때, occluder로 작동할 수 있다. depth write를 두 번째 pass에서는 DepthMask로 비활성화 시켜라 - 깊이를 두 번 그릴 필요는 없기 때문이다.

 

꼭 필요한 경우가 아니라면, fragment를 discard하거나, depth를 변경하지 말어라, [P5]

일부 작업은 하드웨어가 파이프라인에서 fragment를 미리 reject하는 자동 최적화를 활성화하지 못하게 한다(early-Z). 특히, 몇 가지 criteria로 fragment들에 대해 discard 작업을 해버리면, early-Z가 몇몇 플랫폼에서 비활성화된다.depth writing이 비뢍성회되지 않았다면, discard 사용을 최대한 자제하는 것이 좋다(특히 alpha-testing). 또 다흔 예시는, 몇몇 플랫폼에서 사용 가능한 GL_NV_fragdepth extension이다 - 픽셀 셰이더에서 depth 값을 기록할 수 있다. 이 작업은 올바른 렌더링을 위해 GPU가 early-z를 강제로 reject 하게 만든다.

 

가능하면 셰이더에서는 조건문을 피하자 [P6]

fragment들은 chunk 단위로 처리되기 때문에, false 분기가 GPU에 의해 discard 되기 전에, 조건문의 양쪽 브랜치가 모두 evaluate 될 수 있다. 조건문이 연산을 생략해서 작업 부하를 줄여줄 것이라고 예상하지 않아야 한다. 이것은 특히 fragment 셰이더의 경우와 연관된다. 셰이더를 벤치마킹 해 보면, 버텍스 셰이더 또는 fragment 셰이더의 조건문이 실제로 작업 부하를 줄여주는 지 알 수 있다.어떤 조건문 코드는 대수(algebra)나 빌트인 함수로 대치될 수 있다. 예를 들어, 노멀과 빛 방향 벡터의 내적이 음수일 경우ㅡ 라이트 계산 결과가 필요 없다. 이 경우에는

if (nDotL > 0.0) ...

이렇게 쓰는 대신,

clamp(nDotL, 0.0, 1.0)

라고 써서, 결과값에 무조건 사용되도록 하자(음수 값은 0을 내보내게 된다). clamp는 0.0과 1.0인 경우 min, max보다 빠를 수 있지만, 언제나 그렇듯 벤치마킹을 하면 최종적으로 사실 확인을 할 수 있다. fragment 셰이더에서 조건문을 피할 수 있는 또다른 방법은 런타임 값에 따라 block statement로 실행되엇을 때 mipmap 된 텍스처가 undefined를 반환하는 것이다.

textureglsl 함수를 mipmap LOD에 bias를 주거나, mipmap LOD를 특정할 때 사용할 수 있지만, 수동으로 mipmap LOD를 도출해 내는 것은 비싸다. 게다가, 이 LOD Biasing이 되는 샘플러들은 LOD가 없는 샘플러들보다 빠르게 동작하지 않을 수 있다.

 

적절한 정밀도 qualifier를 사용하라. [P7]

버텍스 셰이더의 기본 정밀도가 highp임을 기억하자. 반면 fragment셰이더는 명시적으로 지정하지 않는 한, 기본 정밀도가 없다. 정밀도 한정자(precision qualifier)는 컴파일러에게 레지스터 부담을 줄이고, 몇 가지 이유로 퍼포먼스를 향상시킬 수 있다. 낮은 정밀도는 높은 정밀도에 비해 두 배 빠르게 실행될 수 있다. 이런 최적화는 처음에 highp를 사용하고, 렌더링 아티팩트가 나타날 때 까지 점차적으로 lowp로 정밀도를 낮춰 보는 방법으로 접근할 수 있다. 만약 적당히 좋아 보인다면, 충분히 최적화 된 것이다. 사용하기 좋은 기준으로, 버텍스 위치(vertex position)과 exponential, 삼각함수는 highp가 필요하다. 텍스처 좌표는 텍스처의 width와 height에 따라 lowp, highp 어떤 것이든 될 수 있다. 어플리케이션 단에서 정의된 많은 uniform 변수, 보간된 색상, 노멀과 텍스처 샘플들은 lowp를 사용해 표현할 수 있다. 하지만, floating point 텍스처 샘플러는 low precision 보다 정밀도가 더 필요하다 - 이것은 floating point 텍스처 사용을 최소화 해야 하는 몇 가지 이유 중 하나이다.

 

내장 함수와 변수를 사용하자. [P8]

내장 함수 혹은 변수들은 컴파일 과정이 최적화 되어 있을 확률이 놓고, 하드웨어 단에서 구현되어 있을 수도 있다. 예슷 들어, 프리미티브 면의 앞 면을 구분하거나 반사 벡터를 내적이나 대수를 사용해 연산하는 셰이더 코드를 짜지 말도록 한다 : 대신, gl_FrontFacing 또는 reflect 내장 함수를 사용하도록 한다.

 

텍스처에 복잡한 함수들은 encoding하는 것을 고려하자. [P9]

셰이더에는 산수[ALU] 와 텍스처 연산을 모두 포함하고 있다. ALU 작업 배칭(Batching)은 병렬로 일어나기 때문에, 텍스처에서 샘플을 fetch 해 오는 데 드는 레이턴시를 감출 수 있다 만약 셰이더가 주된 병목이라면, 그리고 ALU 작업이 텍스처 작업보다 특별히 많은 경우, 이 작업들이 텍스처에 인코딩 될 수 있는지 살펴볼 가치가 있다. 셰이더의 sub expression들을 LUT (Look-Up Tables) 에 저장할 수도 있다. LUT는 충분한 precision으로 1D나 2D 텍스처로 구현되고, Nearest filtering으로 접근할 수 있다.

 

💡 vector를 normalize 할 때 cubemap을 이용하는 오랜 트릭은, 분리된 GPU(discrete GPU) 들에서는 퍼포먼스를 저하시킨다. 만약 이 아이디어를 고수하고자 한다면, 퍼포먼스를 저하시켰는지 향상시켰는지 꼭 벤치마크 해 보길 바란다.

 

 

 

(작성중)

'GPU' 카테고리의 다른 글

Ep 2.5 Content best practices  (0) 2024.05.26
Ep 2.4 : Engine and API best practices  (1) 2024.04.15
Ep 2.2 : Best practice principles  (1) 2024.04.08
Unreal에서의 GPU Crash Debugging  (1) 2023.12.17