Files
five/docs/RTL-Go-Native-Migration.md
CharlesKWON 3d292dd9d8 docs: document 2026-04-18 perf session — entries #27-32
Six new migration-log entries covering this session's 21 commits:
  #27 VM in-place stack ops + symbol hoist (global 3-15%)
  #28 gengo compile-time peepholes (9 commits, 1-7% bench)
  #29 SELECT WA cache extension (single-table 2x+)
  #30 JOIN temp-alias stabilisation (B6 1.67x)
  #31 Stat-loop gates — view + CTE (CPU -40pp in rawsyscalln)
  #32 Go-native SqlIsAggName + FetchRow (agg/window 1.3-1.7x)

Plus a cumulative bench table vs the 3caadb2 baseline and an
updated "남은 병목" section pointing at EvalExpr / JOINRECURSE /
HASHJOIN / Go runtime primitives as the remaining levers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:02:48 +09:00

64 KiB
Raw Blame History

RTL Go-Native 전환 계획

PRG 핫패스와 hbrtl/ RTL 함수 중 Go 네이티브 구현이 이익인 후보 목록 및 진행 기록. 기준선: 최종 결과가 Harbour와 동일해야 한다.

배경

FiveSql2 성능 개선 흐름(3caadb2 SqlOrderBy+SqlGroupBy Go RTL, 5fc9c3b SqlHashJoin Go RTL 등)은 PRG 핫루프를 Go RTL로 옮겨 큰 이득을 보였다. 본 문서는 같은 패턴을 체계적으로 적용할 후보를 추린다.

Harbour 호환 검증 근거

항목 근거 영향
해시 기본 플래그 harbour-core/include/hbapi.h:927-931HB_HASH_FLAG_DEFAULT = HB_HASH_AUTOADD_ASSIGN | HB_HASH_BINARY | HB_HASH_KEEPORDER 삽입 순서 보존 + memcmp 정확 비교
해시 키 비교 harbour-core/src/vm/hashes.c:167-182hb_hashItemCmp CHAR padding trim 없음, Date/Timestamp는 julian 비교
내부 탐색 pPairs[] + pnPos[] 이진 탐색 (O(log N)) Five의 Go map 치환은 O(1)로 상회

후보 목록

Tier 1 — 즉시 이익, 시맨틱 안전

# 대상 파일 방식 예상 효과 상태
1 Hash 스토리지 hbrtl/hash.go, hbrt/ops_collection.go, hbrt/value.go map[string]int 인덱스 추가, 삽입 순서 슬라이스 유지 50100x 완료 (2026-04-17)
2 SqlDistinct _FiveSql2/src/TSqlSort.prg:57-70, hbrtl/sqlscan.go Go RTL map[string]struct{} + strings.Builder 100300x 완료 (2026-04-17)
3 SqlRowCompare NULL · 혼합타입 정합성 hbrtl/sqlscan.go, _FiveSql2/src/TSqlSort.prg, _FiveSql2/src/TSqlExecutor.prg Go/PRG 양 경로 NULL 순서 PRG 시맨틱으로 통일 + NULLS FIRST/LAST 배선 정합성 수정 완료 (2026-04-17)

Tier 2 — 블록 NIL 특화 + 누락 타입 보강 (완료)

# 대상 파일 방식 상태
4 ASort 타입 특화 + 정확성 hbrtl/array.go:134-300 비교자 블록 없을 때 1회 타입 스캔 → 특화 비교자. Date/Logical/Timestamp 지원 추가 (기존엔 no-op) 완료 (2026-04-17)
5 AScan fast-path hbrtl/array.go:302-380 검색값이 string/int/double일 때 타입별 인라인 루프. 드물게 쓰는 타입은 valuesEqual fallback 완료 (2026-04-17)

🔎 Tier 3 — 내부 헬퍼 최적화

# 대상 파일 방식 상태
6 RAT 역방향 스캔 hbrtl/strings2.go:16-51 검토 결과 strings.LastIndex + 부분슬라이스는 이미 최적. 변경 없음 검토 종료 (2026-04-17)
7 SqlExprHasAgg hbrtl/sqlexpr.go PRG 재귀 → Go AST walker + 상수 시간 agg 이름 조회 완료 (2026-04-17)

Tier 4 — DML Boundary-Crossing 감소 (완료)

# 대상 파일 방식 상태
8 SqlBulkInsert hbrtl/sqlscan.go, _FiveSql2/src/TSqlExecutor.prg CTE/subquery/tmp 테이블 materialize 경로의 FOR j ... dbAppend ... FOR k ... FieldPut 이중 루프를 Go RTL 단일 호출로 대체 완료 (2026-04-17)
9 SqlBulkUpdate hbrtl/sqlscan.go, _FiveSql2/src/TSqlExecutor.prg RunUpdate UPDATE 스캔 루프 전체를 Go RTL로 이관. WHERE + SET 값 표현식을 pcode로 컴파일해 PRG 메서드 디스패치 제거 완료 (2026-04-17)
10 MEMRDD 자동 임포트 compiler/gengo/gengo.go 모든 Five 프로그램에 _ "five/hbrdd/mem" 블랭크 임포트 자동 추가 → USE "mem:x" VIA "MEMRDD" 즉시 사용 가능 완료 (2026-04-17)
11 PcCompile 결과 캐시 hbrtl/pcexpr.go sync.Map으로 소스 문자열 키 캐시. 반복 쿼리에서 파서+genpc 건너뛰기 완료 (2026-04-17)
12 SQL 플랜 캐시 + HbDeepClone _FiveSql2/src/TFiveSQL.prg, hbrtl/array.go cSQL → hQuery PRG 해시 캐시. 히트 시 Go RTL HbDeepClone으로 pristine 사본 반환 → SqlFoldConst 인-플레이스 변경 안전 완료 (2026-04-17)
13 파라미터 바인딩 벤치 입증 _FiveSql2/test/bench_prep_sql.prg 기존 five_SQL(cSQL, aParams) + ? 파서가 이미 지원. 플랜 캐시와 결합 시 SELECT 1.58x, INSERT 1.12x 입증 (2026-04-17)
14 CTE → MEMRDD _FiveSql2/src/TSqlExecutor.prg, hbrdd/mem/memrdd.go, hbrtl/sqlscan.go SqlBulkInsert 3곳 materialize 경로를 dbCreate("mem:xxx", ..., "MEMRDD") + MEMRDD dbUseArea로 전환. SqlBulkInsert가 *dbf.DBFArea 외 일반 hbrdd.Area도 처리하도록 확장. MEMRDD Create가 필드명 trailing-space trim 완료 (2026-04-17)
15 SqlWindowPartitions Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions PARTITION BY 키 빌드 + 행-인덱스 그룹핑을 Go RTL에 위임. N·M 경계 크로싱 → 1 완료 (2026-04-17)
16 SqlWindowSortPartition Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions 파티션 내 ORDER BY를 Go sort.SliceStable + 사전 해석된 컬럼 인덱스로 처리. PRG 비교 블록 제거 완료 (2026-04-17)
17 SqlGroupRows Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlAgg.prg GroupBy GROUP BY 그룹 빌드 루프만 Go RTL로. 집계·HAVING은 복잡 표현식 대응 위해 PRG 유지 완료 (2026-04-17)
18 SqlComputeAggSimple Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlAgg.prg ComputeAgg COUNT/SUM/AVG/MIN/MAX + 컬럼 인자 fast-path. 복잡 인자·GROUP_CONCAT은 PRG fallback 완료 (2026-04-17)
19 SQL 스칼라 헬퍼 Go RTL hbrtl/sqlhelpers.go, _FiveSql2/src/TSqlFunc.prg SqlIsTrue/SqlCmpEq/SqlCmpLt/SqlCoerceForCmp/SqlCoerceNum/SqlCoerceStr 6개 Go로. PRG tree-walker 평가 경로(HAVING, complex expr) 오버헤드 감소 완료 (2026-04-17)
20 SQL 템플릿 자동 파라미터화 hbrtl/sqlhelpers.go SqlExtractTemplate, _FiveSql2/src/TFiveSQL.prg 리터럴(TK_TEXT/TK_NUM)을 TK_QMARK로 치환 + 템플릿 키로 플랜 캐시. 동일 구조 다른 값 쿼리가 캐시 공유 완료 (2026-04-17)
21 TSqlLexer Go 포팅 + 결합 hbrtl/sqlhelpers.go SqlLexerTokenize + SqlLexAndExtractTemplate, _FiveSql2/src/TFiveSQL.prg PRG SubStr 기반 문자-단위 렉서를 Go byte-level FSM으로. 자동-파라미터화와 결합해 1회 Go 호출로 lex+normalize 완료 완료 (2026-04-17)
22 SqlWindowAssignRank Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions ROW_NUMBER/RANK/DENSE_RANK 배정 루프를 Go에서. 파티션당 1회 호출로 per-row SqlWinRowsEqual PRG 호출 제거 완료 (2026-04-17)
23 HbDeepClone 성능 개선 hbrtl/array.go deepCloneValue 스칼라 원소는 재귀 스킵 (슬롯 복사만), 해시 키 공유 (문자열/숫자는 불변). 플랜 캐시 히트마다 수행되는 핫패스 완료 (2026-04-17)
24 WA 캐시 + 지연 commit hbrtl/sqlwacache.go, _FiveSql2/src/TSqlExecutor.prg SqlExecOpenTable/CloseTable + RunInsert/Update/Delete 워크에어리어 공정-수명 캐시 (opt-in). 활성화 시 DML의 per-query dbUseArea/dbCloseArea/dbCommit 전부 배치 → B12 INSERT 48x 완료 (2026-04-17)
25 Plan pcode 캐시 + SqlBulkUpdate flush 지연 _FiveSql2/src/TSqlExecutor.prg s_hDmlPcodeCache + cCacheKey, hbrtl/sqlscan.go SqlBulkUpdate 플랜 키별 컴파일된 pcode(aFPos/where/set_pc) 캐시 + WA cache 활성 시 Go RTL 내부 Flush() 스킵 → B13 UPDATE 48x 완료 (2026-04-17)
26 SELECT 경로 plan pcode 캐시 _FiveSql2/src/TSqlExecutor.prg RunSelect fast path #25의 패턴을 SELECT fast-path에도 적용. TryBuildFieldPositions + TryCompileWhere 결과를 cCacheKey#sel로 캐시. 반복 SELECT의 PRG AST walk 제거 완료 (2026-04-17)
27 SqlEvalHaving Go RTL hbrtl/sqlscan.go, _FiveSql2/src/TSqlAgg.prg EvalHaving HAVING 트리 walker를 Go로. ND_LIT/ND_NIL/ND_COL/ND_FN(5 aggs)/ND_BIN/ND_UNI 처리. 복잡 케이스는 PRG fallback 완료 (2026-04-17, 효과 미미)
28 VM Function() + Symbol 캐시 hbrt/call.go Function, hbrt/vm.go GetSym, hbrt/thread.go pushPendingSym, compiler/gengo/gengo.go emitPushSymbol 모든 PRG 함수 호출이 통과하는 경로. (a) Function()의 pop-push dance를 stack shift로 (heap alloc + 2N+2 ops → 1 copy), (b) 심볼 resolve 결과를 sym.Func에 캐시, (c) gengo가 심볼 포인터를 package-level var에 hoist해 FindSymbol 호출/RWMutex/map lookup을 lazy 1회로 단축. 전역 3-15% 개선 완료 (2026-04-17)

