From 07b9b8ddbdf6b814ad68288fb655e7c7202919f8 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Thu, 21 May 2026 14:37:22 +0100 Subject: [PATCH] test(bench): merge bench_strings + bench_objects into bench_lists The multi-instance generic miscompile that forced the two-program split is fixed (b6736d3), so the four workloads now run in a single program: TStringList, TList, TObjectList, TList. Add a fresh results entry on the post-fix compiler; timings are within noise of the split run on the same hardware. --- tests/{bench_strings.pas => bench_lists.pas} | 196 ++++++++++++-- tests/bench_lists_results.txt | 41 ++- tests/bench_objects.pas | 257 ------------------- 3 files changed, 207 insertions(+), 287 deletions(-) rename tests/{bench_strings.pas => bench_lists.pas} (56%) delete mode 100644 tests/bench_objects.pas diff --git a/tests/bench_strings.pas b/tests/bench_lists.pas similarity index 56% rename from tests/bench_strings.pas rename to tests/bench_lists.pas index 4c62973..9f4f965 100644 --- a/tests/bench_strings.pas +++ b/tests/bench_lists.pas @@ -6,32 +6,29 @@ See LICENSE file in the project root for full license terms. } -program BenchStrings; +program BenchLists; -{ Temporary micro-benchmark — TStringList vs TList. +{ Micro-benchmark — TStringList vs TList and TObjectList vs + TList. For each container we measure: 1. Bulk insert — 1_000_000 Adds 2. Sequential read — Get(i) for every index 3. Random read — 1_000_000 indexed reads in a pseudo-random pattern - 4. Linear search — 1000 lookups on a 100_000-item list - (TList has no IndexOf — hand-rolled scan) - 5. for..in iterate — checksum across all items + 4. Linear search — 1000 lookups; TList uses a hand-rolled scan + since it has no IndexOf method + 5. for..in iterate — full enumeration (strings workload only) All times are wall-clock milliseconds via TInstant.Subtract. Run from the project root: - compiler/target/blaise --source tests/bench_strings.pas --output /tmp/bench_strings - /tmp/bench_strings - - NOTE: this benchmark and tests/bench_objects.pas are split because the - compiler currently miscompiles a program that instantiates the same - generic class with two different concrete type arguments - (e.g. TList and TList in the same program) — a - pre-existing limitation tracked separately. } + compiler/target/blaise --source tests/bench_lists.pas --output /tmp/bench_lists + /tmp/bench_lists +} uses Classes, + Contnrs, Generics.Collections, DateUtils, SysUtils; @@ -42,6 +39,11 @@ const N_RANDOM = 1000000; N_SEARCH = 1000; +type + TItem = class + FValue: Integer; + end; + var GRand: Integer; @@ -170,14 +172,14 @@ end; procedure BenchGenericListString; var - L: TList; - I: Integer; - Idx: Integer; - J: Integer; - T0: TInstant; - S: string; - Sum: Int64; - Hits: Integer; + L: TList; + I: Integer; + Idx: Integer; + J: Integer; + T0: TInstant; + S: string; + Sum: Int64; + Hits: Integer; Found: Boolean; begin WriteLn('TList'); @@ -249,13 +251,161 @@ begin WriteLn end; +procedure BenchTObjectList; +var + OL: TObjectList; + I: Integer; + Idx: Integer; + T0: TInstant; + Item: TItem; + Sum: Int64; + Hits: Integer; + Needle: TItem; begin - WriteLn('=== Blaise list micro-benchmark - strings ==='); + WriteLn('TObjectList(OwnsObjects=True)'); + + { 1. Bulk insert } + OL := TObjectList.Create(True); + T0 := InstantNow; + for I := 0 to N_BIG - 1 do + begin + Item := TItem.Create; + Item.FValue := I; + OL.Add(Item) + end; + Report('insert 1_000_000', ElapsedMs(T0)); + + { 2. Sequential read } + T0 := InstantNow; + Sum := 0; + for I := 0 to OL.Count - 1 do + begin + Item := TItem(OL.Get(I)); + Sum := Sum + Item.FValue + end; + Report('sequential Get(i) x Count (sum=' + IntToStr(Sum) + ')', + ElapsedMs(T0)); + + { 3. Random read } + SeedRand(42); + T0 := InstantNow; + Sum := 0; + for I := 0 to N_RANDOM - 1 do + begin + Idx := NextRand mod N_BIG; + Item := TItem(OL.Get(Idx)); + Sum := Sum + Item.FValue + end; + Report('random Get(i) x 1_000_000', ElapsedMs(T0)); + + { 4. IndexOf — search for known objects (pointer-equality match) } + SeedRand(7); + T0 := InstantNow; + Hits := 0; + for I := 0 to N_SEARCH - 1 do + begin + Idx := NextRand mod N_BIG; + Needle := TItem(OL.Get(Idx)); + if OL.IndexOf(Needle) >= 0 then + Hits := Hits + 1 + end; + Report('IndexOf x 1000 (hits=' + IntToStr(Hits) + ')', + ElapsedMs(T0)); + + OL.Free; + WriteLn +end; + +procedure BenchGenericListObject; +var + L: TList; + I: Integer; + J: Integer; + Idx: Integer; + T0: TInstant; + Obj: TObject; + Item: TItem; + Sum: Int64; + Hits: Integer; + Needle: TObject; + Found: Boolean; +begin + WriteLn('TList (manual Free)'); + + { 1. Bulk insert } + L := TList.Create; + T0 := InstantNow; + for I := 0 to N_BIG - 1 do + begin + Item := TItem.Create; + Item.FValue := I; + L.Add(Item) + end; + Report('insert 1_000_000', ElapsedMs(T0)); + + { 2. Sequential read } + T0 := InstantNow; + Sum := 0; + for I := 0 to L.Count - 1 do + begin + Item := TItem(L.Get(I)); + Sum := Sum + Item.FValue + end; + Report('sequential Get(i) x Count (sum=' + IntToStr(Sum) + ')', + ElapsedMs(T0)); + + { 3. Random read } + SeedRand(42); + T0 := InstantNow; + Sum := 0; + for I := 0 to N_RANDOM - 1 do + begin + Idx := NextRand mod N_BIG; + Item := TItem(L.Get(Idx)); + Sum := Sum + Item.FValue + end; + Report('random Get(i) x 1_000_000', ElapsedMs(T0)); + + { 4. Hand-rolled IndexOf — pointer equality } + SeedRand(7); + T0 := InstantNow; + Hits := 0; + for I := 0 to N_SEARCH - 1 do + begin + Idx := NextRand mod N_BIG; + Needle := L.Get(Idx); + Found := False; + for J := 0 to L.Count - 1 do + if L.Get(J) = Needle then + begin + Found := True; + break + end; + if Found then + Hits := Hits + 1 + end; + Report('hand-rolled scan x 1000 (hits=' + IntToStr(Hits) + ')', + ElapsedMs(T0)); + + { Manually release each item since TList is not owning. } + for I := 0 to L.Count - 1 do + begin + Obj := L.Get(I); + Obj.Free + end; + L.Free; + WriteLn +end; + +begin + WriteLn('=== Blaise list micro-benchmark ==='); WriteLn('N_BIG=' + IntToStr(N_BIG) + ' N_MID=' + IntToStr(N_MID) + ' N_RANDOM=' + IntToStr(N_RANDOM) + ' N_SEARCH=' + IntToStr(N_SEARCH)); WriteLn; BenchTStringList; - BenchGenericListString + BenchGenericListString; + BenchTObjectList; + BenchGenericListObject end. diff --git a/tests/bench_lists_results.txt b/tests/bench_lists_results.txt index 6e1bec1..00f87c8 100644 --- a/tests/bench_lists_results.txt +++ b/tests/bench_lists_results.txt @@ -2,14 +2,13 @@ Blaise list micro-benchmark — results log ========================================== Each entry records the median of two consecutive runs of -tests/bench_strings.pas and tests/bench_objects.pas, taken on the -machine identified in the entry header. Add new entries below the -previous one (newest at the bottom) so the file forms a chronological -trend. +tests/bench_lists.pas, taken on the machine identified in the entry +header. Add new entries below the previous one (newest at the +bottom) so the file forms a chronological trend. -Workloads: - - bench_strings: TStringList vs TList - - bench_objects: TObjectList vs TList +Workloads (all in one program): + - TStringList vs TList + - TObjectList vs TList Phases (all ms wall-clock): insert 1,000,000 Adds (item_ strings or fresh TItem instances) @@ -57,3 +56,31 @@ Phases (all ms wall-clock): (e.g. TList and TList in one program) — that is why the benchmark is split into two files. (Fixed in a later commit via per-instance AST body cloning.) + + +------------------------------------------------------------------------ +2026-05-21 — Graeme @ AMD Ryzen 7 5800X (Linux 6.14) + Blaise compiler v0.9.0-dev + Commit: b6736d3 (fix semantic: clone method bodies per generic instance) + + --- merged into a single program (tests/bench_lists.pas) --- + TStringList TList + insert 1_000_000 99 ms 89 ms + sequential Get(i) x Count 10 ms 10 ms + random Get(i) x 1_000_000 20 ms 19 ms + for..in over 1_000_000 15 ms 11 ms + IndexOf / hand-rolled scan x 1000 681 ms 287 ms + + TObjectList TList + insert 1_000_000 36 ms 40 ms + sequential Get(i) x Count 4 ms 6 ms + random Get(i) x 1_000_000 9 ms 13 ms + IndexOf / hand-rolled scan x 1000 16 ms 131 ms + + Notes: + * Multi-instance generic miscompile is fixed — TList and + TList now co-exist in one program, so the two split + benchmarks have been merged into tests/bench_lists.pas. + * Numbers are within noise of the previous split-program run on + the same hardware; combining the workloads does not perturb + timings. diff --git a/tests/bench_objects.pas b/tests/bench_objects.pas deleted file mode 100644 index f02df7c..0000000 --- a/tests/bench_objects.pas +++ /dev/null @@ -1,257 +0,0 @@ -{ - Blaise - An Object Pascal Compiler - Copyright (c) 2026 Graeme Geldenhuys - SPDX-License-Identifier: Apache-2.0 WITH Swift-exception - Licensed under the Apache License v2.0 with Runtime Library Exception. - See LICENSE file in the project root for full license terms. -} - -program BenchObjects; - -{ Temporary micro-benchmark — TObjectList vs TList. - - For each container we measure: - 1. Bulk insert — 1_000_000 Adds of fresh TItem instances - 2. Sequential read — Get(i) + read FValue for every index - 3. Random read — 1_000_000 indexed reads in a pseudo-random pattern - 4. Linear search — 1000 lookups on the 1M-item list (we look for - objects we know are present; pointer equality) - 5. for..in iterate — checksum across all items (TObjectList only; - TList.GetEnumerator returns Current as - the element type which round-trips fine here) - - All times are wall-clock milliseconds via TInstant.Subtract. - Run from the project root: - - compiler/target/blaise --source tests/bench_objects.pas --output /tmp/bench_objects - /tmp/bench_objects - - NOTE: split from tests/bench_strings.pas — see that file's header for - the compiler limitation that forces the split. } - -uses - Contnrs, - Generics.Collections, - DateUtils, - SysUtils; - -const - N_BIG = 1000000; - N_RANDOM = 1000000; - N_SEARCH = 1000; - -type - TItem = class - FValue: Integer; - end; - -var - GRand: Integer; - -procedure SeedRand(S: Integer); -begin - GRand := S -end; - -function NextRand: Integer; -begin - GRand := GRand * 1103515245 + 12345; - Result := (GRand shr 16) and $7FFFFFFF -end; - -function ElapsedMs(Start: TInstant): Int64; -var - Now: TInstant; - D: TDuration; -begin - Now := InstantNow; - D := Now.Subtract(Start); - Result := D.TotalMilliseconds -end; - -function PadRight(const S: string; Width: Integer): string; -var - N: Integer; -begin - Result := S; - N := Width - Length(S); - while N > 0 do - begin - Result := Result + ' '; - N := N - 1 - end -end; - -function PadLeft(const S: string; Width: Integer): string; -var - N: Integer; -begin - Result := S; - N := Width - Length(S); - while N > 0 do - begin - Result := ' ' + Result; - N := N - 1 - end -end; - -procedure Report(const ALabel: string; AMs: Int64); -begin - WriteLn(' ' + PadRight(ALabel, 50) + PadLeft(IntToStr(AMs), 6) + ' ms') -end; - -procedure BenchTObjectList; -var - OL: TObjectList; - I: Integer; - Idx: Integer; - T0: TInstant; - Item: TItem; - Sum: Int64; - Hits: Integer; - Needle: TItem; -begin - WriteLn('TObjectList(OwnsObjects=True)'); - - { 1. Bulk insert } - OL := TObjectList.Create(True); - T0 := InstantNow; - for I := 0 to N_BIG - 1 do - begin - Item := TItem.Create; - Item.FValue := I; - OL.Add(Item) - end; - Report('insert 1_000_000', ElapsedMs(T0)); - - { 2. Sequential read } - T0 := InstantNow; - Sum := 0; - for I := 0 to OL.Count - 1 do - begin - Item := TItem(OL.Get(I)); - Sum := Sum + Item.FValue - end; - Report('sequential Get(i) x Count (sum=' + IntToStr(Sum) + ')', - ElapsedMs(T0)); - - { 3. Random read } - SeedRand(42); - T0 := InstantNow; - Sum := 0; - for I := 0 to N_RANDOM - 1 do - begin - Idx := NextRand mod N_BIG; - Item := TItem(OL.Get(Idx)); - Sum := Sum + Item.FValue - end; - Report('random Get(i) x 1_000_000', ElapsedMs(T0)); - - { 4. IndexOf — search for known objects (pointer-equality match) } - SeedRand(7); - T0 := InstantNow; - Hits := 0; - for I := 0 to N_SEARCH - 1 do - begin - Idx := NextRand mod N_BIG; - Needle := TItem(OL.Get(Idx)); - if OL.IndexOf(Needle) >= 0 then - Hits := Hits + 1 - end; - Report('IndexOf x 1000 (hits=' + IntToStr(Hits) + ')', - ElapsedMs(T0)); - - OL.Free; - WriteLn -end; - -procedure BenchGenericListObject; -var - L: TList; - I: Integer; - J: Integer; - Idx: Integer; - T0: TInstant; - Obj: TObject; - Item: TItem; - Sum: Int64; - Hits: Integer; - Needle: TObject; - Found: Boolean; -begin - WriteLn('TList (manual Free)'); - - { 1. Bulk insert } - L := TList.Create; - T0 := InstantNow; - for I := 0 to N_BIG - 1 do - begin - Item := TItem.Create; - Item.FValue := I; - L.Add(Item) - end; - Report('insert 1_000_000', ElapsedMs(T0)); - - { 2. Sequential read } - T0 := InstantNow; - Sum := 0; - for I := 0 to L.Count - 1 do - begin - Item := TItem(L.Get(I)); - Sum := Sum + Item.FValue - end; - Report('sequential Get(i) x Count (sum=' + IntToStr(Sum) + ')', - ElapsedMs(T0)); - - { 3. Random read } - SeedRand(42); - T0 := InstantNow; - Sum := 0; - for I := 0 to N_RANDOM - 1 do - begin - Idx := NextRand mod N_BIG; - Item := TItem(L.Get(Idx)); - Sum := Sum + Item.FValue - end; - Report('random Get(i) x 1_000_000', ElapsedMs(T0)); - - { 4. Hand-rolled IndexOf — pointer equality } - SeedRand(7); - T0 := InstantNow; - Hits := 0; - for I := 0 to N_SEARCH - 1 do - begin - Idx := NextRand mod N_BIG; - Needle := L.Get(Idx); - Found := False; - for J := 0 to L.Count - 1 do - if L.Get(J) = Needle then - begin - Found := True; - break - end; - if Found then - Hits := Hits + 1 - end; - Report('hand-rolled scan x 1000 (hits=' + IntToStr(Hits) + ')', - ElapsedMs(T0)); - - { Manually release each item since TList is not owning. } - for I := 0 to L.Count - 1 do - begin - Obj := L.Get(I); - Obj.Free - end; - L.Free; - WriteLn -end; - -begin - WriteLn('=== Blaise list micro-benchmark - objects ==='); - WriteLn('N_BIG=' + IntToStr(N_BIG) + - ' N_RANDOM=' + IntToStr(N_RANDOM) + - ' N_SEARCH=' + IntToStr(N_SEARCH)); - WriteLn; - BenchTObjectList; - BenchGenericListObject -end.