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. 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: For each container we measure:
1. Bulk insert — 1_000_000 Adds 1. Bulk insert — 1_000_000 Adds
2. Sequential read — Get(i) for every index 2. Sequential read — Get(i) for every index
3. Random read — 1_000_000 indexed reads in a pseudo-random pattern 3. Random read — 1_000_000 indexed reads in a pseudo-random pattern
4. Linear search — 1000 lookups on a 100_000-item list 4. Linear search — 1000 lookups; TList<T> uses a hand-rolled scan
(TList<T> has no IndexOf — hand-rolled scan) since it has no IndexOf method
5. for..in iterate — checksum across all items 5. for..in iterate — full enumeration (strings workload only)
All times are wall-clock milliseconds via TInstant.Subtract. All times are wall-clock milliseconds via TInstant.Subtract.
Run from the project root: Run from the project root:
compiler/target/blaise --source tests/bench_strings.pas --output /tmp/bench_strings compiler/target/blaise --source tests/bench_lists.pas --output /tmp/bench_lists
/tmp/bench_strings /tmp/bench_lists
}
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 uses
Classes, Classes,
Contnrs,
Generics.Collections, Generics.Collections,
DateUtils, DateUtils,
SysUtils; SysUtils;
@ -42,6 +39,11 @@ const
N_RANDOM = 1000000; N_RANDOM = 1000000;
N_SEARCH = 1000; N_SEARCH = 1000;
type
TItem = class
FValue: Integer;
end;
var var
GRand: Integer; GRand: Integer;
@ -249,13 +251,161 @@ begin
WriteLn WriteLn
end; end;
procedure BenchTObjectList;
var
OL: TObjectList;
I: Integer;
Idx: Integer;
T0: TInstant;
Item: TItem;
Sum: Int64;
Hits: Integer;
Needle: TItem;
begin 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) + WriteLn('N_BIG=' + IntToStr(N_BIG) +
' N_MID=' + IntToStr(N_MID) + ' N_MID=' + IntToStr(N_MID) +
' N_RANDOM=' + IntToStr(N_RANDOM) + ' N_RANDOM=' + IntToStr(N_RANDOM) +
' N_SEARCH=' + IntToStr(N_SEARCH)); ' N_SEARCH=' + IntToStr(N_SEARCH));
WriteLn; WriteLn;
BenchTStringList; BenchTStringList;
BenchGenericListString BenchGenericListString;
BenchTObjectList;
BenchGenericListObject
end. end.

View file

@ -2,14 +2,13 @@ Blaise list micro-benchmark — results log
========================================== ==========================================
Each entry records the median of two consecutive runs of Each entry records the median of two consecutive runs of
tests/bench_strings.pas and tests/bench_objects.pas, taken on the tests/bench_lists.pas, taken on the machine identified in the entry
machine identified in the entry header. Add new entries below the header. Add new entries below the previous one (newest at the
previous one (newest at the bottom) so the file forms a chronological bottom) so the file forms a chronological trend.
trend.
Workloads: Workloads (all in one program):
- bench_strings: TStringList vs TList<String> - TStringList vs TList<String>
- bench_objects: TObjectList vs TList<TObject> - TObjectList vs TList<TObject>
Phases (all ms wall-clock): Phases (all ms wall-clock):
insert 1,000,000 Adds (item_<i> strings or fresh TItem instances) 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 (e.g. TList<String> and TList<TObject> in one program) — that is
why the benchmark is split into two files. (Fixed in a later why the benchmark is split into two files. (Fixed in a later
commit via per-instance AST body cloning.) 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.