perf: 50K benchmark — Harbour vs Five on ext4

50K records benchmark on native ext4 (home directory):
- APPEND 50K: Five 140ms / Harbour 61ms (2.3x)
- INDEX 50K:  Five 31ms / Harbour 6ms (5.2x)
- SEEK 50K:   Five 142ms / Harbour 23ms (6.2x)
- SCAN 50K:   Five 35ms / Harbour 5ms (7x)
- PACK 50K:   Five 19ms / Harbour 16ms (1.2x)

All within acceptable Go vs C overhead (2-7x).
PACK nearly identical. APPEND close (2.3x).

Known issue: 3-level NTX bulk build has separator duplication
at interior→root level (count=50083 vs 50000).
Does not affect correctness for <= 2-level trees (100 records OK).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 09:36:12 +09:00
parent adede5cd69
commit 7fec4ce150

174
examples/bench_heavy.prg Normal file
View File

@@ -0,0 +1,174 @@
// Heavy RDD benchmark — 50K records, multiple indexes, mixed operations
PROCEDURE Main()
LOCAL i, j, nH, nStart, nEnd, nCount, nSum
LOCAL aStruct, aCities, nIdx
ErrorBlock({|e| Break(e)})
aCities := {"Seoul","Tokyo","Beijing","London","NYC","Paris","Berlin","Rome","Madrid","Oslo"}
aStruct := { ;
{"ID","N",8,0}, {"NAME","C",30,0}, {"CITY","C",15,0}, ;
{"SALARY","N",12,2}, {"ACTIVE","L",1,0}, {"CODE","C",10,0} ;
}
BEGIN SEQUENCE
// ======== APPEND 50K ========
nStart := Seconds()
dbCreate("heavy_test", aStruct)
USE "heavy_test" NEW
FOR i := 1 TO 50000
APPEND BLANK
REPLACE ID WITH i
REPLACE NAME WITH PadR("Name_" + PadL(LTrim(Str(i)), 5, "0"), 30)
nIdx := ((i-1) % 10) + 1
REPLACE CITY WITH PadR(aCities[nIdx], 15)
REPLACE SALARY WITH 20000 + i * 1.50
REPLACE ACTIVE WITH (i % 3 != 0)
REPLACE CODE WITH PadR(LTrim(Str(Int(((i-1)/500))+1)), 10)
NEXT
nEnd := Seconds()
R("H1_APPEND_50K", FmtMs(nEnd - nStart) + " rc=" + LTrim(Str(RecCount())))
// ======== INDEX 50K ========
nStart := Seconds()
INDEX ON FIELD->NAME TO heavy_name
nEnd := Seconds()
R("H2_INDEX_50K", FmtMs(nEnd - nStart))
CLOSE ALL
// ======== SEEK 50K sequential ========
USE "heavy_test" NEW
SET INDEX TO heavy_name
nStart := Seconds()
nCount := 0
FOR i := 1 TO 50000
SEEK PadR("Name_" + PadL(LTrim(Str(i)), 5, "0"), 30)
IF Found()
nCount++
ENDIF
NEXT
nEnd := Seconds()
R("H3_SEEK_50K", FmtMs(nEnd - nStart) + " f=" + LTrim(Str(nCount)))
// ======== SEEK 50K random ========
nStart := Seconds()
nCount := 0
FOR i := 1 TO 50000
j := ((i * 7919) % 50000) + 1
SEEK PadR("Name_" + PadL(LTrim(Str(j)), 5, "0"), 30)
IF Found()
nCount++
ENDIF
NEXT
nEnd := Seconds()
R("H4_SEEK_RND", FmtMs(nEnd - nStart) + " f=" + LTrim(Str(nCount)))
CLOSE ALL
// ======== FULL SCAN 50K ========
USE "heavy_test" NEW
SET INDEX TO heavy_name
nStart := Seconds()
GO TOP
nCount := 0
DO WHILE !EOF()
nCount++
SKIP
ENDDO
nEnd := Seconds()
R("H5_SCAN_50K", FmtMs(nEnd - nStart) + " c=" + LTrim(Str(nCount)))
CLOSE ALL
// ======== CITY INDEX 50K ========
USE "heavy_test" NEW
nStart := Seconds()
INDEX ON FIELD->CITY TO heavy_city
nEnd := Seconds()
R("H6_IDX_CITY", FmtMs(nEnd - nStart))
// ======== DUP KEY SEEK+SCAN ========
nStart := Seconds()
nCount := 0
FOR i := 1 TO 10
SEEK PadR(aCities[i], 15)
DO WHILE !EOF() .AND. RTrim(FieldGet(3)) == aCities[i]
nCount++
SKIP
ENDDO
NEXT
nEnd := Seconds()
R("H7_DUPKEY_50K", FmtMs(nEnd - nStart) + " c=" + LTrim(Str(nCount)))
CLOSE ALL
// ======== DELETE 10K + SET DELETED SCAN ========
USE "heavy_test" NEW
SET INDEX TO heavy_name
SET ORDER TO 0
FOR i := 1 TO 50000
GO i
IF i % 5 == 0
DELETE
ENDIF
NEXT
SET ORDER TO 1
SET DELETED ON
nStart := Seconds()
GO TOP
nCount := 0
DO WHILE !EOF()
nCount++
SKIP
ENDDO
nEnd := Seconds()
R("H8_DELSCAN_50K", FmtMs(nEnd - nStart) + " c=" + LTrim(Str(nCount)))
SET DELETED OFF
SET ORDER TO 0
FOR i := 1 TO 50000
GO i
IF Deleted()
RECALL
ENDIF
NEXT
CLOSE ALL
// ======== COMPOUND INDEX ========
USE "heavy_test" NEW
nStart := Seconds()
INDEX ON FIELD->CITY + FIELD->NAME TO heavy_comp
nEnd := Seconds()
R("H9_IDX_COMP", FmtMs(nEnd - nStart))
SEEK PadR("Seoul", 15) + PadR("Name_00001", 30)
R("H10_COMP_SEEK", IIF(Found(),".T.",".F.") + " " + LTrim(Str(RecNo())))
CLOSE ALL
// ======== PACK 50K (10K deleted) ========
USE "heavy_test" NEW
FOR i := 1 TO 50000
GO i
IF i % 5 == 0
DELETE
ENDIF
NEXT
nStart := Seconds()
PACK
nEnd := Seconds()
R("H11_PACK_50K", FmtMs(nEnd - nStart) + " rc=" + LTrim(Str(RecCount())))
CLOSE ALL
RECOVER
? "ERROR"
END SEQUENCE
RETURN
FUNCTION FmtMs(nSec)
RETURN LTrim(Str(Int(nSec * 1000))) + "ms"
PROCEDURE R(cKey, cVal)
? cKey + "=" + cVal
RETURN