Lumen 사이드 4편 – JIT, 컴퓨터가 자기 자신을 위한 코드를 짜는 마법

An artistic arrangement of golden gears on a dark

3편에서 컴파일러가 코드를 어셈블리로 바꾸는 4단계를 풀었어요. 스케치 → 평면도(AST) → 시공 도면(IR) → 작업 지시서(코드 생성). 이번 편은 그 마지막 단계가 언제 일어나느냐의 얘기예요. JIT(Just-In-Time) 컴파일.

이게 진짜 신기한 영역이에요. 평소엔 컴파일러가 코드를 미리 다 변환해서 실행 파일(.exe)로 만들어 두고 그걸 실행하는데, JIT은 프로그램 실행 중에 새 코드를 즉석에서 만들어 메모리에 쓰고 그걸 자기가 함수처럼 호출해요. “컴퓨터가 자기 자신을 위한 코드를 짠다”는 느낌이 들죠.

처음 들으면 마법처럼 느껴지는데, 직접 짜보니까 그게 마법이 아니더라구요. 건축의 “사전 계획 시공”과 “현장 도착해서 즉석 시공”의 차이로 풀면 의외로 단순해요. 이번 편에서 그 메커니즘을 풀어볼게요.

흑백으로 맞물린 기어 부품

사전 계획 시공 vs 현장 시공

3편 끝에서 시공 도면이 어셈블리(현장 작업 지시서)로 변환되는 마지막 단계를 봤어요. 이 마지막 단계가 언제 일어나느냐에 따라 두 가지 방식이 나뉩니다. 사전(AOT)이냐, 실행 중(JIT)이냐.

사전 계획 시공 (AOT, Ahead-Of-Time)

도시 전체 청사진을 처음에 다 그려서 모든 시공 도면을 미리 완성해 둡니다. 그 다음 공사 시작. 도시가 완성될 때까지 도면은 한 번 그린 그대로 안 바뀝니다. 모든 상황에 대비한 일반 도면이라 시공은 가능하지만 그 자리 지형에 맞춤은 아니에요.

컴파일러로 풀면 일반 프로그래밍 언어(C, Rust, Go)가 이 방식이에요. 코드를 미리 한 번 컴파일해서 실행 파일(.exe)을 만들어 두고, 사용자는 그 파일을 실행합니다. 컴파일 시점에는 입력 데이터가 어떻게 생겼는지 모르니까 모든 가능한 경우에 동작하는 일반 코드가 나옵니다.

장점: 컴파일이 끝났으니 실행은 빠름. 사용자는 컴파일 시간 부담 없음.

단점: 입력 데이터에 맞춰 최적화가 안 됨. 모든 케이스 다 대응 가능한 일반 도면이라 특정 케이스에선 비효율.

현장 시공 (JIT, Just-In-Time)

큰 청사진만 미리 가지고 있고, 공사 현장에 도착해서 그 자리 지형을 보고 시공 도면을 즉석에서 만듭니다. 그 지형에 정확히 맞춘 도면이라 일반 도면보다 시공이 빠르고 결과물도 더 좋아요. 단점은 도면 만드는 시간이 추가로 들어가는 거.

컴파일러로 풀면 JIT이 이 방식. 프로그램이 실행 중일 때 지금 들어온 입력 데이터의 모양을 보고 그 모양에 정확히 맞는 어셈블리를 그 자리에서 만들어요. 만든 어셈블리는 즉시 실행 가능합니다.

장점: 입력 데이터에 정확히 맞춤 최적화. 한 번 짠 코드가 여러 형태에서 모두 최적.

단점: 컴파일 비용이 실행 시점에 부담. 메모리에 동적으로 코드를 써야 해서 OS 보안 제약 있음.

두 방식의 흐름 비교

사전 계획 시공 (AOT):
  코드 작성  ──────────►  컴파일  ──────────►  실행 파일  ──►  실행
   (개발자)              (한 번, 컴파일러)         (.exe)         (사용자)