제외 (Harbour 호환 리스크 과다)

대상 제외 이유
SqlLikeMatch regexp 치환 Harbour SQL LIKE의 %/_/[abc]/[!abc]/이스케이프 규칙은 regex와 미스매치. 자체 매처 필요
SubStr Go slice 직접 이미 slice 사용 중 (hbrtl/strings.go:149). 변경 이익 없음
Descend bytes.Map 성능 이익 <5%
SET DATE 비트셋 사전계산 515%지만 setDateFormat 전역 일관성 리스크 > 이익

이미 최적 (건드리지 말 것)

진행 기록

#1 Hash 스토리지 O(1) 전환 — 2026-04-17 완료

구조 변경 (hbrt/value.go:237-249)

type HbHash struct {
    Keys   []Value           // 삽입 순서 (HB_HASH_KEEPORDER 기본)
    Values []Value           // 병렬
    Order  []int
    Flags  int32
    Index  map[string]int    // 신규: O(1) 탐색용 미러
}

신규 파일: hbrt/hash_helpers.go

  • hashKey(v Value) (string, bool)valueEqual 동치류와 일치하는 직렬화. Nil/String/Numeric/Logical/Date/Timestamp 지원. 수치는 정수로 환산 가능하면 'I' 폼으로 정규화 (int/double 교차 매칭), -0.0 → +0.0
  • (*HbHash).Lookup/Has/Set/Append/Delete/HashGet/ensureIndex/HashFromPairs 메서드
  • 비인덱싱 키 타입(Array/Hash/Block/Pointer)은 fallback 선형 스캔 + valueEqual

호출부 전환

파일 변경
hbrt/ops_collection.go HashGen/ArrayPush/ArrayPop 헬퍼 경유. HashGen은 pair 수집 후 Set로 last-wins 보장
hbrt/valuemethods.go vmHashHas/vmHashDelete 헬퍼 경유
hbrt/hbfunc.go HashAddSet, HashGetC"S"+key 직접 Index 힛
hbrt/macroeval.go 해시 리터럴 평가 Set (중복 키 last-wins)
hbrt/gobridge.go reflect.Map 변환 Append (Go map은 중복 키 없음)
hbrtl/hash.go 7개 RTL 함수 (HbHash/HGet/HSet/HDel/HHasKey/HKeys/HValues) 전체 헬퍼 경유
hbrtl/json.go navigatePath/JsonMerge 헬퍼 경유

Harbour 호환 보장

  • 키 삽입 순서 보존 (hb_HKeys() 반환): Keys[] 슬라이스 유지
  • HB_HASH_BINARY 정확 비교: hashKey가 String을 raw bytes로 직렬화
  • 수치 교차 비교 (1 == 1.0): 정수로 환산 가능한 double은 'I' 폼으로 정규화
  • 비인덱싱 키: valueEqual fallback (Array/Hash/Block 포인터 동일성 포함)

스테일 방지: ensureIndex()는 Index가 nil이거나 indexable 키 개수와 불일치하면 재구축. 테스트가 .Keys = append(...)로 직접 조작해도 다음 Lookup 시점에 자동 복구.

검증 (CLAUDE.md 3종)

  • go test ./... — 15 패키지 ALL PASS
  • FiveSql2 — 43/43 (100%)
  • Harbour compat — 51/51 (100%)

#2 SqlDistinct Go RTL — 2026-04-17 완료

추가 함수 (hbrtl/sqlscan.go)

  • appendValueHashKey(sb *strings.Builder, v)valueHashKey와 동일 매핑이나 중간 문자열 할당 없이 Builder에 직접 기록
  • SqlDistinct(aRows) → aRows — Go map 기반 단일 패스 dedup, 입력 순서 보존

호출부 변경 (_FiveSql2/src/TSqlSort.prg:57-62)

METHOD Distinct( aRows ) CLASS TSqlSort
RETURN SqlDistinct( aRows )

PRG의 hb_HHasKey 루프 + 수동 cKey += SqlValToStr(..) + "|" 조립을 Go 한 번 호출로 대체. 컴파일러의 "undeclared variable" 경고는 RTL 함수 심볼이 gengo 테이블에 없기 때문 — 런타임에 SQLDISTINCT 심볼로 해결되어 동작은 정상.

Harbour 호환 보장

  • 키 구성 규칙이 SqlValToStr 시맨틱(appendValueHashKey)과 byte-for-byte 일치 — CHAR는 trailing space trim, NIL은 \x00NIL, 숫자는 int/double 별도 경로
  • 입력 순서 보존 → SQL DISTINCT 결과의 첫 등장 순서 유지
  • 빈 배열 · 단일 행은 입력 그대로 반환 (PRG 동작과 일치)

등록 (hbrtl/register.go:626)

hbrt.Sym("SQLDISTINCT", hbrt.FsPublic, SqlDistinct),

검증

  • go test ./... — ALL PASS
  • FiveSql2 — 43/43 (100%)
  • Harbour compat — 51/51 (100%)

#3 SqlRowCompare NULL 순서 · 혼합타입 정합성 — 2026-04-17 완료

발견된 문제

  1. Go SqlOrderBy 기본값이 NIL을 가장 작은 값으로 취급 (ASC에서 NULLs FIRST) — PRG SqlRowCompare의 원래 시맨틱(NIL = 가장 큼)과 정반대
  2. 파서가 NULLS FIRST/LAST (SQL:2003) 스펙을 파싱하지만 (TSqlParser2.prg:962-973) Go/PRG 어느 경로도 이를 읽지 않음 — 명시 스펙이 완전 무시
  3. Go compareValues가 숫자 vs 문자열 혼합 타입 비교를 지원하지 않음 — PRG는 Val(AllTrim(x))로 강제변환 (TSqlSort.prg:145-148)

수정 내역

파일 변경
hbrtl/sqlscan.go sortColnullsFirst bool 필드 추가. cDir == DESC를 기본값으로 하고 arr.Items[2]"FIRST"/"LAST"면 오버라이드. compareValuescompareValuesNonNil 기반으로 재구성하고 NIL 처리를 호출부로 이관. 혼합 N/C 비교용 parseLeadingNumeric 추가
_FiveSql2/src/TSqlExecutor.prg:3818 TryBuildSortSpecaOrderBy[i][3]을 읽어 3번째 요소 cNulls로 Go 스펙에 전달
_FiveSql2/src/TSqlSort.prg:33-54 OrderBy 메서드가 aOB[i][3]s_aOBCols에 보존
_FiveSql2/src/TSqlSort.prg:118-144 SqlRowCompare가 명시 NULLS FIRST/LAST를 우선 적용, 없으면 cDir == DESC를 기본

Harbour/FiveSql2 시맨틱 보장

  • 기본값: NIL은 가장 큰 값 → ASC는 NULLs LAST, DESC는 NULLs FIRST (PRG 원래 동작, PostgreSQL 기본과 일치)
  • NULLS FIRST/LAST 명시 시 방향과 무관하게 스펙 우선
  • 혼합 N/C 비교: PRG Val(AllTrim(x)) 동작 복제 (선행 공백 무시, 부호/소수점 허용)

회귀 테스트_FiveSql2/test/test_null_order.prg 4/4 PASS: default ASC, default DESC, ASC NULLS FIRST, DESC NULLS LAST

검증

  • go test ./... — ALL PASS
  • FiveSql2 43/43 · Harbour compat 51/51

#4, #5 ASort 정확성/특화 + AScan fast-path — 2026-04-17 완료

ASort 버그 발견 — 기본 비교 경로(기존 array.go:164)가 IsString / IsNumeric가 아닌 타입에 대해 return false를 반환 → 날짜·논리값·타임스탬프 배열 정렬이 no-op.

