GPU 없이 3초 만에 위성 위치 3,300만 개 계산하기: Zig와 SIMD가 보여준 가능성
최근 테크 업계, 특히 AI 붐 이후로 우리는 ‘고성능 연산 = GPU 필수’라는 공식에 너무 익숙해져 버렸습니다. 하지만 모든 고성능 컴퓨팅(HPC)이 수천만 원짜리 H100을 필요로 하는 것은 아닙니다. 때로는 잘 튜닝된 CPU 코드와 똑똑한 컴파일러, 그리고 SIMD(Single Instruction, Multiple Data)만으로도 믿기 힘든 성능을 낼 수 있습니다.
오늘은 Anthony Templeton이 블로그에 공개한 **“Zig로 GPU 없이 3초 만에 3,300만 개의 위성 위치 계산하기”**라는 글을 깊이 파보려 합니다. 이 글은 단순히 “Zig가 빠르다”는 뻔한 이야기를 넘어, 모던 CPU의 잠재력을 어떻게 밑바닥까지 긁어낼 수 있는지 보여주는 훌륭한 엔지니어링 사례입니다.
1. 왜 SGP4 알고리즘인가?
SGP4(Simplified General Perturbations-4)는 위성의 궤도 요소(TLE)를 기반으로 위치를 예측하는 표준 알고리즘입니다. 1980년대부터 사용된 이 알고리즘은 그 자체로는 복잡하지 않지만, 문제는 ‘규모’입니다.
- 한 달 치 데이터를 1초 단위로 생성하려면 위성 하나당 260만 번의 연산이 필요합니다.
- 수천 개의 위성, 혹은 지상국 네트워크의 교신 시간을 예측하려면 연산량은 기하급수적으로 늘어납니다.
기존의 Python(pip install sgp4)이나 일반적인 구현체들은 초당 2~3백만 번(2-3M props/s) 정도의 연산을 수행합니다. 나쁘지 않지만, 대규모 시뮬레이션에서는 여전히 병목입니다. 작성자는 이를 극한으로 최적화해보고 싶었고, 그 도구로 Zig를 선택했습니다.
2. Zig: 우연히 발견한 SIMD 강자
작성자가 처음부터 SIMD를 노린 것은 아니었습니다. 단순히 Zig의 기본 기능만 활용해 스칼라(Scalar) 코드를 짰는데도, 이미 Rust의 최적화된 구현체(sgp4 crate)와 대등하거나 더 빠른 성능(초당 520만 연산)을 보였습니다.
이유는 두 가지였습니다.
- Branchless Hot Paths: 조건문(if/else)을 최대한 줄이고 수식으로 처리하여 CPU 파이프라인 효율을 높였습니다.
- Comptime Precomputation: Zig의 강력한 기능인
comptime을 활용해 중력 상수나 다항식 계수 등을 컴파일 타임에 미리 계산해버렸습니다. 런타임 초기화 비용이 ‘0’이 된 셈이죠.
하지만 진짜 게임 체인저는 SIMD였습니다. C++에서 SIMD를 짜본 분들은 아실 겁니다. 인트린식(Intrinsic) 함수들의 난해함, 플랫폼별 분기 처리(AVX vs NEON 등)… 정말 고통스럽습니다. 그런데 Zig는 이걸 놀라울 정도로 우아하게 처리합니다.
Zig의 SIMD 추상화
// 64비트 float 4개를 담는 벡터 타입 선언
const Vec4 = @Vector(4, f64);
// 스칼라 값을 모든 레인에 뿌리기 (Broadcast)
const twoPiVec: Vec4 = @splat(constants.twoPi);
// LLVM이 알아서 플랫폼에 맞는 최적의 인트린식으로 변환
pub fn sinSIMD(x: Vec4) Vec4 {
return @sin(x);
}
복잡한 인트린식 없이 @Vector, @splat, @sin 같은 내장 함수만으로 LLVM 백엔드가 타겟 아키텍처(x86_64, ARM64 등)에 맞는 최적의 기계어를 뽑아줍니다. 이는 엔지니어가 ‘어떤 명령어를 쓸까’가 아니라 ‘데이터 흐름을 어떻게 설계할까’에 집중하게 해줍니다.
3. SIMD 사고방식으로의 전환 (Thinking in Lanes)
SIMD 프로그래밍의 핵심은 **“분기(Branching)를 없애는 것”**입니다. 4개의 레인(Lane)이 동시에 돌아가는데, 레인 0은 if 문을, 레인 1은 else 문을 탈 수 없기 때문입니다.
조건문 처리: Select의 미학
스칼라 코드에서는 이렇게 짭니다.
if (eccentricity < 1.0e-4) {
result = simple_calc(x);
} else {
result = complex_calc(x);
}
SIMD에서는 두 경로를 모두 계산한 뒤, 마스크(Mask)를 써서 선택합니다.
const simple = simple_calc(x);
const complex = complex_calc(x);
// 마스크 생성 (결과는 boolean 벡터)
const mask = eccentricity < @as(Vec4, @splat(1.0e-4));
// @select로 결과 취합
const result = @select(f64, mask, simple, complex);
Hacker News의 한 유저는 *“둘 다 계산하면 더 느린 것 아니냐?”*라고 질문했지만, 현대 CPU에서는 분기 예측 실패(Branch Misprediction)로 파이프라인이 깨지는 비용이 단순 연산 비용보다 훨씬 큽니다. 특히 위성 궤도처럼 데이터 패턴이 예측 가능한 경우, 브랜치리스 방식이 압도적으로 유리합니다.
루프 처리: Reduce의 활용
수렴할 때까지 도는 while 루프는 어떻게 할까요? 어떤 레인은 3번 만에 끝나고, 어떤 레인은 5번 돌이야 한다면요? Zig의 @reduce를 사용해 “모두 끝날 때까지” 돌립니다.
var converged: @Vector(4, bool) = @splat(false);
// 모든 레인이 true가 될 때까지 반복
while (!@reduce(.And, converged)) {
// ... 연산 ...
converged = @abs(delta) <= tolerance_vec;
}
4. 성능의 벽을 넘어서: 아키텍처와 트레이드오프
작성자는 단순히 코드만 바꾼 게 아니라, 데이터 접근 패턴(Memory Access Pattern)도 최적화했습니다.
- Time Batched: 위성 1개 × 시간 4개 (캐시 친화적, 가장 빠름)
- Satellite Batched: 위성 4개 × 시간 1개 (SoA 레이아웃 필요)
- Constellation Mode: 타일링(Tiling) 기법을 사용해 L1 캐시(약 32KB)에 데이터를 가둬두고 처리.
atan2의 부재와 근사 해법
가장 흥미로운 기술적 난관은 LLVM에 벡터화된 atan2 내장 함수가 없다는 점이었습니다. 스칼라 버전을 호출하면 SIMD의 이점이 사라집니다. 작성자는 여기서 **다항식 근사(Polynomial Approximation)**를 선택했습니다. 약 1e-7 라디안의 오차가 발생하지만, 이는 LEO(저궤도) 기준 10mm 정도의 오차로, SGP4 모델 자체가 가진 오차(수 km)에 비하면 무시할 수준입니다.
엔지니어링은 언제나 트레이드오프입니다. 완벽한 정밀도를 포기하고 압도적인 속도를 얻은 이 결정은 매우 타당해 보입니다.
5. 결과 및 커뮤니티의 반응
결과적으로 Zig 구현체(astroz)는 Python 바인딩을 통해서도 기존 python-sgp4 대비 약 2.7~2.9배 빠르며, 네이티브 SIMD 사용 시 초당 1,100만~1,300만 번의 연산을 처리합니다.
Hacker News의 날카로운 지적들
이 글에 대한 HN의 반응 중 주목할 만한 것들이 있습니다.
- 그래프의 왜곡: 초기 그래프가 Y축을 0부터 시작하지 않아 성능 향상이 10배처럼 보이게 그려졌다는 지적이 있었습니다. (작성자가 수정함). 데이터 시각화에서 정직함은 엔지니어의 신뢰도와 직결됩니다.
- 보간(Interpolation) vs 연산: *“굳이 매초 위치를 다 계산해야 하냐? 듬성듬성 계산하고 보간(Interpolation)하면 안 되냐?”*라는 지적. 매우 타당합니다. 실제로 게임 물리 엔진이나 렌더링에서는 이렇게 많이 합니다. 하지만 충돌 감지(Conjunction Screening)처럼 정밀도가 생명인 분야에서는 직접 연산이 필요할 수 있습니다.
- heyoka.py와의 비교: 대량의 위성을 한 시점에 처리하는 배치 작업에서는
heyoka.py(LLVM JIT 사용)가 더 빠를 수 있습니다. 하지만astroz는pip install한 번으로 설치되는 편리함(순수 NumPy 의존성)과 시계열 데이터 처리에서의 강점을 가집니다.
6. 결론: Principal Engineer의 시선
이 프로젝트는 **“하드웨어의 잠재력을 깨우는 언어”**로서 Zig의 가능성을 확실히 보여줍니다. C++로 SIMD를 작성할 때의 그 끔찍한 보일러플레이트 코드 없이, 직관적인 문법으로 LLVM의 벡터화 성능을 끌어냈다는 점이 인상적입니다.
GPU가 모든 문제의 정답은 아닙니다. 데이터 전송 비용(PCIe 병목)과 복잡한 설정 없이, 로컬 CPU와 L1/L2 캐시를 영리하게 활용하는 것만으로도 수천만 건의 연산은 순식간에 처리할 수 있습니다.
요약하자면:
- Zig의 SIMD는 ‘First-class’ 시민이다. 더 이상 인트린식 지옥에 빠질 필요가 없다.
- 분기 없는(Branchless) 프로그래밍은 현대 CPU 파이프라인 최적화의 핵심이다.
- 때로는 **정밀도를 약간 희생(atan2 근사)**하는 것이 전체 시스템 성능에 거대한 이득을 준다.
혹시 여러분의 파이프라인에도 무지성으로 GPU를 붙이려던 부분이 있다면, 다시 한번 CPU와 메모리 레이아웃, 그리고 SIMD를 점검해보시길 바랍니다. 답은 의외로 가까운 곳에 있을지도 모릅니다.