현장 시공 (JIT):
  코드 작성  ──►  실행 시작  ──►  컴파일  ──►  메모리에 코드 쓰기  ──►  즉시 실행
                    (런타임)        (그 자리)        (실행 가능 영역)

JIT의 차별점은 컴파일과 실행이 같은 프로세스 안에서 일어난다는 것입니다. 실행 중인 프로그램이 새 코드를 만들고, 그 코드를 자기 자신이 즉시 실행해요. 그래서 “컴퓨터가 자기 자신을 위한 코드를 짜는” 같은 느낌이 듭니다.

저도 사이드 시작 전엔 이 부분이 가장 신비하게 느껴졌어요. “실행 중인 프로그램이 자기 코드를 어떻게 만들지?” 싶었거든요. 막상 짜보니까 그게 마법이 아니라 OS의 메모리 권한 관리 + 함수 포인터 캐스팅이라는 두 가지 기법의 조합이더라구요. 5절에서 자세히 풀어볼게요.

왜 LLM 추론에 JIT이 잘 어울리나

LLM 추론에서 매트멀을 한다고 합시다. 그런데 LLM 안에는 매트멀 형상이 5가지 정도 있어요 (1편에서 본 qkv, wo, gate, up, down). 각각 (M, K, N) 모양이 다릅니다.

Qwen2.5-0.5B 한 동(layer)의 매트멀 5종 (decode 모드):
  qkv:   (1, 896,  2560)
  wo:    (1, 896,  896)
  gate:  (1, 896,  4864)
  up:    (1, 896,  4864)
  down:  (1, 4864, 896)

도시 건축으로 풀면 — 한 도시 안에 5가지 다른 형태의 동(洞)이 있는 거예요. 어떤 동은 좁고 길고, 어떤 동은 넓고 짧고. 각 동마다 가장 효율적인 시공 방법이 달라요. 좁은 동에는 작은 트럭, 넓은 동에는 큰 트럭이 어울리는 식.

AOT로 짠다면 두 가지 선택지가 있어요.

옵션 A — 일반 시공 도면 1개

모든 형태의 동에서 동작하는 일반 시공 도면 1개. 코드 짧음. 그런데 각 형태에 최적화 안 됨. 형태별로 시공 단계 unroll 정도가 달라야 진짜 빠른데 일반 도면은 그게 안 돼요.

옵션 B — 형태별 도면 5개

(1, 896, 2560)용, (1, 896, 896)용 등 각각 손코딩. ggml(llama.cpp)이 이렇게 짭니다. 매우 빠른데 코드가 5배. 새 형태 추가될 때마다 또 짜야 해요. 새 모델(예: Qwen2.5-1.5B)이 다른 형상 가지면 또 새 커널 작성. 유지 보수 부담이 큼.

옵션 C — JIT 합성 (Lumen의 선택)

일반 IR 1개를 두고, 실행 시점에 (M, K, N)을 보고 그 형태에 정확히 맞는 어셈블리를 생성. 코드는 한 번만 짜는데 각 형태별로 최적화된 결과를 얻어요.

건축으로 풀면 — 건축가 한 명이 5가지 동 형태의 일반 원리만 머리에 두고 현장에 도착해서 그 동의 모양 보고 즉석에서 정확한 시공 도면을 그리는 거예요. 5개 동마다 각각 사전 도면을 만들어 두는 것보다 도면 작성 코드는 적고 결과는 같은 품질.

이게 JIT의 진짜 가치입니다. 한 번 짠 코드가 여러 형태에서 모두 최적화됩니다. ggml은 옵션 B로 가서 수백 줄 손코딩 매트멀 커널이 있고, Lumen은 옵션 C로 가서 IR 자동 합성으로 같은 결과를 얻어요. Lumen의 매트멀 codegen이 약 1000줄 정도인데, 이게 모든 형상에 대응해요.

캐시까지 더하면 – 24번 도면 한 번 그리기

