Shader Graph / 머티리얼 그래프도 결국 HLSL로 컴파일되기 때문에, 같은 수식/로직이면 성능은 거의 동일하다
반복되는 연산을 공통 서브식으로 묶어줄 수 있는데, 그래프에서는 의도대로 CSE가 안 되거나 구조가 지저분해지기 쉽습니다
CSE는 “Common Subexpression Elimination(공통 부분식 제거)”
코드 안에서 똑같은 계산을 여러 번 하는 부분을 찾아서
한 번만 계산하고, 그 결과를 변수에 저장해두고, 이후에는 그 변수만 쓰도록 바꿔주는 최적화
float a = x y + 3;
float b = x y + 5;
CSE를 하면
float t = x * y;
float a = t + 3;
float b = t + 5;
HLSL 소스 수준에서 보면 x * y 같은 공통 수식을 잘 구조화해주면, 컴파일러가 CSE를 적용하거나, 적어도 우리가 직접 중간 변수로 빼서 연산 횟수를 줄일 수 있음.
일부 노드 그래프(특히 옛날 버전/엔진)는 동적 루프, 복잡한 if/else를 제대로 지원하지 못해서, 편법으로 풀어쓰며 인스트럭션 수가 불필요하게 많아지는 경우가 있습니다. Shader Graph의 Branch 노드 역시 겉으로는 분기처럼 보이지만, 내부적으로는 True/False 양쪽 입력을 모두 평가한 뒤 조건에 따라 결과만 선택하는 형태로 동작합니다. 이 말은, 실제로는 lerp나 삼항 연산자에 가까운 패턴이라, 한쪽 경로만 필요하더라도 양쪽 경로의 연산이 모두 수행된다는 뜻입니다.
오래된 엔진이나 구현체에서 루프를 표현하려면 동일한 노드 블록을 여러 번 복사해서 수동으로 펼치는 방식(loop unrolling)을 쓸 수밖에 없습니다. 예를 들어 반복 5회짜리 연산을 노드로 표현하면 같은 노드 묶음이 5벌 복제되어 그래프에 깔리고, 컴파일된 셰이더 인스트럭션도 그대로 5배로 늘어납니다. 분기도 마찬가지로, step, lerp 등으로 우회해서 양쪽 경로를 모두 실행한 뒤 결과를 섞는 방식으로 처리하는 경우가 많습니다. 그 결과 실제로 한쪽 경로만 필요한 상황에서도 불필요한 연산이 전부 수행됩니다.
반면 HLSL에서는 for, while로 실제 동적 루프를 작성하고, if 조건으로 불필요한 경로를 건너뛰는 early-out(return, break, clip)을 직접 걸 수 있습니다. 컴파일러는 이러한 분기를 GPU 하드웨어의 분기 인스트럭션(또는 static branch 최적화)으로 변환할 수 있기 때문에, 인스트럭션 수가 줄고 실제 실행 경로도 짧아질 여지가 생깁니다. 다만 GPU는 워프(warp)나 웨이브(wave) 단위로 실행하기 때문에, 픽셀마다 분기 결과가 다르면 양쪽 경로를 모두 수행하는 divergence 문제가 발생할 수 있습니다. 그래서 HLSL로 작성하더라도 분기 조건이 uniform(전체 픽셀에 동일)할수록 early-out이나 동적 분기의 실질적인 이득이 커집니다.