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<String>, TObjectList, TList<TObject>.

Add a fresh results entry on the post-fix compiler; timings are
within noise of the split run on the same hardware.
This commit is contained in:
Graeme Geldenhuys 2026-05-21 14:37:22 +01:00
parent b6736d3925
commit 07b9b8ddbd
3 changed files with 207 additions and 287 deletions

View file

@ -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<String>.
{ Micro-benchmark TStringList vs TList<String> and TObjectList vs
TList<TObject>.
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
4. Linear search 1000 lookups; TList<T> 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<String> and TList<TObject> 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<String>;
I: Integer;
Idx: Integer;
J: Integer;
T0: TInstant;
S: string;
Sum: Int64;
Hits: Integer;
L: TList<String>;
I: Integer;
Idx: Integer;
J: Integer;
T0: TInstant;
S: string;
Sum: Int64;
Hits: Integer;
Found: Boolean;
begin
WriteLn('TList<String>');
@ -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<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 ===');
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.

View file

@ -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<String>
- bench_objects: TObjectList vs TList<TObject>
Workloads (all in one program):
- TStringList vs TList<String>
- TObjectList vs TList<TObject>
Phases (all ms wall-clock):
insert 1,000,000 Adds (item_<i> strings or fresh TItem instances)
@ -57,3 +56,31 @@ Phases (all ms wall-clock):
(e.g. TList<String> and TList<TObject> 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<String>
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<TObject>
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<String> and
TList<TObject> 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.

View file

@ -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<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.