'무쓸모 if'로 CPU 분기 예측을 조작해 성능을 4배 높인 비결
컴파일러 최적화의 함정: 루프 내 종속성으로 인한 지연 시간(Latency) 문제 발생
CPU 분기 예측 활용: '무쓸모 if' 문으로 예측 실패를 유도, 성능 병목 해소
`volatile` 키워드 사용: 컴파일러가 종속성을 무시하도록 강제하는 트릭
실질적 성능 향상: 합성 벤치마크에서 4배, 실제 실험에서 2배 속도 증가 확인
루프 내 종속성으로 인한 지연 시간 병목
본문에서는 도메인 특화 압축기(Domain-Specific Compressor) 최적화 과정에서 발견된 루프 성능 저하 문제를 다룹니다. 특히, `j = next_j[i][j]`와 같이 이전 반복의 결과(`j`)에 의존하는 코드는 CPU의 명령어 수준 병렬성(Instruction-Level Parallelism)을 저해하여 지연 시간(Latency)에 의해 성능이 제한된다고 설명합니다. 캐시(Cache)를 활용하더라도 메모리 접근 지연은 불가피하다는 점을 지적합니다.
분기 예측(Branch Prediction)을 이용한 성능 개선
이 문제를 해결하기 위해 개발자는 `if (j != next_j[i][j])` 조건을 삽입하여 CPU의 분기 예측(Branch Prediction) 메커니즘을 역이용합니다. CPU가 `if` 문의 조건이 거짓일 것으로 예측하고 건너뛰도록 유도하면, 반복 간의 데이터 종속성(Data Dependency)이 무시되어 루프가 처리량(Throughput) 중심으로 동작하게 됩니다. 조건이 참일 경우 분기 미스(Branch Misprediction)가 발생하지만, 이는 오히려 원하는 결과를 얻기 위한 과정으로 작용합니다.
컴파일러의 최적화 방해: `volatile` 키워드의 역할
컴파일러는 `if` 문이 의미 없다고 판단하여 제거하려 합니다. 이를 방지하기 위해 `volatile` 키워드를 사용하여 `j` 변수에 대한 접근이 예측 불가능하며, `if` 조건과 `j`의 할당이 독립적인 것처럼 보이게 만듭니다. 이는 컴파일러가 코드 제거(Code Elimination)나 상수 전파(Constant Propagation)와 같은 최적화를 수행하지 못하게 하여, 개발자가 의도한 대로 종속성 없는 루프 실행(Dependency-Free Loop Execution)을 가능하게 합니다.
대안: `[[unlikely]]` 속성과 컴파일러별 차이
LLVM 컴파일러의 경우 C++20의 `[[unlikely]]` 속성이나 `__builtin_expect`를 사용하여 유사한 효과를 얻을 수 있습니다. 하지만 GCC에서는 효과가 없으며, `volatile` 트릭이 더 보편적이고 예측 가능한 결과를 제공한다고 언급됩니다. 이는 컴파일러마다 최적화 전략(Optimization Strategy)이 다르며, 특정 아키텍처에 대한 깊은 이해가 필요함을 시사합니다.
성능 향상 수치 및 실용성 논쟁
합성 벤치마크(Synthetic Benchmark)에서는 루프 실행 시간이 320 마이크로초(µs)에서 80 마이크로초(µs)로 4배 향상되었습니다. 실제 압축 실험에서는 2배의 성능 증가가 관찰되었으며, 이는 LLVM의 코드 생성(Codegen) 최적화가 완벽하지 않기 때문일 수 있다고 분석합니다. 해당 기법은 1% 최적화(1% Optimization)의 영역이지만, 반복 실행되는 루프에서는 충분히 가치 있다고 평가됩니다.