매트멀 형태가 5개라고 했죠. 매번 똑같은 형태가 또 들어옵니다. 첫 번째 호출에서 JIT 컴파일하고, 그 결과를 캐시해 두면 두 번째부터는 컴파일 없이 캐시된 코드를 즉시 실행하면 됩니다.

첫 호출 (1, 896, 2560):
  → IR 변환 → 어셈블리 생성 → 메모리에 쓰기 (약 10밀리초 소요)
  → 실행 (약 300마이크로초)

두 번째 호출 (1, 896, 2560):
  → 캐시 hit → 바로 실행 (300마이크로초만)

LLM 추론은 단어 32개 만든다고 했으니 같은 형태가 24 layer × 5 종 × 32 token = 3840번 호출됩니다. 첫 5번만 컴파일 비용을 내고 나머지 3835번은 캐시 hit. 첫 컴파일 비용을 전체로 나누면 평균 거의 0이에요.

건축으로 풀면 — 같은 모양 동을 24개 짓는다고 합시다. 첫 번째 동 시공할 때 도면 한 번 그리고, 같은 도면을 23번 더 재사용. 24번 도면 그리는 게 아니에요.

Lumen은 MatmulJitCache라는 컬렉션을 가지고 있어서 (M, K, N) 키로 컴파일된 함수 포인터를 보관합니다 (crates/lumen-jit/src/matmul_cache.rs). HashMap 기반인데 단순하지만 효과가 강력해요. 한 모델 한 번 로딩하면 그 모델의 매트멀 형상 모음이 캐시에 다 쌓이고 그 뒤로는 컴파일 없이 추론.

이 캐시가 어찌 보면 6주 사이드의 작은 win 중 하나였어요. 처음엔 캐시 없이 짜다가 측정해 보니 “매번 JIT 비용이 30%야?” 충격이라, 그 다음 commit에서 캐시 추가. 그러니까 첫 호출만 비용이고 나머지 99.87%(3835/3840)는 무료. 이런 작은 인프라가 결과를 크게 바꾸는 사례를 한 번 체험했어요.

메모리에 코드를 쓰고 실행한다는 게 무슨 의미인가

가장 신기한 부분입니다. 보통 우리는 코드를 디스크의 .exe 파일로 알고 있어요. 그런데 JIT은 어셈블리 바이트를 메모리에 직접 쓰고 그걸 함수처럼 호출합니다. 어떻게 가능할까요.

CPU 입장에서 보면 “코드”와 “데이터”의 구분이 본질적으로 없어요. 둘 다 메모리에 저장된 바이트 열입니다. 차이는 운영체제가 메모리 영역에 “실행 가능 권한”을 줬느냐 없느냐예요.

건축으로 풀면 — 같은 건물 안에 시공 도면 종이와 자재 종이가 둘 다 있다고 합시다. 종이 자체는 같은 종이예요. 차이는 공사팀이 어느 종이를 보고 행동하느냐입니다. 공사팀이 “이 종이를 시공 도면으로 본다”고 정해진 종이만 시공 행동에 들어가요. 그 규칙이 메모리 권한과 같아요.

운영체제(Windows, Linux, macOS)는 메모리를 페이지 단위로 관리합니다. 각 페이지에는 권한이 붙어 있어요.

  • R: 읽기 가능
  • W: 쓰기 가능
  • X: 실행 가능

일반 데이터 영역은 R+W. 실행 파일의 코드 영역은 R+X. JIT으로 메모리에 코드를 쓰려면 그 영역에 R+W 권한을 줘서 쓴 다음, R+X 권한으로 바꿔서 실행해야 합니다.

검은 배경의 금색 기어 메커니즘

W^X 보안 정책 – 시공과 입주는 동시에 안 됨

W와 X를 동시에 주는 건 보안상 위험합니다(악성 코드 실행 가능성). 그래서 현대 OS는 “W^X” 정책을 강제해요. Write 또는 eXecute, 둘 중 하나만 가능.