수정 내역 (hbrtl/array.go)

  • detectArrayKind(items) — 1회 스캔으로 동종 배열 분류 (Int / Numeric / String / Date / Timestamp / Logical / Mixed)
  • 분류 결과에 따라 타입 특화 sort.SliceStable 선택. Int 배열은 AsNumInt만 써서 double 변환 생략
  • Mixed는 valueLess fallback — Harbour < 시맨틱 (NIL 가장 작음, 타입 내 비교)

AScan fast-path (hbrtl/array.go:302-380)

  • 검색값이 문자열·정수·실수일 때 타입별 인라인 루프 — valuesEqual 호출·switch·타입 체크 생략
  • 정수 검색 + 배열 내 double 원소는 cross-type 비교 (item.AsNumDouble() == float64(n)) — 기존 valuesEqual 시맨틱 그대로
  • Date/Timestamp/Logical/NIL 검색은 valuesEqual fallback

회귀 테스트 (tests/compat_harbour.prg:328-349)

  • 9c1 ASort dates ascending — julian 기준 정렬 (신규)
  • 9c2 ASort logicals: F,F,T,T — 논리값 정렬 (신규)
  • 9e1 AScan int found — 정수 탐색 (신규)
  • 9e2 AScan int cross-type — 정수로 저장된 배열에 double 검색 (신규)
  • 9e3 AScan int not found — 부재 케이스 (신규)

Harbour 호환 보장

  • 블록이 주어지면 100% 기존 동작 유지 (부작용 보존)
  • 블록 NIL 경로는 Harbour 기본 < 시맨틱 복제. 이전엔 깨져 있던 날짜/논리값이 이제 올바르게 정렬

검증

  • go test ./... — ALL PASS
  • FiveSql2 — 43/43
  • Harbour compat — 56/56 (51 기존 + 5 신규)

#6 RAT 재검토 — 2026-04-17 종결(변경 없음)

strings.LastIndex는 Boyer-Moore/Rabin-Karp 최적화 내장. target[:from] 슬라이스는 Go에서 O(1)·할당-프리. nOccurrence=1 경로(실제 대부분)는 이미 단일 LastIndex 호출. >1 경로는 이전 매치 위치에서 시작해 누적 O(n)이므로 수동 역방향 스캔 대비 이득 없음. 원본 유지.

#7 SqlExprHasAgg Go walker — 2026-04-17 완료

동기: 매 쿼리마다 SELECT 컬럼 표현식 × 재귀 깊이 만큼 호출 — 깊은 식에서 PRG VM 프레임 셋업 비용이 누적됨. 호출 지점: TSqlAgg.prg:41,62,85,134, TSqlExecutor.prg:1298 등 6곳.

구현 (hbrtl/sqlexpr.go)

  • aggFuncSetmap[string]struct{} (상수시간 룩업). AGG_FUNCTIONS 매크로와 완전 일치 (COUNT/SUM/AVG/MIN/MAX/GROUP_CONCAT/STRING_AGG/LISTAGG/JSON_ARRAYAGG/JSON_OBJECTAGG/XMLAGG/ANY_VALUE/BOOL_AND/BOOL_OR)
  • sqlExprHasAggWalk — PRG SqlExprHasAgg와 byte-for-byte 동일한 재귀 트리 순회. ND_FN/ND_BIN/ND_UNI/ND_CASE 가지 커버. ND_WINDOW/ND_SUB 의도적 미순회 (각자 집계 스코프 보유)
  • 상수 ndLit, ndCol, ndFn 등 — FiveSqlDef.ch의 kind 번호와 동일

호출부 변경

  • _FiveSql2/src/TSqlExpr.prg:45-49 — PRG FUNCTION SqlExprHasAgg 제거 (심볼 충돌 방지). 주석으로 Go RTL 위임 명시
  • hbrtl/register.goSQLEXPRHASAGG 공개 심볼 등록
  • 기존 호출부(SqlExprHasAgg(xE)) 그대로 동작 — RTL 심볼이 해결

Harbour 호환 보장: AST kind 번호가 PRG와 정확히 일치. agg 함수 이름 집합이 AGG_FUNCTIONS 매크로와 정확히 일치. 재귀 가지 로직이 PRG와 줄 단위로 매치 (IF xE[1] == ND_FN .AND. SqlIsAggName(xE[2]) 등).

검증

  • go test ./... — ALL PASS
  • FiveSql2 — 43/43
  • Harbour compat — 56/56

#8 SqlBulkInsert Go RTL — 2026-04-17 완료

동기: dbAppend/FieldPut은 이미 Go RTL. 병목은 PRG 루프가 행·컬럼 단위로 Go RTL을 호출하는 boundary crossing. N행 × M컬럼 = N·M 회 VM 프레임 셋업 + 스택 push/pop + 파라미터 마샬링.

구현 (hbrtl/sqlscan.go)

  • SqlBulkInsert(aRows) → nInserted — 현재 workarea의 *DBFArea에 직접 Append() + PutValue() + Flush()
  • NIL 원소는 필드 건너뜀 (PRG IF aRows[j][k] != NIL 보존)
  • 행 길이가 필드 수 초과 시 초과분 무시, 부족 시 나머지 필드는 default

호출부 치환 — 동일 형상 루프 3곳 → 1줄

위치 맥락
TSqlExecutor.prg:2310 CREATE TABLE AS SELECT / 임시테이블 로드
TSqlExecutor.prg:2630 subquery driving-table materialization
TSqlExecutor.prg:2935 CTE materialization

A/B 벤치마크 (_FiveSql2/test/bench_bulk.prg, 10k 행 테이블, 20 iteration)

테스트 PRG 루프 (before) SqlBulkInsert (after) 개선
BULK_CTE_10k (5k 행 materialize) 260 ms 194 ms 1.34x
BULK_SUBQ_10k (2k 행 materialize) 121 ms 107 ms 1.13x

쿼리당 환산: CTE 10k에서 (260-194)/20 = 3.3ms/쿼리 절감. 5000행 × 3컬럼 = 15000 boundary crossing → ≈ 220ns/crossing 절감 (VM 프레임 setup 비용).

기존 bench_sql(100행 규모) 효과 미미: 40행 × 2컬럼 = 80 crossing × 220ns ≈ 18µs/쿼리 절감. 4.3ms 쿼리에서 <1% noise. 실제 이득은 N이 커질수록 선형 증가.

Harbour 호환 보장

  • NIL 원소 스킵 동작 정확히 보존
  • 행/필드 길이 불일치 처리 동일
  • Flush() 호출로 dbCommit() 대체 — 동일한 디스크 반영 시점

검증

  • go test ./... — ALL PASS
  • FiveSql2 — 43/43
  • Harbour compat — 56/56

진행 순서

  1. #1 Hash 스토리지 — 완료
  2. #2 SqlDistinct — 완료
  3. #3 SqlRowCompare NULL·혼합타입 — 완료
  4. #4 ASort 정확성/특화 — 완료
  5. #5 AScan fast-path — 완료
  6. #6 RAT 재검토 — 변경 없음
  7. #7 SqlExprHasAgg Go walker — 완료
  8. #8 SqlBulkInsert — 완료 (Tier 4)
  9. #9 SqlBulkUpdate — 완료 (Tier 4)
  10. #10 MEMRDD 자동 임포트 — 완료 (Tier 4 인프라)
  11. #11 PcCompile 결과 캐시 — 완료 (Tier 4 회수 최적화)
  12. #12 SQL 플랜 캐시 + HbDeepClone — 완료 (Tier 4 상위 계층)
  13. #13 파라미터 바인딩 입증 — 완료 (기존 기능 + 플랜 캐시 결합 효과)
  14. #14 CTE → MEMRDD — 완료 (디스크 임시파일 제거)
  15. #15 SqlWindowPartitions Go RTL — 완료 (윈도우 파티션 빌드)
  16. #16 SqlWindowSortPartition Go RTL — 완료 (윈도우 정렬)
  17. #17 SqlGroupRows Go RTL — 완료 (GROUP BY 그룹 빌드)
  18. #18 SqlComputeAggSimple Go RTL — 완료 (집계 함수 fast-path)
  19. #19 SQL 스칼라 헬퍼 Go RTL — 완료 (IsTrue/CmpEq/CmpLt/Coerce×3)
  20. #20 SQL 템플릿 자동 파라미터화 — 완료 (리터럴 → ? + 플랜 캐시 공유)
  21. #21 TSqlLexer Go 포팅 + 결합 — 완료 (#20 효과 증폭)
  22. #22 SqlWindowAssignRank Go RTL — 완료 (ROW_NUMBER/RANK/DENSE_RANK)
  23. #23 HbDeepClone 성능 개선 — 완료 (스칼라 재귀 스킵 + 해시 키 공유)
  24. #24 WA 캐시 + 지연 commit — 완료 (B12 INSERT 48x)
  25. #25 Plan pcode 캐시 + Flush 지연 — 완료 (B13 UPDATE 48x)
  26. #26 SELECT plan pcode 캐시 — 완료 (SELECT fast-path 캐시 확장)
  27. #27 SqlEvalHaving Go RTL — 완료 (효과 미미, 복잡 HAVING 워크로드용)
  28. #28 VM Function() + Symbol 캐시 — 완료 (전역 3-15%, 호출 경로 핫패스)

전체 계획 완료 (2026-04-17). 각 단계 후 go test ./... + FiveSql2 43/43 + Harbour compat 필수 원칙 준수.

#9 SqlBulkUpdate Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlBulkUpdate(aFieldPositions, pcWhere, aValuePcodes) → nAffected — WHERE + SET 값 모두 컴파일된 pcode를 받아 Go 내부에서 스캔·평가·PutValue. FastFieldGetter 설치로 pcode 내부 FieldGet도 인터페이스 디스패치 없이 *DBFArea.GetValue 직접 호출
  • 공유 모드면 LockRecord/UnlockRecord로 레코드 락, 독점 모드면 생략
  • 비-DBF 워크에어리어는 제네릭 hbrdd.Area 경로로 fallback

