Skip to content

Instantly share code, notes, and snippets.

@PhDuck
Last active June 7, 2022 18:26
Show Gist options
  • Save PhDuck/a8e8a6d7cd2f68255ed454777cbafea6 to your computer and use it in GitHub Desktop.
Save PhDuck/a8e8a6d7cd2f68255ed454777cbafea6 to your computer and use it in GitHub Desktop.
Template benchmark code for AL
codeunit 50102 Benchmark
{
trigger OnRun()
begin
BenchmarkTemplate(20000);
end;
// TODO: Replace with logic to benchmark, argument handling is manual :(.
local procedure Target(input: Text): Boolean
var
i: Integer;
limit: Integer;
c: Char;
begin
// NOTE: Code has not been tested, use with caution.
// The code below doesn't check the first hour char in hh:mm since it doesn't matter, a2:22 is anyways valid.
limit := StrLen(input);
for i := 2 to limit - 2 do begin
c := input[i];
if c = ':' then begin
// AL doesn't short-circuit so this check needs to be in here to avoid index out of bounds.
c := input[i - 1];
if (c >= '0') and (c <= '9') then begin
c := input[i + 1];
if (c >= '0') and (c <= '5') then begin
c := input[i + 2];
if (c >= '0') and (c <= '9') then begin
exit(true);
end;
end;
end;
end;
end;
exit(false);
end;
/// <summary>
/// Run the benchmark with dynamic adjustments to resolve resolution issues and tries to run roughly (very rough) the time specified in <paramref name="timeToRunInMs"/>.
/// Removes some common pitfalls and prints the resulting per benchmark call measurment.
/// </summary>
/// <param name="timeToRunInMs">Rougly how long to run the benchmark, must be strictly larger than zero.</param>
local procedure BenchmarkTemplate(timeToRunInMs: Integer)
var
outStartT: Time;
startT: Time;
dur: Duration;
total: Duration;
timings: List of [Duration];
limit: Integer;
innerLimit: Integer; // Amount of times to run benchmark method to ensure duration is larger than time resolution of BC's Time data type.
i: Integer;
j: Integer;
begin
// Code is heavily inspired by BenchmarkDotNet - https://github.com/dotnet/BenchmarkDotNet
if (timeToRunInMs <= 0) then
Error('Benchmark must run for more than zero ms. Aim for 20 seconds (20 000).');
outStartT := Time;
// TODO: Replace with your benchmark here once to ensure one-time overhead is removed.
Target('23:59');
// Dynamically determine the inner limit, i.e., how many times to run the code before it takes longer than 1ms
// which is the resolution of time.
// This isn't stable if the benchmark time is too varied i.e. a few long calls take more than 5 ms,
// but later the same amount of calls suddenly takes < 1ms. Using 5 ms makes this less likely to happen.
startT := Time;
i := 0;
repeat
Target('23:59');
dur := Time - startT;
i := i + 1;
until (dur > 5); // Five ms to ensure overhead of calling doesn't impact it too much, this isn't necessary for macro-benchmarks.
innerLimit := i;
// We dynamically adjust the call count to make take ~timeToRunInMs.
limit := System.Round(timeToRunInMs / dur, 1);
for i := 0 to limit do begin
startT := Time;
for j := 0 to innerLimit do begin
Target('23:59');
end;
dur := Time - startT;
timings.Add(dur);
end;
//i := 0;
foreach dur in timings do begin
total := dur + total;
// Consider uncommenting the below lines to inspect individual timings and look for large variance.
//Message('Timing %1 took: %2 ms per operation.', i, dur / innerLimit);
//i += 1;
end;
Message('Benchmark took: %1 ms. Executed benchmark %2 times. Mean execution time per operation: %3 ms', Format(Time - outStartT), innerLimit * limit, total / (innerLimit * limit));
end;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment