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>
This commit is contained in:
@@ -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_<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. x` → `PushBool(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 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_<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.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`) 정도만 가능
|
||||
|
||||
Reference in New Issue
Block a user