PRG 연결 (_FiveSql2/src/TSqlExecutor.prg RunUpdate)

  • ::oTxn:IsActive() 이면 반드시 PRG 루프 (txn 로그 보존) — 안전 게이트
  • txn 없으면 WHERE + 각 SET 값을 SqlExprToPrgPcCompile로 pcode 변환 시도
  • 하나라도 실패(복잡 CASE·서브쿼리·UDF 등) 시 PRG 루프로 폴백
  • 모두 성공 시 SqlBulkUpdate(aFPos, pcWhere, aValuePc) 한 번 호출

A/B 벤치마크 (_FiveSql2/test/bench_bulk_upd.prg, 10k 행 테이블)

테스트 PRG 루프 SqlBulkUpdate 개선
2500행 매치 × 50회 (쓰기 지배적) 2140 ms 2153 ms noise (쓰기 비용 동일)
10k 전체 매치 × 10회 508 ms 145 ms 3.5x
0행 매치 × 100회 (WHERE만) 2288 ms 214 ms 10.7x

관찰

  • WHERE 평가가 지배적일수록 이득 큼 (pcode가 PRG EvalExpr보다 훨씬 빠름)
  • 쓰기 지배 워크로드는 PutValue 디스크 I/O가 병목 — RTL 효과 제한적
  • B13 소형 벤치(100행 × 1 매치)는 PcCompile 오버헤드 회수 전에 끝나 개선 미미. 규모 커질수록 선형 이득

Harbour 호환 보장

  • Txn 활성 시 반드시 PRG 경로 → 롤백·savepoint 시맨틱 보존 (test_sql1999.prg 4b SAVEPOINT + ROLLBACK TO 통과)
  • 복잡 표현식도 PRG 폴백 → SqlExprToPrg가 NIL 반환 시 기존 동작 그대로
  • 공유/독점 모드에 맞춘 락 정책

검증

  • go test ./... — ALL PASS
  • FiveSql2 — 43/43
  • Harbour compat — 56/56

#10 MEMRDD 자동 임포트 — 2026-04-17 완료

Five 컴파일러가 생성하는 Go 코드에 _ "five/hbrdd/mem" 블랭크 임포트를 자동 추가. 기존에는 mem 패키지가 init()에서 드라이버 등록하지만 아무도 임포트하지 않아 MEMRDD 미등록 상태였음.

변경 (compiler/gengo/gengo.go:103-120)

if hasXBaseCommands(file) {
    g.imports["five/hbrdd/mem"] = true
    g.importAlias["five/hbrdd/mem"] = "_"
}

이제 PRG에서 USE "mem:x" VIA "MEMRDD" / dbCreate("mem:x", aStruct, "MEMRDD") 즉시 사용 가능. 임시테이블·CTE materialize의 in-memory 전환 기반.

#11 PcCompile 결과 캐시 — 2026-04-17 완료

동기: #9 SqlBulkUpdate가 쿼리마다 SqlExprToPrgPcCompile을 호출. 파서+preprocess+genpc가 ~50200µs — B13(100행 × 1 매치) 같은 소형 쿼리에서는 RTL 절감분을 먹어치움.

구현 (hbrtl/pcexpr.go)

var pcCompileCache sync.Map // map[string]*hbrt.PcodeFunc
  • 캐시 히트 시 파서/genpc 건너뛰고 즉시 pointer 반환
  • sync.Map — read-mostly 패턴에 최적. PcodeFunc는 컴파일 후 불변이라 goroutine 간 공유 안전
  • 무한 캐시 — 실제 워크로드의 distinct 표현식 수는 작음 (쿼리 템플릿 수준). LRU는 이후 필요시 추가

효과 (bench_sql, 1000 iteration 반복 쿼리)

쿼리 캐시 전 캐시 후 개선
B13 UPDATE (1행 매치, SqlBulkUpdate 경로) 4309 ms 3536 ms 18%
B12 INSERT 3033 ms 3001 ms noise (파서가 별도 — 이 캐시는 PcCompile만 다룸)

SqlBulkUpdate가 PcCompile을 호출하는 쿼리(B13, 대량 UPDATE)에서 직접 이득. 타 벤치는 PcCompile을 호출하지 않거나 이미 1회만 호출해서 효과 없음.

Harbour 호환 보장: PcodeFunc는 immutable, 소스 문자열이 키. 동일 소스 → 동일 결과 보장. 컴파일 실패 시 캐시에 저장 안 함.

검증

  • go test ./... — ALL PASS
  • FiveSql2 43/43 · Harbour compat 56/56

#12 SQL 플랜 캐시 + HbDeepClone — 2026-04-17 완료

동기: TFiveSQL:Execute가 매 호출마다 lex + parse 실행. 반복 쿼리(B1B11, B13B15 등 벤치 대부분)는 동일 SQL 텍스트 → 파싱을 한 번만 수행하고 재사용하면 큰 이득.

안전 이슈: 기존 코드 주석(Parse — no caching (plan trees are mutated during execution))이 경고했듯 SqlFoldConst 등이 AST 노드를 in-place 변경 (_FiveSql2/src/TSqlExpr.prg:75-151). 캐시에서 포인터를 그대로 반환하면 첫 실행이 캐시를 오염.

구현

  • Go RTL HbDeepClone(xVal) → xNewVal (hbrtl/array.go) — deepCloneValue 재귀로 Array/Hash를 element별 복제. 스칼라는 불변이라 그대로 반환. HBDEEPCLONE · HB_DEEPCOPY 두 이름으로 등록
  • PRG 정적 캐시 s_hPlanCache (_FiveSql2/src/TFiveSQL.prg)
    • 히트: HbDeepClone(s_hPlanCache[cSQL]) 반환 → Run이 마음껏 변경해도 캐시 불변
    • 미스: 파싱 후 HbDeepClone(hQuery)를 캐시에 저장, 원본은 Run에 넘김
STATIC s_hPlanCache := { => }
...
IF hb_HHasKey( s_hPlanCache, cSQL )
   hQuery := HbDeepClone( s_hPlanCache[ cSQL ] )
ELSE
   ...parse...
   s_hPlanCache[ cSQL ] := HbDeepClone( hQuery )
ENDIF

효과 (bench_sql, 1000 iteration, µs/query)

# 쿼리 #11 이전 #12 적용 후 개선
B1 SELECT * 148 113 1.31x
B2 WHERE 166 85 1.95x
B3 ORDER BY 178 96 1.85x
B4 GROUP HAVING 877 731 1.20x
B5 DISTINCT 122 81 1.51x
B6 INNER JOIN 357 231 1.55x
B7 CTE simple 4415 4000 1.10x
B9 ROW_NUMBER 1134 1017 1.11x
B11 SUM OVER 621 493 1.26x
B13 UPDATE 3536 3301 1.07x
B15 CTE+WIN+JOIN 5751 5502 1.04x

B12 (INSERT)는 문자열 리터럴 i 값이 매 iteration마다 달라 캐시 미스 — 향후 파라미터 바인딩 도입 시 대상.

Harbour 호환 보장

  • 캐시 히트든 미스든 Run이 받는 hQuery는 항상 pristine — 첫 번째든 천 번째든 동일한 파싱 결과 트리
  • 공유 상태 (static hash)는 동일 프로세스 내 호환 — 멀티스레드 시 PRG STATIC이 goroutine-local이라는 Five의 스레드 모델 준수
  • SqlFoldConst를 포함한 모든 in-place 변경이 HbDeepClone 덕분에 격리
  • 43/43 FiveSql2 · 56/56 Harbour compat · go test ALL PASS

#13 파라미터 바인딩 입증 — 2026-04-17

기능은 기존에 이미 있었음 (five_SQL(cSQL, aParams) + 파서 ? 토큰 처리 + ND_PAR 노드). #12 플랜 캐시와 결합 시 동일 SQL 템플릿은 100% 캐시 히트. 사용자가 문자열 연결 대신 ? 전환 시 자동으로 이득.

A/B (1000 iteration)

패턴 문자열 연결 프리페어 ? 개선
INSERT 3214 ms 2881 ms 1.12x (쓰기 I/O 지배)
SELECT 254 ms 161 ms 1.58x (파서 지배)

응용 가이드: 반복 DML/SELECT는 ? + aParams 패턴 권장. 문자열 연결은 매번 파싱 비용 발생.

#14 CTE → MEMRDD — 2026-04-17 완료

동기: CTE materialize가 매 쿼리마다 dbCreate/USE/CLOSE로 디스크 임시 .dbf 생성. 단일 프로세스에서 불필요한 디스크 오염 + syscall 비용.

구현

  • MaterializeCTE (TSqlExecutor.prg:2287-2315), SqlMaterializeSubquery (:2672-2675), MaterializeRecursiveCTE (:2969-2988) — 3곳 dbCreate(cFile+".dbf")dbCreate("mem:"+cTmpFile, aStruct, "MEMRDD"), USEdbUseArea(.T., "MEMRDD", ...)
  • Sub-executor의 CTE 재오픈 경로 (:1222-1245) — MEMRDD 우선 시도, 실패 시 legacy .dbf fallback (기존 디스크 임시파일 호환)
  • 버그 수정 1: SqlBulkInsert (hbrtl/sqlscan.go)가 *dbf.DBFArea에 하드 타입 어설션 → MEMRDD 경로에서 0 반환. 일반 hbrdd.Area 인터페이스 fallback 추가
  • 버그 수정 2: MEMRDD Create (hbrdd/mem/memrdd.go)가 호출자가 넘긴 DBF 스타일 PadR(name, 10)을 그대로 저장 → FieldPos("ID")"ID "와 미스매치. Create에서 TrimRight(name, " ") 정규화

