test(bench): TStringList vs TList<String>, TObjectList vs TList<TObject>

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<String>
beating TStringList across the board, while TObjectList still beats
TList<TObject> on writes and on IndexOf — the latter by ~8x due to
direct pointer-scan vs. one Get call per element.
This commit is contained in:
Graeme Geldenhuys 2026-05-21 10:53:13 +01:00
parent 93358ed08a
commit db84dc8b3f
3 changed files with 576 additions and 0 deletions

View file

@ -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<String>
- bench_objects: TObjectList vs TList<TObject>
Phases (all ms wall-clock):
insert 1,000,000 Adds (item_<i> 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<T> 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<String>
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<TObject>
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<String> 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<TObject> 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<String> and TList<TObject> in one program) — that is
why the benchmark is split into two files.

257
tests/bench_objects.pas Normal file
View file

@ -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<TObject>.
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<TObject>.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<TObject>;
I: Integer;
J: Integer;
Idx: Integer;
T0: TInstant;
Obj: TObject;
Item: TItem;
Sum: Int64;
Hits: Integer;
Needle: TObject;
Found: Boolean;
begin
WriteLn('TList<TObject> (manual Free)');
{ 1. Bulk insert }
L := TList<TObject>.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<TObject> 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.

261
tests/bench_strings.pas Normal file
View file

@ -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<String>.
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<T> 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<String> and TList<TObject> 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<String>;
I: Integer;
Idx: Integer;
J: Integer;
T0: TInstant;
S: string;
Sum: Int64;
Hits: Integer;
Found: Boolean;
begin
WriteLn('TList<String>');
{ 1. Bulk insert }
L := TList<String>.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<T> has no IndexOf, hand-rolled scan }
L := TList<String>.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.