건축으로 풀면 — 시공 중인 곳에는 다른 자재 트럭이 못 들어와요. 시공 끝나고 검수 완료된 곳만 입주 가능. 시공과 입주는 같은 공간에서 동시에 일어나지 않아요. 만약 동시에 가능하다면, 시공 중에 다른 트럭이 들어와서 도면 갈아끼우는 식의 공격이 가능. 그게 OS 입장에선 악성 코드 주입과 같은 패턴이에요.

JIT 코드는 다음 두 단계를 거쳐야 해요.

1. R+W 권한으로 메모리 할당 (도면 작성 가능한 빈 종이 한 장 받기)
2. 어셈블리 바이트 쓰기 (도면 그리기)
3. R+W를 제거하고 R+X로 변경 (시공 가능한 정식 도면으로 인정)
   mprotect (Linux/macOS) / VirtualProtect (Windows) 시스템 콜
4. 그 메모리 주소를 함수 포인터로 캐스팅해서 호출 (시공팀에게 도면 전달)

Lumen에서 이 로직은 crates/lumen-jit/src/exec.rs에 있어요. Rust에서 Windows API 호출하는 unsafe 코드가 약 50줄. 처음 짤 땐 진짜 막막했거든요. Win32 API 문서 보면서 한 줄씩 짰어요. 다 짜고 첫 JIT 코드가 실행됐을 때의 그 감동이 기억나요. “내가 만든 어셈블리 바이트가 진짜로 함수처럼 실행됐다”는 게 진짜 마법 같았어요.

함수 호출이 가능한 이유 – transmute의 마법

“메모리 주소를 함수 포인터로 캐스팅”이 마법 같지만 사실 모든 함수 호출이 본질적으로 같습니다. C나 Rust에서 함수를 호출한다는 건 “그 함수의 시작 주소로 jump 하라”는 명령을 CPU에 내리는 거예요. 함수가 미리 파일에 컴파일됐든, 방금 메모리에 쓰여졌든 CPU에게는 차이가 없어요. 그냥 그 주소로 jump하고 거기 있는 명령을 실행할 뿐.

이걸 Rust 코드로 표현하면 이런 모양이에요.

// 어셈블리 바이트가 메모리 주소 ptr에 쓰여 있고 R+X 권한이 있다고 가정.
// 함수 시그너처는 (a: *const f32, b: *const f32, out: *mut f32) -> ()
type MatmulFn = unsafe extern "C" fn(*const f32, *const f32, *mut f32);

let f: MatmulFn = unsafe { std::mem::transmute(ptr) };
unsafe { f(a.as_ptr(), b.as_ptr(), out.as_mut_ptr()) };

transmute로 메모리 주소를 함수 포인터 타입으로 강제 변환했고, 그 다음 f(...)로 일반 함수처럼 호출합니다. CPU는 ptr 주소로 jump해서 거기 있는 어셈블리를 실행하고, ret 명령을 만나면 호출자로 돌아옵니다.

JIT의 마법은 이 한 줄(transmute)에 다 들어 있어요. 메모리에 어셈블리만 잘 써 넣으면 함수처럼 호출할 수 있습니다.

저도 이거 처음 봤을 땐 “이게 진짜 되네?” 싶었거든요. 메모리 주소를 함수 포인터로 캐스팅하는 게 Rust 같은 안전 언어에서 가능한 게 신기했어요. 그래서 unsafe 블록 안에서만 가능하긴 해요. Rust는 이런 위험한 동작을 안전 영역과 분리해서 관리하거든요. 이게 Rust 선택한 이유 중 하나이기도 했어요. C에서 이렇게 짜면 어디서 unsafe인지 안 보이는데, Rust는 명확히 보이거든요. 디버깅이 쉬워져요.

어셈블리 바이트는 어떻게 만드나

CPU 명령어는 정해진 바이트 인코딩이 있어요. 예를 들어 x86_64에서 mov rax, rbx(rbx의 값을 rax로 복사)는 48 89 D8 세 바이트로 인코딩됩니다. 메모리에 이 세 바이트를 쓰면 CPU가 그걸 mov 명령으로 해석해요.