효과

  • bench_sql B7/B8 (40행 CTE): 4000→4075 / 3947→4010 ms — noise (OS 파일 캐시로 소형 DBF도 이미 빠름)
  • bench_bulk 5000행 CTE: 194→185 ms — 5% 개선
  • 정확성: 디스크에 __cte_*.dbf 임시파일 생성 제거 → 동시 실행 시 파일명 충돌 없음, 권한 이슈 없음

Harbour 호환

  • aTables[i][1] 값(cTmpFile)은 여전히 "__cte_xxx" 형태 — 외부 로직 변경 없음
  • sub-executor fallback 경로로 기존 .dbf 파일 운용 케이스도 호환
  • test_sql1999.prg 43/43 전부 통과 (CTE/RECURSIVE CTE/CTE+Window/CTE+JOIN 포함)

#15 SqlWindowPartitions Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlWindowPartitions(aRows, aPartColIdx) → aPartitions — PARTITION BY 컬럼 인덱스 배열을 받아 행-인덱스별 그룹 배열 반환. 첫 등장 순서 보존
  • appendValueHashKey 공유로 키 구성이 SqlValToStr와 byte-for-byte 일치
  • aPartColIdx → 전체 행을 단일 파티션으로 반환 (no-PARTITION-BY 시맨틱)

PRG 호출부 (ApplyWindowFunctions)

  • PARTITION BY 컬럼을 한 번만 SqlFindColIdx로 해석해 aPartColIdx로 묶음
  • SqlWindowPartitions( aRows, aPartColIdx ) 1회 호출로 루프 전체 대체
  • FOR EACH aPartIdx IN hb_HValues(hPartitions)FOR EACH aPartIdx IN aPartitions

bench_sql 효과 (직전 → 현재)

# 쿼리 직전 (µs) 지금 (µs) 개선
B7 CTE simple 4075 127 32.1x
B8 RECURSIVE CTE 4010 155 25.9x
B9 ROW_NUMBER 1030 971 1.06x
B10 RANK PARTITION 1249 1145 1.09x
B11 SUM OVER 492 384 1.28x
B15 CTE+WIN+JOIN 5271 2547 2.07x

대형 개선 원인 해설: 이 변경 자체는 B9~B11의 PARTITION BY 루프 하나만 건드렸지만, B7/B8/B15 같은 CTE 쿼리에서도 큰 개선이 나타남. CTE materialize 후 재실행 경로에서 stale __cte_*.dbf 디스크 파일이 섞여 있던 이전 상태 → #14 MEMRDD 도입 + 깨끗한 상태에서 재측정된 효과로 판단. 반복 실행 확인 결과 수치는 안정적 (127ms ± 1%).

#16 SqlWindowSortPartition Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlWindowSortPartition(aRows, aPartIdx, aSortSpec) → aPartIdx — 파티션 배열을 sort.SliceStable로 in-place 정렬. aSortSpec: 사전 해석된 {nCol, lDesc}
  • NIL 시맨틱: PRG SqlWinRowCmp byte-for-byte 일치 (NIL = 가장 큼 → NULLS LAST in ASC, NULLS FIRST in DESC)
  • 혼합 타입: PRG 동일하게 ValType 미일치 시 다음 정렬 키로 이동
  • Stable sort로 SqlWindowPartitions의 first-seen 순서 보존

PRG 호출부 (ApplyWindowFunctions)

  • ORDER BY 컬럼 인덱스를 윈도우 컬럼마다 한 번만 해석 → aSortSpec
  • 파티션마다 SqlWindowSortPartition(aRows, aPartIdx, aSortSpec) 호출
  • 기존 ASort(aPartIdx,,, {|a,b| SqlWinRowCmp(...) < 0}) PRG 블록 경로 제거

bench_sql 효과

# 쿼리 직전 (µs) 지금 (µs) 개선
B9 ROW_NUMBER 971 270 3.60x
B10 RANK PARTITION 1145 462 2.48x
B11 SUM OVER (no ORDER BY) 384 382 noise (정렬 미사용)
B15 CTE+WIN+JOIN 2547 2158 1.18x

개선 원인: ASort가 PRG 블록 콜백을 O(N log N)번 호출. 블록마다 SqlWinRowCmpSqlFindColIdx 컬럼 재해석이 반복됨. Go 경로는 (i) 블록 경계 크로싱 제거, (ii) 컬럼 인덱스를 쿼리당 1회만 해석. 20행 파티션 × 5개 × 100 비교 ≈ 500 크로싱/쿼리 → 0.

Harbour 호환: 43/43 FiveSql2 · 56/56 compat · go test ALL PASS. NULL 순서 · mixed-type 처리 모두 PRG SqlWinRowCmp와 동일.

#17 SqlGroupRows Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlGroupRows(aRows, aGroupColIdx) → aGroupedRows — 행 값(인덱스 아님) 기준으로 그룹 배열 반환. first-seen 순서 보존
  • appendValueHashKey 공유로 SqlValToStr 시맨틱 byte-for-byte 일치
  • aGroupColIdx → 전체 행이 단일 그룹 (no-GROUP-BY aggregate 시맨틱)

PRG 호출부 (TSqlAgg.prg GroupBy)

  • PRG의 cKey += SqlValToStr(...) + "|" → hb_HHasKey → AAdd 루프를 SqlGroupRows(aRows, aGroupIdx) 1회 호출로 대체
  • FOR EACH aGroupRows IN hb_HValues(hGroups)FOR EACH aGroupRows IN aGroupedRows
  • 집계·HAVING 평가는 PRG 유지 (복잡한 표현식 처리 — 서브쿼리, CASE, COUNT DISTINCT 등)

bench_sql 효과

# 쿼리 직전 (µs) 지금 (µs) 개선
B4 GROUP_HAVING 738 659 1.12x
B10 RANK PART (GROUP도 씀) 462 397 1.16x
B15 CTE+WIN+JOIN 2158 2065 1.04x

한계: 소규모 벤치(100행·5그룹)에선 집계 계산·HAVING 평가가 PRG에 남아 이득 제한적. 대량 행·다중 그룹 키 쿼리에선 선형 이득 증가.

Harbour 호환 보장

  • 첫 등장 순서 유지 → 결과 행 순서 불변
  • SqlValToStr 시맨틱 동일 → 그룹 키 동등성 불변
  • ROLLUP/CUBE/GROUPING SETS 경로는 재귀 호출로 동일하게 이 함수를 이용
  • 43/43 · 56/56 · go test ALL PASS

#18 SqlComputeAggSimple Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlComputeAggSimple(aGR, nCol, cFunc) — 사전 해석된 컬럼 인덱스로 단일-pass 집계 루프. 타입 구분 비교 (compareValuesNonNil)로 PRG SqlCmpLt와 일치
  • 지원: COUNT / SUM / AVG / MIN / MAX (컬럼 인자 한정)
  • COUNT(*) / 전체 카운트는 nCol=0 케이스로 처리
  • SUM/AVG는 모든 값 NIL이면 NIL 반환 (SQL 표준)

PRG 호출부 (TSqlAgg.prg ComputeAgg)

IF nCol > 0 .AND. xArg[ 1 ] == ND_COL .AND. ;
   ( cFunc == "COUNT" .OR. cFunc == "SUM" .OR. cFunc == "AVG" .OR. ;
     cFunc == "MIN" .OR. cFunc == "MAX" )
   RETURN SqlComputeAggSimple( aGR, nCol, cFunc )
ENDIF
/* 복잡한 인자(CASE/BIN/UDF) + GROUP_CONCAT은 기존 PRG 경로 유지 */

bench_sql 효과

# 쿼리 직전 (µs) 지금 (µs) 개선
B4 GROUP_HAVING 659 585 1.13x
B14 COUNT 374 364 1.03x
B15 CTE+WIN+JOIN 2065 1980 1.04x

Harbour 호환 보장

  • PRG SqlCmpLt 시맨틱 그대로 (타입 내 비교, NIL 제외)
  • SQL 표준 NULL 처리 (SUM of all NULLs = NULL)
  • 복잡 인자·GROUP_CONCAT은 자동으로 PRG fallback — 기능 회귀 없음
  • 43/43 · 56/56 · go test ALL PASS

#19 SQL 스칼라 헬퍼 Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlhelpers.go)

  • SqlIsTrue(x) — SQL truthiness (NIL/빈문자/0 → false)
  • SqlCmpEq(a,b) — 대소문자 무시 + trim + cross-type N↔C 강제변환 비교
  • SqlCmpLt(a,b) — 대소문자 무시 + trim + cross-type 미만 비교
  • SqlCoerceForCmp(x) — 비교용 정규화 (trim + upper for strings)
  • SqlCoerceNum(x) / SqlCoerceStr(x) — 스칼라 변환

버그 수정: 초기 구현에서 at == bt 같은 타입-엄격 검사로 NumInt vs Double 비교 실패. PRG ValType은 둘 다 "N"으로 반환하지만 Go Type()tInt vs tDouble 구분. IsNumeric() && IsNumeric()로 일원화해 수정. 테스트 6b (SUM(amount) > 1000) 회귀로 발견.

PRG 정의 제거 (TSqlFunc.prg) — 심볼 충돌 방지. 기존 호출자는 자동으로 Go RTL 해결.

효과: 벤치 대부분이 이미 pcode 경로 사용 중이라 제한적 — B13 UPDATE 3451 → 3341 µs (~3%). 주 이득은 HAVING 평가 + 비-컴파일 가능 복잡 표현식 경로에서 누적됨. 대량 행·복잡한 WHERE의 장기 워크로드에서 누적 효과 예상.

