From db84dc8b3f6830aa1df8b3d6c4876067ac11f903 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Thu, 21 May 2026 10:53:13 +0100 Subject: [PATCH] test(bench): TStringList vs TList, TObjectList vs TList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Micro-benchmark comparing the legacy list types against the generic list equivalents introduced for self-hosting. Logs the median of two runs to tests/bench_lists_results.txt for trend tracking — append a new dated section after each meaningful runtime or codegen change. Phases per container: bulk insert (1M), sequential read, random read (1M), for..in iteration (strings only), and indexed lookup (IndexOf or hand-rolled scan). Initial run on AMD Ryzen 7 5800X shows TList beating TStringList across the board, while TObjectList still beats TList on writes and on IndexOf — the latter by ~8x due to direct pointer-scan vs. one Get call per element. --- tests/bench_lists_results.txt | 58 ++++++++ tests/bench_objects.pas | 257 +++++++++++++++++++++++++++++++++ tests/bench_strings.pas | 261 ++++++++++++++++++++++++++++++++++ 3 files changed, 576 insertions(+) create mode 100644 tests/bench_lists_results.txt create mode 100644 tests/bench_objects.pas create mode 100644 tests/bench_strings.pas diff --git a/tests/bench_lists_results.txt b/tests/bench_lists_results.txt new file mode 100644 index 0000000..d7be30b --- /dev/null +++ b/tests/bench_lists_results.txt @@ -0,0 +1,58 @@ +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. + +Workloads: + - bench_strings: TStringList vs TList + - bench_objects: TObjectList vs TList + +Phases (all ms wall-clock): + insert 1,000,000 Adds (item_ strings or fresh TItem instances) + sequential Get(i) over the full list (linear sweep) + random 1,000,000 indexed reads in a pseudo-random pattern + (deterministic LCG, seed=42) + for..in full enumeration via the for-in protocol + (strings benchmark only; objects benchmark omits this) + IndexOf/scan 1,000 lookups on the list (TList uses a hand-rolled + linear scan since it has no IndexOf method). Note: the + string benchmark searches a 100,000-item list; the object + benchmark searches the full 1,000,000-item list. + + +------------------------------------------------------------------------ +2026-05-21 — Graeme @ AMD Ryzen 7 5800X (Linux 6.14) + Blaise compiler v0.9.0-dev + Commit: 93358ed (fix codegen ARC for ^TClass writes) + + --- bench_strings --- + TStringList TList + insert 1_000_000 98 ms 90 ms + sequential Get(i) x Count 10 ms 10 ms + random Get(i) x 1_000_000 19 ms 19 ms + for..in over 1_000_000 15 ms 11 ms + IndexOf / hand-rolled scan x 1000 590 ms 277 ms + + --- bench_objects --- + TObjectList TList + insert 1_000_000 36 ms 41 ms + sequential Get(i) x Count 4 ms 6 ms + random Get(i) x 1_000_000 10 ms 13 ms + IndexOf / hand-rolled scan x 1000 16 ms 131 ms + + Notes: + * TList beats TStringList across every phase; the + TStringList.IndexOf cost includes sorted-mode/duplicates checks + the hand-rolled scan does not, hence the 2x gap. + * TObjectList beats TList on writes and reads (raw pointer + storage vs. typed ^T with ARC), and is dramatically faster on + IndexOf because the call goes via a direct pointer scan in a + single function instead of one method call per element. + * The compiler currently miscompiles a program that instantiates + the same generic class with two different concrete arguments + (e.g. TList and TList in one program) — that is + why the benchmark is split into two files. diff --git a/tests/bench_objects.pas b/tests/bench_objects.pas new file mode 100644 index 0000000..f02df7c --- /dev/null +++ b/tests/bench_objects.pas @@ -0,0 +1,257 @@ +{ + 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. diff --git a/tests/bench_strings.pas b/tests/bench_strings.pas new file mode 100644 index 0000000..4c62973 --- /dev/null +++ b/tests/bench_strings.pas @@ -0,0 +1,261 @@ +{ + 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 BenchStrings; + +{ Temporary micro-benchmark — TStringList 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 + + 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. } + +uses + Classes, + Generics.Collections, + DateUtils, + SysUtils; + +const + N_BIG = 1000000; + N_MID = 100000; + N_RANDOM = 1000000; + N_SEARCH = 1000; + +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 BenchTStringList; +var + SL: TStringList; + I: Integer; + Idx: Integer; + T0: TInstant; + S: string; + Sum: Int64; + Hits: Integer; +begin + WriteLn('TStringList'); + + { 1. Bulk insert } + SL := TStringList.Create; + T0 := InstantNow; + for I := 0 to N_BIG - 1 do + SL.Add('item_' + IntToStr(I)); + Report('insert 1_000_000', ElapsedMs(T0)); + + { 2. Sequential read } + T0 := InstantNow; + Sum := 0; + for I := 0 to SL.Count - 1 do + begin + S := SL.Get(I); + Sum := Sum + Length(S) + 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; + S := SL.Get(Idx); + Sum := Sum + Length(S) + end; + Report('random Get(i) x 1_000_000', ElapsedMs(T0)); + + { 5. for..in iterate } + T0 := InstantNow; + Sum := 0; + for S in SL do + Sum := Sum + Length(S); + Report('for..in over 1_000_000 (sum=' + IntToStr(Sum) + ')', + ElapsedMs(T0)); + + SL.Free; + + { 4. Linear search on a 100k list } + SL := TStringList.Create; + for I := 0 to N_MID - 1 do + SL.Add('item_' + IntToStr(I)); + SeedRand(7); + T0 := InstantNow; + Hits := 0; + for I := 0 to N_SEARCH - 1 do + begin + Idx := NextRand mod N_MID; + if SL.IndexOf('item_' + IntToStr(Idx)) >= 0 then + Hits := Hits + 1 + end; + Report('IndexOf x 1000 (hits=' + IntToStr(Hits) + ')', + ElapsedMs(T0)); + SL.Free; + WriteLn +end; + +procedure BenchGenericListString; +var + L: TList; + I: Integer; + Idx: Integer; + J: Integer; + T0: TInstant; + S: string; + Sum: Int64; + Hits: Integer; + Found: Boolean; +begin + WriteLn('TList'); + + { 1. Bulk insert } + L := TList.Create; + T0 := InstantNow; + for I := 0 to N_BIG - 1 do + L.Add('item_' + IntToStr(I)); + Report('insert 1_000_000', ElapsedMs(T0)); + + { 2. Sequential read } + T0 := InstantNow; + Sum := 0; + for I := 0 to L.Count - 1 do + begin + S := L.Get(I); + Sum := Sum + Length(S) + 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; + S := L.Get(Idx); + Sum := Sum + Length(S) + end; + Report('random Get(i) x 1_000_000', ElapsedMs(T0)); + + { 5. for..in iterate } + T0 := InstantNow; + Sum := 0; + for S in L do + Sum := Sum + Length(S); + Report('for..in over 1_000_000 (sum=' + IntToStr(Sum) + ')', + ElapsedMs(T0)); + + L.Free; + + { 4. Linear search — TList has no IndexOf, hand-rolled scan } + L := TList.Create; + for I := 0 to N_MID - 1 do + L.Add('item_' + IntToStr(I)); + SeedRand(7); + T0 := InstantNow; + Hits := 0; + for I := 0 to N_SEARCH - 1 do + begin + Idx := NextRand mod N_MID; + S := 'item_' + IntToStr(Idx); + Found := False; + for J := 0 to L.Count - 1 do + if L.Get(J) = S then + begin + Found := True; + break + end; + if Found then + Hits := Hits + 1 + end; + Report('hand-rolled scan x 1000 (hits=' + IntToStr(Hits) + ')', + ElapsedMs(T0)); + L.Free; + WriteLn +end; + +begin + WriteLn('=== Blaise list micro-benchmark - strings ==='); + WriteLn('N_BIG=' + IntToStr(N_BIG) + + ' N_MID=' + IntToStr(N_MID) + + ' N_RANDOM=' + IntToStr(N_RANDOM) + + ' N_SEARCH=' + IntToStr(N_SEARCH)); + WriteLn; + BenchTStringList; + BenchGenericListString +end.