복잡한 명령은 더 긴 바이트 시퀀스입니다. SIMD 명령어 vfmadd231ps ymm0, ymm1, [r13] 같은 건 5~6바이트로 인코딩되고, 명령어의 다양한 옵션(레지스터 번호, 메모리 offset 등)에 따라 바이트가 달라져요.

건축으로 풀면 — 시공 도면에서 “벽돌 N개, 시멘트 M kg”이 정해진 기호로 표시되는 것과 같아요. 똑같은 “벽돌 N개”라도 도면에 표시하는 방법이 정해져 있고, 그 표시를 보고 공사팀이 실제로 N개를 들고 옵니다. 기호와 의미의 약속이 정해져 있는 거.

Lumen은 crates/lumen-codegen/src/avx2_enc.rs에 약 50개 SIMD 명령어 인코더 함수를 가지고 있습니다. 각 함수가 명령어의 한 가지 변형을 바이트로 인코딩해요. 예를 들면:

// vfmadd231ps ymm_acc, ymm_a, [base + index*scale + disp32]
pub fn vfmadd231ps_mem(
    em: &mut Emitter,
    acc: Ymm,
    a: Ymm,
    base: Reg,
    index: Option<(Reg, Scale)>,
    disp: i32,
) {
    // VEX 인코딩 헤더 (3바이트)
    // opcode (1바이트)
    // ModR/M + SIB + disp32 (5~7바이트)
    ...
}

Emitter는 단순히 Vec<u8> wrapper입니다. 한 명령어를 인코딩한다는 건 이 Vec 끝에 적절한 바이트들을 push한다는 뜻이에요. 매트멀 함수 하나를 만든다는 건 약 50개 인코더 함수를 적절한 순서로 호출해서 Vec을 채우는 작업이고요. 다 채우고 나면 메모리 페이지 할당해서 거기에 복사하고, R+X 권한으로 바꾸고, 함수 포인터로 캐스팅하면 끝입니다.

한 매트멀 함수의 어셈블리 크기

작은 매트멀 함수(M=1, K=896, N=8) 어셈블리는 약 200~400 바이트입니다. 함수 한 개당 1KB 미만. 형태별로 5개 가지고 있어도 합쳐서 수 KB 정도. 메모리 부담이 거의 없어요. 캐시 hit률이 매우 높기도 하고요.

CPU 명령어 인코딩 자체를 손으로 짜는 건 처음엔 막막해요. Intel SDM(Software Developer’s Manual)이라는 약 2000페이지짜리 문서를 봐야 하거든요. PDF로 받으면 100MB 정도. 그런데 한 명령어 짜는 데 익숙해지면 다음 명령어부터는 패턴이 보입니다. AVX2 명령어 50개를 약 50시간에 다 짤 수 있어요(Lumen은 그렇게 짰습니다). 건축으로 풀면 — 첫 시공 도면 기호 익히는 데 며칠 걸리지만 한 번 익히면 그 다음부터는 새 기호도 패턴으로 빠르게 익혀요.

저는 이 50시간 작업을 매일 1~2시간씩 약 한 달 잡고 갔어요. 첫 일주일은 진짜 더디게 갔거든요. VEX 인코딩 헤더가 3바이트인데 그 안에 R, X, B, vvvv, L, pp 등 비트 필드가 한 가득. 그걸 정확히 셋팅해야 명령어가 작동. 한 명령어 인코딩에 1~2시간 걸리던 게 둘째 주부터는 30분, 셋째 주부터는 10분으로 줄었어요. 이런 게 직접 짜본 사람만 얻는 직관이에요.

JIT의 단점 – 만능은 아니다

JIT이 만능은 아닙니다. AOT 대비 단점도 있어요. 솔직히 짤 때 이거 다 인지하고 가야 해요.

1. 첫 호출이 느리다