Harbour 호환 보장

  • 43/43 · 56/56 · go test ALL PASS
  • PRG 원본과 byte-for-byte 동일 (NULL/cross-type/trim-upper 전부 유지)

#20 SQL 템플릿 자동 파라미터화 — 2026-04-17 완료

구현 (hbrtl/sqlhelpers.go)

  • SqlExtractTemplate(aTokens) → { cKey, aParams } — 토큰 배열을 in-place 수정:
    • TK_TEXT/TK_NUM 리터럴 → TK_QMARK 치환 + 값을 aParams에 순서대로 추출
    • 비-리터럴 토큰은 타입+이름을 템플릿 키에 포함해 셰이프 구분

PRG 연결 (TFiveSQL.prg)

  • 사용자가 명시 aParams를 넘기지 않았으면 자동-파라미터화 경로:
    1. 렉싱 1회
    2. SqlExtractTemplate로 템플릿 키 + 추출된 aParams
    3. 템플릿 키로 플랜 캐시 조회; 히트 시 HbDeepClone; 미스 시 파싱 후 저장
    4. 추출된 aParams를 Executor에 전달 → ND_PAR 노드가 정상 해석
  • 명시 aParams가 있으면 기존 cSQL-키 경로 유지 (prepared statement 그대로)

효과

쿼리 이전 µs 현재 µs 개선
B12 INSERT (concat) 3037 3086 noise (lex 비용이 parse 절감 상쇄)
PREPARED_INSERT 2881 2755 1.05x (plan cache 히트율 상승)
PREPARED_SELECT 161 166 noise

한계

  • 1000회 반복 벤치에서 lex 비용 (PRG SubStr 기반 렉서)이 parse 절감과 비슷한 수준 → 단독 효과 미미
  • 진짜 이득은 다양한 쿼리 셰이프가 반복되는 실제 워크로드 (예: 보고서 쿼리) — 플랜 캐시 히트율 상승
  • 향후 렉서 Go 포팅 또는 SQL 텍스트 직접 정규화(pre-lex normalization)로 lex 비용도 절감 가능

Harbour 호환 보장

  • 기능적으로 동일 — PRG가 ? + aParams 수동 사용했을 때와 완전 동등
  • 사용자 명시 aParams와 충돌 방지 (별도 경로)
  • 43/43 · 56/56 · go test ALL PASS

#21 TSqlLexer Go 포팅 + lex-and-extract 결합 — 2026-04-17 완료

구현 (hbrtl/sqlhelpers.go)

  • lexSQL(s string) []hbrt.Value — Go byte-level FSM. TSqlLexer:Tokenize의 PRG SubStr 기반 버전 대체. 동일한 {nType, cText} 배열 반환
    • 공백/라인주석/블록주석 스킵
    • 문자열 리터럴 ('' 이스케이프)
    • 숫자 리터럴 (정수/소수)
    • 식별자/키워드 (대문자 정규화)
    • 브래킷 식별자 [name]
    • 파라미터 ?
    • 단일/다중 문자 연산자 (<=, <>, >=, !=, ||)
  • SqlLexerTokenize(cSQL) → aTokens — 단순 lex RTL
  • SqlLexAndExtractTemplate(cSQL) → {aTokens, cKey, aParams} — lex + 템플릿 정규화 1회 결합 (PRG→Go boundary 크로싱 감소)

PRG 연결 (TFiveSQL.prg)

  • TSqlLexer:New + Tokenize + GetTokens 제거 (PRG 렉서 객체 미사용)
  • 자동-파라미터화 경로: SqlLexAndExtractTemplate 1회 호출로 {tokens, cKey, aParams} 획득
  • 명시 aParams 경로: SqlLexerTokenize로 단순 lex 후 파서에 전달

bench_sql 효과

# 쿼리 이전 (µs) 지금 (µs) 개선
B8 RECURSIVE CTE 156 148 1.05x
B10 RANK PART 400 377 1.06x
B11 SUM OVER 382 336 1.14x
B12 INSERT 3086 2991 1.03x
B13 UPDATE 3480 3415 1.02x
B15 CTE+WIN+JOIN 1981 1922 1.03x

bench_prep_sql (1000 iter)

패턴 이전 µs 지금 µs 개선
CONCAT_INSERT 3142 2996 1.05x
CONCAT_SELECT 260 251 1.04x
PREPARED_INSERT 2755 2734 1.01x
PREPARED_SELECT 166 161 1.03x

CONCAT_INSERT(2996)가 이제 PREPARED_INSERT(2734)에 근접 — 자동 파라미터화 효과가 드러남. 남은 차이는 쓰기 I/O 비용(둘 다 동일).

Harbour 호환 보장

  • 토큰 형식·타입 코드 완전 일치 (FiveSqlDef.ch의 TK_* 상수와 동일)
  • 문자열 이스케이프·주석·연산자 파싱 byte-for-byte 매치
  • 기존 PRG TSqlLexer는 유지 (아직 사용 안 하지만 외부 참조 가능)
  • 43/43 · 56/56 · go test ALL PASS

#22 SqlWindowAssignRank Go RTL — 2026-04-17 완료

구현 (hbrtl/sqlscan.go)

  • SqlWindowAssignRank(aRows, aPartIdx, aSortSpec, nColIdx, cFunc) — 정렬된 파티션 한 번 순회하며 랭크 값을 결과 컬럼에 기록
  • aSortSpec은 #16 SqlWindowSortPartition에서 사전 해석된 {nCol, lDesc} 배열 그대로 재사용
  • 3개 함수 통합 처리:
    • ROW_NUMBER: 순서대로 1..N 배정
    • RANK: 동일 값 → 같은 랭크, 다음은 k+1
    • DENSE_RANK: 동일 값 → 같은 랭크, 다른 값 → rank+1

PRG 호출부 (ApplyWindowFunctions)

  • 3개 CASE를 통합된 Go 호출로:
    CASE cFunc == "ROW_NUMBER" .OR. cFunc == "RANK" .OR. cFunc == "DENSE_RANK"
       SqlWindowAssignRank( aRows, aPartIdx, aSortSpec, nColIdx, cFunc )
    
  • 기존 PRG 루프 + per-row SqlWinRowsEqual 호출 제거

bench_sql 효과

# 쿼리 이전 (µs) 지금 (µs) 개선
B9 ROW_NUMBER 270 265 1.02x (이미 빠름 — 동순위 검사 불필요)
B10 RANK PARTITION 377 309 1.22x
B11 SUM OVER 336 334 noise (RANK 미사용)

Harbour 호환

  • NIL 동등 검사 정확 재현 (NIL == NIL, NIL ≠ non-NIL)
  • 타입 내 비교는 compareValuesNonNil 재사용 → 기존 SqlCmpLt == 0 시맨틱과 일치
  • 43/43 · 56/56 · go test ALL PASS

#23 HbDeepClone 성능 개선 — 2026-04-17 완료

변경 (hbrtl/array.go deepCloneValue)

  • 배열 원소가 Array/Hash일 때만 재귀 호출; 스칼라(문자열/숫자/논리/Date/NIL)는 슬롯 복사만
  • 해시 키는 복사하지 않고 공유 (Five Hash는 문자열/숫자 키가 일반적 + 불변)

배경: 플랜 캐시 히트마다 전체 hQuery 트리를 deep clone. AST 노드는 {nKind, xVal, xLeft, xRight, xExtra} 5-element 배열이고 대부분 내부 요소가 스칼라. 기존 구현은 스칼라에도 함수 호출+switch 수행.

bench_sql 효과 (측정 내 변동 ±1%, 누적 영향)

쿼리 이전 (µs) 지금 (µs) 개선
B1 SELECT * 117 106 1.10x
B8 RECURSIVE CTE 150 149 noise
B12 INSERT 3082 3000 1.03x
B15 CTE+WIN+JOIN 1930 1932 noise

작은 쿼리에선 노이즈 수준이지만, 대형 AST (복잡한 CTE, 깊은 서브쿼리)에선 선형적 이득.

Harbour 호환

  • Hash 키 공유는 PRG Hash API가 키 변경 비공식(삽입 후 변경은 보통 Delete+Insert)이라 안전
  • 43/43 · 56/56 · go test ALL PASS

#24 WA 캐시 + 지연 commit — 2026-04-17 완료 (최대 이득)

Go RTL 설계 (hbrtl/sqlwacache.go)

  • 프로세스-전역 sync.Mutex 보호 map[alias→nWA] + enabled bool
  • 노출 심볼: SqlWACacheEnable / Disable / IsEnabled / Get / Put / Invalidate / CloseAll
  • 기본 disabled — 회귀 테스트·일회성 스크립트는 기존 동작 보존
  • 사용자가 opt-in 해야 활성화 (벤치·서버·긴-러닝 앱용)

PRG 연결 (TSqlExecutor.prg SqlExecOpenTable/CloseTable)

  • SqlExecOpenTable(cTable, cAlias): 캐시 enabled + 적중 → dbSelectArea 재사용; 아니면 dbUseAreaPut
  • SqlExecCloseTable(cAlias, nWA): 캐시 켜지고 등록된 WA면 스킵, 아니면 기존처럼 close
  • RunInsert / RunUpdate / RunDelete 3곳 교체
  • 핵심 트릭: 캐시 enabled 시 각 메서드 끝의 dbCommit()도 스킵 (IF ! SqlWACacheIsEnabled()) → 배치 commit은 dbCloseAll() 시점 또는 사용자 통제

bench_sql (cache 활성화)

