---
title: "SIMD는 고수 전용 흑마법이 아니라, 모든 개발자가 알아야 할 반복문 최적화임"
published: 2026-07-22T17:48:18.000Z
canonical: https://jeff.news/article/5068
---
# SIMD는 고수 전용 흑마법이 아니라, 모든 개발자가 알아야 할 반복문 최적화임

미첼 하시모토가 SIMD를 ‘복잡한 고성능 영역’으로만 볼 필요가 없다고 설명한 글이다. Ghostty의 실제 Zig 코드를 예로 들며, 큰 배열이나 문자열을 훑는 뜨거운 반복문은 벡터 너비만큼 한 번에 처리하는 5단계 패턴으로 생각할 수 있다고 풀어낸다.

## SIMD가 어렵다는 편견부터 걷어내기

- 미첼 하시모토의 주장은 단순함. SIMD는 극한 성능 괴수들만 만지는 흑마법이 아니라, 모든 개발자가 기본 감각 정도는 가져야 하는 도구임
  - SIMD는 CPU가 여러 값을 한 번에 처리하게 해주는 방식임
  - `for (byte in bytes)`처럼 바이트, 문자, 배열 값을 하나씩 훑는 코드가 있으면 SIMD 후보가 될 수 있음
  - 데이터가 몇십 바이트면 굳이 할 필요 없지만, 수백·수천·수백만 개를 반복 처리한다면 효과가 확 커짐

- 글에서 말하는 ‘일상적인 SIMD’는 simdjson이나 simdutf 같은 고난도 최적화 트릭이 아님
  - 핵심은 “N개 값을 한 번에 처리하는 반복문”으로 바꾸는 패턴임
  - 처음엔 낯설어 보여도, 형태 자체는 일반 for 루프만큼 반복적이라고 봄

## SIMD 코드의 공통 5단계

- 대부분의 기본 SIMD 코드는 거의 같은 흐름을 따름
  - 필요한 상수를 벡터 전체에 브로드캐스트하고, 누적 벡터가 있으면 초기화함
  - 입력을 벡터 너비만큼 잘라서 루프를 돎
  - 비교나 산술 연산을 모든 레인에 병렬로 적용함
  - 벡터 결과를 필요한 형태로 축약하거나 저장함
  - 벡터 크기로 딱 안 나누어떨어지는 나머지는 기존 스칼라 루프로 처리함

- 이 5단계를 알면 반복문을 보는 눈이 바뀜
  - “이 루프가 한 번에 하나씩 처리해야만 하나?”가 아니라 “벡터 한 덩어리씩 처리할 수 있나?”를 먼저 떠올리게 됨
  - 문자열 스캔, 바이트 비교, 숫자 배열 변환 같은 작업이 특히 잘 맞음

## Ghostty에서 나온 실제 예시

- 예시는 Ghostty 터미널에서 디코딩된 코드포인트를 훑다가 `0xF` 이하 값을 만나면 멈추는 루프임
  - 터미널 입력은 대부분 출력 가능한 일반 문자라서, 제어 문자가 나오기 전까지 최대한 빠르게 묶어서 처리하고 싶음
  - 원래 스칼라 코드는 `cps[end] > 0xF`를 하나씩 검사하는 한 줄짜리 while 루프였음

- SIMD 버전은 Zig의 generic vector를 써서 CPU별 intrinsic 없이 작성됨
  - `simd.lanes(u32)`로 현재 CPU가 한 번에 처리할 수 있는 u32 개수를 구함
  - ARM NEON은 4개, AVX2는 8개, AVX-512는 16개 레인을 처리할 수 있음
  - 지원할 벡터 너비가 없으면 SIMD 경로를 건너뛰고 기존 스칼라 루프만 실행함

- 비교 자체는 벡터 전체에 한 번에 적용됨
  - `@splat(0xF)`로 모든 레인에 임계값을 채운 벡터를 만들고, 입력 벡터와 `>` 비교를 수행함
  - 예를 들어 `{0x41, 0x42, 0x43, 0x0A, ...}`를 비교하면 네 번째 레인만 false가 됨
  - 스칼라였다면 8번 비교할 일을 AVX2에서는 벡터 비교 한 번으로 처리하는 식임

- 결과를 찾는 부분이 SIMD에서 제일 낯설 수 있음
  - 모든 레인이 true면 그대로 다음 벡터로 넘어감
  - false가 하나라도 있으면 boolean 벡터를 비트 마스크로 바꾸고, `@ctz`로 첫 실패 위치를 찾음
  - 위 예시에서는 세 개의 true 뒤에 첫 false가 있으니 인덱스 3을 얻고 거기서 멈춤