컴파일 시간 자체가 들어가요. 단일 매트멀 컴파일은 10밀리초 정도라 작지만, 함수 종류가 100개라면 첫 실행이 1초 늦어집니다. LLM은 함수 종류가 5~7개로 적어서 큰 부담은 아니에요. 건축으로 풀면 — 현장 도착해서 도면 그리는 시간이 사전 도면 가져오는 것보다는 느려요. 다만 그 도면을 24번 재사용하니까 평균은 거의 무료.

2. 메모리에 코드가 추가로 쌓인다

캐시된 함수들이 메모리를 차지해요. 작은 함수라 영향 적지만 무한히 쌓일 수는 없습니다. Lumen은 함수 5~7개라 메모리 부담 거의 없지만, 더 큰 LLM에서는 캐시 eviction(오래된 거 비우기) 정책 필요해요. 다행히 우리 6주 사이드에서는 그 정도 규모까지 안 가서 단순 HashMap으로 충분.

3. 디버깅이 어렵다

JIT 코드는 디버거가 추적하기 어려워요. gdblldb 같은 도구가 JIT 영역의 함수를 잘 못 봅니다. 디버깅용 별도 도구가 필요해요. Lumen에서도 매트멀 결과가 이상할 때 어디서 어셈블리가 잘못됐는지 찾기가 진짜 까다로웠어요. 그래서 매 JIT 함수를 짠 뒤 naive 구현과 비트 단위 비교하는 자동 테스트가 필수.

이거 체득한 게 Lumen에서 큰 자산 중 하나예요. JIT 짜는데 자동 검증 없이 가면 99% 막혀요. 짜고 측정하고 정답 다른 거 보고 “어디가 문젠지” 알 방법이 없거든요. naive 구현이 정답 oracle 역할을 해줘서 살았어요.

4. 보안 제약

W^X 정책 때문에 OS API 호출 필요. iOS 같은 일부 환경은 JIT 자체가 거의 금지됩니다(Just-In-Time이 허용되는 앱이 거의 없음). Lumen은 데스크탑 Linux/Windows/macOS 위에서 돌리는 거라 제약 없지만, 모바일 LLM 추론을 한다면 JIT이 옵션에서 빠져야 해요.

5. 복잡도가 늘어난다

JIT 인프라(메모리 할당, 권한 변경, 캐시 관리, codegen)가 모두 직접 짜는 거라 코드 양 자체가 늘어요. 작은 프로젝트라면 AOT가 더 단순합니다. Lumen은 학습 목적이라 복잡도가 자산이 되지만, 그냥 도구가 목표면 AOT로 가는 게 보통.

LLM 추론은 JIT의 장점(형상별 최적화 + 캐시 hit률)이 단점을 충분히 압도하는 케이스라서 적절한 선택이었어요. 일반 앱이라면 굳이 JIT 안 써도 됩니다.

다른 분야에서 쓰는 JIT – 이거 우리 일상에 이미 있다

JIT은 Lumen만의 기술이 아니라 여러 분야에서 쓰여요. 사실 우리가 매일 사용하는 도구들이 JIT을 내부적으로 쓰고 있어요.

  • Java/.NET: 바이트코드를 JIT으로 네이티브 코드로 변환. 자바 컴파일이 빠른 이유. HotSpot JIT 같은 게 매우 성숙한 인프라.
  • JavaScript V8 엔진(Chrome, Node.js): JS는 동적 타입이라 런타임에 타입 보고 최적화 필요. JIT 없이는 매우 느려요. V8이 JIT의 정점.
  • PyTorch torch.compile, TensorFlow XLA: 텐서 연산을 JIT으로 융합 커널 합성. Lumen과 같은 분야.
  • 데이터베이스 (PostgreSQL, ClickHouse): 쿼리 실행 계획에서 매번 다른 SQL을 JIT으로 컴파일. 빠른 데이터베이스의 비밀 중 하나.
  • 정규표현식 엔진(PCRE): 정규식 패턴을 JIT으로 매칭 코드 생성.
  • LuaJIT: Lua 스크립트의 JIT 컴파일러. 게임 엔진에서 자주 사용.

