When optimising code, never guess, always measure
solipsys.co.uk
solipsys.co.uk
f3
------
92 LOAD_FAST 2 (a2)
94 LOAD_FAST 3 (c)
96 BINARY_ADD
98 LOAD_FAST 3 (c)
100 LOAD_CONST 1 (2)
102 BINARY_ADD
104 ROT_TWO
106 STORE_FAST 2 (a2)
108 STORE_FAST 3 (c)
f4
------
92 LOAD_FAST 2 (a2)
94 LOAD_FAST 3 (c)
96 INPLACE_ADD
98 STORE_FAST 2 (a2)
100 LOAD_FAST 3 (c)
102 LOAD_CONST 1 (2)
104 INPLACE_ADD
106 STORE_FAST 3 (c)
And perhaps the culprit is that INPLACE_ADD requires more work than BINARY ADD https://stackoverflow.com/questions/15376509/when-is-i-x-dif...I strongly believe that the idea that it's running on multiple cores is not the case. Python doesn't implicitly schedule any user code on separate threads, and if it did, the coherence cost of doing so would swamp any benefit of doing two simple operations in parallel.
Disassembly is as simple as:
with open('f3', 'w') as f:
dis.dis(factor_fermat3, file = f)
with open('f4', 'w') as f:
dis.dis(factor_fermat4, file = f)
Edit: Whoops, I see that I used Python3, while the post was about 2.7.4, and there's some wrangling over the differences below. I reran the disassmbly under 2.7.4. The bytecode was more verbose, but differed in the same way.Without actually reading the assembly/bytecode/etc, you end up speculating about silly things like 'the two evaluations and assignments can happen in parallel, and so may happen on different cores.'.
But you are right: even when you isolate where the code is slow, you've still got a lot of work to do to find out why it is slow.
Added in edit: The magic term is "execution unit" not "core". As I say, things have changed, and the bundling of multiple execution units into each core, and multiple cores into each processor, is different in interesting and subtle ways from the situation I used to code, where I had a few hundred, or a few thousand, processors in each machine, but the individual processors were simpler.
The problem is that the abstraction layer between the python code in question and the processor's instruction stream is so thick that it's hard to say one way or the other that the processor is indeed executing that particular pair of instructions in parallel. It's definitely executing many instructions out of order, but it's unclear (without inspection of the python interpreter and its assembly) what's happening at the machine level.
Looking at the bytecode of the python program at least begins to tells us that the python bytecode of the two versions is fundamentally different, which could account for the performance difference. Although, what exactly makes the material difference is also under debate elsewhere in the thread. :)
I'm mentioning obvious because sometimes it really is, like doing a linear search in a hotspot where it should be a binary tree and things like that.
Looking at assembly/byte code is interesting to understand what you could tweak, but again you need measurements to verify.
Here's INPLACE_ADD:
https://github.com/python/cpython/blob/874809ea389e6434787e7...
And here's BINARY_ADD:
https://github.com/python/cpython/blob/874809ea389e6434787e7...
They're almost identical :)
Line 1296 is PyNumber_Add(left, right) Line 1495 is PyNumber_InPlaceAdd(left, right)
Those go to:
PyNumber_Add https://github.com/python/cpython/blob/874809ea389e6434787e7...
PyNumber_InPlaceAdd https://github.com/python/cpython/blob/874809ea389e6434787e7...
And those are different, though I can't tell how it should matter.
It would not surprise me if the culprit was instead:
98 STORE_FAST 2 (a2)
100 LOAD_FAST 3 (c)
Due to them being the same amalgamated memory location in practice, with the load hitting the in-flight store. But that's more typical of a POWER performance characteristic than an Intel one...Each CPython VM stack frame has three tuples associated with it:
* co_consts is the constant/literal names local to the frame
* co_varnames is all other names local to the frame
* co_names is the all nonlocal names referenced in the frame
Under the hood, Python creates a structure called 'fastlocals' which is basically an array of (pointer to) PyObject, where the PyObject at each index of the array is the value corresponding to the name at the same index of co_varnames. LOAD_FAST and STORE_FAST are extremely simple opcodes; they do a tiny bit of error checking, refcount management (increment on load, decrement on store), and then just manipulate 'fastlocals'.
So, for example, 'LOAD_FAST i' consists in its entirety of:
1. Grab the PyObject at fastlocals[i]
2. If it's null, throw an UnboundLocalError
3. Otherwise, increment its refcount and push it to the top of the frame's evaluation stack.
concurrent: multiple tasks at the same time are "in progress"
parallel: multiple tasks are progressing at the same time
You mean concurrent.
factor_fermat0: 1.599 ms
factor_fermat1: 1.488 ms
factor_fermat2: 1.387 ms
factor_fermat3: 1.421 ms
factor_fermat4: 1.422 ms
Of course, you should still measure when you optimize, but dealing with issues such as whether a=a+1 or a+=1 is faster seems annoying. At that point, you are benchmarking the language and not your own code.But that's useful feedback - thank you.
The performance will of course change accordingly.
So `sqrt(2.0)` will, in the end, call the assembly-instruction (depending on your specific CPU) `vsqrtss` since the input type is a 64-bit float, `sqrt(Float32(2.0))` will end up calling `vsqrtsd`, `sqrt(big"2.0")` will call the sqrt from the arbitrary precision library (MPFR) since we now have a "BigFloat" number etc.
Directly from a Julia REPL session:
julia> sqrt(2.0)
1.4142135623730951
julia> sqrt(Float32(2.0))
1.4142135f0
julia> sqrt(big"2.0")
1.414213562373095048801688724209698078569671875376948073176679737990732478462102
julia> setprecision(512) # Bump the precision
512
julia> sqrt(big"2.0")
1.41421356237309504880168872420969807856967187537694807317667973799073247846210703885038753432764157273501384623091229702492483605585073721264412149709993586really? I'd say that's quite significant, if it's repeatable.
But in effect it's an algorithm change, so it's not surprising, and it would be interesting to see how it changes as the numbers involved get bigger. That's not why I was doing it in this case, but it is intriguing.
Measuring performance is hard. It's easy when there are big differences, like the new exceptions in 2.5 were 60% slower. But small things can add up, and it's hard to measure them.
One piece of wisdom I got from Tim Peters: Run short benchmarks. The longer you run them the more likely they are to be influenced by context switches and the like. I used to always run long benchmarks hoping to push these changes down into the noise. Instead I started running short tests and looking at the fastest of the runs.
Another area of speed improvement though was less about A/B testing and more about thinking hard about where time is used and how to just get rid of it entirely. Some use cases really suffered because in Python strings are immutable. If you are reading from the network and, say, looking for the end of a data frame, you may create a lot of new objects as you append incoming data. IIRC, a "bytearray" was created that was basically a mutable string to get rid of that problem entirely.
You can speed up an algorithm 1000 fold but if it only accounts for 1% of your total execution time you've only gained a 1% speedup.
In a pure program, yes. In an impure one, not necessarily. The change in algorithm could result in some different data structures or layout, and this could cause another part of the program to run much faster or slower (e.g. because of cache locality issues).
It's one way of measuring, and can really help narrow things down in larger programs.
i.e., pipeline takes 300s to run. Suspect the final output may be worth profiling and optimizing. Stick a 'return' at the top of the output function and see the pipeline now takes 295s to run. Conclusion: final output not the problem.
For example, one function thrash the cache, which ends up greatly slowing down a second function. If you remove the first function entirely (comment it out, etc), the change will reflect that, but a flame graph won't (since it only shows the actual runtime for the second function: it has no way to determine how much impact it is having on other functions indirectly).
Beware measuring, though. Sometimes, just thinking about the problem ahead of time can clearly expose where you are spending time. At least, it can give a hypothesis to test for where you are spending time. With measurements to confirm/refute. :)
"Longest total." I don't care if its a big slice of pie or a lot of little slices. But it better be one of those.
> Sometimes, just thinking about the problem ahead of time can clearly expose where you are spending time.
That's certainly true. But 99% of people overestimate their ability to do this. And are surprised by the results.
If so, good, but it wasn't clear to me that that's what you meant. If you mean something else then I don't know what you mean at all.
To that end, I was taking it to mean that longest "synchronous" path through your system. Not necessarily a single method, by any measure. But, systems have plenty of what I will call "checkpoints" where code can be restarted/rerun with no hard to recover penalty. That is what I took to mean by longest.
Why beware measuring? In my experience, people are generally terrible at guessing where the bulk of the time is being spent. Measure as soon as you care.
a2, c = a2+c, c+2
ran faster than the fragment: a2 += c
c += 2
Context and timings in the post. Speculation welcome as to why this might be true, and your predictions for Python 3 invited.Usually, in an interpreted environment, I reason about other things: number of database calls, HTTP fetches, that kind of thing. If I need CPU performance, I'll fire up something faster.
Finding the meatiest primitive that does the job in one go is often best - batching, where the batch operation is implemented in native code - but not always, you're at the mercy of the implementer.
You are mistaken - this investigation does make sense in my context.
> If you're having Python bottlenecks, it is the wrong language.
But you are missing the point. The object of the underlying exercise was not to try to make this specific instance of the algorithm run fast, Python was not the bottleneck. The activity was exploring the effects of algorithmic changes and investigating the Big-O performance of the different variants and changes. As a part of that the underlying code was being reworked and modified to allow the larger algorithmic changes to be made, and this was one of those changes. It says so in the post:
> Then I made a small change to the code layout ready for another shot at an optimisation.
As always I ran the timing tests (in addition to the functional tests) to make sure nothing substantial had changed, and there was an anomaly which attracted my attention and piqued my curiosity. Hence the write up.
And I believe you are wrong when you say that this sort of thing is, or should be, only of interest to Python VM Devs. I think that curiosity about this sort of thing is, or should be, important to everyone. Perhaps you will then decide you don't have time to pursue it, or that your time is better spent elsewhere, and that's fair enough, but curiosity and a desire to learn has been a pervasive theme among the best devs I've ever worked with.
So again, you are wrong, this investigation does make sense in my context. Your mistake is in assuming the post is about a Python bottleneck. As I say, you've missed the point.
Languages don't get slower on a single dimension, and computers have multiple dimensions of performance. Hitting a performance bottleneck in one dimension on one language doesn't mean you'll hit the same bottleneck on a different language. Different scales might have different bottlenecks as well, think exceeding L2 cache or finally using all your GPU cores on problems that parallelize with scale. If you're using Python you'll never hit those other bottlenecks because the VM is always 40x slower than a naive native implementation.
Unless, of course, your bottleneck is in native code, which is where the easy prototyping of Python really shines.
That would require using multiple threads, which I'm pretty sure python won't do automagically. And even if it did, trying to do those calculations concurrently would almost certainly be slower due to the additional synchronization overhead.
It would be interesting to know how different python implementations react to that change.
Instruction level parallelism (on common platforms) is an optimization a processor applies, based on the conditions when an instruction is decided, but it isn't visible directly in the code. You would really need to inspect the low level interpretter loop, with some idea of the specific bytecode it was processing and specifics about the processor you're using. Setting up the order of instructions to reduce data dependencies is one of the things an optimizing c compiler does, to help the processor be able to utilize instruction level parallelism, but to compute things with thread level parallelism would require too much coordination -- starting a thread is a many stepped thing, and then transferring the values is too, even if writing to memory addresses, there's hidden coordination there.
$ for STMT in "a2, c = a2+c, c+2" "a2 += c; c += 2" "a2 = a2+c; c = c+2"; do
echo \[$(python3 -m timeit "a2 = 1; c = 1; $STMT")\] $STMT
done | sort -n
[... 0.0547 usec per loop] a2 = a2+c; c = c+2
[... 0.0569 usec per loop] a2, c = a2+c, c+2
[... 0.0608 usec per loop] a2 += c; c += 2But definitely it seemed that python2 and python3 had significantly different performance.
"a2 = a2+c; c = c+2" is still fastest on both 2 and 3, though don't know enough to say why.
Edit: did you compare the bytecode?
I'm hoping someone with a better background will find this sufficiently surprising and be provoked into doing that analysis, writing it up, and letting us know.
I've got it in my boomerang queue.
So everything that comes to hand gets a unique reference (auto-generated) and I put tags on them. I don't try to be complete, I don't try to make sure tags are unique, I just put what's on my mind and seems relevant in a meta field. The system then auto-cross-references by content and tags, so when I put tags related to stuff I'm working on it uses a sort of TF-IDF system to find relevant documents and other reference material and puts links between them.
But sometimes I don't want to deal with something just yet, so there is a tool that lets me send things into the future - I call that my "Boomerang Queue".
Edit: it's been a while since you asked the question, so I tried to find some way to let you know I've replied, but you have no contact information in your profile. I hope you see this.
I've long considered and wanted a system like you've described. It seems like it would have incredible long-term value. Is everything for yours self-hosted? What wiki do you use?
But recently I've been experimenting with zim to see if I can use that instead. It saves pages in markdown in a directory structure, so my external scripts can be adapted to run over them and create the augmenting pages. It could possibly insert the free-text links as well in the background. Just an idle thought.
It's all running on my home machine, but I can connect to it from (nearly) anywhere. It's effectively unusable on my phone, so I have a staging area in which I make notes, and then transfer things later. The act of transferring implies a review of the material, and that's of value anyway, so it's not wasted effort.
For years, decades, I've been thinking of polishing, bundling, and releasing it, but it would be one of a gazillion organisation systems and no one would find it. If they did, I'd then be committed to supporting it, so that's not going to happen. I might, one day, convert these replies into a blog post, but again, limited value.
I worry about not having the discipline to regularly curate such a system. That's probably my biggest roadblock towards just going for it.
Quite quickly the system was useful, and I had fewer decisions to make by using than by not using it, so it saved me effort. That was the key.
[0] ... or file/email/document on the screen
People take advices like “don’t optimize” and then in 2030 someone 300byte-patches system updates so they would install at ~ssd write speed instead of sudden 1.5 hours. And everyone is like wow, how we overlooked that. You never hired an engineer, that’s how.
You say:
> the conclusion bases itself on silly 0.1 which plays no sensible role at all.
You've missed the point. This specific code change is not the point for the task overall. This specific code change was preparation for a significant algorithmic shift that was about to happen. On the way I happened to notice something odd, and I thought people would be mildly curious, so I wrote it down for people to see.
Please don't assume that I'm clueless about the wider picture.
Especially when you deal with optimizing compilers it's often totally counterintuitive where the real bottlenecks are.
Obviously you shouldn't be stupid and always shout "premature optimization" but make it a habit to profile from item and time and learn from it.
But machine code, and thus optimized compiled code, has moved into the same space -- the machine instructions are interpreted, too, now. Speeds are a thousand times more, but things affecting speed are way more complex, so reasoning is similarly compromised. It's a deal with the devil: in exchange for typical speedups of an order of magnitude or more, you lose the ability to tell what is making it slow, and even whether it is slow at all. What does slow mean, now? Only than another program is faster. But you need to have found it to know.
On modern chips (Haswell and later), a loop of only four instructions -- two comparisons, two branches, which may run for a microsecond or months, depending on input, may take time t or 2t depending on whether the compiler guesses right about which way one of the tests goes. It happens that gcc tends, systematically, to guess wrong on these. The difference comes down to whether the loop takes one cycle or two per typical iteration.
Using __builtin_expect() can overpower the compiler's guess, but who wants that in their code? But it doesn't always work either. Slightly older chips (Sandybridge) would always take the 2t months, regardless. So we got a really significant speedup, sometimes, at the cost of not knowing whether we are getting it, or what we need to do to get it.
Engineering is turned into shamanism and guessing.
At least we always know that a Python program will always be slower than any equivalent compiled program. But Mercurial is fast...
1. Get the data structure right
0. Use a language that helps you get the data structure right
Establish time budgets for high-level operations. Measure performance continuously as part of integration testing to catch regressions.
Stockfish Chess has a benchmark command, Cinema4d has Cinebench, and Blender now has its benchmark suite.
And really, a benchmark is just a unit test where you're testing for speed.
Performance issues are often surprising where they come from too, so most of the time budget testing value comes from some sort of integration, UI or end to end test. You also need in the wild perf tracking to find the regional or subset performance issues.
For something with a core loop, and all the value is in the core loop, sure, you can perf test the core loop.
But many applications are big, with lots of entry points, and data being worked with can come in different shapes and sizes. Sometimes it can have different dimensions that stress different bits of code. And it may be difficult to plan the breakdown of a global budget in this context.
Part of the catch-22 of optimization is that you need to profile before you optimize, otherwise you're likely to optimize the wrong thing. If you put your benchmarks at the unit level, you run the risk of having individual bits that are fast, but the whole doesn't run as fast as you'd hoped.
Now true, there can still be UI issues which need to be optimized, but my point is that if you want to test the speed of your rendering engine, of the animation framework, unit-pathing, or whatnot, its best to build a benchmark where you can repeatedly document and execute the critical code path.
A singular, automatic test, which can be run nearly automatically (without user-intervention)... as well as a documentation regime which can document progress on the benchmark, is key to improving the code.
It is worth pointing out that, currently, compilers can basically only do what they're told to do (Like a state machine) so this kind of stuff would be unlikely given that the effort with either be wasted or only part of a larger program (Just optimize it away).
Edit: guess it doesn't matter - Python is so ridiculously slow either way...
Fast code is often unreadable or unmaintainable, software should be written with it's lifespan in mind. That's not an invitation to write slow code, but microoptimizations can be a disatrous temptation (Especially when writing low level code). Compilers are pretty fucking good these days so I think trusting them can be very helpful.
How can you know if you don't measure?
Generally speaking, the cases where you are unsure, it's sensitive to platform details or unstable compiler optimizations, or you don't know what sort of user input you'll really get.
Or in the case of the blog post, if you don't have a good model of the execution environment in your head (because Python).
If you want to optimize away from the plain version you have to support the introduced loss in legibility with relevant measurements.
I didn't say that and was assuming the opposite, so I think you've misread me.
Obviously if the faster version is substantially more complicated it's a downside.
Don't tell me I'm misreading you when I make you aware of the distinction. I've understood what you wanted to say, albeit with difficulty. Just say "Ah, I meant simplified."
When they are the same thing, there is no trade-off to be made.