5 ms·
I've never really spent any time reading Python bytecode, but disassembly suggests the difference could be: f3 ------ 92 LOAD_FAST 2
by hyperpape 8y ago
I've never really spent any time reading Python bytecode, but disassembly suggests the difference could be:
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-different-from-i-i-x-in-python 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.
- bazizbaziz 8y agoThis comment is excellent. The title of the original post should be: "When optimising code, never guess, always read the bytecode/assembly." 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.'.
- ColinWright 8y agoIndeed, I was unduly influenced by the code I was writing in the late 80s and early 90s that really did take languages with multiple assignments like this and ran them on different processors. You say it's a silly thing, but we used to do it - things have changed. 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.
- bazizbaziz 8y agoI didn't mean to say this was a silly thing to do - most modern processors execute instructions out of order on multiple ALUs. 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. :)
- mikekchar 8y agoPossibly a subtitle: It's never what you think it is. I've worked in embedded and real time environments before and optimisation used to be my thing, but I am always surprised at how badly I guess what the problem is. It's a hard lesson to teach other people though because programmers are smart and smart people tend to default to thinking that they are correct :-) 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.
- m_mueller 8y agoI'd say without measuring first you have too much to look through for anything large enough to be interesting. So measure, drill in, eliminate some first obvious culprits, measure again, then look at assembly. 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.
- detaro 8y agoMeasuring is more important. Knowing "a is faster than b" without understanding why is more useful than guessing what should be faster based on assembly and getting it wrong because you don't perfectly understand how the CPU is actually executing your code. Looking at assembly/byte code is interesting to understand what you could tweak, but again you need measurements to verify.
- blattimwind 8y agoI mean this is CPython, so its bytecode never, ever runs in parallel anyway.
- jhayward 8y agoYou mean concurrently, if you're referring to the GIL.
- blattimwind 8y agoNo. I mean parallel. concurrent: multiple tasks at the same time are "in progress" parallel: multiple tasks are progressing at the same time
- jhayward 8y agoAre you certain that CPython doesn't compile to any of the SIMD parallel instructions available in e.g., AVX? You mean concurrent.
- deleted 8y ago[deleted]
- toast0 8y agoAs a non python user, that stack overflow post is very interesting. It seems that in python x += y is a significantly different operation than x = x + y, despite my expectation that one is just syntax magic for the other.
- ubernostrum 8y agoIf you want to test your theory, you can look at the implementation of those opcodes. Here's INPLACE_ADD: https://github.com/python/cpython/blob/874809ea389e6434787e773a6054a08e0b81f734/Python/ceval.c#L1486 https://github.com/python/cpython/blob/874809ea389e6434787e7... And here's BINARY_ADD: https://github.com/python/cpython/blob/874809ea389e6434787e773a6054a08e0b81f734/Python/ceval.c#L1280 https://github.com/python/cpython/blob/874809ea389e6434787e7... They're almost identical :)
- hyperpape 8y agoThanks for the links! However: 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/874809ea389e6434787e773a6054a08e0b81f734/Objects/abstract.c#L952 https://github.com/python/cpython/blob/874809ea389e6434787e7... PyNumber_InPlaceAdd https://github.com/python/cpython/blob/874809ea389e6434787e773a6054a08e0b81f734/Objects/abstract.c#L1105 https://github.com/python/cpython/blob/874809ea389e6434787e7... And those are different, though I can't tell how it should matter.
- ShroudedNight 8y ago> And perhaps the culprit is that INPLACE_ADD requires more work than BINARY ADD 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...
- ubernostrum 8y agoHere it's worth knowing a bit about how Python manages data. 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.