쿼리 이전 (µs) 지금 (µs) 개선
B12 INSERT 3011 62 48.6x
B13 UPDATE (1행 매치) 3439 3275 1.05x (scan/eval가 지배, commit 비중 작음)
SELECT 계열 거의 동일 거의 동일 -

누적 개선 (2026-04-08 원본 → 현재): B12 INSERT 4,319 → 62 µs = 69.7x

Harbour 호환 보장

  • opt-in 설계라 기본 동작 불변 (43/43 통과)
  • 열린 WA lifecycle은 사용자 책임 (CREATE/DROP 시 SqlWACacheInvalidate 호출; 프로세스 종료 시 dbCloseAll)
  • CREATE/DROP TABLE 자동 invalidate 통합은 향후 확장 — 현재는 명시적 API 제공

사용 예 (bench_sql.prg)

SqlWACacheEnable()
FOR i := 1 TO 1000000
   five_SQL( "INSERT INTO log VALUES (?, ?, ?)", { ... } )
NEXT
SqlWACacheDisable()
dbCloseAll()  // flush + close all

검증: go test ALL PASS · FiveSql2 43/43 (cache disabled 기본) · Harbour compat 56/56

#28 VM Function() + Symbol hoist — 2026-04-17 완료 (전역 3-15%)

동기: 모든 PRG 함수 호출이 Function() + FindSymbol을 거침. 프로파일 결과 기존 코드는 (a) 매 호출 heap 할당 (args := make([]Value, nArgs)), (b) 인자 pop-push 왕복, (c) 매 호출 strings.ToUpper + RWMutex + map 조회. 개별 비용은 작지만 쿼리당 수십~수백 회 호출 → 누적이 큼.

구현

  • hbrt/call.go Function: pop-push dance를 single-slice-shift로 대체. N+2 pops + N pushes → 1 copy + sp 조정. Heap alloc 제거. Resolve 성공 시 sym.Func = fn으로 캐시
  • hbrt/vm.go GetSym: GetSym(cache **Symbol, name string)*cache != nil이면 즉시 반환, 아니면 FindSymbol 후 캐시 (nil은 init-order 재시도 허용해 캐시 안 함)
  • hbrt/thread.go pushPendingSym: depth=1 호출(대부분)을 위한 scalar fast slot 추가
  • compiler/gengo/gengo.go emitPushSymbol: t.PushSymbol(t.VM().FindSymbol(%q))t.PushSymbol(t.GetSym(&_sym_file_NAME, %q)). 파일별 prefix로 다중-PRG 빌드 충돌 방지

bench_sql 효과

쿼리 이전 (µs) 현재 (µs) 개선
B1 SELECT * 114 97 15%
B4 GROUP_HAVING 584 554 5%
B8 RECURSIVE CTE 150 141 6%
B10 RANK PART 310 296 5%
B11 SUM OVER 335 320 4%
B14 COUNT 295 281 5%

전체 쿼리에서 3-15%. 평균 ~5% 단순 개선이지만 모든 쿼리가 혜택.

버그 수정 (구현 중 발견)

  • pendingSymFast = nil이 "빈 슬롯"과 "nil 심볼 저장" 양쪽 의미라 ambiguous. nil 심볼은 슬라이스 경로로 fallback해 해결
  • GetSym이 nil resolve 결과 캐시하면 init 순서 문제로 영구 미해결 가능. nil 캐시 생략해 재시도 허용
  • gengo의 중복 호출 분기가 t.PushSymbol(varName)을 직접 emit해 lazy init 우회. 모든 호출을 emitPushSymbol 통일

Harbour 호환 보장: 43/43 · 56/56 · go test ALL PASS.

#27 SqlEvalHaving Go RTL — 2026-04-17 완료 (효과 미미)

구현 (hbrtl/sqlscan.go SqlEvalHaving)

  • SqlEvalHaving(xE, aNewRow, aCols, aGR, aFN, aParams) → {lOk, lPass}
  • Go AST walker: ND_LIT / ND_NIL / ND_COL / ND_FN(COUNT/SUM/AVG/MIN/MAX with ND_COL 인자) / ND_BIN (AND/OR/비교) / ND_UNI (NOT/-)
  • 지원 외 노드 만나면 lOk=.F. 반환 → PRG fallback

PRG 연결 (TSqlAgg.prg EvalHaving)

  • 먼저 Go RTL 호출, lOk=.T.이면 결과 사용, 아니면 기존 EvalHavingExpr PRG walker

프로파일 결과 (별도 측정, 5 그룹 × 3 컬럼 GROUP BY)

패턴 이전 현재 차이
GROUP BY + HAVING 589 µs 579 µs -10 µs (1.7%)
GROUP BY no HAVING 568 565 noise

솔직한 평가: HAVING 자체가 B4 전체의 ~21 µs (3.6%) 차지. Go RTL 호출 오버헤드 (array allocation × 그룹 수 + PRG-Go 경계)가 절감을 상쇄. 단일 비교 HAVING에선 PRG 버전이 이미 충분히 빠름.

의미 있는 케이스: 복잡한 HAVING (다중 AND/OR, CASE) 또는 많은 그룹 (수백~수천)에서 이론적 이득 있음. 현재 벤치 규모에선 드러나지 않음.

Harbour 호환: 43/43 · 56/56 · go test ALL PASS. 복잡한 케이스 PRG fallback으로 안전.

#26 SELECT 경로 plan pcode 캐시 — 2026-04-17 완료

구현 (TSqlExecutor.prg RunSelect fast path)

  • aFP (TryBuildFieldPositions 결과) + pcW (TryCompileWhere 결과)를 s_hDmlPcodeCache[cCacheKey + "#sel"]에 캐시
  • 반복 SELECT (같은 SQL 템플릿)는 SqlExprToPrg AST walk 생략

효과: 벤치에선 이미 PcCompile source-string 캐시가 있어 소폭 변화. 복잡한 WHERE 표현식을 가진 대량 반복 SELECT 워크로드에서 추가 이득.

Harbour 호환 보장: 43/43 · 56/56 · go test ALL PASS

#25 Plan pcode 캐시 + SqlBulkUpdate Flush 지연 — 2026-04-17 완료 (B13 48x)

동기: B13 UPDATE가 1행-매치임에도 3275µs. 프로파일 결과 SqlBulkUpdate Go RTL 내부 dbfArea.Flush()가 1.6ms 차지 — macOS APFS fsync 비용이 매 UPDATE 누적.

구현 2단계

(A) Plan-level pcode 캐시TSqlExecutor.cCacheKey + s_hDmlPcodeCache

  • TFiveSQL:Execute가 plan-cache 키(cKey 또는 cSQL)를 ::oExec:cCacheKey로 전달
  • RunUpdate가 cache hit 시 SqlExprToPrg + PcCompile 왕복 완전 생략
  • 처음 1회 컴파일 후 {set_fpos, set_pc, where_pc} stash

(B) Go RTL Flush 지연 — 실제 B13 병목의 주요 원인

  • SqlBulkUpdatewaCacheEnabledSafe() 체크 후 Flush() 스킵
  • 캐시 활성 시 PRG dbCommit과 Go Flush 모두 배치됨 → dbCloseAll()에서 일괄 fsync

효과

쿼리 이전 (µs) 현재 (µs) 개선
B13 UPDATE 3275 67 48.9x
B12 INSERT 62 62 유지
기타 동일 동일 -

프로파일 (10k iter, 단일 행 UPDATE)

  • 이전: 1640 µs/call, SqlBulkUpdate Go 내부 1602µs
  • 이후: 14.4 µs/call, SqlBulkUpdate 7.6 µs

Harbour 호환 보장

  • WA 캐시 disabled 기본값에서 Flush 여전히 수행 (durability 유지)
  • 43/43 · 56/56 · go test ALL PASS
  • 사용자 dbCommit / dbCloseAll 명시 호출 시 배치된 변경 정상 flush

#27 VM in-place stack ops + symbol hoist — 2026-04-18 완료 (글로벌 3-15%)

동기: RTL 대변동 이후 남은 VM 핫패스. 매 산술/비교 연산마다 pop→pop→push 왕복이 값 복사 3회 + 슬롯 클리어 3회. PushSymbol(FindSymbol("QOUT")) 패턴이 매 호출 심볼 테이블 조회.

구현 (5 커밋: 1f63c7f 15aa6dd 54b35f9 0fd3698 523d3fc)

  • Symbol hoist: 함수 호출 심볼을 package-level var _sym_<file>_<NAME> *hbrt.Symbol로 캐시. 런타임 첫 호출 시 resolve → cache
  • Function() stack shift: 인자를 아래로 shift해 PushNil 슬롯 덮어쓰기 (pop+push 왕복 제거)
  • In-place ops: Plus/Minus/Mult/Equal/NotEqual/Less/Greater/GreaterEqual/And/Or 전부 sp-=2; *dst = op(stack[sp], stack[sp+1]) 형태로. tInt+tInt fast path 포함
  • ArrayGen/EvalBlock: 단일 copy + 스택 shift

효과

  • 모든 벤치에서 글로벌 3-15% 감소
  • 특히 tight loop (FOR/WHILE + 산술/비교)에서 최대 이득

Harbour 호환 보장: 43/43 · 56/56 · go test ALL PASS

#28 gengo 컴파일타임 peephole — 2026-04-18 완료 (9 커밋)

동기: Harbour는 PRG 소스를 pcode로 컴파일 → VM이 해석하는 2단계 구조. Five는 PRG를 직접 Go 소스로 emit (gengo). 이 Go 출력 자체를 최적화하면 런타임 VM 비용이 사라진다.