> [!IMPORTANT]
> 이 루프는 이론상 ARM NEON에서 최대 4배, AVX2에서 8배, AVX-512에서 16배 처리량 향상이 가능함. 실제 AVX2 인텔 데스크톱에서는 터미널 프로그램부터 최종 터미널 상태까지의 엔드투엔드 경로가 약 5배 빨라졌음.

## 컴파일러가 알아서 해주면 좋겠지만

- 컴파일러 자동 벡터화는 되는 경우도 있지만, 믿고 맡기기엔 한계가 큼
  - 단순한 산술 루프는 최적화될 수 있음
  - 하지만 제어 흐름이 있거나 결과를 특정 위치로 줄여야 하는 루프는 컴파일러가 놓치기 쉬움
  - 자동 벡터화는 수십 년째 연구 중인 영역인데도, 실제 프로덕션 컴파일러는 여전히 기회를 자주 놓친다고 글은 지적함

- 중요한 핫 루프라면 명시적 SIMD가 주는 장점이 있음
  - 코드 변경이나 컴파일러 업데이트 때문에 조용히 스칼라 루프로 되돌아가는 일을 줄일 수 있음
  - 성능이 정말 중요해서 5배 개선을 챙기려는 상황이라면, 예측 가능성이 꽤 큰 가치가 됨

- 결론은 “모든 코드를 SIMD로 바꾸자”가 아님
  - 큰 연속 데이터를 스캔·비교·카운트·변환하는 뜨거운 루프를 보면 SIMD 가능성을 알아차리자는 얘기임
  - 그리고 언어가 벡터 타입을 잘 제공하면 어셈블리나 CPU별 문법을 몰라도 꽤 현실적인 최적화를 할 수 있음

---
## 기술 맥락

- 이 글에서 선택한 최적화는 스칼라 루프를 SIMD 벡터 루프로 바꾸는 거예요. 왜냐하면 터미널은 대부분 일반 문자를 계속 처리하고, 제어 문자가 나올 때까지만 빠르게 훑으면 되는 패턴이라서 한 글자씩 보는 비용이 아깝거든요.

- 구현 방식은 꽤 보수적이에요. CPU별 전용 코드를 직접 쓰는 대신 Zig의 generic vector를 사용하고, 지원 가능한 레인 수가 없으면 기존 스칼라 루프로 돌아가요. 그래서 성능 경로를 추가하면서도 fallback을 자연스럽게 유지할 수 있어요.

- 트레이드오프는 명확해요. 코드는 한 줄에서 12줄 정도로 늘어나고, 마스크와 `@ctz` 같은 개념을 이해해야 해요. 대신 AVX2 환경에서 실제 엔드투엔드 처리량이 약 5배 좋아졌다면, 터미널 렌더링처럼 자주 실행되는 경로에서는 충분히 값어치가 있어요.

- 컴파일러 자동 벡터화에만 기대지 않는 이유도 여기에 있어요. 단순 루프는 컴파일러가 잘 바꿔줄 때도 있지만, 첫 실패 레인을 찾아 멈추는 식의 제어 흐름은 놓칠 수 있거든요. 중요한 핫 루프라면 개발자가 의도를 코드에 직접 남기는 게 더 예측 가능해요.

## 핵심 포인트

- SIMD는 CPU가 여러 값을 한 명령으로 병렬 처리하게 해주는 기술임
- 일상적인 SIMD 코드는 상수 브로드캐스트, 벡터 단위 루프, 병렬 연산, 결과 축약, 스칼라 꼬리 처리라는 5단계 패턴을 따름
- Ghostty의 코드포인트 스캔 루프는 ARM NEON에서 최대 4배, AVX2에서 8배, AVX-512에서 16배 처리량 향상이 가능함
- 실제 AVX2 인텔 데스크톱의 터미널 처리 경로에서는 엔드투엔드 기준 약 5배 빨라졌음
- 컴파일러 자동 벡터화는 도움이 되지만 복잡한 제어 흐름에서는 놓치는 경우가 많아, 중요한 루프는 명시적 SIMD가 더 예측 가능함

## 인사이트

이 글은 ‘SIMD를 배워라’보다 ‘반복문을 보는 눈을 바꿔라’에 가깝다. 문자열, 바이트 배열, 숫자 배열을 많이 훑는 코드가 있다면 한 번에 하나씩이 아니라 한 덩어리씩 처리할 수 있는지 떠올리는 것만으로도 성능 감각이 확 달라진다.
