원문

https://youtu.be/KCL9xiN_UbM

 

개요

Mali GPU에서 더 나은 성능을 낼 수 있는 쉐이더 프로그래밍 Best Practices 소개

 

 

 

1. Use lower precision - 낮은 정밀도 사용

Mediump 또는 Vulkan Relaxed Precision을 사용하는 것을 권장. 

(중간 정밀도를 의미하며 HLSL에서 half가 mediump에 가장 가까운 자료형.)

 

중간 정밀도는 16비트 자료형 및 정수 유형에 매핑되고 사용은 에너지 효율이 더 좋고 더 작은 레지스터(메모리)를 사용하고 연산이 빠르다, Mail GPU에서 16비트 Vector2 2개를 연산하는 속도와 32비트 연산과 동일한 속도로 처리된다.

(GPU API마다 특정 조건에서 16비트가 더 낮은 퍼포먼스가 나오는 경우도 있음)

 

중간 정밀도 자료형의 단점은 동적 범위와 정밀도에서 제한이 있다.

(최대 & 최소 값과 소수점 길이 제한이 있다는 뜻)

일반적으로 월드 공간에서 위치나 거리를 계산하는 공간 연산, 그림자 맵핑 & 뎁스 맵핑과 같은 샘플링 & 텍스처 샘플링에 사용되는 UV 좌표에는 32비트 보간자를 권장한다.

(추가로 _Time변수와 연산하는 변수에도 32비트 권장)

 

형변환 주의, 이것은 공짜가 아니다.(퍼포먼스 공짜가 아니다)

 

 

2. 부동소수점 연산 주의 (부동소수점 = float, half와 같은 자료형)

1번 코드에서 2번 코드로 곱연산을 1번으로 줄여 최적화한것 처럼 보이겠지만, 부동소수점을 사용하고 있다면 컴파일러가 에러가 발생할 수 있다.

예시로 위와 같이 a와 b에 부동 소수점의 최대값으로 세팅한다면, 우측 최적화 시도한 코드에서 덧셈이 먼저 진행되어 부동소수점이 초과되는 이슈가 발생한다.

(이러한 경우 GPU마다 다르겠지만 대체로 큰 퍼포먼스 저하와 무한대 값이 들어가 픽셀 이상 이슈 발생)

 

 

 

3. 내장 함수를 사용하자

쉐이더 언어에는 내장 함수 및 대규모 내장 함수 라이브러리가 있다. 
(ESSL = OpenGLES Shading Language, ESSL 뿐만 아니라 HLSL와 같이 각 쉐이더 언어마다 비슷하게 내장 함수 있음)

쉐이더 언어에서 제공하는 내장 함수를 사용하는 것이 직접 구현하는 것보다 거의 항상 퍼포먼스적으로 효율적이다.

왜냐하면 하드웨어에 의해 지원되거나 특정 GPU 버전에 대한 핸드 튜닝했기 때문이다.

 

대부분 고정밀도 인자(inputs)를 사용하는 함수는 비싸다.

(void function(float a) <- float a을 의미)

내장 함수 중에 같은 함수라도 중간 정밀도를 사용하는 함수가 더 빠르기 때문에 권장한다.

 

아래 내장 함수들은 퍼포먼스가 무거우니 피하도록 노력해라 - 삼각함수 계열(sin, cos, tan), Atomics, textureGrad

 

 

 

4. 유니폼 계산은 CPU에서 처리 

유니폼 연산은 드로우 콜이나 컴퓨트 디스패치의 모든 스레드에 대해 동일한 결과를 내는 연산을 의미한다. 이러한 경우 CPU에서 연산을 처리하는 것을 권장한다.

Mali GPU에서 이러한 연산에 대해서 어느정도 최적화 할 수 있지만, 완전 최적화할 수 없다.

(예 : 오브젝트 Pivot과 카메라와 거리 계산은 모든 픽셀의 결과 값이 동일하기 때문에 CPU에서 처리하는 것이 효율적임)

 

 

5. 쉐이더 베리에이션 관련 

쉐이더 사용 용도에 맞게 Specailization(특화)해라.

(용도 외에 쓸데없는 기능 빼라)

복잡한 우버 쉐이더 1개보다 특정 용도로 특화된 쉐이더를 사용하는 것을 권장한다.

 

특화 쉐이더는 컴파일 시간 단축에 효과적이다.

- 런타임 쉐이더 변경은 부하가 있다.

특화 방식

- if-else 조건문을 특수화하면 쉐이더에서 중복 연산을 방지할 수 있다. 
  (if else쓰지 말라는 뜻)

- for문 같은 반복문은 unroll 구문과 같은 고정 반복문으로 변경

- 유니폼 함수에서 절대 변동되지 않는 상수를 반환하게 하면 컴파일 시간 단

- 단인 일스턴스를 드로우 할때 인스턴싱을 사용하지 않는 것이 좋음.
   (예 : GPU Instancing)

 

결론 = 단일 기능이 있는 특화 쉐이더와 여러 기능이 포함된 쉐이더를 만들어 관리가 용의하게 하는 것은 프로젝트 및 개발자에 맞게 균형을 잡고 개발하는 것이 중요.

 

 

6. 브렌치 구문 주의점 (if-else)

 

