Measure code execution time accurately in Python
knasmueller.net
knasmueller.net
I found a lot of performance issues that others missed, and there were a number of quite notable cases where I had rather extraordinary results. Like 20% from tweaking (not even deleting) a method measured as taking 5% of run time. Not the weirdest I every did, but a solid number of top ten entries from this category.
How did I find them? Sorting by invocation count instead of run time. You can blindly jump into any of functions and get some useful results, but a more sophisticated approach is to think first about expected versus actual results. You think to yourself, “I just ran this task ten times. It has this much data to deal with. How many calls to this particular function should I have expected? 250? Then why was it called 759 times?”
What you find next will be architectural problems, or problems of data flow through the system (while some, including me, would argue that I just repeated myself, not everyone accepts that).
The fastest methods are the ones you never call, and I’m surprised at how frequently people ignore call count even though it’s often right there in the results. Maybe because it’s often the last value displayed, as if to say “This is barely worth mentioning.”
Another one is to only look at memory allocations. Usually when there is a lot of allocations, a lot of data is being handled. This gives a bit of a different perspective where you start thinking of folding chunks of data together.
The nice thing about those two approaches is that they don't vary between CPU and architectures.
Cache invalidation can follow a similar misattribution pattern.
When you do succeed in making an improvement, the % of time for everything else should increase. 10% of 90% is 11%. Look back at the old results and see if anything else went up disproportionately, and see if you can understand why.
Related to that notion, and as a parting thought, since this is getting some love: game and A/V programmers understand something the rest of us do not. And that is a computation/time budget. It doesn’t matter if one thing takes four times as long as the other if they both take too long. You haven’t fixed “the problem” until you’ve fixed them all. Late is late.
There’s another “systematic” approach to making very large perf improvements that doesn’t fix problems from most to least significant but by functional unit. Fix all of the problems in that functional unit even if it’s 1% of the total (if you’re after a 5x speed gain it’s actually 5% of the total budget). If you make large scale changes you are going to have to do some expensive regression testing on this part of the code, tests or no tests. Once you’ve paid that price once, you will never go back for that last 2% because you can not justify it to your organization. You’re stuck with those in ten different functional areas, forever. What you can justify is boosting a raft of work from 18% to a 20% overall improvement.
I used 4 or 5 performance tools to look at the performance of Oil shell, and uftrace was one of the most important ones.
I hope to write a blog post about it soon. The overall results were very good -- Oil is now faster than bash despite doing more parsing by design -- though the biggest win was translating the code to C++ from Python :)
So it's unusual, but after translation, uftrace was a winner, and I used it largely by counting function calls and allocations, not measuring timing. Although you can use it in many different ways.
Oil's Parser is 160x to 200x Faster Than It Was 2 Years Ago
http://www.oilshell.org/blog/2020/01/parser-benchmarks.html
Another lesson is that aside from the big difference in execution speed, tools for measuring C++ performance are just way better than those for Python. Debugging tools are also better. Of course I still avoid C++ for most problems because of build times, headers, memory management, library style smorgasbord, etc.
Not perfect by any means but usually a nice check to verify optimization potential.
I learnt about this tool from Emery Berger's talk [2] on this (at strangeloop), which I highly recommend. Lots of really nice insight, even outside of this tool.
[1] https://github.com/plasma-umass/coz [2] https://www.youtube.com/watch?v=r-TLSBdHe1A
Also, quote from the timeit page:
> Note: It’s tempting to calculate mean and standard deviation from the result vector and report these. However, this is not very useful. In a typical case, the lowest value gives a lower bound for how fast your machine can run the given code snippet; higher values in the result vector are typically not caused by variability in Python’s speed, but by other processes interfering with your timing accuracy. So the min() of the result is probably the only number you should be interested in. After that, you should look at the entire vector and apply common sense rather than statistics.
The only real solution for avoiding noise is to control its sources.
Reference here: https://tratt.net/laurie/blog/entries/minimum_times_tend_to_....
Of course, caching of deterministic code complicates matters too.
https://vstinner.github.io/journey-to-stable-benchmark-syste... https://vstinner.github.io/journey-to-stable-benchmark-deadc... https://vstinner.github.io/journey-to-stable-benchmark-avera...
This is why the JMH samples warn against including loops[0] of the operations you're trying to measure, and have an API that's designed to avoid the programmer having to every write their own looping code.
[0] https://hg.openjdk.java.net/code-tools/jmh/file/7bc6011260aa...
What’s the equivalent of JMH for Python?
Statistics is hard. Like, really really hard. Stuff goes wrong all the time. Please leave it to the experts.
If you're going to do this, please look up the methods behind (for example) robust least squares, outlier detection, L_1 regression, etc. The right way to do this is to start with a small dataset that is with very high probability free of outliers, and slowly grow it by never adding points which have large residuals. (If you've done 3D scan registration and image alignment, this is what RANSAC does.)
The principle is, intuitively, that once an out-of-distribution point gets into your linear model, the model is poisoned forever. You can't trust the model to tell you that the bad points are the outliers. The way this paper does is is irreparably broken, sorry :/
They are the outliers. These experiments are typically measuring something that is effectively constant with almost zero noise, rather than some complex physical phenomena.
If you don't get an almost perfect fit, there is something going on that invalidates what you are doing (e.g. cache effects, clock effects, etc.).
In fact, if there are any outliers, I would not trust the benchmark at all. So removing them seems like trying to fix a bad benchmark with statistics.
> Statistics is hard. Like, really really hard. Stuff goes wrong all the time. Please leave it to the experts.
That is unnecessary gatekeeping. Benchmarking in CS is hard not because the maths/stats that are needed are hard, but because setting up the right experiment is hard and most people don't know all the pitfalls.
Therefore, if anything, you should leave benchmarks to CS/SE experts, rather than a statistician!
2, 3, 4 function calls in a row will benefit from that caching, but also help train the speculative execution engine. At some point that speculation may radically alter performance. Later, with a sufficient number of runs, the timing will also suffer from preemption, context-switches, or other interruptions that can invalidate both the cache and the speculative execution engine.
The idea that there is a fixed performance time for a function is naive on modern hardware.
While there are dozens of things that can go south in benchmarking, how to solve them is not about applying advanced stats, but about understanding and eliminating the sources of noise.
Most benchmarks in CS can be done quite easily as long as one understands all the technology stack.
When I said "small dataset", I should have said "small subset of all collected points that are initially fed to the model". The issue isn't that you have to collect the data little by little. The issue is that, once you've given a linear model an input that is outside the distribution which you're hoping to model, nothing about the model can be trusted.
So you collect all the data points, but you don't give them all to the model at once. (I'm describing the RANSAC method here now) You start with a large number of "candidate models" that are all fit with a small number of input points, and then test which of the candidate models predict well the points you have not yet given the model. Then you feed the best of these candidate models only the points which it predicts well, and create a more refined, still outlier-free model. This can be proven to work in the presence of a small number of out-of-distribution points.
I would hazard that the RANSAC method sounds a lot like what you’d get out of a larger hybrid model, where e.g. many Bayesian agents with different priors are bred under a genetic algorithm after being ranked by their predictive power. (Like humans surviving to reproduce and pass down models to their children!)
Still, you just gave me reason to mention one of my favorite "no, that won't work either" paper :) on how Bayes will not save you in the presence of model misspecification. Instead of butchering it any further, I'll just point you to this piece explaining the work, written by the author of the paper himself: http://bactra.org/weblog/601.html
Extraordinary claims require extraordinary proof, and people can be rightly insistent.
Most of these benchmarks are not done to predict performance in a target machine, but to find out which is faster.
n * timing_overhead + n(n+1)/2 * runtime
Seems like you'd do better by measuring n^2 runs and dividing by n^2. But that's not very good because it's not taking into account the observed variance. What you really want to know it something like the 95% confidence interval after observing n runs.My teammate has been working on a library to do just this for JavaScript, with added automation for running a set of benchmarks until some set of thresholds, like A is 95% likely faster than B.
Great in theory, but almost always relies on a normality assumption which just doesn't hold in general for performance measurements.
How well does this package handle pareto distributed run times?
Today I develop mostly in Python on a web backend system. Now, the most common performance issues I see are making multiple network calls when one would suffice (e.g., not caching repeated database queries). I have no doubt that there are many hidden O(N^2) issues lurking in the python code, but because network calls are so expensive, they seem to dominate optimization efforts.
I suspect that I'm not alone here, as today, there are many I/O performance tools than there were in the past.
time_start = time.perf_counter()
time_end = time.perf_counter()
execution_time = time_end - time_start