Create Five-1.0-Phase-C-TODO.md capturing the remaining 1.0 work: three Harbour contrib libraries (hbct Clipper Tools, hbnf Numeric Functions, hbtip TCP/IP/SMTP/POP3/HTTP). Each entry lists the Harbour source path, a minimum first-pass scope, and an effort estimate. Suggested order: hbct → hbtip → hbnf. Total ~6-10 days. Update RTL-Go-Native-Migration.md "남은 병목" with the Phase A/B completion list — six features shipped this session — plus a note that the 11 HBTYPE functions the initial analysis flagged are actually Harbour's internal scalar class factories, not user-facing blockers (Five's SendBuiltin covers the same surface). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1036 lines
65 KiB
Markdown
1036 lines
65 KiB
Markdown
# 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-931](../harbour-core/include/hbapi.h#L927-L931) — `HB_HASH_FLAG_DEFAULT = HB_HASH_AUTOADD_ASSIGN \| HB_HASH_BINARY \| HB_HASH_KEEPORDER` | 삽입 순서 보존 + `memcmp` 정확 비교 |
|
||
| 해시 키 비교 | [harbour-core/src/vm/hashes.c:167-182](../harbour-core/src/vm/hashes.c#L167-L182) — `hb_hashItemCmp` | CHAR padding trim 없음, Date/Timestamp는 julian 비교 |
|
||
| 내부 탐색 | `pPairs[]` + `pnPos[]` 이진 탐색 (O(log N)) | Five의 Go map 치환은 O(1)로 상회 |
|
||
|
||
## 후보 목록
|
||
|
||
### ✅ Tier 1 — 즉시 이익, 시맨틱 안전
|
||
|
||
| # | 대상 | 파일 | 방식 | 예상 효과 | 상태 |
|
||
|---|------|------|------|-----------|------|
|
||
| 1 | Hash 스토리지 | [hbrtl/hash.go](../hbrtl/hash.go), [hbrt/ops_collection.go](../hbrt/ops_collection.go), [hbrt/value.go](../hbrt/value.go) | `map[string]int` 인덱스 추가, 삽입 순서 슬라이스 유지 | 50–100x | **완료 (2026-04-17)** |
|
||
| 2 | SqlDistinct | [_FiveSql2/src/TSqlSort.prg:57-70](../_FiveSql2/src/TSqlSort.prg#L57-L70), [hbrtl/sqlscan.go](../hbrtl/sqlscan.go) | Go RTL `map[string]struct{}` + `strings.Builder` | 100–300x | **완료 (2026-04-17)** |
|
||
| 3 | SqlRowCompare NULL · 혼합타입 정합성 | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlSort.prg](../_FiveSql2/src/TSqlSort.prg), [_FiveSql2/src/TSqlExecutor.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](../hbrtl/array.go#L134-L300) | 비교자 블록 없을 때 1회 타입 스캔 → 특화 비교자. Date/Logical/Timestamp 지원 추가 (기존엔 no-op) | **완료 (2026-04-17)** |
|
||
| 5 | AScan fast-path | [hbrtl/array.go:302-380](../hbrtl/array.go#L302-L380) | 검색값이 string/int/double일 때 타입별 인라인 루프. 드물게 쓰는 타입은 `valuesEqual` fallback | **완료 (2026-04-17)** |
|
||
|
||
### 🔎 Tier 3 — 내부 헬퍼 최적화
|
||
|
||
| # | 대상 | 파일 | 방식 | 상태 |
|
||
|---|------|------|------|------|
|
||
| 6 | RAT 역방향 스캔 | [hbrtl/strings2.go:16-51](../hbrtl/strings2.go#L16-L51) | 검토 결과 `strings.LastIndex` + 부분슬라이스는 이미 최적. 변경 없음 | **검토 종료 (2026-04-17)** |
|
||
| 7 | SqlExprHasAgg | [hbrtl/sqlexpr.go](../hbrtl/sqlexpr.go) | PRG 재귀 → Go AST walker + 상수 시간 agg 이름 조회 | **완료 (2026-04-17)** |
|
||
|
||
### ✅ Tier 4 — DML Boundary-Crossing 감소 (완료)
|
||
|
||
| # | 대상 | 파일 | 방식 | 상태 |
|
||
|---|------|------|------|------|
|
||
| 8 | SqlBulkInsert | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg](../_FiveSql2/src/TSqlExecutor.prg) | CTE/subquery/tmp 테이블 materialize 경로의 `FOR j ... dbAppend ... FOR k ... FieldPut` 이중 루프를 Go RTL 단일 호출로 대체 | **완료 (2026-04-17)** |
|
||
| 9 | SqlBulkUpdate | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg RunUpdate](../_FiveSql2/src/TSqlExecutor.prg) | UPDATE 스캔 루프 전체를 Go RTL로 이관. WHERE + SET 값 표현식을 pcode로 컴파일해 PRG 메서드 디스패치 제거 | **완료 (2026-04-17)** |
|
||
| 10 | MEMRDD 자동 임포트 | [compiler/gengo/gengo.go](../compiler/gengo/gengo.go) | 모든 Five 프로그램에 `_ "five/hbrdd/mem"` 블랭크 임포트 자동 추가 → `USE "mem:x" VIA "MEMRDD"` 즉시 사용 가능 | **완료 (2026-04-17)** |
|
||
| 11 | PcCompile 결과 캐시 | [hbrtl/pcexpr.go](../hbrtl/pcexpr.go) | `sync.Map`으로 소스 문자열 키 캐시. 반복 쿼리에서 파서+genpc 건너뛰기 | **완료 (2026-04-17)** |
|
||
| 12 | SQL 플랜 캐시 + HbDeepClone | [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg), [hbrtl/array.go](../hbrtl/array.go) | `cSQL → hQuery` PRG 해시 캐시. 히트 시 Go RTL `HbDeepClone`으로 pristine 사본 반환 → `SqlFoldConst` 인-플레이스 변경 안전 | **완료 (2026-04-17)** |
|
||
| 13 | 파라미터 바인딩 벤치 입증 | [_FiveSql2/test/bench_prep_sql.prg](../_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](../_FiveSql2/src/TSqlExecutor.prg), [hbrdd/mem/memrdd.go](../hbrdd/mem/memrdd.go), [hbrtl/sqlscan.go SqlBulkInsert](../hbrtl/sqlscan.go) | 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](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | PARTITION BY 키 빌드 + 행-인덱스 그룹핑을 Go RTL에 위임. N·M 경계 크로싱 → 1 | **완료 (2026-04-17)** |
|
||
| 16 | SqlWindowSortPartition Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | 파티션 내 ORDER BY를 Go `sort.SliceStable` + 사전 해석된 컬럼 인덱스로 처리. PRG 비교 블록 제거 | **완료 (2026-04-17)** |
|
||
| 17 | SqlGroupRows Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg GroupBy](../_FiveSql2/src/TSqlAgg.prg) | GROUP BY 그룹 빌드 루프만 Go RTL로. 집계·HAVING은 복잡 표현식 대응 위해 PRG 유지 | **완료 (2026-04-17)** |
|
||
| 18 | SqlComputeAggSimple Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg ComputeAgg](../_FiveSql2/src/TSqlAgg.prg) | COUNT/SUM/AVG/MIN/MAX + 컬럼 인자 fast-path. 복잡 인자·GROUP_CONCAT은 PRG fallback | **완료 (2026-04-17)** |
|
||
| 19 | SQL 스칼라 헬퍼 Go RTL | [hbrtl/sqlhelpers.go](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TSqlFunc.prg](../_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](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg) | 리터럴(`TK_TEXT`/`TK_NUM`)을 `TK_QMARK`로 치환 + 템플릿 키로 플랜 캐시. 동일 구조 다른 값 쿼리가 캐시 공유 | **완료 (2026-04-17)** |
|
||
| 21 | TSqlLexer Go 포팅 + 결합 | [hbrtl/sqlhelpers.go SqlLexerTokenize + SqlLexAndExtractTemplate](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg) | PRG `SubStr` 기반 문자-단위 렉서를 Go byte-level FSM으로. 자동-파라미터화와 결합해 1회 Go 호출로 lex+normalize 완료 | **완료 (2026-04-17)** |
|
||
| 22 | SqlWindowAssignRank Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | ROW_NUMBER/RANK/DENSE_RANK 배정 루프를 Go에서. 파티션당 1회 호출로 per-row SqlWinRowsEqual PRG 호출 제거 | **완료 (2026-04-17)** |
|
||
| 23 | HbDeepClone 성능 개선 | [hbrtl/array.go deepCloneValue](../hbrtl/array.go) | 스칼라 원소는 재귀 스킵 (슬롯 복사만), 해시 키 공유 (문자열/숫자는 불변). 플랜 캐시 히트마다 수행되는 핫패스 | **완료 (2026-04-17)** |
|
||
| 24 | WA 캐시 + 지연 commit | [hbrtl/sqlwacache.go](../hbrtl/sqlwacache.go), [_FiveSql2/src/TSqlExecutor.prg SqlExecOpenTable/CloseTable + RunInsert/Update/Delete](../_FiveSql2/src/TSqlExecutor.prg) | 워크에어리어 공정-수명 캐시 (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](../_FiveSql2/src/TSqlExecutor.prg), [hbrtl/sqlscan.go SqlBulkUpdate](../hbrtl/sqlscan.go) | 플랜 키별 컴파일된 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](../_FiveSql2/src/TSqlExecutor.prg) | #25의 패턴을 SELECT fast-path에도 적용. `TryBuildFieldPositions` + `TryCompileWhere` 결과를 `cCacheKey#sel`로 캐시. 반복 SELECT의 PRG AST walk 제거 | **완료 (2026-04-17)** |
|
||
| 27 | SqlEvalHaving Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg EvalHaving](../_FiveSql2/src/TSqlAgg.prg) | 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/call.go), [hbrt/vm.go GetSym](../hbrt/vm.go), [hbrt/thread.go pushPendingSym](../hbrt/thread.go), [compiler/gengo/gengo.go emitPushSymbol](../compiler/gengo/gengo.go) | 모든 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](../hbrtl/strings.go#L149)). 변경 이익 없음 |
|
||
| Descend `bytes.Map` | 성능 이익 <5% |
|
||
| SET DATE 비트셋 사전계산 | 5–15%지만 `setDateFormat` 전역 일관성 리스크 > 이익 |
|
||
|
||
### ✅ 이미 최적 (건드리지 말 것)
|
||
|
||
- [hbrtl/crypto.go](../hbrtl/crypto.go) MD5/SHA256/BASE64/CRC32 — `crypto/md5`, `crypto/sha256`, `encoding/base64` 사용 중
|
||
- [hbrtl/binconv.go](../hbrtl/binconv.go) BIN2I/L/W — `encoding/binary` 사용 중
|
||
- [hbrtl/regex.go](../hbrtl/regex.go) — `regexp` 사용 중
|
||
|
||
## 진행 기록
|
||
|
||
### #1 Hash 스토리지 O(1) 전환 — 2026-04-17 완료
|
||
|
||
**구조 변경** ([hbrt/value.go:237-249](../hbrt/value.go#L237-L249))
|
||
```go
|
||
type HbHash struct {
|
||
Keys []Value // 삽입 순서 (HB_HASH_KEEPORDER 기본)
|
||
Values []Value // 병렬
|
||
Order []int
|
||
Flags int32
|
||
Index map[string]int // 신규: O(1) 탐색용 미러
|
||
}
|
||
```
|
||
|
||
**신규 파일**: [hbrt/hash_helpers.go](../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](../hbrt/ops_collection.go) | `HashGen`/`ArrayPush`/`ArrayPop` 헬퍼 경유. `HashGen`은 pair 수집 후 `Set`로 last-wins 보장 |
|
||
| [hbrt/valuemethods.go](../hbrt/valuemethods.go) | `vmHashHas`/`vmHashDelete` 헬퍼 경유 |
|
||
| [hbrt/hbfunc.go](../hbrt/hbfunc.go) | `HashAdd`→`Set`, `HashGetC`는 `"S"+key` 직접 Index 힛 |
|
||
| [hbrt/macroeval.go](../hbrt/macroeval.go) | 해시 리터럴 평가 `Set` (중복 키 last-wins) |
|
||
| [hbrt/gobridge.go](../hbrt/gobridge.go) | `reflect.Map` 변환 `Append` (Go map은 중복 키 없음) |
|
||
| [hbrtl/hash.go](../hbrtl/hash.go) | 7개 RTL 함수 (HbHash/HGet/HSet/HDel/HHasKey/HKeys/HValues) 전체 헬퍼 경유 |
|
||
| [hbrtl/json.go](../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](../hbrtl/sqlscan.go))
|
||
- `appendValueHashKey(sb *strings.Builder, v)` — `valueHashKey`와 동일 매핑이나 중간 문자열 할당 없이 Builder에 직접 기록
|
||
- `SqlDistinct(aRows) → aRows` — Go map 기반 단일 패스 dedup, 입력 순서 보존
|
||
|
||
**호출부 변경** ([_FiveSql2/src/TSqlSort.prg:57-62](../_FiveSql2/src/TSqlSort.prg#L57-L62))
|
||
```harbour
|
||
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](../hbrtl/register.go#L626))
|
||
```go
|
||
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](../_FiveSql2/src/TSqlParser2.prg#L962-L973)) Go/PRG 어느 경로도 이를 읽지 않음 — 명시 스펙이 완전 무시
|
||
3. Go `compareValues`가 숫자 vs 문자열 혼합 타입 비교를 지원하지 않음 — PRG는 `Val(AllTrim(x))`로 강제변환 ([TSqlSort.prg:145-148](../_FiveSql2/src/TSqlSort.prg#L145-L148))
|
||
|
||
**수정 내역**
|
||
| 파일 | 변경 |
|
||
|------|------|
|
||
| [hbrtl/sqlscan.go](../hbrtl/sqlscan.go) | `sortCol`에 `nullsFirst bool` 필드 추가. `cDir == DESC`를 기본값으로 하고 `arr.Items[2]`가 `"FIRST"`/`"LAST"`면 오버라이드. `compareValues`를 `compareValuesNonNil` 기반으로 재구성하고 NIL 처리를 호출부로 이관. 혼합 N/C 비교용 `parseLeadingNumeric` 추가 |
|
||
| [_FiveSql2/src/TSqlExecutor.prg:3818](../_FiveSql2/src/TSqlExecutor.prg#L3818) | `TryBuildSortSpec`이 `aOrderBy[i][3]`을 읽어 3번째 요소 `cNulls`로 Go 스펙에 전달 |
|
||
| [_FiveSql2/src/TSqlSort.prg:33-54](../_FiveSql2/src/TSqlSort.prg#L33-L54) | `OrderBy` 메서드가 `aOB[i][3]`을 `s_aOBCols`에 보존 |
|
||
| [_FiveSql2/src/TSqlSort.prg:118-144](../_FiveSql2/src/TSqlSort.prg#L118-L144) | `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](../_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](../hbrtl/array.go#L164))가 `IsString` / `IsNumeric`가 아닌 타입에 대해 `return false`를 반환 → 날짜·논리값·타임스탬프 배열 정렬이 **no-op**.
|
||
|
||
**수정 내역** ([hbrtl/array.go](../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](../hbrtl/array.go#L302-L380))
|
||
- 검색값이 문자열·정수·실수일 때 타입별 인라인 루프 — `valuesEqual` 호출·switch·타입 체크 생략
|
||
- 정수 검색 + 배열 내 double 원소는 cross-type 비교 (`item.AsNumDouble() == float64(n)`) — 기존 `valuesEqual` 시맨틱 그대로
|
||
- Date/Timestamp/Logical/NIL 검색은 `valuesEqual` fallback
|
||
|
||
**회귀 테스트** ([tests/compat_harbour.prg:328-349](../tests/compat_harbour.prg#L328-L349))
|
||
- `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](../_FiveSql2/src/TSqlAgg.prg), [TSqlExecutor.prg:1298](../_FiveSql2/src/TSqlExecutor.prg#L1298) 등 6곳.
|
||
|
||
**구현** ([hbrtl/sqlexpr.go](../hbrtl/sqlexpr.go))
|
||
- `aggFuncSet` — `map[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](../_FiveSql2/src/TSqlExpr.prg#L45-L49) — PRG `FUNCTION SqlExprHasAgg` 제거 (심볼 충돌 방지). 주석으로 Go RTL 위임 명시
|
||
- [hbrtl/register.go](../hbrtl/register.go) — `SQLEXPRHASAGG` 공개 심볼 등록
|
||
- 기존 호출부(`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](../hbrtl/sqlscan.go))
|
||
- `SqlBulkInsert(aRows) → nInserted` — 현재 workarea의 `*DBFArea`에 직접 `Append()` + `PutValue()` + `Flush()`
|
||
- NIL 원소는 필드 건너뜀 (PRG `IF aRows[j][k] != NIL` 보존)
|
||
- 행 길이가 필드 수 초과 시 초과분 무시, 부족 시 나머지 필드는 default
|
||
|
||
**호출부 치환** — 동일 형상 루프 3곳 → 1줄
|
||
| 위치 | 맥락 |
|
||
|------|------|
|
||
| [TSqlExecutor.prg:2310](../_FiveSql2/src/TSqlExecutor.prg#L2310) | CREATE TABLE AS SELECT / 임시테이블 로드 |
|
||
| [TSqlExecutor.prg:2630](../_FiveSql2/src/TSqlExecutor.prg#L2630) | subquery driving-table materialization |
|
||
| [TSqlExecutor.prg:2935](../_FiveSql2/src/TSqlExecutor.prg#L2935) | CTE materialization |
|
||
|
||
**A/B 벤치마크** ([_FiveSql2/test/bench_bulk.prg](../_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](../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](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- `::oTxn:IsActive()` 이면 **반드시 PRG 루프** (txn 로그 보존) — 안전 게이트
|
||
- txn 없으면 WHERE + 각 SET 값을 `SqlExprToPrg` → `PcCompile`로 pcode 변환 시도
|
||
- 하나라도 실패(복잡 CASE·서브쿼리·UDF 등) 시 PRG 루프로 폴백
|
||
- 모두 성공 시 `SqlBulkUpdate(aFPos, pcWhere, aValuePc)` 한 번 호출
|
||
|
||
**A/B 벤치마크** ([_FiveSql2/test/bench_bulk_upd.prg](../_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](../compiler/gengo/gengo.go#L103-L120))
|
||
```go
|
||
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](#9-sqlbulkupdate-go-rtl--2026-04-17-완료)가 쿼리마다 `SqlExprToPrg` → `PcCompile`을 호출. 파서+preprocess+genpc가 ~50–200µs — B13(100행 × 1 매치) 같은 소형 쿼리에서는 RTL 절감분을 먹어치움.
|
||
|
||
**구현** ([hbrtl/pcexpr.go](../hbrtl/pcexpr.go))
|
||
```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 실행. 반복 쿼리(B1~B11, B13~B15 등 벤치 대부분)는 동일 SQL 텍스트 → 파싱을 한 번만 수행하고 재사용하면 큰 이득.
|
||
|
||
**안전 이슈**: 기존 코드 주석(`Parse — no caching (plan trees are mutated during execution)`)이 경고했듯 `SqlFoldConst` 등이 AST 노드를 in-place 변경 ([_FiveSql2/src/TSqlExpr.prg:75-151](../_FiveSql2/src/TSqlExpr.prg#L75-L151)). 캐시에서 포인터를 그대로 반환하면 첫 실행이 캐시를 오염.
|
||
|
||
**구현**
|
||
- Go RTL `HbDeepClone(xVal) → xNewVal` ([hbrtl/array.go](../hbrtl/array.go)) — `deepCloneValue` 재귀로 Array/Hash를 element별 복제. 스칼라는 불변이라 그대로 반환. `HBDEEPCLONE` · `HB_DEEPCOPY` 두 이름으로 등록
|
||
- PRG 정적 캐시 `s_hPlanCache` ([_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg))
|
||
- 히트: `HbDeepClone(s_hPlanCache[cSQL])` 반환 → Run이 마음껏 변경해도 캐시 불변
|
||
- 미스: 파싱 후 `HbDeepClone(hQuery)`를 캐시에 저장, 원본은 Run에 넘김
|
||
|
||
```prg
|
||
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](../_FiveSql2/src/TSqlExecutor.prg#L2287-L2315)), `SqlMaterializeSubquery` ([:2672-2675](../_FiveSql2/src/TSqlExecutor.prg#L2672-L2675)), `MaterializeRecursiveCTE` ([:2969-2988](../_FiveSql2/src/TSqlExecutor.prg#L2969-L2988)) — 3곳 `dbCreate(cFile+".dbf")` → `dbCreate("mem:"+cTmpFile, aStruct, "MEMRDD")`, `USE` → `dbUseArea(.T., "MEMRDD", ...)`
|
||
- Sub-executor의 CTE 재오픈 경로 ([:1222-1245](../_FiveSql2/src/TSqlExecutor.prg#L1222-L1245)) — MEMRDD 우선 시도, 실패 시 legacy `.dbf` fallback (기존 디스크 임시파일 호환)
|
||
- **버그 수정 1**: `SqlBulkInsert` ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))가 `*dbf.DBFArea`에 하드 타입 어설션 → MEMRDD 경로에서 0 반환. 일반 `hbrdd.Area` 인터페이스 fallback 추가
|
||
- **버그 수정 2**: MEMRDD Create ([hbrdd/mem/memrdd.go](../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](../hbrtl/sqlscan.go))
|
||
- `SqlWindowPartitions(aRows, aPartColIdx) → aPartitions` — PARTITION BY 컬럼 인덱스 배열을 받아 행-인덱스별 그룹 배열 반환. 첫 등장 순서 보존
|
||
- `appendValueHashKey` 공유로 키 구성이 `SqlValToStr`와 byte-for-byte 일치
|
||
- 빈 `aPartColIdx` → 전체 행을 단일 파티션으로 반환 (no-PARTITION-BY 시맨틱)
|
||
|
||
**PRG 호출부** ([ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- 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](../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](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- 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)번 호출. 블록마다 `SqlWinRowCmp` → `SqlFindColIdx` 컬럼 재해석이 반복됨. 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](../hbrtl/sqlscan.go))
|
||
- `SqlGroupRows(aRows, aGroupColIdx) → aGroupedRows` — 행 값(인덱스 아님) 기준으로 그룹 배열 반환. first-seen 순서 보존
|
||
- `appendValueHashKey` 공유로 `SqlValToStr` 시맨틱 byte-for-byte 일치
|
||
- 빈 `aGroupColIdx` → 전체 행이 단일 그룹 (no-GROUP-BY aggregate 시맨틱)
|
||
|
||
**PRG 호출부** ([TSqlAgg.prg GroupBy](../_FiveSql2/src/TSqlAgg.prg))
|
||
- 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](../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](../_FiveSql2/src/TSqlAgg.prg))
|
||
```harbour
|
||
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](../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](../_FiveSql2/src/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](../hbrtl/sqlhelpers.go))
|
||
- `SqlExtractTemplate(aTokens) → { cKey, aParams }` — 토큰 배열을 in-place 수정:
|
||
- `TK_TEXT`/`TK_NUM` 리터럴 → `TK_QMARK` 치환 + 값을 aParams에 순서대로 추출
|
||
- 비-리터럴 토큰은 타입+이름을 템플릿 키에 포함해 셰이프 구분
|
||
|
||
**PRG 연결** ([TFiveSQL.prg](../_FiveSql2/src/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](../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](../_FiveSql2/src/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](../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](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- 3개 CASE를 통합된 Go 호출로:
|
||
```harbour
|
||
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](../hbrtl/array.go))
|
||
- 배열 원소가 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](../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](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- `SqlExecOpenTable(cTable, cAlias)`: 캐시 enabled + 적중 → `dbSelectArea` 재사용; 아니면 `dbUseArea` 후 `Put`
|
||
- `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)
|
||
```harbour
|
||
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](../hbrt/call.go): pop-push dance를 single-slice-shift로 대체. N+2 pops + N pushes → 1 copy + sp 조정. Heap alloc 제거. Resolve 성공 시 `sym.Func = fn`으로 캐시
|
||
- [hbrt/vm.go GetSym](../hbrt/vm.go): `GetSym(cache **Symbol, name string)` — `*cache != nil`이면 즉시 반환, 아니면 `FindSymbol` 후 캐시 (nil은 init-order 재시도 허용해 캐시 안 함)
|
||
- [hbrt/thread.go pushPendingSym](../hbrt/thread.go): depth=1 호출(대부분)을 위한 scalar fast slot 추가
|
||
- [compiler/gengo/gengo.go emitPushSymbol](../compiler/gengo/gengo.go): `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](../hbrtl/sqlscan.go))
|
||
- `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](../_FiveSql2/src/TSqlAgg.prg))
|
||
- 먼저 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](../_FiveSql2/src/TSqlExecutor.prg))
|
||
- `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 병목의 주요 원인
|
||
- `SqlBulkUpdate`가 `waCacheEnabledSafe()` 체크 후 `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. 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 — 이전 세션에서 이미 커버).
|
||
|
||
## 아직 남은 병목 (차기 검토 후보)
|
||
|
||
### 언어/VM 커버리지 (Phase A + B, 2026-04-18 완료)
|
||
|
||
- ✅ `::super:Method()` 디스패치 (`3a56bd3`)
|
||
- ✅ `METHOD ... INLINE expr` + `MESSAGE ... INLINE` (`34485cd`)
|
||
- ✅ `OPERATOR "<op>" ARG x INLINE` + VM 이항 연산 디스패치 (`66f045b`)
|
||
- ✅ `&var` / `&(expr)` 런타임 매크로 컴파일 (`e089c81`)
|
||
- ✅ `DATA x, y, z` 다중-이름 파싱 (`327f75b`)
|
||
- ✅ `DO(xTarget, args...)` 동적 디스패치 (`2a66252`)
|
||
- ⏸ `ACCEPT` — TUI 인터랙티브 입력. 사용자 판단으로 보류
|
||
- ✗ `HBARRAY` 등 11개 HB*TYPE — 조사 결과 Harbour 내부 클래스 팩토리로,
|
||
Five의 `SendBuiltin`이 이미 동등 기능 제공. 1.0 blocker 아님
|
||
|
||
### Phase C — Contrib 라이브러리 (별도 작업 예정)
|
||
|
||
[Five-1.0-Phase-C-TODO.md](Five-1.0-Phase-C-TODO.md) 참조. hbct (Clipper
|
||
Tools 40개) + hbnf (재무/통계 FT_*) + hbtip (네트워크 I/O) — 약 6-10일.
|
||
|
||
### 성능 영역 (Phase A/B와 무관, 계속 유효)
|
||
|
||
- **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`) 정도만 가능
|