이 모든 분야의 공통점은 “입력 데이터의 모양에 따라 최적 코드가 다르다”는 것입니다. 모양을 실행 시점에 알 수 있을 때 JIT이 빛납니다. 사전엔 어떤 모양인지 모르고 일반 코드 만드는 것보다, 실행 시점에 모양을 알고 정확히 맞춤 코드 만드는 게 훨씬 효율.

크롬 브라우저에서 JavaScript가 자바보다 빠를 수도 있다는 사실 알고 계셨나요? 그게 V8 JIT의 위력이에요. 런타임에 hot path 식별해서 그 부분만 적극 최적화. 같은 코드라도 사용 패턴 보고 다른 어셈블리 생성. 진짜 똑똑한 시스템이에요.

4편 정리

  1. JIT은 프로그램 실행 중에 어셈블리를 만들어 즉시 실행하는 컴파일 방식. 일반 AOT(사전 계획 시공)와 달리 도면 작성과 실제 시공이 같은 프로세스에서. “공사 현장에 도착해서 그 자리 지형 보고 즉석에서 도면 그리기”.
  2. LLM 추론에 어울리는 이유: 매트멀 형태별로 코드가 다르면 최적인데 그 형태가 실행 시점에 결정됨. JIT은 형태별 최적화 코드를 자동으로 합성. 같은 모양 동 24개 짓는데 도면 한 번만 그리고 재사용.
  3. 메모리에 코드를 쓰고 실행하는 메커니즘: R+W 권한으로 메모리 할당 → 어셈블리 바이트 쓰기 → R+X 권한으로 변경 → 함수 포인터 캐스팅으로 호출. “시공 중에는 다른 자재 못 들어오고, 시공 끝나야 입주 가능”.
  4. CPU 입장에서 코드와 데이터의 차이는 권한뿐. 명령어는 정해진 바이트 인코딩. 바이트를 적절히 메모리에 써 두면 그게 곧 함수. 시공 도면 종이와 자재 종이가 같은 종이지만 공사팀이 어느 종이를 보고 행동하느냐가 다른 것과 같음.
  5. 단점도 있음: 첫 호출 컴파일 비용, 디버깅 어려움, 일부 환경(iOS)에서 금지, 코드 복잡도 증가. 장점이 단점을 압도할 때만 적합.

이 편이 시리즈 중에서 가장 신비한 영역을 다룬 것 같아요. “컴퓨터가 자기 자신을 위한 코드를 짠다”는 게 처음 들으면 진짜 마법이지만, 까보면 OS의 메모리 권한 관리와 함수 포인터 캐스팅이라는 두 가지 평범한 기법의 조합이에요. 이 마법이 풀린 순간이 사이드 4주차쯤이었어요. 그 전까진 JIT을 “뭔가 마법” 영역으로 두고 있었는데, 직접 짜보니 의외로 간단했어요.

다음 편 미리보기

5편은 SIMD (Single Instruction Multiple Data)를 다룹니다. CPU 한 명령으로 여러 숫자를 동시 처리하는 기술. 매트멀이 빨라지는 진짜 이유의 핵심이에요. 건축으로 풀면 “1차선 도로 vs 8차선 도로”의 차이. AVX2, AVX-512 같은 단어가 등장하고, 한 명령어로 8개 또는 16개 float을 한 번에 곱하는 신기한 동작을 봅니다.

4편에서 본 어셈블리 인코딩이 SIMD에서 어떻게 응용되는지도 자연스럽게 이어집니다. 그리고 2편에서 짧게 언급했던 AVX-512 ZMM이 Zen 4에서 -4.5% 회귀라는 의외 결과도 5편에서 본격적으로 풀어볼게요.

이 시리즈 본문 전체는 GitHub에도 정리해 뒀어요. github.com/redchupa/lumen/docs/tutorial에서 원문 격으로 보실 수 있어요.

그럼 다음 편에서 뵐게요. JIT 또는 컴파일러 관련해서 궁금한 거 있으시면 댓글 남겨주세요.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