Everyone Should Know SIMD
모든 개발자가 SIMD를 알아야 하는 이유
Mitchell Hashimoto는 SIMD를 대량의 연속 데이터를 처리하는 일반적인 최적화 기법으로 소개하며, 벡터화 코드의 공통 구조를 다섯 단계로 설명합니다. Ghostty의 코드포인트 탐색 사례에서는 AVX2 환경의 실제 처리량이 약 5배 향상됐지만, 먼저 측정하고 컴파일러의 자동 벡터화 결과도 확인해야 한다고 말합니다.
- 주제
AI 요약
SIMD(Single Instruction, Multiple Data)는 CPU 명령 하나로 여러 값을 동시에 처리하는 기법입니다. Mitchell Hashimoto는 이를 일부 고성능 소프트웨어만 다루는 어려운 기술로 여길 필요가 없다고 말합니다. 수백에서 수백만 개의 연속된 값을 반복 처리하는 코드라면 SIMD 적용을 검토할 수 있으며, 일반적인 벡터화 코드는 일정한 구조를 따릅니다. 반대로 처리할 값이 몇 개뿐이라면 SIMD를 쓰는 이득이 거의 없습니다.
벡터화 코드의 다섯 단계
일반적인 SIMD 루프는 다섯 단계로 정리됩니다. 먼저 필요한 상수를 각 벡터 lane에 복제하고, 필요하다면 벡터 누산기를 초기화합니다. 다음으로 입력을 벡터 너비만큼 읽으며 반복합니다. 각 lane에서 비교나 산술 연산을 동시에 실행한 뒤, 벡터 결과를 줄여 필요한 값을 얻거나 결과를 저장합니다. 마지막에는 벡터 너비에 맞지 않아 남은 원소를 원래의 스칼라 루프로 처리합니다. 이 마지막 부분을 scalar tail이라고 부릅니다.
저자는 Zig의 일반 벡터 기능으로 이 구조를 설명합니다. CPU마다 다른 intrinsic을 직접 쓰지 않아도 벡터 연산을 표현할 수 있지만, 실제 기계어는 대상 CPU의 명령 집합에 맞춰 생성됩니다. Ghostty에서는 지원하는 벡터 너비를 찾지 못하면 SIMD 코드를 건너뛰고 스칼라 경로만 실행합니다.
Ghostty의 코드포인트 탐색 사례
예제는 디코딩한 코드포인트 배열에서 값이 0xF 이하인 C0 제어 문자를 처음 만날 때까지 읽는 루프입니다. 터미널은 대부분 출력할 일반 문자로 이루어지므로, 제어 문자를 찾기 전까지의 구간을 한꺼번에 처리하면 효율이 좋아집니다. 기존 스칼라 코드는 각 값을 하나씩 확인합니다.
SIMD 버전은 먼저 u32 벡터의 lane 수를 구합니다. ARM NEON에서는 4개, AVX2에서는 8개, AVX-512에서는 16개 값을 한 번에 다룹니다. 비교 기준인 0xF를 모든 lane에 복제한 뒤, 입력을 벡터 너비만큼 읽고 각 값이 기준보다 큰지 병렬로 비교합니다.
모든 비교가 참이면 다음 벡터를 읽습니다. 거짓인 lane이 있다면 비교 결과를 비트 마스크로 바꾸고, 첫 실패 위치를 찾아 현재 인덱스에 더합니다. 예를 들어 여덟 값 중 네 번째 값에서 비교가 실패하면, 앞선 세 값을 건너뛴 뒤 해당 위치에서 처리를 멈춥니다. 벡터 루프가 끝나면 남은 값은 기존 스칼라 루프로 확인합니다. 입력 길이가 벡터 너비의 배수가 아닌 경우와 SIMD를 지원하지 않는 CPU도 이 스칼라 루프가 처리합니다.
저자는 이 코드가 ARM NEON에서 최대 4배, AVX2에서 8배, AVX-512에서 16배의 처리량 향상을 낼 수 있다고 설명합니다. 다만 실제 프로그램에서는 주변 작업도 처리해야 하므로 이상적인 배수만큼 빨라지지는 않습니다. AVX2를 쓰는 Intel 데스크톱에서 터미널 프로그램부터 최종 터미널 상태까지 측정한 결과는 약 5배 향상이었습니다.
자동 벡터화와 적용 기준
컴파일러가 간단한 루프를 자동 벡터화하는 경우도 있습니다. 특히 규칙적인 산술 연산을 반복하는 루프는 컴파일러가 처리하기 쉽습니다. 따라서 저자는 직접 SIMD 코드를 쓰기 전에 최적화 빌드에서 컴파일러가 무엇을 생성하는지 확인하라고 권합니다. 다만 복잡한 제어 흐름이 있는 루프에서는 자동 벡터화가 놓치는 기회가 많다고 지적합니다. 성능이 중요한 구간이라면 명시적으로 벡터화를 작성해, 컴파일러 업데이트나 주변 코드 변경 뒤에도 벡터 연산이 유지되도록 할 수 있습니다.
글의 권고는 모든 루프를 SIMD로 바꾸라는 뜻이 아닙니다. 성능이 중요한지 먼저 확인하고, 큰 연속 데이터를 스캔하거나 비교하거나 변환하는 핫 루프에서 적용을 검토하라는 내용입니다. SIMD 구현이 복잡해지거나 이득이 분명하지 않다면 당장은 건너뛰어도 된다고 덧붙입니다.
Reddit 반응
- @u/nerdly90 — 왜 이 글이 계속 올라오나요? 이미 알고 있습니다.
- @u/unski_ukuli — DHH가 보고 뭔가 배울지도 몰라서 올라오는 것 아닐까요?
- @u/floodyberry — 성능이 중요한 코드에서만 필요합니다. 그 밖에는 쓸모가 없습니다. CPU와 무관한 Zig DSL 예제를 읽는 것도 SIMD를 이해하는 좋은 방법이 아닙니다. SIMD는 맞는 문제에 적용하면 강력하고, 창의적으로 쓰면 문제에 딱 맞지 않아도 쓸 수 있지만, 이 글은 그 점을 잘 보여주지 못하고 SIMD의 신비를 걷어내지도 못합니다.
- @u/garyk1968 — “모든 사람이 SIMD를 알아야 한다”는 한 사람의 의견입니다. 딱 한 사람 말입니다. 그 이상은 아닙니다.
- @u/Ma4r — 그 한 사람은 Consul, Terraform, Vault, Nomad 작업에 참여했습니다.
- @u/Poincarina — 왜 알아야 하나요? SIMD를 알지만, SIMD가 필요한 코드를 직접 쓰는 건 보통 바퀴를 다시 발명한다는 신호입니다.
- @u/Ma4r — 분야에 따라 크게 다릅니다. CRUD 웹앱을 쓴다면 맞는 말입니다. 하지만 GC나 지연 시간에 민감한 코드를 쓴다면, 제약 조건에 딱 맞는 라이브러리를 찾기 어려운 경우가 많습니다. 컴파일러가 SIMD를 잘 활용한다는 통념과 달리 실제로는 그렇지 않습니다.
- @u/Poincarina — 그렇더라도 개발자와 사용 사례의 98%는 직접 최적화 코드를 쓰기보다 라이브러리를 쓰는 편이 낫습니다.
- @u/cdb_11 — 모든 기능이 라이브러리로 제공되는 것은 아닙니다. 사실 대부분은 그렇지 않습니다. 기존 라이브러리도 최적화가 안 된 경우가 많고, 범용성을 고려해야 하므로 특정 프로그램에 맞춘 최적화를 하기 어렵습니다. SIMD는 데이터 흐름을 먼저 올바르게 잡고 도메인 지식을 활용하는 수준의 작업입니다. 이를 무시하면 그런 문제가 성능 병목이 될 수 있고, SIMD를 무작정 추가해도 해결되지 않을 수 있습니다.
- @u/LeeHide — 컴파일러 자동 벡터화가 여전히 최선입니다. -ftree-vectorizer-verbose 같은 옵션으로 컴파일러가 왜 루프를 벡터화하지 않았는지 확인할 수도 있습니다. 최적화의 기본은 성능을 측정하기 전에 최적화하지 않는 것입니다.
- @u/Ma4r — 둘 다 맞을 수 있습니다. 프로파일링을 해도 가장 흔하게 보이는 성능 문제는 자동 벡터화 실패입니다. 저는 매일 LLVM을 다룹니다. 현재 자동 벡터화 단계는 정말 서툽니다. 직접 백엔드를 만들거나 사용자가 SIMD를 쓰게 하는 방법이 있습니다.
- @u/LeeHide — 자동 벡터화가 똑똑하지 않다는 말은 아닙니다. 작동하면 공짜 성능 향상이라는 뜻입니다.
- @u/Ma4r — “작동하면” 그렇습니다. 대부분은 잘 작동하지 않습니다. 벡터화를 켜면 호환성을 잃을 수도 있습니다. 컴파일러가 더 잘 알고 더 똑똑하다는 선입견이 이런 해로운 생각을 만들었습니다. SIMD를 신경 쓰는 단계라면 컴파일러의 자동 최적화만으로는 부족할 가능성이 큽니다.
- @u/LeeHide — 적어도 Rust와 C++에 std::simd가 들어오고 있으니, 동료가 intrinsic으로 SIMD 루프를 직접 쓰자고 고집하는 일은 줄어들겠네요.
- @u/Ma4r — 좋은 변화입니다. 다만 널리 채택되고 안정화되려면 적어도 2년은 더 걸릴 것 같습니다. 그전까지는 intrinsic을 써야 합니다. 언어 수준 SIMD API의 등장은 자동 벡터화가 가까운 미래에 크게 개선되기 어렵다는 점을 보여주기도 합니다.
- @u/floodyberry — 자동 벡터화는 일부 인위적인 예제를 빼면 대부분 쓸모가 없습니다.
- @u/davidalayachew — 어떤 언어에서 그런가요? Java는 JIT가 자동 벡터화한 코드로 꽤 인상적인 결과를 내는 경우가 있습니다.
- @u/Ma4r — Java에는 JIT와 런타임 PGO라는 장점이 있습니다. 대부분의 컴파일러에는 그런 기능이 없습니다.
- @u/CarnivorousSociety — SIMD는 저수준 기술입니다. 개발자 99%는 직접 저수준 코드를 쓸 필요 없이 누군가 만든 라이브러리를 쓰면 됩니다.
- @u/cdb_11 — “필요하지 않다”와 “바퀴를 다시 발명한다”는 다른 말입니다. SIMD를 쓴다면 최적화되지 않은 코드를 개선하거나 자기 프로그램의 특정 상황에 맞게 최적화하는 경우일 가능성이 큽니다. 그건 바퀴를 다시 발명하는 게 아닙니다.
- @u/CarnivorousSociety — 보통은 아예 필요하지 않습니다. 대개 코드를 복잡하고 읽기 어렵게 만들면서 얻는 성능은 적습니다. 라이브러리 핵심 코드를 쓰는 경우가 아니라면, SIMD가 필요하다고 느끼는 건 바퀴를 다시 발명한다는 신호인 경우가 많습니다. 보통은 라이브러리 핵심을 만드는 사람이 아니고, 가독성이 SIMD로 얻는 작은 성능 이득보다 중요하기 때문입니다.
- @u/GeneralSEOD — 엄마가 공짜 SIMD를 권하는 낯선 사람은 조심하라고 했습니다. 그래서 저는 PHP를 쓰며 안전한 볼풀에 있습니다.
- @u/Sorry-Substance-6397 — Go에서 SIMD를 쓰는 게 특히 좋습니다. 다른 사람이 쓰지 않더라도 컴파일러 비용을 부담하지 않아도 되니까요.
원문: Mitchell Hashimoto / 번역·요약: Trawling