문법 및 메모리 관련

[GPU병렬 처리] 쉐이더코드에서 if-else문을 안 쓰는 이유

doyyy_0 2024. 11. 30. 11:03

평소 CPU코딩에만 익숙해져있다면, 다음과 같은 코드를 보고 굉장히 비효율적이라고 생각할 수 있습니다.

[unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
for (i = 0; i < NUM_DIR_LIGHTS; ++i)
{
    color += ComputeDirectionalLight(lights[i], material, input.normalWorld, toEye);
}

[unroll]
for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
{
    color += ComputePointLight(lights[i], material, input.posWorld, input.normalWorld, toEye);
}

[unroll]
for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
{
    color += ComputeSpotLight(lights[i], material, input.posWorld, input.normalWorld, toEye);
}

 

hlsl 픽셀세이더에서는 GPU병렬처리를 이용하기 때문에 if-else문을 사용한다고 해서 조건에 참인 값만 계산되는 것이 아닙니다. 비트 마스킹을 이용하기 때문이죠.

예를들어 이 코드는

float4 color = // some color;
if (someValue > 0.5)
    color = ExpensiveCalculation(); // 비싼 계산

 

실제로 이 코드로 실행이 됩니다.

float4 color = // some color;
float4 temp = ExpensiveCalculation();  // 조건에 관계없이 항상 계산됨
color = (someValue > 0.5) * temp + (1 - (someValue > 0.5)) * color;

 조건에 상관없이 실행이 되는 모습이죠.  

 

하지만 병렬처리를 사용한다면, if-else문에서의 분기로 인해 성능 저하가 발생할 수 있습니다. 그렇다면 맨 위의 코드에서 반복문은 분기가 발생할까요? 그렇지 않습니다. 왜냐하면 반복문의 형태를 띄고있는 코드일 뿐이지, 사실 반복적으로 코드를 나열한 것과 같기 때문이죠.

[unroll]
for (int i = 0; i < 4; i++)
    foo += bar[i];
foo += bar[0];
foo += bar[1];
foo += bar[2];
foo += bar[3];

이렇게 그냥 코드 나열입니다. 이러한 방식으로 불필요한 인덱스 계산과 반복문이 한번 돌 때마다 조건을 체크할 필요가 없어집니다. 

 

하지만 [unroll] 사용 시 반복문의 횟수를 정확히 명시해줘야합니다. 왜냐하면 [unroll]에서 반복 횟수를 명시하지 않으면, 컴파일러는 가능한 최적의 반복 횟수를 추측하려고 시도하기 때문에 불필요하게 반복횟수를 많이 계산할 수 있기 때문입니다.

 

더 자세하게 내용을 이해하고 싶다면 다음 Unity Forum을 참고해주세요

https://discussions.unity.com/t/what-are-unroll-and-loop-when-to-use-them/881945/3