diff --git a/docs/RTL-Go-Native-Migration.md b/docs/RTL-Go-Native-Migration.md index d057c11..0cf3449 100644 --- a/docs/RTL-Go-Native-Migration.md +++ b/docs/RTL-Go-Native-Migration.md @@ -859,9 +859,158 @@ dbCloseAll() // flush + close all - 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__ *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. x` → `PushBool(false)` | short-circuit | +| `.NOT. ` | `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 k` → `FOR 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`에 추가 안 함 +- `CloseOpened`는 `aOpened`만 순회하므로 캐시 보유 영역은 건드리지 않음 — 다음 쿼리 `Select(alias) > 0` → `OpenTable` 자체 스킵 +- 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_.dbf` stat (VIEW 안 쓰는 쿼리에서도 실행) +2. CTE cleanup의 `__cte_.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.go`의 `aggFuncSet` (`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 재구현 -- **CTE 결과 Go 캐시**: 동일 CTE 재사용 시 materialize 생략 +- **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 복합 쿼리 (CTE+Win+JOIN 1891µs)**: 각 단계 Go화 되었으나 조립 비용 잔존 +- **B15 복합 쿼리**: 각 단계 Go화 되었으나 JOINRECURSE/HASHJOIN이 여전히 PRG 메서드 dispatch. FetchRow 스타일 포팅 확장 가능 +- **Go runtime 자체**: `kevent` 16.7%, `madvise` 9.6%, `pthread_cond_*` 12% — 애플리케이션 레벨에서 못 줄임. GC 튜닝(`GOGC`, `GOMEMLIMIT`) 정도만 가능