# 쓸모없는 if로 코드 성능 4배 높이기

> Clean Markdown view of GeekNews topic #31414. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31414](https://news.hada.io/topic?id=31414)
- GeekNews Markdown: [https://news.hada.io/topic/31414.md](https://news.hada.io/topic/31414.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-14T09:13:39+09:00
- Updated: 2026-07-14T09:13:39+09:00
- Original source: [purplesyringa.moe](https://purplesyringa.moe/blog/quadrupling-code-performance-with-a-useless-if/)
- Points: 1
- Comments: 1

## Topic Body

- 도메인 특화 압축기의 최적 인코딩 경로를 순회하는 루프에 **조건문 하나를 추가**해, 합성 벤치마크 실행 시간을 320µs에서 80µs로 줄임
- 반복마다 메모리 주소가 직전 반복의 `j`에 의존하므로, 단일 `mov` 명령도 **명령어 수준 병렬성**을 활용하지 못한 채 메모리 접근 지연시간에 묶임
- `next_j[i][j]`가 대부분 기존 `j`와 같다는 특성을 분기 예측에 반영하면, CPU가 반복 사이의 의존성을 우회해 여러 반복을 **추측 실행**할 수 있음
- 컴파일러가 의미상 불필요한 `if`를 제거하지 못하도록 `volatile` 캐스트를 사용함. LLVM에서는 `[[unlikely]]`나 `__builtin_expect(..., 0)`도 작동하지만, `volatile`이 더 나은 코드를 생성하고 GCC도 지원함
- 현실적인 실험에서는 LLVM의 최적이 아닌 코드 생성 때문일 수 있는 **약 2배의 성능 향상**을 얻었으며, `j`를 예측하기 어렵다면 지연시간이 1사이클인 `pshufb`를 대안으로 사용할 수 있음

---

### 압축기의 최적 경로 순회
- 입력 문자열을 여러 청크로 나누고, 청크마다 가장 작은 인코딩을 선택해야 함
  - 인코딩마다 잘 압축하는 문자가 달라 청크 경계를 바로 결정하기 어려움
  - [이전 글](https://purplesyringa.moe/blog/optimal-parse-with-phminposuw/)의 알고리듬은 이 문제를 격자 위 **최단 경로 탐색**으로 변환함
- 각 셀에는 다음으로 이동할 최적 셀을 기록함
  - 첫 셀부터 마지막 셀까지 참조를 따라가면 최적 인코딩 순서를 얻을 수 있음
- `next_j`를 채우는 첫 번째 루프는 SIMD로 이미 최적화돼 있음
  - 실제 병목은 다음과 같은 단순한 경로 순회 루프임

```c
uint8_t encoding[n_symbols];
uint8_t j = 0;

for (int i = 0; i < n_symbols; i++) {
    j = next_j[i][j];
    encoding[i] = j;
}
```

- 쓰기를 제외한 핵심 연산 `j = next_j[i][j]`는 단일 `mov` 명령으로 컴파일됨

### 단일 명령도 느려지는 의존성
- 현대 CPU는 [명령어 수준 병렬성](https://en.wikipedia.org/wiki/Instruction-level_parallelism)을 이용해 여러 명령과 서로 다른 반복의 작업을 동시에 실행함
  - 일반적인 루프에서 `i < n_symbols` 검사와 `i++` 비용이 다른 작업을 막지 않는 이유임
- 하지만 서로 **의존하는 명령**은 동시에 실행할 수 없음
  - 현재 반복의 메모리 주소 `next_j[i][j]`를 계산하려면 직전 반복에서 얻은 `j`가 필요함
  - 다음 반복은 이전 반복의 결과가 나올 때까지 시작할 수 없어, 데이터가 캐시에 있어도 메모리 접근 지연시간의 영향을 받음

### 분기 예측을 값 추측으로 활용하기
- 이 압축기는 청크 수가 많지 않을 것으로 예상되므로 `next_j[i][j]`가 기존 `j`와 같은 경우가 대부분임
- CPU에 `j`가 유지될 것이라고 직접 주소 예측을 지시할 수는 없지만, **분기 예측**을 이용하면 같은 효과를 만들 수 있음

```c
for (int i = 0; i < n_symbols; i++) {
    if (j != next_j[i][j]) {
        j = next_j[i][j];
    }
    encoding[i] = j;
}
```

- CPU가 `if` 본문을 실행하지 않을 것으로 예측하면 `j`를 갱신하는 반복 간 의존성이 없다고 보고 여러 반복을 추측 실행함
- 조건이 실제로 참이면 **분기 예측 실패 복구**가 일어남
  - 잘못 추측해 수행한 쓰기를 폐기함
  - 올바른 `j`를 사용해 실행을 다시 시작함
- `j`가 거의 바뀌지 않는 동안 루프의 병목은 지연시간에서 처리량으로 전환됨

### 컴파일러가 `if`를 제거하지 못하게 하기
- 컴파일러 관점에서는 조건문을 추가한 코드와 원래 코드의 의미가 같음
  - `j`가 메모리에 있다면 불필요한 쓰기나 읽기 전용 메모리에 대한 쓰기를 피할 수 있지만, 여기서는 **레지스터 값**임
  - [공통 부분식 제거(CSE)](https://en.wikipedia.org/wiki/Common_subexpression_elimination) 같은 최적화가 조건과 중복 접근을 제거할 수 있음
- 일반적인 컴파일러 힌트는 분기를 없애는 데 쓰이지만, 여기서는 반대로 분기 없는 코드를 **분기 코드로 유지**해야 함
- 조건 검사와 대입의 메모리 접근이 독립적인 것처럼 보이도록 대입 쪽에 `volatile` 캐스트를 적용함

```c
for (int i = 0; i < n_symbols; i++) {
    if (j != next_j[i][j]) {
        j = *(uint8_t volatile *)&next_j[i][j];
    }
    encoding[i] = j;
}
```

- 7월 13일 추가 확인에서 [ibookstein이 발견한 방법](https://lobste.rs/s/1an425/quadrupling_code_performance_with#c_4clhdw)처럼 `[[unlikely]]` 또는 `__builtin_expect(..., 0)`을 사용해도 LLVM에서 같은 효과를 얻을 수 있음
- `volatile` 방식은 더 나은 코드를 생성하고 **GCC에서도 동작**함

### 벤치마크에서 확인한 성능 변화
- 합성 벤치마크에서 루프 실행 시간이 **320µs에서 80µs**로 감소해 4배 빨라짐
  - 한 번의 실행 시간 차이는 작지만, 압축 과정에서 루프가 여러 번 실행되므로 전체 비용에 누적됨
- 더 현실적인 실험에서는 약 **2배의 성능 향상**을 기록함
  - LLVM이 최적이 아닌 코드를 생성한 영향일 가능성이 있지만, 여전히 적용할 가치가 있는 수준이었음

### 비트마스크 기반 표현이라는 대안
- 이 알고리듬에서 `next_j[i][j]`는 두 값 중 하나만 가질 수 있음
  - 대부분은 현재 `j`임
  - 나머지는 `j`와 무관하고 `i`에만 의존하는 값임
- 8개 원소 배열인 `next_j[i]`를 대체 값과 **비트마스크**의 쌍으로 바꿀 수 있음
  - 이 표현에서는 `if`가 의미상 필요하므로 `volatile` 우회를 쓰지 않아도 됨
  - 다만 x86에서는 가변 위치의 비트를 검사하는 연산이 비교보다 느릴 수 있어 전체 성능이 오히려 낮아질 가능성이 있음

### `j`를 예측할 수 없을 때
- [값 추측으로 연결 리스트 순회를 가속하는 글](https://mazzo.li/posts/value-speculation.html)에서도 같은 방식으로 성능을 높이는 방법을 다룸
- `j`를 예측하기 어렵다면 벡터 인덱스 연산인 **`pshufb`** 를 사용할 수 있음
  - `pshufb`의 지연시간은 1사이클이므로 추측 실행 없이 더 줄이기 어려움
  - 벡터 연산으로 가능한 각 시작 `j`의 경로를 병렬 계산할 수 있음
  - 작업을 여러 스레드로 나누고 결과를 병합하는 방식도 가능함

## Comments



### Comment 61756

- Author: neo
- Created: 2026-07-14T09:13:40+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/1an425/quadrupling_code_performance_with) 
- C++20의 **`[[unlikely]]` 속성**을 if 문에 붙이면 clang이 해당 코드를 제거하지 못하게 하는 데 충분해 보임: https://clang.godbolt.org/z/r4xYWfPfe  
  다른 컴파일러에서는 확인하지 못함
  - 다시 확인해 보니 LLVM에서는 실제로 작동했으며, 아마 처음에는 효과가 없는 GCC로 잘못 시험했던 듯함  
    다만 `[[unlikely]]`는 코드를 **빠른 경로 밖으로 이동**시키므로 LLVM이 아래처럼 다소 비효율적인 코드를 생성함  
    ```x86asm  
            cmp     r8b, dl  
            jne     not_equal  
    continue:  
            ...  
    not_equal:  
            mov     ecx, edx  
            jmp     continue  
    ```  
    다음 형태가 더 합리적으로 보임  
    ```x86asm  
            cmp     r8b, dl  
            je      skip  
            mov     ecx, edx  
    skip:  
            ...  
    ```  
    또한 메모리에서 값을 여러 번 읽지 않도록 최적화하면서 뜨거운 루프에 명령어 하나를 추가하기도 함. 그래서 **`volatile`을 이용한 기법**은 여전히 유용하지만, 원래 코드도 그대로 작동할 수 있다는 점은 반가움

- 이 기법은 다른 곳에서 **값 추측(value speculation)** 이라고 부름: https://mazzo.li/posts/value-speculation.html
  - 이 방법을 다시 고안하고 나서 아레나에 저장된 **연결 리스트**에도 적용할 수 있겠다고 처음 생각했는데, 링크된 글이 정확히 그 내용을 다루고 있어 내 글에도 링크를 추가함

- 앞으로 2~3년 뒤에는 개발자들이 LLM의 출력에 갈수록 무관심해질 텐데, 이런 종류의 블로그 글에도 여전히 관심을 가질지 궁금함
  - 지금도 관심을 두는 사람은 매우 적고, 전체 프로그래밍 분야에서 보면 **틈새 지식**임  
    그래도 관심 있는 사람에게는 여전히 매우 유용하고 좋은 글임
  - Godbolt는 매우 인기 있음. 코드를 읽지 않는 습관을 비판받을 때 LLM에 취한 개발자들이 “어셈블리까지 보느냐?”를 대단한 반박처럼 내놓지만, 실제로 **어셈블리를 확인**하며 앞으로도 그럴 것임  
    세부 사항이 중요하지 않다고 여기는 개발자는 좋은 개발자가 되기 어렵다. 훌륭한 토목 기술자가 대형 프로젝트의 볼트와 너트까지 신경 쓰듯, 훌륭한 개발자도 작은 세부 사항까지 파고들어야 함. 전체는 세부 사항으로 이루어져 있으므로 자신이 만드는 결과물을 중시한다면 이를 피할 수 없음
  - 지금과 근본적으로 다르지 않다고 봄. 현재도 대부분은 사람이 작성한 코드의 **컴파일러 출력**에 관심이 없음  
    AI가 작성한 코드의 컴파일러 출력을 살펴보는 사람은 더 줄 수 있지만, 기본 구도가 달라지지는 않을 것 같음
  - 달라지는 것은 관심사의 유무보다 **최적화 우선순위의 재배치**에 가까움  
    10년 전 Bash 스크립트가 조금 느리다고 해서 명령어 수준 병렬성이나 CPU 분기 예측부터 고민하지는 않았을 것임. 컴파일 및 최적화 가능한 언어로 작성된 성능 핵심부가 있고, 그 안에 세부적으로 살펴볼 뜨거운 경로가 있으며, 다시 그 안에서 벡터화나 최신 CPU의 동작 차이를 확인할 가치가 있는 뜨거운 루프 한두 개가 존재할 수 있음  
    LLM을 사용하더라도 성능상 가장 뜨거운 지점은 예외적으로 직접 검토할 가치가 있음. 결과물의 낮은 품질을 의식적으로 감수하거나, LLM으로 70%를 만든 뒤 나머지 30%를 손으로 고치는 방식이 85:15까지 개선될 수는 있어도 이 글은 여전히 **상위 1% 수준의 세부 영역**을 다루는 셈임
  - C++을 작성하지 않으므로 그런 의미에서는 지금도 직접적인 관심 대상이 아니지만, 그래도 흥미롭게 읽었고 **2~3년 뒤에도** 이런 내용은 계속 재미있을 것 같음
