컴퓨터의 GPU는 CPU처럼 개별로 돌지 않고 여러 쓰레드(코어)가 묶음이 되어 하나의 명령을 수행한다. 일반적으로 32개 혹은 64개인데, 묶음으로 연산할 때 분기 발산이라는 문제를 마주할 수 있다. 해당 문제는 if/else
문을 셰이더 코딩에 포함했을 때 맞닥뜨릴 수 있는 문제이다.
예를 들어 다음과 같은 예시 코드가 있다.
if (condition)
{
pixelColor = SkinShading();
}
else
{
pixelColor = BackgroundShading();
}
condition
에 따라 해당 픽셀이 캐릭터 오브젝트의 피부인지 배경인지 검사한다. 이때 한 픽셀에 둘이 공존하는 경우는 없다. GPU에선 여러 개의 쓰레드 묶음이 동시에 위와 같은 명령을 처리하는데, 분기 발산 상황에서 두 분기에 대한 명령을 모두 처리하고 필요한 값만 취한다.
즉, if를 만나 필요한 부분만 코드를 실행하는 것이 아니라, 일단 코드를 실행하고 결과를 반환한 후에 조건을 검사하는 것이다. 따라서 SkinShading()
과 BackgroundShading()
이라는 함수 계산을 모두 끝내고 condition
을 만족하는 결과만 pixelColor
에 저장하고 만족하지 않은 값의 계산 결과는 버린다.
필요하지 않은 코드까지 실행하는 상황이 발생하게 되는 것이다. 최신 하드웨어에서도 이러한 분기 발산은 확인되고 있다.
이러한 문제가 발생하는 이유는 condition
이 true
와 false
두 값을 모두 가지기 때문이다.
앞서 말한 것처럼 분기 발산은 동시에 실행하는 쓰레드에서 조건 분기에 사용하는 condition
이 여러 값을 가지기 때문이다. condition
의 모든 값이 true
거나 모두 false
라면 GPU에서 특정 브랜치의 명령만 수행해도 된다는 것을 런타임에 알 수 있어 필요하지 않은 브랜치의 작업은 수행하지 않는다. 이러한 최적화 기법을 다이나믹 브랜칭이라고 하며, 이렇게 동일한 분기로 처리될 때 응집성(coherence)이 좋다고 말한다.
그나마 위의 코드는 응집성이 좋다면 다이나믹 브랜칭이 가능하지만,
Color skinColor = SkinShading();
Color backgroundColor = BackgroundShading();
if (condition)
{
pixelColor = skinColor;
}
else
{
pixelColor = backgroundColor;
}
처럼 코드를 작성하였다면 다이나믹 브랜칭조차도 불가능한 아주 답도 없는 상황이 된다. 이러한 코드 작성을 피해야 한다.
만약 두 셰이딩 함수에서 공통된 계산이 있다면, 함수 안에서 하지 않고 밖에서 처리하도록 빼주면 비교적 연산량을 줄여줄 수 있다.
언리얼에선 Shader permutation, 유니티에선 Shader variants라고도 하는데, 셰이더 코드 안에서 #define
으로 하나의 코드를 여러 코드로 분리하는 것으로 미약한 성능 향상을 기대할 수 있다.
셰이더 프로그래밍에서 if 조건문에 의한 분기가 비싸다는 것은 위와 같은 이유이다. 그 이유를 알고 시스템을 이해할 필요가 있다.