March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount! Early bird discount ends December 31.
Register NowBe one of the first to start using Fabric Databases. View on-demand sessions with database experts and the Microsoft product team to learn just how easy it is to get started. Watch now
Hi All,
I was doing some testing with excution times of various M code solutions, and that proved more difficult than I first imaged.
The main reason is that M code is not excuted in the order of the statements, but is evaluated lazily. Just in time so to speak.
I cam accross a number of 'obvious' solutions that did not work (or were very complicated), so I created my own.
The main function is this:
Benchmark.Time = (f as function) => // making 1 call to the function, returning the number of seconds it took
let
f = () => Logical.From(f()), // to cast the result to a logical
start_time = DateTime.LocalNow(), // not evaluated yet!!
end_time = DateTime.LocalNow(), // not evaluated yet!!
duration =
if start_time <> null then // start_time evaluates here
if f() ?? true <> null then // function is called here and never returns null
(end_time - start_time) // so, this is always executed and evaluates the end_time to calculate the duration
else null // never executed
else null // never executed
in
Duration.TotalSeconds(duration) // in seconds as a floating point number
Note the hoops I go through to make sure start_time and end_time are evaluated at the right moment...
The full solution includes running many samples of a list of functions and producing some basix statistics:
// Function.Benchmark Compare produces some basic statistics comparing 2 benchmark samples
(functionList as list, sampleSize as number) => // a list of functions and the number of times to call each function
let
Benchmark.Time = (f as function) => // making 1 call to the function, returning the number of seconds it took
let
f = () => Logical.From(f()), // to cast the result to a logical
start_time = DateTime.LocalNow(), // not evaluated yet!!
end_time = DateTime.LocalNow(), // not evaluated yet!!
duration =
if start_time <> null then // start_time evaluates here
if f() ?? true <> null then // function is called here and never returns null
(end_time - start_time) // so, this is always executed and evaluates the end_time to calculate the duration
else null // never executed
else null // never executed
in
Duration.TotalSeconds(duration) // in seconds as a floating point number
,
Benchmark.Sample = (f as function) => // repeatedly call, and time, the function
List.Generate(
() => 0, // from 0
each _ < sampleSize, // to sampleSize -1 (taken from the enlosing function)
each _ + 1, // by 1
each Benchmark.Time(f)), // call and time
Benchmark.Statistics = (runTimes as list) => // calculate some basic statistics and return them as a record
[
Average = List.Average(runTimes),
StandardDeviation = List.StandardDeviation(runTimes),
Median = List.Median(runTimes),
Percentile25 = List.Percentile(runTimes, 0.25),
Percentile75 = List.Percentile(runTimes, 0.75)
],
results = List.Accumulate( // loop through the list of functions
functionList,
{}, // start with an empty list
(state,current) =>
state & {Benchmark.Statistics(Benchmark.Sample(current))}) // take the sample and append the statistics
in
results // a list of records
Enjoy!
I did a simple test with 10000 calls with the same parameters, so the range of the shortest and longest should be very close to the mean. Therefore, we can calculate the total with mean * number of calls.
According to your measurement, both functions take about 5 seconds for everything.
The queries are included in the same example file.
Even if I refresh the preview with the PQ Editor open, the whole process takes a bit more than 25 seconds on my machine!
IMHO your time measurement cannot be correct, even if we estimate the execution time for your code and subtract it, we still get a difference of more than 4 times.
Andreas.
Hm. I see your point. And I agree that it is unlike for my code to take 25 seconds...
I'll do some more testing myself... I didsee lots of 0 second elapse times if when looking at the details. Initially I thought that could be due to function call caching, but that does not explain your observations.
Now I think it is the measurement process itself...
Any idease? Anyone?
You copy the code into a new query. Let's say you name it "fnBenchmark".
In a 2nd query, where your own functions reside or where they are at least accessible:
let
...
test_results = fnBenchmark({()=>fnCompareWildcards(<your paremeters>), ()=>fnRegexExtr(<your paremeters>)},1000)
in
test_results
Be aware: If you functions are quick, they may be to quick for the precision of DateTime.LocalNow() to measure. I tested with calculating 5000 fibonacci numbers and that proved to be too quick...
If that would be the case with you, you may need to create your own function to invoke them multiple times. If your functions are meant to invoked on all rows in a table, create your test cases like that.
Here I have a test file that contains 2 functions: fnCompareWildcards and fnRegexExtr
Let us assume I copied your code as query fnBenchmark into that file. How can I call it to check the runtimes of the two functions?
Andreas.
March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount!
Your insights matter. That’s why we created a quick survey to learn about your experience finding answers to technical questions.
Arun Ulag shares exciting details about the Microsoft Fabric Conference 2025, which will be held in Las Vegas, NV.
User | Count |
---|---|
34 | |
30 | |
20 | |
19 | |
12 |