Three Optimization Tips for C++
facebook.com
facebook.com
* Measure, then optimize
* Pay attention to data layout to keep the cache hot
* Techniques for devirtualization
[0] http://channel9.msdn.com/Events/GoingNative/2013/Writing-Qui...
1. Replace strings with integers wherever possible. Create a mapping table, use the indexes.
2. Layout related objects close in memory. Even if it requires special data structures to do. Suppose that if I need A I am likely to need B soon. If B is physically close to A, then I am likely to find it in cache, which is about 10x as fast. So, for instance, don't store a tree as a bunch of random pointers. Store it as offsets inside of a fixed container of memory.
3. Process data according to how it is laid out in memory. For instance walk through a vector, don't do random access into it. Again this is all about trying to make sure that stuff is in cache as often as possible.
For the second and third points I found it was a case of do it perfectly or don't worry about it. For example if I am missing cache regularly in three places, fixing one only mades my code 50% faster. Fixing the second one almost doubled my speed. Getting the last one was an order of magnitude speed increase.
Even with hyperthreading and all the other fancy tricks that modern CPUs have, it's still a huge deal. I learned about all this stuff in general terms at university years ago, but it didn't really click until recently that even when the CPU is at 100%, it's very very often stuck waiting for a memory read to complete.
If you're going to be doing a lot of comparisons of identical strings, use boost::flyweight<std::string>.
As a side note, definitely don't follow the advice about using position-dependent code unless you are working in a very performance-intensive and isolated environment. Just a single non-relocatable module/dll completely undermines the benefits of ASLR, so an attacker with a copy of your program will have a much easier time crafting an attack against it.
1. Comparisons (no branch, just compare) 2. u/int add/subtract/bit operations 3. FP mul 4. FP add/sub 5. FP div 6. u/int mul 7. indexed array access (data in L1, including latency for data to move to registers) 8. u/int div
That rough order seems to generally hold on a modern Haswell processor or the in-order PowerPC cores in an Xbox 360, though the cycle counts will vary.
Being thoughtful with respect how you access your data, paying attention to the effects of access patterns on caching, will almost always help regardless of the target architecture :)
Is there a reason you follow Andrei's lead and suggest that comparisons operations are faster than integer subtraction? At least for x64 on Intel, I think they would always be the same speed and thus should be grouped together. Are they different on the other platforms?
The problem is that there is no way to know at compile time the position of an instruction relative to its target. Non-PI code handles this by having the linker update the relevant constants in the machine code. PI code has to function without modification, so it must indirect through a global table.
It's this indirection that causes the speed hit. On the other hand, PI code benefits from the ability to share code pages, so it can be faster in some circumstances.
$ cat div10.cpp
#include <cstdint>
uint64_t div10(uint64_t y) {
return y / 10;
}
$ g++ -std=c++11 -march=native -Ofast -c div10.cpp
$ objdump -C -d --no-show-raw-insn div10.o
0000000000000000 <div10 (unsigned long)>:
0: movabs $0xcccccccccccccccd,%rdx
a: mov %rdi,%rax
d: mul %rdx
10: mov %rdx,%rax
13: shr $0x3,%rax
17: retq
His basic implementation likely isn't using division at all."Truth be told, it's a multiplication because many compilers transform all divisions by a constant into multiplications; see e.g. http://goo.gl/LhPeH "
uint64_t div10(uint64_t y) {
const uint64_t magic = 0xCCCCCCCCCCCCCCCDULL;
__uint128_t prod = magic * (__uint128_t)y;
return (uint64_t)(prod >> (64 + 3));
}
See http://libdivide.com for how to get this codegen with runtime constants (I am the author).A deep understanding of the algorithm took me a long time, I think a few months. That was mainly due a lack of information describing the technique. The paper that Andrei linked to (Granlund-Montgomery) is dense and contains a significant error, which I was never able to get resolved. Henry Warren's celebrated Hacker's Delight is more accessible, but is also more of a proof-of-correctness than a learning resource. So my intuitive understanding came from my own investigating and playing around, which is what lead me to find an improvement on the algorithm.
Implementing libdivide took me maybe six months of my hobby time. It's not just the core algorithm - there's a lot of auxiliary functions, for example to compute the high half of a 64 bit multiply in SSE. But working at that level is tons of fun.
Incidentally, I wrote up what I hope to be the most accessible (yet still rigorous) description of the algorithm at http://ridiculousfish.com/blog/posts/labor-of-division-episo... . I advise anyone interested in learning more to start there, instead of the Granlund paper.
This case allows for a simpler method, exemplified here (it also works for signed integers, but more care is needed with the shifting): http://goo.gl/D5q9IO
EDIT: On second thought, compilers are also not doing their best job on that example. f1 could be simplified to
mov rax, 4865095698
add edi, 1
imul rdi
shrd rax, rdx, 39
ret
which has a shorter critical path.How is a compiler supposed to track that information? That requires some serious dependent typing.
What it's happening here is something more clever:
let z be: (2^67 + 2)/10.
2^67 isn't divisible by 10 (that's why we add 2). z = 14757395258967641293L
Now, I claim that n/10 = floor(z * n/ 2^67)
z is an integer, but it represents the exact division of (2^67 + 2)/10
we distribute the n: (n * 2^67)/10 + 2 * n/10
we distribute the 2^67 division: (n * 2^67)/(10 * 2^67) + 2 * n / (10 * 2^67)
simplify the terms: n/10 + n/(10 * 2^66) [1]
Now: floor(x/d + c) == floor(x/d) if c < 1/d
we want to take floor of that number[1], but as n/(10 * 2^66) < 1/10
(the maximum value for n is 2^64 - 1), it follows that floor(z * n / 2^67) = n/10.
That's what the code is doing:
0xcccccccccccccccd is 14757395258967641293L
the multiplication of z * n fits in 128 bits, and the mul instruction stores the higher 64 bits of $rax * $rdx into $rdx.We need to divide by 2^67, whichs is shift 3 67 bits to the right. If we take the higher part (rdx) we get the higher 64 bits, and then we shift 3 bits to the right (shr $0x3, %rax) and that's our answer :).
Cool trick.
>The speed hierarchy of operations is:
>
> comparisons
> (u)int add, subtract, bitops, shift
> floating point add, sub (separate unit!)
> ...
> (u)int division, remainder
This can't be right. A comparison necessitates either a branch or a cmov, and there's no way that either of those is faster than a basic ALU operation.Throughput is somewhat more reasonable, but then large parts of the hierarchy collapse entirely. A more reasonable approach is to think about resource (ALU, LSU, branch, cache, etc) pressure, but that doesn’t fit nicely into a soundbite and requires actual thought.
A branch miss on the other hand is much more expensive, about 14 cycles last time I was testing (on a core i5 desktop cpu from a few years ago).
i.e. this is why measuring the whole is so important in isolation it is slower but in more complicated code it is likely to be faster.
float rcpDiv = 1.0f / value;
float finalValue = myValue * rcpDiv;
In a lot of cases when speed matters. With fpmath=fast, I've seen compilers do that for you, but not always, as it potentially changes the answer very slightly.Wait, how is that possible? Like, you can have amd64 optimized code that uses 32-bit pointers?
On a meta-note, I'm really happy to see more highly technical submissions like this hitting and sticking on the front page.
EDIT: from the slides:
Prefer 32 bit ints to all other sizes
1. 64 bit may make some code slower [no additional explanation given]
2. 8, 16 bit computation use conversion to 32 bits and back
Additionally, code that handles 32-bit registers in x86_64 is often shorter than its 64-bit counterpart due to the lack of REX prefixes in instructions. This also reduces the instruction cache footprint. Some instructions like integer division and multiplication are also faster when handling 32-bit quantities, but this depends on the microarchitecture in question.
Excellent point, thank you. I was only thinking about the data cache. Interestingly, I believe 64-bit ARM (AArch64) instructions are still only 32 bits long though I haven't confirmed via google. Maybe someone can correct me.
Correct, but I wouldn’t say “still”, since some “thumb” AArch32 instructions are 16b.
This all is a bit hackish. With the x32 abi you can simply have 32 bit pointers everywhere.
I'll compliment that with another advice. On any modern Linux there is "perf top". If you haven't yet, I'd suggest learning how to use it. And using it.
http://people.cs.clemson.edu/~dhouse/courses/405/papers/opti...