From 7fec4ce1505ea02fabb87f7cdd86623029193717 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Tue, 7 Apr 2026 09:36:12 +0900 Subject: [PATCH] =?UTF-8?q?perf:=2050K=20benchmark=20=E2=80=94=20Harbour?= =?UTF-8?q?=20vs=20Five=20on=20ext4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- examples/bench_heavy.prg | 174 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 examples/bench_heavy.prg diff --git a/examples/bench_heavy.prg b/examples/bench_heavy.prg new file mode 100644 index 0000000..70aeb69 --- /dev/null +++ b/examples/bench_heavy.prg @@ -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