(KO) PostgreSQL JIT — LLVM 기반 표현식 및 튜플 변환 컴파일
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”관계형 엔진은 CPU 예산의 상당 부분을 “질의를 처리하는 것”이 아니라 범용성을 유지하는 것에 쓴다. WHERE a.col = 3 같은 술어를 평가하는 데 원론적으로 필요한 기계어 명령은 많지 않다. 컬럼을 읽고, 상수와 비교하고, 분기하면 된다. 그런데 범용 인터프리터는 어떤 표현식 트리에도, 어떤 테이블 구조에도, 어떤 확장의 연산자에도 대응해야 하므로 수백 사이클을 소비한다. 이 범용성의 대가가 간접 호출(OID로 조회한 연산자 구현으로의 디스패치), 예측 불가 분기(튜플마다 다음 인터프리터 스텝을 switch로 결정), 메모리 트래픽(범용 구조체로 Datum/isnull 쌍을 전달)으로 나타난다. 단발성 OLTP 구문에서 이 오버헤드는 무시할 수 있다. 수백만 개 튜플을 같은 표현식으로 처리하는 분석 질의에서는 지배적인 비용이 된다.
JIT(Just-in-Time) 컴파일은 이 오버헤드를 질의 실행 시점에 해당 표현식과 테이블에 특화된 네이티브 함수를 생성해 제거한다. PostgreSQL jit/README는 이를 “어떤 형태의 인터프리티드 프로그램 평가를 네이티브 프로그램으로 변환하되, 런타임에 수행하는 것”으로 정의한다. 파싱된 표현식, 구체적인 TupleDesc, 해석된 연산자 OID 등 특화 입력이 런타임 전에는 알 수 없으므로 AOT(ahead-of-time)가 아닌 JIT다. 설계 공간은 세 가지 속성으로 정의된다.
-
무엇을 특화할 것인가. JIT 컴파일러는 런타임에 알게 된 사실을 코드에 구워넣을 수 있을 때만 오버헤드를 제거한다. 표현식 평가에서는 표현식 트리 구조와 연산자 함수 주소가 플랜이 확정되는 순간 상수가 되므로 간접 디스패치가 직접(인라인 가능) 호출로 바뀐다. 튜플 변환에서는 컬럼 수, 고정 폭, NOT NULL 여부, 정렬 규칙이
TupleDesc의 속성이므로 “속성마다 디스크립터를 참조하는 루프”가 대부분의 분기를 컴파일 시점에 해소한 직선 코드로 전개된다. -
컴파일 비용을 언제 부담할 것인가. 컴파일은 공짜가 아니다. IR 생성, 최적화, 기계어 방출에 밀리초 단위 시간이 걸리며, 이는 단순한 질의 기준으로 영원에 가깝다. 컴파일이 상환되려면 생성된 코드가 충분히 많이 실행되어야 한다. 비용 모델이 필요하다. 예상 작업량이 충분히 클 때만 컴파일한다.
-
인터프리터와 컴파일러를 어떻게 동기화할 것인가. 손으로 작성된 인터프리터용 JIT는 사실상 같은 의미론의 두 번째 구현이다. 인터프리터에 컴파일러가 모르는 연산자가 추가되면 결과가 갈린다. 유지 가능한 설계는 두 구현의 구조를 평행하게 유지한다. 각 옵코드마다 하나의
case, 각case가 인터프리터의 대응 분기가 하는 일과 정확히 같은 IR을 방출한다. 공통 헬퍼 함수와 타입 정의를 공유해 중복을 피한다.
Database System Concepts(Silberschatz 외)는 표현식과 술어 평가를 이터레이터 모델의 내부 루프로 다루며, “컴파일된” 평가가 System R의 액세스 모듈 생성으로 거슬러 올라가는 역사적 대안임을 지적한다. 현대 계보인 Krikellas 외의 holistic 질의 컴파일과 Neumann의 HyPer produce/consume 모델은 단일 표현식이 아닌 파이프라인 전체를 컴파일하는 방향으로 나아간다. PostgreSQL은 의도적으로 보수적인 지점을 선택한다. 실행기는 인터프리터로 남기고, 분석 질의에서 “흔히 주요 CPU 병목”인 두 핫 스팟(표현식 평가와 튜플 변환)만 JIT 컴파일한다. 두 핫 스팟은 나머지 엔진과의 인터페이스가 좁고 명확하다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”기존 인터프리터 기반 실행기에 JIT를 덧붙이는 엔진들이 공유하는 엔지니어링 패턴을 정리한다. PostgreSQL의 구체적 선택이 이 공유 공간 안에서 어떤 위치인지 파악하기 위해서다.
얇은 래퍼 뒤의 프로바이더 추상화
섹션 제목: “얇은 래퍼 뒤의 프로바이더 추상화”JIT 백엔드는 무거운 의존성이다. LLVM은 수십 메가바이트의 C++ 코드를 동반하며, 모든 배포 환경이 이를 코어 서버 바이너리에 링크하길 원하지는 않는다. 일반적인 해법은 간접 계층이다. 실행기는 의존성 없는 작은 래퍼(jit_compile_expr)를 호출하고, 이 래퍼는 고정 콜백 인터페이스를 구현한 프로바이더에게 위임한다. 프로바이더는 별도 공유 객체로 로드된다. 메인 바이너리는 컴파일러 의존성에서 자유롭고, OS 패키지로 컴파일러를 따로 제공할 수 있으며, 프로바이더를 교체할 수 있다는 보너스도 있다.
옵코드 단위로 인터프리터를 반영하는 구조
섹션 제목: “옵코드 단위로 인터프리터를 반영하는 구조”컴파일러와 인터프리터는 의미론에 동의해야 한다. 두 구현이 같은 모양을 갖도록 하는 것이 오류 가능성을 최소화하는 방법이다. 옵코드 열거형에 같은 switch, 각 arm이 인터프리터의 대응 분기와 같은 동작을 하는 IR을 방출한다. 드물거나 복잡한 스텝은 IR로 재구현하지 않는다. 생성된 코드가 기존 인터프리터 헬퍼(ExecEval*)를 직접 콜백하므로, 자주 쓰이는 스텝만 전용 IR을 얻고 긴 꼬리는 C 구현을 재사용한다.
디스크립터로 튜플 접근을 특화하는 패턴
섹션 제목: “디스크립터로 튜플 접근을 특화하는 패턴”Deforming이란 디스크에 압축된 튜플을 Datum 배열과 isnull 플래그로 펼치는 작업이다. 범용 구현은 컬럼마다 디스크립터에서 길이, 정렬, null 비트맵 위치를 확인한다. 스캔 대상 디스크립터는 고정이므로, JIT는 고정 폭 컬럼을 상수 포인터 증분으로 대체하고, NOT NULL 컬럼의 null 비트맵 확인을 생략하고, 알려진 정렬을 바탕으로 컬럼별 align 연산을 없애는 디스크립터 특화 deform 루틴을 생성할 수 있다. “프로그램이 아닌 프로그램”의 사례다. deforming은 표현식이 아니지만 컴파일 시점 지식으로 얻는 이득이 크다.
연산자 본문 인라이닝
섹션 제목: “연산자 본문 인라이닝”표현식 평가에서 가장 큰 성능 이득은 SQL 연산자 구현(int4eq, float8pl 등)으로의 호출을 없애고 그 본문을 인라인해 상수 폴딩과 죽은 분기 제거를 노출하는 것이다. 인라이닝을 위해 모든 연산자의 복사본을 별도로 유지하는 것은 불가능하다. 표준 기법은 엔진의 C 연산자 소스를 빌드 시점에 LLVM bitcode로 컴파일해 바이너리 옆에 설치하고, JIT가 필요할 때 그 bitcode에서 연산자 정의를 꺼내 쓰는 것이다.
비용 조건부, 지연 방출 컴파일
섹션 제목: “비용 조건부, 지연 방출 컴파일”컴파일은 비싸므로 엔진은 비용 추정값으로 컴파일 여부를 결정한다. 플래너가 이미 총 플랜 비용을 계산하므로 그 비용에 임계치를 두면 추가 계측 없이 트리거를 얻는다. 방출 오버헤드 절감을 위해 성숙한 설계는 방출을 지연한다. 함수는 플랜 초기화 시 모듈에 정의되지만 기계어는 실제로 처음 호출되는 시점에 생성된다. 한 질의 분량의 함수가 한꺼번에 방출된다.
flowchart TD
subgraph plan["플래너 — standard_planner"]
cost["top_plan->total_cost"] --> gate{"비용 ><br/>jit_above_cost?"}
gate -- 아니오 --> none["jitFlags = PGJIT_NONE<br/>(순수 인터프리터)"]
gate -- 예 --> flags["PGJIT_PERFORM 설정<br/>+ EXPR / DEFORM<br/>+ OPT3 / INLINE<br/>추가 임계치로 결정"]
end
subgraph exec["실행기 — ExecInitNode 시점"]
flags --> compile["jit_compile_expr(state)"]
compile --> provider["llvmjit.so 로드<br/>via provider_init()"]
provider --> emitIR["각 ExprState에<br/>LLVM IR 방출"]
end
subgraph run["첫 번째 평가"]
emitIR --> lazy["ExecRunCompiledExpr<br/>-> llvm_get_function"]
lazy --> machine["LLVM이 모듈 전체의<br/>기계어 방출"]
machine --> fast["이후 호출은<br/>네이티브 함수 직접 실행"]
end
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”PostgreSQL JIT는 위에서 설명한 계층 설계를 그대로 따른다. 모든 서버에 컴파일되는 프로바이더 독립 코어(src/backend/jit/jit.c)와, 별도로 로드 가능한 llvmjit 공유 라이브러리로 제공되는 LLVM 특화 프로바이더(src/backend/jit/llvm/)로 나뉜다. README는 “JIT를 수행하려는 코드는 jit.c에 위치한 LLVM 독립 래퍼를 호출한다”고 명시하며, 래퍼가 “JIT 프로바이더를 로드할 수 없는 경우 실패해도 된다”고 설명한다. 이 실패 허용이 핵심이다. LLVM 없이 빌드하거나 LLVM 패키지를 설치하지 않은 환경은 단순히 인터프리터로 실행된다.
프로바이더 인터페이스와 지연 라이브러리 로드
섹션 제목: “프로바이더 인터페이스와 지연 라이브러리 로드”프로바이더 계약은 세 개의 함수 포인터로 구성된다. 프로바이더의 init 진입점이 이를 채우고 단일 정적 구조체에 저장한다.
// _PG_jit_provider_init — src/backend/jit/llvm/llvmjit.cvoid_PG_jit_provider_init(JitProviderCallbacks *cb){ cb->reset_after_error = llvm_reset_after_error; cb->release_context = llvm_release_context; cb->compile_expr = llvm_compile_expr;}코어는 LLVM 심볼을 직접 참조하지 않는다. 오직 이 포인터로만 호출한다. 로드는 지연되고 한 번만 수행된다. provider_init()은 공유 라이브러리를 디스크에서 탐색하고, 성공과 실패 모두 캐시해 프로바이더 부재가 표현식마다 재시도되지 않도록 한다.
// provider_init — src/backend/jit/jit.cif (!jit_enabled) return false;if (provider_failed_loading) // never retry a known failure return false;if (provider_successfully_loaded) return true;snprintf(path, MAXPGPATH, "%s/%s%s", pkglib_path, jit_provider, DLSUFFIX);if (!pg_file_exists(path)) { // probe before dlopen, which would ERROR provider_failed_loading = true; return false;}provider_failed_loading = true; // assume failure until init() returnsinit = (JitProviderInit) load_external_function(path, "_PG_jit_provider_init", true, NULL);init(&provider);provider_successfully_loaded = true;provider_failed_loading = false;jit_provider GUC(기본값 "llvmjit")가 라이브러리 이름을 결정한다. 같은 _PG_jit_provider_init 심볼을 구현하는 다른 공유 객체로 교체할 수 있는 진정한 플러그인 구조다.
진입 관문: jit_compile_expr
섹션 제목: “진입 관문: jit_compile_expr”모든 JIT 컴파일 요청은 코어의 jit_compile_expr를 통과한다. per-query 플래그를 확인하고 프로바이더에 위임할지 결정하는 단일 장소다.
// jit_compile_expr — src/backend/jit/jit.cif (!state->parent) // need an EState lifetime return false;if (!(state->parent->state->es_jit_flags & PGJIT_PERFORM)) return false;if (!(state->parent->state->es_jit_flags & PGJIT_EXPR)) return false;if (provider_init()) // also checks !jit_enabled return provider.compile_expr(state);return false;첫 번째 가드가 중요하다. parent PlanState가 없는 ExprState는 JIT 컨텍스트의 수명을 묶을 EState가 없으므로 컴파일되지 않는다. 트랜잭션이 끝날 때까지 컴파일된 함수가 누수되는 상황을 막는다. 모든 가드를 통과하면 provider.compile_expr(즉 llvm_compile_expr)가 인계받는다.
인터프리터를 반영하는 옵코드별 IR 생성
섹션 제목: “인터프리터를 반영하는 옵코드별 IR 생성”llvm_compile_expr은 구조적으로 execExprInterp.c의 ExecInterpExpr를 복제한다. 선형화된 ExprState->steps[] 배열을 걸으며 switch (opcode)로 각 스텝의 IR을 방출한다. 인터프리터와 동일한 C 시그니처를 갖는 LLVM 함수를 먼저 생성하고(llvmjit_types.c에서 타입을 가져와 동기화를 보장), 스텝끼리 서로 분기할 수 있도록 스텝마다 기본 블록을 미리 만든다.
// llvm_compile_expr — src/backend/jit/llvm/llvmjit_expr.ceval_fn = LLVMAddFunction(mod, funcname, llvm_pg_var_func_type("ExecInterpExprStillValid"));LLVMSetLinkage(eval_fn, LLVMExternalLinkage);llvm_copy_attributes(AttributeTemplate, eval_fn);/* ... load v_state, v_econtext, slot value/null arrays ... */opblocks = palloc(sizeof(LLVMBasicBlockRef) * state->steps_len);for (int opno = 0; opno < state->steps_len; opno++) opblocks[opno] = l_bb_append_v(eval_fn, "b.op.%d.start", opno);LLVMBuildBr(b, opblocks[0]); // jump into first stepfor (int opno = 0; opno < state->steps_len; opno++) { op = &state->steps[opno]; opcode = ExecEvalStepOp(state, op); LLVMPositionBuilderAtEnd(b, opblocks[opno]); switch (opcode) { /* one arm per ExprEvalOp ... */ }}이미 변환된 컬럼 변수를 읽는 핫 스텝은 슬롯의 tts_values/tts_isnull 배열에서 직접 로드하는 코드로 방출된다. 함수 호출이 전혀 없다.
// EEOP_*_VAR — src/backend/jit/llvm/llvmjit_expr.cv_attnum = l_int32_const(lc, op->d.var.attnum);value = l_load_gep1(b, TypeSizeT, v_values, v_attnum, "");isnull = l_load_gep1(b, TypeStorageBool, v_nulls, v_attnum, "");LLVMBuildStore(b, value, v_resvaluep);LLVMBuildStore(b, isnull, v_resnullp);LLVMBuildBr(b, opblocks[opno + 1]);함수 호출 스텝(EEOP_FUNCEXPR_STRICT)은 인수마다 기본 블록 하나씩 구성하는 명시적 null 검사 체인을 방출한다. 모든 인수가 non-null일 때만 실제 호출로 진행하는 strict 의미론을 루프로 구현하는 인터프리터를 그대로 복제한다.
// EEOP_FUNCEXPR_STRICT — src/backend/jit/llvm/llvmjit_expr.cfor (int argno = 0; argno < op->d.func.nargs; argno++) { LLVMPositionBuilderAtEnd(b, b_checkargnulls[argno]); b_argnotnull = (argno + 1 == op->d.func.nargs) ? b_nonull : b_checkargnulls[argno + 1]; v_argisnull = l_funcnull(b, v_fcinfo, argno); // load fcinfo->args[i].isnull LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, v_argisnull, l_sbool_const(1), ""), opblocks[opno + 1], // any null -> skip call b_argnotnull);}LLVMPositionBuilderAtEnd(b, b_nonull);v_retval = BuildV1Call(context, b, mod, fcinfo, &v_fcinfo_isnull);드물거나 복잡한 스텝은 IR로 재구현하지 않는다. 생성된 코드가 build_EvalXFunc 매크로로 인터프리터 헬퍼를 직접 호출한다. “반영하되 긴 꼬리는 재사용” 패턴의 구체적 표현이다.
// EEOP_PARAM_EXTERN — src/backend/jit/llvm/llvmjit_expr.ccase EEOP_PARAM_EXTERN: build_EvalXFunc(b, mod, "ExecEvalParamExtern", v_state, op, v_econtext); LLVMBuildBr(b, opblocks[opno + 1]); break;직접 호출 및 인라인 가능 연산자 호출: BuildV1Call
섹션 제목: “직접 호출 및 인라인 가능 연산자 호출: BuildV1Call”간접 연산자 디스패치를 직접 호출로 바꾸는 메커니즘이 BuildV1Call과 llvm_function_reference다. 직접 호출은 나중에 인라이닝이 가능한 선결 조건이다. 심볼이 확인되는 연산자(fmgr_symbol이 해석)는 명명된 함수(int4eq, pgextern.<module>.<fn> 등)로 직접 참조된다. 불투명 함수 포인터만 상수 포인터 로드로 폴백한다. 결과는 인라이너가 나중에 연산자 본문으로 치환할 수 있는 명명된 callee를 갖는 IR이다.
// BuildV1Call — src/backend/jit/llvm/llvmjit_expr.cv_fn = llvm_function_reference(context, b, mod, fcinfo);v_fcinfo = l_ptr_const(fcinfo, l_ptr(StructFunctionCallInfoData));v_fcinfo_isnullp = l_struct_gep(b, StructFunctionCallInfoData, v_fcinfo, FIELDNO_FUNCTIONCALLINFODATA_ISNULL, "");LLVMBuildStore(b, l_sbool_const(0), v_fcinfo_isnullp);v_retval = l_call(b, LLVMGetFunctionType(AttributeTemplate), v_fn, &v_fcinfo, 1, "funccall");LLVM 22 미만에서는 수명 종료 어노테이션을 방출해 옵티마이저에게 인수 메모리를 호출 이후까지 보존할 필요가 없음을 알린다. 인라이너가 불필요한 store를 제거할 가능성을 높인다.
술어 스텝(EEOP_QUAL, WHERE 절 단락 평가)은 인터프리터에서 C if/goto로 표현된 제어 흐름이 IR에서 명시적 기본 블록 분기가 되는 방식을 보여준다. null이거나 false인 결과는 qualfail 블록으로 점프해 결과를 non-null false로 정규화하고 jumpdone 타겟으로 이동한다. true 결과는 그냥 다음으로 진행한다.
// EEOP_QUAL — src/backend/jit/llvm/llvmjit_expr.cv_resvalue = l_load(b, TypeSizeT, v_resvaluep, "");v_resnull = l_load(b, TypeStorageBool, v_resnullp, "");v_nullorfalse = LLVMBuildOr(b, LLVMBuildICmp(b, LLVMIntEQ, v_resnull, l_sbool_const(1), ""), LLVMBuildICmp(b, LLVMIntEQ, v_resvalue, l_sizet_const(0), ""), "");LLVMBuildCondBr(b, v_nullorfalse, b_qualfail, opblocks[opno + 1]);LLVMPositionBuilderAtEnd(b, b_qualfail);LLVMBuildStore(b, l_sbool_const(0), v_resnullp); /* result not null */LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); /* result is false */LLVMBuildBr(b, opblocks[op->d.qualexpr.jumpdone]); /* short-circuit out */EEOP_CONST는 “런타임에 알게 된 사실을 코드에 구워넣는다”는 원칙을 가장 직접적으로 표현한다. 플랜이 확정되는 순간 고정되는 상수의 Datum과 null 플래그를 IR 상수로 방출한다. 옵티마이저는 나중에 이 상수를 소비하는 연산자를 상수 폴딩할 수 있다.
// EEOP_CONST — src/backend/jit/llvm/llvmjit_expr.cv_constvalue = l_sizet_const(op->d.constval.value);v_constnull = l_sbool_const(op->d.constval.isnull);LLVMBuildStore(b, v_constvalue, v_resvaluep);LLVMBuildStore(b, v_constnull, v_resnullp);TupleDesc에 특화된 튜플 변환
섹션 제목: “TupleDesc에 특화된 튜플 변환”튜플 변환 JIT는 “런타임에 알게 된 사실에 특화한다”는 원칙의 가장 명확한 사례다. slot_compile_deform은 TupleTableSlot * 하나만 받아 특정 튜플 형태를 natts 컬럼까지 변환하는 함수를 생성한다. 처리할 수 없는 슬롯 종류는 생성 자체를 거부한다. 가상 슬롯은 변환이 필요 없고, 힙/버퍼 힙/미니멀 슬롯만 지원한다.
// slot_compile_deform — src/backend/jit/llvm/llvmjit_deform.cif (ops == &TTSOpsVirtual) return NULL;if (ops != &TTSOpsHeapTuple && ops != &TTSOpsBufferHeapTuple && ops != &TTSOpsMinimalTuple) return NULL;IR 방출 전에 디스크립터를 분석해 컬럼별 두 가지 컴파일 시점 사실을 파악한다. 마지막으로 보장된 컬럼(NOT NULL이고 missing 값 없고 삭제되지 않은 컬럼의 후행 연속)까지는 튜플의 natts 확인 없이 읽을 수 있다. 현재까지 알려진 정렬은 고정 폭 컬럼이 연속되는 동안 유지되며, 가변 길이나 nullable 컬럼이 나타나는 순간 불확실해진다.
// slot_compile_deform — src/backend/jit/llvm/llvmjit_deform.cif (att->attnullability == ATTNULLABLE_VALID && !att->atthasmissing && !att->attisdropped) guaranteed_column_number = attnum; // can skip natts check up to here/* ... */if (att->attlen < 0) { // varlena: alignment now unknown known_alignment = -1; attguaranteedalign = false;} else if (att->attnullability == ATTNULLABLE_VALID && attguaranteedalign && known_alignment >= 0) { known_alignment += att->attlen; // offset stays a constant}저장 루프는 컬럼마다 위 사실에 따라 다른 형태의 코드를 방출한다. 값 타입은 정확한 정수 폭으로 온디스크 바이트를 로드해 Datum으로 부호 확장한다. 참조 타입은 데이터 포인터를 저장한다. 데이터 포인터 전진은 고정 폭 컬럼에서 상수로, varlena와 cstring에서만 varsize_any/strlen 호출로 처리한다.
// slot_compile_deform store loop — src/backend/jit/llvm/llvmjit_deform.cif (att->attbyval) { LLVMTypeRef vartype = LLVMIntTypeInContext(lc, att->attlen * 8); v_tmp_loaddata = l_load(b, vartype, LLVMBuildPointerCast(b, v_attdatap, LLVMPointerType(vartype, 0), ""), ""); v_tmp_loaddata = LLVMBuildSExt(b, v_tmp_loaddata, TypeSizeT, ""); LLVMBuildStore(b, v_tmp_loaddata, v_resultp);} else { LLVMBuildStore(b, LLVMBuildPtrToInt(b, v_attdatap, TypeSizeT, "attr_ptr"), v_resultp);}if (att->attlen > 0) v_incby = l_sizet_const(att->attlen); // constant strideelse if (att->attlen == -1) { v_incby = l_call(b, llvm_pg_var_func_type("varsize_any"), llvm_pg_func(mod, "varsize_any"), &v_attdatap, 1, ""); l_callsite_alwaysinline(v_incby); // mark varsize_any for inlining}deform 함수는 모든 스캔에 앞서 미리 생성되지 않는다. PGJIT_DEFORM이 설정된 상태에서 컴파일된 표현식의 EEOP_*_FETCHSOME 스텝이 처음 실행될 때 요청에 따라 생성된다. fetch 스텝은 슬롯에 이미 충분한 속성이 변환돼 있는지(tts_nvalid >= last_var) 확인하고, 부족할 때만 deform 호출로 분기한다.
// EEOP_*_FETCHSOME — src/backend/jit/llvm/llvmjit_expr.cv_nvalid = l_load_struct_gep(b, StructTupleTableSlot, v_slot, FIELDNO_TUPLETABLESLOT_NVALID, "");LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntUGE, v_nvalid, l_int16_const(lc, op->d.fetch.last_var), ""), opblocks[opno + 1], b_fetch); // already deformed -> skip/* ... in b_fetch: */if (tts_ops && desc && (context->base.flags & PGJIT_DEFORM)) l_jit_deform = slot_compile_deform(context, desc, tts_ops, op->d.fetch.last_var);if (l_jit_deform) // call the specialized fn l_call(b, LLVMGetFunctionType(l_jit_deform), l_jit_deform, params, 1, "");else // fall back to interpreter l_call(b, llvm_pg_var_func_type("slot_getsomeattrs_int"), llvm_pg_func(mod, "slot_getsomeattrs_int"), params, 2, "");bitcode에서 연산자 본문 인라이닝
섹션 제목: “bitcode에서 연산자 본문 인라이닝”PGJIT_INLINE이 설정되면 llvm_compile_module이 최적화 전에 llvm_inline을 실행한다. 인라이너는 각 연산자의 두 번째 복사본을 유지하지 않는다. 빌드 시점에 엔진의 C 소스를 LLVM bitcode로 컴파일해 $pkglibdir/bitcode/postgres/ 아래에 설치하고 요약 인덱스를 만든다. llvm_inline은 임포트 계획을 수립한다. 모듈 안의 외부 함수 참조 중 크기가 충분히 작고 bitcode에서 이용 가능한 것을 찾아 그 정의를 가져온다.
// llvm_inline — src/backend/jit/llvm/llvmjit_inline.cppllvm_inline(LLVMModuleRef M){ llvm::Module *mod = llvm::unwrap(M); std::unique_ptr<ImportMapTy> globalsToInline = llvm_build_inline_plan(lc, mod); if (!globalsToInline) return; llvm_execute_inline_plan(mod, globalsToInline.get());}BuildV1Call이 방출한 직접 호출이 실제 심볼 이름으로 연산자를 참조하므로, 인라이너는 bitcode에서 대응하는 정의를 찾아 본문을 삽입한다. 이후 이제 보이게 된 연산자 로직에 상수 폴딩과 죽은 분기 제거가 적용된다. README는 이것이 JIT의 “가장 큰 이점”으로, PostgreSQL의 확장 가능한 함수/연산자 디스패치를 무너뜨리는 것이라고 설명한다.
최적화 수준, 지연 방출, 비용 게이팅
섹션 제목: “최적화 수준, 지연 방출, 비용 게이팅”llvm_optimize_module은 컨텍스트 플래그에서 패스 파이프라인을 선택한다. PGJIT_OPT3 없이는 default<O0>,mem2reg(+ PGJIT_INLINE이면 인라인 패스 추가)를 실행한다. PGJIT_OPT3이면 인라이너 임계치 512로 default<O3>를 실행한다.
// llvm_optimize_module (LLVM >= 17) — src/backend/jit/llvm/llvmjit.cif (context->base.flags & PGJIT_OPT3) passes = "default<O3>";else if (context->base.flags & PGJIT_INLINE) passes = "default<O0>,mem2reg,inline";else passes = "default<O0>,mem2reg";LLVMPassBuilderOptionsSetInlinerThreshold(options, 512);err = LLVMRunPasses(module, passes, NULL, options);IR은 ExecInitNode 시점에 정의되고, 기계어는 지연해서 방출된다. llvm_compile_expr는 표현식의 eval 함수로 ExecRunCompiledExpr를 설치한다. 첫 번째 실제 평가가 llvm_get_function을 호출하면 llvm_compile_module(인라인 + 최적화 + ORC에 모듈 추가)이 트리거되고 심볼이 해석된다. ORC 자체도 심볼이 처음 조회될 때 지연해서 코드를 구체화한다.
// ExecRunCompiledExpr — src/backend/jit/llvm/llvmjit_expr.cCheckExprStillValid(state, econtext);llvm_enter_fatal_on_oom();func = (ExprStateEvalFunc) llvm_get_function(cstate->context, cstate->funcname);llvm_leave_fatal_on_oom();state->evalfunc = func; // remove the indirection for future callsreturn func(state, econtext, isNull);이 모든 것이 일어날지를 플래너가 한 번에 결정한다. 최종 플랜을 생성한 뒤 standard_planner가 최상위 플랜 비용을 JIT 임계치와 비교해 per-query jitFlags를 설정한다.
// standard_planner (jitFlags) — src/backend/optimizer/plan/planner.cresult->jitFlags = PGJIT_NONE;if (jit_enabled && jit_above_cost >= 0 && top_plan->total_cost > jit_above_cost) { result->jitFlags |= PGJIT_PERFORM; if (jit_optimize_above_cost >= 0 && top_plan->total_cost > jit_optimize_above_cost) result->jitFlags |= PGJIT_OPT3; if (jit_inline_above_cost >= 0 && top_plan->total_cost > jit_inline_above_cost) result->jitFlags |= PGJIT_INLINE; if (jit_expressions) result->jitFlags |= PGJIT_EXPR; if (jit_tuple_deforming) result->jitFlags |= PGJIT_DEFORM;}기본값이 계층을 명확히 드러낸다. jit_above_cost = 100000이면 JIT가 활성화된다. jit_inline_above_cost = jit_optimize_above_cost = 500000은 훨씬 비싼 플랜에만 인라이닝과 전체 최적화를 추가한다. 음수 임계치는 해당 계층을 비활성화한다. 이것이 학술적 배경에서 언급한 비용 모델의 구체적 구현이다. 평가 횟수를 계측하는 대신 플래너가 이미 계산한 비용 추정값을 재사용한다.
flowchart TD
init["ExecInitExpr / ExecInitNode"] --> jce["jit_compile_expr(state)"]
jce --> lce["llvm_compile_expr"]
lce --> mut["llvm_mutable_module:<br/>LLVM 모듈 획득 또는 생성"]
mut --> sw["steps[] 순회:<br/>옵코드별 IR 방출"]
sw --> fetchq{"FETCHSOME 스텝?"}
fetchq -- 예 --> deform["slot_compile_deform:<br/>TupleDesc 특화<br/>deform 함수를 같은 모듈에"]
fetchq -- 아니오 --> sw
sw --> install["ExecRunCompiledExpr를<br/>evalfunc로 설치 (방출 없음)"]
install --> firstcall["첫 번째 평가"]
firstcall --> getfn["llvm_get_function"]
getfn --> cmod["llvm_compile_module:<br/>인라인 -> 최적화 -> ORC 추가"]
cmod --> orc["ORC가 심볼 조회 시<br/>기계어 구체화"]
orc --> native["네이티브 함수 포인터가<br/>state->evalfunc에 캐시"]
소스 코드 가이드
섹션 제목: “소스 코드 가이드”JIT 서브시스템은 프로바이더 경계를 기준으로 깔끔하게 분리된다. 아래에서 심볼은 파일과 호출 흐름별로 묶었다. 줄 번호는 섹션 끝의 위치 힌트 표에 정리했다.
프로바이더 독립 코어 (src/backend/jit/jit.c)
섹션 제목: “프로바이더 독립 코어 (src/backend/jit/jit.c)”- GUC 변수.
jit_enabled,jit_provider,jit_expressions,jit_tuple_deforming,jit_above_cost,jit_inline_above_cost,jit_optimize_above_cost,jit_debugging_support,jit_profiling_support,jit_dump_bitcode— 플래너와 프로바이더가 읽는 설정 노브. provider_init— 지연, 단회 로더.$pkglibdir/<jit_provider>$DLSUFFIX를pg_file_exists로 탐색한 뒤load_external_function으로_PG_jit_provider_init심볼을 로드하고,provider_successfully_loaded/provider_failed_loading에 결과를 캐시.pg_jit_available— 로드를 강제 시도하고 프로바이더 사용 가능 여부를 반환하는 SQL 호출 가능 래퍼.jit_compile_expr— 단일 진입 관문.state->parent,PGJIT_PERFORM,PGJIT_EXPR를 확인한 뒤provider_init()을 호출하고provider.compile_expr에 위임.jit_release_context,jit_reset_after_error— 프로바이더의release_context/reset_after_error콜백으로 전달.InstrJitAgg— per-contextJitInstrumentation카운터(생성된 함수 수, 생성/deform/인라이닝/최적화/방출 시간)를EXPLAIN용 집계로 접음.JitProviderCallbacks(jit/jit.h)와PGJIT_*플래그 매크로(PGJIT_PERFORM,PGJIT_OPT3,PGJIT_INLINE,PGJIT_EXPR,PGJIT_DEFORM) — 프로바이더 계약과 per-query 플래그 비트.
LLVM 프로바이더 코어 (src/backend/jit/llvm/llvmjit.c)
섹션 제목: “LLVM 프로바이더 코어 (src/backend/jit/llvm/llvmjit.c)”_PG_jit_provider_init— 세 콜백(reset_after_error,release_context,compile_expr)을 채움.llvm_create_context/llvm_release_context—LLVMJitContext할당/해제, 현재ResourceOwner에 등록(ResourceOwnerRememberJIT,jit_resowner_desc)해 오류나 트랜잭션 종료 시 정리.llvm_jit_context_in_use_count추적.llvm_recreate_llvm_context— 인라이닝으로 발생하는 타입 누수를 바운딩하기 위해LLVMJIT_LLVM_CONTEXT_REUSE_MAX사용 후 공유LLVMContextRef를 재생성.llvm_inline_reset_caches선행 호출.llvm_mutable_module— 진행 중인LLVMModuleRef반환. 없으면 올바른 triple/layout으로 생성.llvm_expand_funcname— 외부에서 볼 수 있는 고유 함수 이름(<base>_<module_generation>_<counter>) 생성,created_functions증가.llvm_get_function— 모듈이 미컴파일이면llvm_compile_module을 강제 실행한 뒤LLVMOrcLLJITLookup으로 심볼 조회.emission_counter누적(ORC는 조회 시점에 지연 방출).llvm_pg_var_type/llvm_pg_var_func_type/llvm_pg_func— bitcode로 로드한llvm_types_module에서 타입과 함수 시그니처를 꺼내 JIT IR과 C 구조체를 동기화.llvm_function_reference—fcinfo를 명명된 callee(pgextern.<mod>.<fn>, 내부 이름, 또는 상수 포인터 글로벌)로 해석해 직접/인라인 가능 호출 활성화.llvm_optimize_module— 컨텍스트 플래그에서 패스 파이프라인 선택(default<O0>,mem2regvs.default<O3>), 인라이너 임계치 512.llvm_compile_module—PGJIT_INLINE이면llvm_inline실행, 최적화, 선택적 bitcode 덤프(jit_dump_bitcode),LLVMOrcLLJITAddLLVMIRModuleWithRT로 opt0/opt3 ORCLLJIT인스턴스에 모듈 추가.llvm_session_initialize/llvm_shutdown— 백엔드당 한 번 수행하는 네이티브 타겟, 호스트 CPU/기능, opt0/opt3 타겟 머신, 두LLVMOrcLLJITRef인스턴스 초기화.proc_exit시 해제.llvm_create_types—llvmjit_types.bc를 로드하고 전역LLVMTypeRef(StructTupleTableSlot,StructExprState,StructFunctionCallInfoData,AttributeTemplate등)를 바인딩.llvm_split_symbol_name/llvm_resolve_symbol/llvm_create_jit_instance— SQL 호출 가능 함수와 메인 바이너리 심볼을 해석하는 커스텀 정의 생성기를 포함한 ORC 심볼 해석 배관.
표현식 코드 생성 (src/backend/jit/llvm/llvmjit_expr.c)
섹션 제목: “표현식 코드 생성 (src/backend/jit/llvm/llvmjit_expr.c)”llvm_compile_expr— 프로바이더의 핵심.evalexpr함수를 생성하고, 슬롯 value/null 배열을 로드하고,ExprState스텝마다 기본 블록을 미리 할당한 뒤ExecEvalStepOp에 따라switch로 옵코드별 IR을 방출.execExprInterp.c의ExecInterpExpr를 반영.- 옵코드 분기 — 주요 사례:
EEOP_DONE_RETURN(isnull 저장, 값 반환),EEOP_*_FETCHSOME(deform 트리거),EEOP_*_VAR(슬롯 직접 로드),EEOP_CONST,EEOP_FUNCEXPR/EEOP_FUNCEXPR_STRICT(null 검사 체인 +BuildV1Call),EEOP_QUAL(null/false 단락),EEOP_PARAM_EXTERN(인터프리터 헬퍼 위임). BuildV1Call— V1 함수 직접 호출 방출,isnull저장, LLVM 22 미만에서 **create_LifetimeEnd**로llvm.lifetime.end어노테이션.build_EvalXFuncInt(및build_EvalXFunc매크로) — 긴 꼬리 옵코드를 위해 명명된ExecEval*인터프리터 헬퍼로의 직접 호출을 조립.ExecRunCompiledExpr—state->evalfunc로 설치되는 썽크. 표현식 유효성 검사,llvm_get_function으로 지연 방출 트리거, 네이티브 포인터 캐시, 테일 콜.
튜플 변환 (src/backend/jit/llvm/llvmjit_deform.c)
섹션 제목: “튜플 변환 (src/backend/jit/llvm/llvmjit_deform.c)”slot_compile_deform—TupleDesc특화 deform 함수 생성. 가상/알 수 없는 슬롯 종류 거부.guaranteed_column_number와known_alignment미리 계산. 컬럼별 로드/저장/포인터 전진을 가능한 경우 상수 보폭으로, varlena/cstring에는varsize_any/strlen호출(항상 인라인 표시)로 방출.
인라이닝 (src/backend/jit/llvm/llvmjit_inline.cpp)
섹션 제목: “인라이닝 (src/backend/jit/llvm/llvmjit_inline.cpp)”llvm_inline— 진입점.llvm_build_inline_plan(function_inlinable참조)으로 임포트 계획을 수립하고llvm_execute_inline_plan으로 적용해 설치된$pkglibdir/bitcode/요약에서 연산자 본문을 꺼냄.llvm_inline_reset_caches— 공유LLVMContextRef재생성 전에 캐시된 bitcode 모듈 해제.
비용 게이팅 (src/backend/optimizer/plan/planner.c)
섹션 제목: “비용 게이팅 (src/backend/optimizer/plan/planner.c)”standard_planner(jitFlags 블록) —top_plan->total_cost를jit_above_cost/jit_optimize_above_cost/jit_inline_above_cost와 비교해 실행 시점에 소비되는PlannedStmt.jitFlags를 설정.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
provider_init | src/backend/jit/jit.c | 67 |
pg_jit_available | src/backend/jit/jit.c | 56 |
jit_compile_expr | src/backend/jit/jit.c | 151 |
jit_release_context | src/backend/jit/jit.c | 137 |
jit_reset_after_error | src/backend/jit/jit.c | 127 |
InstrJitAgg | src/backend/jit/jit.c | 182 |
jit_above_cost (GUC 기본값) | src/backend/jit/jit.c | 39 |
PGJIT_PERFORM / PGJIT_DEFORM | src/include/jit/jit.h | 20 / 24 |
_PG_jit_provider_init | src/backend/jit/llvm/llvmjit.c | 151 |
llvm_create_context | src/backend/jit/llvm/llvmjit.c | 223 |
llvm_release_context | src/backend/jit/llvm/llvmjit.c | 252 |
llvm_recreate_llvm_context | src/backend/jit/llvm/llvmjit.c | 173 |
llvm_mutable_module | src/backend/jit/llvm/llvmjit.c | 316 |
llvm_expand_funcname | src/backend/jit/llvm/llvmjit.c | 341 |
llvm_get_function | src/backend/jit/llvm/llvmjit.c | 362 |
llvm_function_reference | src/backend/jit/llvm/llvmjit.c | 540 |
llvm_optimize_module | src/backend/jit/llvm/llvmjit.c | 603 |
llvm_compile_module | src/backend/jit/llvm/llvmjit.c | 709 |
llvm_session_initialize | src/backend/jit/llvm/llvmjit.c | 825 |
llvm_create_types | src/backend/jit/llvm/llvmjit.c | 995 |
llvm_resolve_symbol | src/backend/jit/llvm/llvmjit.c | 1087 |
llvm_create_jit_instance | src/backend/jit/llvm/llvmjit.c | 1220 |
llvm_compile_expr | src/backend/jit/llvm/llvmjit_expr.c | 80 |
EEOP_*_FETCHSOME 분기 | src/backend/jit/llvm/llvmjit_expr.c | 344 |
EEOP_*_VAR 분기 | src/backend/jit/llvm/llvmjit_expr.c | 444 |
EEOP_FUNCEXPR_STRICT 분기 | src/backend/jit/llvm/llvmjit_expr.c | 665 |
ExecRunCompiledExpr | src/backend/jit/llvm/llvmjit_expr.c | 2988 |
BuildV1Call | src/backend/jit/llvm/llvmjit_expr.c | 3008 |
build_EvalXFuncInt | src/backend/jit/llvm/llvmjit_expr.c | 3060 |
create_LifetimeEnd | src/backend/jit/llvm/llvmjit_expr.c | 3090 |
slot_compile_deform | src/backend/jit/llvm/llvmjit_deform.c | 34 |
llvm_inline | src/backend/jit/llvm/llvmjit_inline.cpp | 167 |
llvm_inline_reset_caches | src/backend/jit/llvm/llvmjit_inline.cpp | 156 |
llvm_build_inline_plan | src/backend/jit/llvm/llvmjit_inline.cpp | 183 |
function_inlinable | src/backend/jit/llvm/llvmjit_inline.cpp | 125 |
standard_planner jitFlags | src/backend/optimizer/plan/planner.c | 604 |
jit_above_cost GUC 항목 | src/backend/utils/misc/guc_tables.c | 3960 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”아래 항목들은 커밋 273fe94852b3a7e34fd171e8abdf1481beb302fa(2026-06-05) REL_18 트리에서 확인했다.
- 프로바이더 추상화는 세 콜백. 확인됨.
_PG_jit_provider_init는 정확히reset_after_error,release_context,compile_expr를 설정한다(llvmjit.c).jit.c의 코어는static JitProviderCallbacks provider만 거쳐 LLVM을 참조한다. - 지연, 캐시된 라이브러리 로드. 확인됨.
provider_init는provider_failed_loading/provider_successfully_loaded에서 일찍 반환하고,load_external_function전에pg_file_exists로 탐색하며,init호출 전에provider_failed_loading = true를 설정해 예외를 던지는init가 재시도되지 않도록 한다. jit_compile_expr가드. 확인됨.state->parent가 NULL이거나PGJIT_PERFORM/PGJIT_EXPR가 설정되지 않으면 프로바이더를 호출하기 전에 false를 반환한다.- GUC 기본값. 확인됨(
jit.c).jit_above_cost = 100000,jit_inline_above_cost = 500000,jit_optimize_above_cost = 500000,jit_enabled = true,jit_expressions = true,jit_tuple_deforming = true. GUC 표 항목은guc_tables.c에 있다. - 플래너가 비용으로 플래그 설정. 확인됨.
standard_planner는top_plan->total_cost > jit_above_cost일 때PGJIT_PERFORM을 설정하고, 추가 임계치와jit_expressions/jit_tuple_deformingGUC에서PGJIT_OPT3/PGJIT_INLINE/PGJIT_EXPR/PGJIT_DEFORM을 계층적으로 추가한다. - 옵코드
switch가 인터프리터를 반영. 확인됨.llvm_compile_expr는state->steps[0 .. steps_len-1]을 순회하고,ExecEvalStepOp를 호출하며,EEOP_LAST까지 전체ExprEvalOp열거형을 처리하는 분기를 가진다(EEOP_LAST는Assert(false)). - deform 특화 사실. 확인됨.
slot_compile_deform은TTSOpsVirtual과 힙/버퍼 힙/미니멀 이외의 슬롯 종류에서 NULL을 반환한다.guaranteed_column_number를ATTNULLABLE_VALID && !atthasmissing && !attisdropped에서 계산한다. 고정 폭 컬럼은l_sizet_const(att->attlen)으로,attlen == -1/-2는varsize_any/strlen으로 데이터 포인터를 전진한다. - 지연 방출. 확인됨.
llvm_compile_expr는state->evalfunc로ExecRunCompiledExpr(컴파일된 포인터가 아님)를 설치한다.llvm_compile_module은llvm_get_function에서 호출되고,llvm_compile_module주석은 ORC가 “실제로 코드를 방출하지 않는다… 심볼이 처음 요청될 때 지연해서 발생한다”고 밝힌다. - 인라이닝은 bitcode에서 가져옴. 확인됨.
README(연산자가$pkglibdir/bitcode/postgres/에 인덱스와 함께 컴파일됨)와llvm_inline→llvm_build_inline_plan→llvm_execute_inline_plan에서 확인. - ResourceOwner 정리. 확인됨.
RELEASE_PRIO_JIT_CONTEXTS와ResOwnerReleaseJitContext를 가진jit_resowner_desc. 컨텍스트는llvm_create_context에서 등록되고llvm_release_context에서 해제된다. - 주의 — 질의 간 캐시 없음.
README“Caching” 절은 생성된 함수가 per-execution 메모리의 포인터를 포함하므로 실행 간에 재사용되지 않는다고 밝힌다. REL_18 트리에는 IR/함수 캐시가 없다. 질의 간 재사용 주장은 사실이 아니다. - 이 문서의 범위 밖.
ExprState/ExprEvalStep선형화, 인터프리터 디스패치, 플랜 비용 계산은postgres-expression-eval.md,postgres-executor.md,postgres-cost-model.md에서 다룬다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”PostgreSQL JIT는 풍부한 설계 공간에서 의도적으로 보수적인 지점에 위치한다. 대안들과 비교해 무엇을 얻고 무엇을 포기했는지를 파악한다.
표현식 JIT vs. 파이프라인 전체 컴파일(HyPer)
섹션 제목: “표현식 JIT vs. 파이프라인 전체 컴파일(HyPer)”PostgreSQL은 표현식과 deforming을 컴파일하되 실행기는 인터프리터로 남긴다. 각 플랜 노드는 여전히 볼케이노 방식 ExecProcNode 이터레이터로 튜플을 가져오며, 튜플당 표현식과 deform 핫 스팟만 네이티브가 된다. Thomas Neumann의 HyPer는 produce/consume(push) 모델로 반대 방향을 택한다. 연산자 파이프라인 전체를 단일 타이트 루프로 컴파일해 연산자 간 튜플당 함수 호출 경계를 없앤다. 파이프라인 브레이커(해시 빌드, 정렬)가 구체화를 강제할 때까지 데이터가 CPU 레지스터에 머문다. 트레이드오프가 명확하다. HyPer의 접근법은 오버헤드를 훨씬 많이 제거하지만 전체 실행기를 코드 생성해야 하는 훨씬 큰 엔지니어링 부담을 요구한다. PostgreSQL README는 “질의의 더 큰 부분을 컴파일하는 것”을 향후 과제로 명시하고, 실행 N회 후 개별 표현식을 JIT 컴파일하는 명백해 보이는 접근법이 “잘 작동하지 않는 것으로 밝혀졌다”고 밝힌다. 많은 소형 함수를 방출하면 함수당 오버헤드가 높다는 것이 이유다. HyPer가 파이프라인 전체 융합으로 나아간 것과 같은 관찰이다.
전체 질의 컴파일(Krikellas 외)
섹션 제목: “전체 질의 컴파일(Krikellas 외)”Krikellas, Viglas, Cintra의 holistic 모델(“generate, compile, link, execute” 파이프라인)은 HyPer의 push 모델보다 앞서며, PostgreSQL이 하는 것과 정신적으로 가깝다. 질의 플랜을 받아 특화된 C 소스를 방출한 뒤 시스템 C 컴파일러를 호출한다. PostgreSQL이 C 방출과 컴파일 대신 LLVM을 선택한 것은 실용적 이유다. 전체 C 툴체인에 대한 런타임 의존성과 디스크 컴파일 단계를 피하고, LLVM C API와 Clang이 방출한 연산자 bitcode로 직접 IR을 얻는다. 대가는 LLVM 의존성 자체이며, 이것이 프로바이더를 별도 로드 가능 공유 객체로 만든 이유다.
벡터화 인터프리티드 실행 vs. 컴파일(MonetDB/X100, DuckDB)
섹션 제목: “벡터화 인터프리티드 실행 vs. 컴파일(MonetDB/X100, DuckDB)”인터프리터 오버헤드에 대한 경쟁적 답은 컴파일이 아닌 벡터화다. 질의당 네이티브 코드를 생성하는 대신 튜플을 배치(벡터)로 처리해 인터프리터 디스패치 비용을 벡터 전체에 분할상환하고 내부 루프를 자동 벡터화한다. MonetDB/X100과 DuckDB가 이 방향을 택하고 컴파일 지연을 완전히 피한다. 2018년 “Everything you always wanted to know about compiled and vectorized queries but were afraid to ask” 연구(Kersten 외)는 두 방식이 대략 경쟁적임을 발견했다. 컴파일은 복잡한 표현식 중심 질의에, 벡터화는 단순하고 메모리 바운드인 질의에 유리하다. PostgreSQL은 완전히 벡터화된 것도, 완전히 컴파일된 것도 아니다. 실행기는 여전히 튜플 단위 인터프리터다. JIT는 튜플당 컴파일이 이득이 되는 두 지점에 결합됐고, 비용 임계치가 컴파일이 유리한 표현식 중심 분석 질의로 방향을 잡는다.
캐싱과 적응형 컴파일 — 열린 프론티어
섹션 제목: “캐싱과 적응형 컴파일 — 열린 프론티어”README가 직접 지적한 가장 큰 공백은 캐싱이다. 생성된 함수가 per-execution 메모리의 절대 포인터를 포함하므로 현재는 실행 간 또는 준비된 구문에 재사용할 수 없다. README가 개략하는 수정 방법은 ExprState가 단일 기반 블록에서의 오프셋으로 per-execution 메모리를 참조하게 하는 것이다. 이것이 생성된 IR을 키로 하는 LRU 캐시의 전제 조건이며, 표현식 컴파일을 플래너로 옮겨 준비된 구문이 컴파일된 형태를 가질 수 있게 하는 선결 조건이기도 하다. 그 너머로는 적응형(“티어드”) JIT — 인터프리팅이나 -O0 컴파일로 시작하고, 질의가 오래 실행됨이 입증되면 백그라운드 스레드에서 최적화된 버전을 재빌드하는 방식 — 이 관리형 언어 VM(HotSpot, V8)의 표준 기법이며 “더 먼 가능성”으로 언급된다. PostgreSQL의 전부 아니면 전무, 비용 게이팅, 단일 컴파일 모델은 단순하고 프로파일링 카운터 관리를 피한다. 대신 플래너 추정값이 실제 비용과 달라질 때 오판한다. “JIT가 내 질의를 느리게 만들었다”는 보고의 흔한 원인이다.
PostgreSQL의 선택이 놓이는 자리
섹션 제목: “PostgreSQL의 선택이 놓이는 자리”총체적 그림은 이렇다. PostgreSQL은 최고 처리량 대신 유지 가능성과 선택지를 택했다. 인터프리터의 옵코드별 반영은 두 구현을 동기화 상태로 유지한다. C 소스에서 bitcode를 얻는 기법은 모든 연산자의 두 번째 복사본을 피한다. 프로바이더 공유 라이브러리는 LLVM을 기반 바이너리 밖에 둔다. 플래너 비용 게이팅은 시스템이 이미 계산하는 추정값을 재사용한다. 비용도 실재한다. 질의 간 캐싱 없음, 파이프라인 전체 융합 없음, 비용 추정이 틀릴 때 가끔 발생하는 오발동(자주 보고되는 “JIT가 느리게 만든다”의 원인). 그러나 각각은 기존의 확장 가능한 인터프리터 기반 엔진이 실제로 출시하고 유지할 수 있는 JIT를 위한 의식적 트레이드오프다.
- PostgreSQL REL_18 소스(커밋
273fe94852b3a7e34fd171e8abdf1481beb302fa, 2026-06-05):src/backend/jit/jit.c— 프로바이더 독립 코어, GUC, 진입 관문.src/backend/jit/README— 설계 근거(무엇을/왜/어떻게/언제 JIT할지, 공유 라이브러리 분리, JIT 컨텍스트, 오류 처리, 타입 동기화, 인라이닝, 캐시 한계).src/backend/jit/llvm/llvmjit.c— LLVM 프로바이더 코어. 컨텍스트 수명, 모듈/함수 관리, 최적화, ORC 방출, 세션 설정, 타입 로드, 심볼 해석.src/backend/jit/llvm/llvmjit_expr.c— 옵코드별 IR 생성.BuildV1Call,build_EvalXFuncInt,ExecRunCompiledExpr.src/backend/jit/llvm/llvmjit_deform.c—slot_compile_deform, TupleDesc 특화 변환.src/backend/jit/llvm/llvmjit_inline.cpp— bitcode 기반 연산자 인라이닝.src/backend/jit/llvm/llvmjit_types.c— C와 JIT IR 간 타입/함수 시그니처 동기화.src/include/jit/jit.h,src/include/jit/llvmjit.h—PGJIT_*플래그,JitProviderCallbacks,LLVMJitContext.src/backend/optimizer/plan/planner.c—standard_planner비용 기반jitFlags설정.src/backend/utils/misc/guc_tables.c—jit_above_cost/jit_inline_above_cost/jit_optimize_above_costGUC 정의.
- 교과서 배경 —
knowledge/research/dbms-general/의 Database System Concepts(Silberschatz, Korth, Sudarshan; 질의 처리 / 이터레이터 모델)와 Database Internals(Petrov; 질의 실행) 캡처. - 연구 계보(방향 파악용. 캡처된 것은
knowledge/research/dbms-papers/참조):- T. Neumann, “Efficiently Compiling Efficient Query Plans for Modern Hardware” (VLDB 2011) — HyPer produce/consume push 모델.
- K. Krikellas, S. Viglas, M. Cintra, “Generating Code for Holistic Query Evaluation” (ICDE 2010).
- P. Boncz, M. Zukowski, N. Nes, “MonetDB/X100: Hyper-Pipelining Query Execution” (CIDR 2005) — 벡터화 실행.
- T. Kersten 외, “Everything You Always Wanted to Know About Compiled and Vectorized Queries But Were Afraid to Ask” (VLDB 2018).
- 교차 참조(이 폴더의 형제 문서):
postgres-expression-eval.md(이 JIT가 반영하는ExprState/ExprEvalStep선형화와 인터프리터),postgres-executor.md(EState에es_jit/es_jit_flags가 위치하는 주변 노드-이터레이터 구조),postgres-cost-model.md(JIT 임계치가 비교하는total_cost계산 방법).