구현 (a0acdf0 111ab8a 3f8ef7d 1b6d913 c3a9eb3 67a9855 7e4079f b829ed4 6974ff9)

Peephole 예시 emit 변화
상수 폴딩 2 + 3 * 4 t.PushInt(14) (1 op)
단항 - 리터럴 -42 t.PushInt(-42)
x := x + y t.LocalAdd(idx) 3 ops → 1 op
죽은 IF .F. / ELSEIF .F. / .T. 본문만 emit wrapper 제거
.AND. / .OR. 리터럴 LHS .F. .AND. xPushBool(false) short-circuit
.NOT. <literal> PushBool(true/false) Not() 호출 제거
DO WHILE .T. / .F. for {} / 본문 삭제 PopLogical per-iter 제거
문자열 concat 좌향 재결합 "a" + x + "b" + "c""a" + x + "bc" 2 Plus 절약
상수 전파 LOCAL LOCAL k := 100; FOR i TO kFOR i TO 100 모든 fold를 깨움
죽은 store 제거 상수 전파된 LOCAL의 init 자체 skip 프롤로그 6 ops 절약

효과: 개별 peephole은 1-3%. 누적 수익은 크지 않지만(FiveSql2 벤치 SELECT 1-7%) 컴파일타임 비용 0이고 모든 Five 프로그램에 적용.

Harbour 호환 보장: 43/43 · 56/56 · go test ALL PASS. 상수 전파 walker는 알 수 없는 AST 노드에서 abort(보수적) — 이상한 함수는 단순히 최적화 대상에서 빠질 뿐 오류 없음.

#29 SELECT 경로 WA 캐시 확장 — 2026-04-18 완료 (단일 테이블 SELECT 2x)

동기: WA 캐시(SqlWACacheEnable)는 DML 경로만 커버. SELECT은 매 쿼리 TSqlExecutor:OpenTable → dbUseArea → ... → CloseOpened → dbCloseArea 수행. 프로파일 결과 rtlDbCloseArea + munmap이 SELECT CPU의 ~30%.

구현 (f27c96c TSqlExecutor.prg)

  • OpenTable이 성공 시 WA 캐시 활성화 상태면 SqlWACachePut(alias, nWA)만 호출하고 aOpened에 추가 안 함
  • CloseOpenedaOpened만 순회하므로 캐시 보유 영역은 건드리지 않음 — 다음 쿼리 Select(alias) > 0OpenTable 자체 스킵
  • AcquireTemp-생성 alias(FA_####)는 캐싱 제외 (매 쿼리 alias 변경 → 캐시 히트 불가능 + 무한 누적)

효과 (bench, 1000 iters, median of 3)

벤치 이전 (µs) 현재 (µs) 개선
B1_SELECT_STAR 82 41 2.0x
B2_WHERE_FILTER 78 35 2.2x
B3_ORDER_BY 90 48 1.88x
B5_DISTINCT 75 32 2.34x
B7_CTE_SIMPLE 120 77 1.56x
B9-B11 Window 15-19%

Harbour 호환 보장: 43/43 · 56/56. 캐시는 opt-in이라 기본 경로 semantics 불변.

#30 JOIN temp-alias 안정화 — 2026-04-18 완료 (B6 1.67x)

동기: TSqlAlias:AcquireTemp가 매 호출 FA_####(순차 생성) 반환 → 같은 JOIN 쿼리를 1000번 반복해도 alias가 매번 바뀌어 WA 캐시 히트 불가능. B6 INNER_JOIN이 단일 SELECT 대비 5배 느린 원인.

구현 (6746ae4 TSqlAlias.prg)

  • AcquireTemp(cPurpose): cPurpose(대문자 테이블명)를 그대로 alias로 반환 — 단, 이번 쿼리의 aSlots에 이미 등록돼 있으면(FROM emp e1, emp e2 자기조인) FA_#### fallback
  • 첫 글자가 알파벳이 아니면 fallback (Harbour alias 규칙)

효과

  • B6_INNER_JOIN: 217 → 130 µs (1.67x)
  • B15_CTE_WIN_JOIN: 1678 → 1595 µs (-5%)
  • B8 recursive CTE flat (서브-executor가 nDepth>1에서 여전히 fresh alias)

Harbour 호환 보장: 43/43 · 56/56.

#31 Stat 루프 게이트 — 2026-04-18 완료 (CPU -17pp, wall-clock flat)

동기: 프로파일 재측정 결과 WA 캐시 후에도 HbFileExists가 28% → 20% CPU 유지. 두 루프가 범인:

  1. RunSelect cleanup의 __view_<table>.dbf stat (VIEW 안 쓰는 쿼리에서도 실행)
  2. CTE cleanup의 __cte_<name>.dbf stat (MEMRDD 사용하므로 파일이 없는데도 매번 stat)

구현 (9bb361b c4ae88e)

  • TSqlIndex.lViewUsed 플래그: CheckView__view_* temp를 materialize하면 set. cleanup은 플래그 set일 때만 실행
  • s_lCteDiskSeen STATIC: CTE 레거시 disk fallback이 실제 발동했을 때만 set. cleanup은 플래그 set일 때만 실행

효과 (pprof)

  • rawsyscalln: 48% → 32% → 8.5% (두 게이트 누적 40pp 감소)
  • HbFileExists: 28% → dropped out of top
  • Wall-clock: flat (ENOENT stat은 Darwin 커널 캐시로 이미 저렴)

의미: Wall-clock 개선은 없지만 CPU 낭비 제거로 다른 워크로드와의 자원 경합 완화. 프로파일이 애플리케이션 코드를 넘어 Go runtime 프리미티브(kevent, madvise, pthread_cond_*) 중심으로 이동.

#32 Go-native SqlIsAggName + FetchRow — 2026-04-18 완료 (집계/윈도우 1.3-1.7x)

동기: B4 GROUP+HAVING 프로파일링 (447µs — 단일 SELECT 대비 10x 느림)

  • SqlIsAggName 8.7%: "," + c + "," ) $ ( "," + AGG_FUNCTIONS + "," ) — 매 함수 호출마다 2회 문자열 alloc + substring scan
  • FetchRow 30%: 캐시 binding이 있어도 PRG FOR 루프가 컬럼마다 dbSelectArea + FieldGet + AllTrim + AAdd 메서드 dispatch

구현

(A) SqlIsAggName Go 네이티브 (c84cde6, hbrtl/sqlhelpers.go)

  • 기존 sqlexpr.goaggFuncSet (map[string]struct{}) 재사용
  • 입력이 이미 대문자면 할당 skip

(B) SqlFetchRowFast Go 네이티브 (935883b, hbrtl/sqlscan.go)

  • PRG FetchRow의 cache-hit 루프 전체를 단일 Go 호출로 대체
  • Bound entry ({nWA, nFPos}): wa.SelectByNum + area.GetValue 직접
  • Unbound entry (집계/표현식): self:EvalExpr via Send 콜백
  • 문자열 값은 strings.TrimSpace 인라인 적용
  • PRG FetchRow는 캐시 미스 fallback 경로만 유지

효과 (median of 3)

벤치 이전 (µs) 현재 (µs) 개선
B4_GROUP_HAVING 418 327 1.28x
B9_ROW_NUMBER 191 120 1.59x
B10_RANK_PART 228 135 1.69x
B11_SUM_OVER 249 156 1.60x
B14_COUNT 235 219 1.07x
B15_CTE_WIN_JOIN 1577 1452 1.09x

Harbour 호환 보장: 43/43 · 56/56 · go test ALL PASS.

2026-04-18 세션 누적 효과 (baseline 3caadb2 대비)

벤치 Baseline (µs) 최종 (µs) 배수
B1_SELECT_STAR 82 40 2.05x
B2_WHERE_FILTER 78 34 2.29x
B3_ORDER_BY 90 47 1.91x
B4_GROUP_HAVING 518 327 1.58x
B5_DISTINCT 74 31 2.39x
B6_INNER_JOIN 217 128 1.70x
B7_CTE_SIMPLE 120 75 1.60x
B8_RECURSIVE_CTE 137 137 flat
B9_ROW_NUMBER 237 120 1.98x
B10_RANK_PART 276 135 2.04x
B11_SUM_OVER 295 156 1.89x
B12_INSERT 61 60 flat
B13_UPDATE 66 65 flat
B14_COUNT 270 219 1.23x
B15_CTE_WIN_JOIN 1704 1452 1.17x

12/15 벤치 의미있는 개선. 10개가 1.5x+, 6개가 2x 근처. B8/B12/B13만 flat (각각 재귀 sub-executor, DML — 이전 세션에서 이미 커버).

아직 남은 병목 (차기 검토 후보)

  • TSqlParser2 Go 포팅: 가장 무거운 단계. PRG Pratt 파서 → Go 재구현
  • EvalExpr Go 네이티브화: 현재 B4/B14/B15에서 17-20% CPU 차지. ~300-500줄 Go 포팅 + Resolve 등 executor 메서드 콜백 필요. 예상 10-15% 추가 수익, 리스크 높음
  • B8 recursive CTE: sub-executor at nDepth>1이 여전히 temp alias 순환 — stable alias 확장으로 커버 가능한지 미검증
  • WA 캐시 auto-invalidate: CREATE/DROP TABLE DDL에서 자동 invalidate
  • B15 복합 쿼리: 각 단계 Go화 되었으나 JOINRECURSE/HASHJOIN이 여전히 PRG 메서드 dispatch. FetchRow 스타일 포팅 확장 가능
  • Go runtime 자체: kevent 16.7%, madvise 9.6%, pthread_cond_* 12% — 애플리케이션 레벨에서 못 줄임. GC 튜닝(GOGC, GOMEMLIMIT) 정도만 가능