이전 세대 Mali GPU 아키텍처인 Utgard/Midgard에서는 분기문을 사용하면 SIMD 처리 방식에서 연산 그룹이 더 잘게 분할되어 비효율적으로 동작한다.

최신 세대 Mali GPU 아키텍처인 Bifrost / Valhall에서는 분기문은 상대적으로 가볍다.

 

- SIMD : Single Instruction, Multiple Data의 약자로 한번에 여러 데이터를 동시에 연산하는 처리 방식의 의미.

- Utgard/Midgard : Utgard는 OpenGLES 2.0까지 지원, Midgard는 OpenGLES 3.1, Vulkan 1.0까지 지원.

- Bifrost / Valhall : 가장 최신 Mali 아키텍처로 OpenGLES 3.2, Vulkan 1.1 이상 지원

 

요약 : 최신 Mali GPU에서는 분기문이 더 가볍다.

 

 

일정 거리 이상의 픽셀에서 라이팅 연산을 하지 않는 조건문을 예시로 모든 라이팅 연산이 진행되지 않을 경우 5 사이클이지만, 라이팅 연산이 모두 진행하면 13사이클이 나왔다.

만약 조건문을 제거하여 항상 라이팅 연산을 진행하게 할 경우, 최소 사이클은 조금 늘어나지만, 최대 사이클은 약 2배 감소되었다.

결과적으로 위 예시와 같은 경우 조건문이 없는게 더 나은 상황이다.

 

조건문은 상황에 따라 좋을 수도 나쁠 수 도있으며 조건문 내부 코드는 심플할 수록 좋다.

 

 

7. Vertex inputs 변수 패킹

버텍스 쉐이더에서 Vector4를 로드하는 것이 Vector2 2개를 로드하는 것보다 효율적이다.

(Vertex 쉐이더에서 Input & Output 구조체를 의미하는 것으로 추측)

프레그먼트 쉐이더에서 단일 Vector4 보간하는 것이 Vector3 + float을 보간하는 것보다 효율적이다.

(? Interpolator-보간이 무엇을 의미하는지 모름)

 

 

8. ZS Test 관련 주의점 (Z Buffer & Stencil Test)

Early ZS 테스트 혹은 숨겨진 표면 제거를 이용해 가능한 많은 중복 오버드로를 제거하는 것이 성능 향상에 좋다.

 

(번역 그대로 작성, 정확한 내용 정리 후 추후 다시 정리)

일부 셰이더 기능은 상태가 셰이더가 실행될 때까지 알려지지 않기 때문에 이러한 최적화를 비활성화할 수 있습니다.
discard 문을 사용하면 조기 ZS 테스팅은 가능하지만, 조기 ZS 업데이트와 숨겨진 표면 제거는 비활성화됩니다.
프로그래마틱 깊이 쓰기를 사용하면 모든 조기 ZS와 숨겨진 표면 제거가 비활성화됩니다.
프레임 버퍼에서 프로그래마틱 깊이 읽기를 사용하면 조기 ZS 테스팅은 가능하지만, 조기 ZS 업데이트와 숨겨진 표면 제거는 비활성화됩니다.
프레임 버퍼 또는 픽셀 로컬 스토리지 구조에서 프로그래마틱 색상 읽기를 사용하면 숨겨진 표면 제거가 비활성화됩니다.
이러한 조기 ZS 작업을 수행하는 콘텐츠의 경우, 업데이트가 필요하지 않은 경우 깊이 테스팅을 비활성화하거나 깊이 쓰기를 마스킹하는 것이 도움이 될 수 있습니다. 이는 성능을 향상시킬 수 있습니다.
또한 셰이더에 layout early_fragment_tests를 추가하는 것이 가능하며, 이는 조기 테스팅을 사용하는 것이 안전하다는 것을 컴파일러에 알려주는 역할을 합니다. 이는 셰이더가 위에서 언급한 작업 중 하나를 사용하더라도 적용됩니다.
이 내용은 그래픽스 렌더링에서 중요한 최적화 기법 중 하나인 조기 ZS 테스팅 또는 숨겨진 표면 제거에 대한 설명입니다. 이를 통해 불필요한 렌더링 작업을 최소화하고 성능을 향상시킬 수 있습니다.

 

 

9. Compute Shader 개발 고려 사항(가이드)

쓰레드 그룹 사이즈는 적당하게 세팅해야하며 보편적인 사이즈는 64다.

최적 쓰레드 그룹 사이즈는 GPU마다 다르기 때문에 게임 최초 실행시 벤치마크를 통해 적합한 크기를 찾는게 좋다.

 

쓰레드 공유 메모리는 쓰레드간에 데이터를 공유하는 역할을 하는데 일부 GPU에서는 성능을 높이기 위해서 메인 메모리에서 전용 RAM으로 데이터를 복사한다. 하지만 Mali GPU는 이에 해당되지 않는다.

전역 메모리에서 쓰레드 로컬 메모리로 복사하면 캐시가 오염되기 떄문에 실제 성능이 느려진다.

따라서 알고리즘 데이터 공유에만 작업 그룹 로컬 메모리를 사용해라.

(Mali GPU에서 쓰레드 공유 메모리 사용시 성능 하락한다는 의미)

 


WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,