Apex memmove – fast memcpy/memmove on x86/x64
codeproject.com
codeproject.com
shrq $3, %rcx
andl $7, %edx
rep movsq
movl %edx, %ecx
rep movsb
Torvalds justified it as a way to force Intel and AMD to do the right thing and optimize the rep movs* instructions. He was annoyed at the code complexity of "more optimal" apporaches, and like you, concerned about icache effects.A hand-written memmove is faster in microbenchmarks, but the icache effects may make the overall performance difference smaller (or even negative.) That's harder to measure.
AFAIK glibc does get better results than the kernel approach, but they've also introduced bugs[3] that way and the code is very complex by comparison. Also worth noting is that the kernel usually runs with a cold(er) icache, which is why they compile for size rather than speed (last I checked anyway, which was long ago) and why their memmove may make more sense in the context of the kernel. Also I suspect copies in the kernel are more likely to be larger (e.g. packets, pages, blocks) as opposed to small objects in userspace.
[1] http://www.intel.com/content/www/us/en/architecture-and-tech...
[2] https://github.com/torvalds/linux/blob/master/arch/x86/lib/m...
For the lazy:
On aligned data REP MOVS will automatically select the largest available register/load/store instruction to use. So invoking this instruction will also determine SSE2 vs AVX vs AVX512 (or more over its burned into the silicon). Furthermore if your allocation is large enough it will by-pass caching mechanisms for an additional speed up.
For the VERY lazy:
REP MOVS will attempt to saturate DRAM bandwidth. The best hand coded ASM or C can hope to do is be as fast as it.
[1] https://stackoverflow.com/questions/26246040/whats-missing-s...
[2] https://www-ssl.intel.com/content/www/us/en/architecture-and...
For small copies with buffers that aren't aligned or cacheline-multiple length and are resident in L1 cache, it's possible to be significantly faster than REP MOVS using a software sequence of AVX instructions. This is because the branches in these sequences are usually perfectly predictable in the real world, but the "microcode sequence"[1] used by REP MOVS does not benefit from the branch predictor. This imposes a static startup cost (a few tens of cycles) that exceeds function-call overhead, which keeps software implementations of memcpy in business.
[1] Not exactly microcode as that term is classically understood, but there isn't really a better term for it that's widely used.
What are technically correct but less widely used terms? And how does the current Intel approach differ from classical microcode? Most of my knowledge is from the Intel manuals, so I don't have a context for how it is different than other approaches.
BTW, the article is quite strangely structured. The initial piece sounds like he has been up for four days straight and he just wants to get his thoughts down. But he forgot a few points, so they are added in five updates. Arguably that's the best place to start reading - around the middle of the page.
Then, he goes back in time two years and adds an earlier draft of the same piece, noting "don't even look at the original article! It's very confusing!".
[0] https://books.google.co.uk/books/about/Inner_Loops.html?id=v...
Microcode has a bit of a delay getting in there and so 'rep movs' is not so great for short moves, but normally does pretty well for large transfers.
That is the big problem with memcpy, the optimal code depends on how it is used. Block copies of large aligned structures wants different code than moving short unaligned strings. Ideally, the compiler has more context and can select the different versions for different call sites.
But they never gave Andy the uop he really wanted which would zero the next cache line. Normally when you start writing to memory the machine will populate the cache line to be written from memory. He wanted the ability to say he plans on writing the whole thing so don't bother and if something happens then assume it is all zeros. (or something like that)
Perhaps the current machines have this now... but probably not.
There are many, many tricks like doing 'look ahead' to start the L1 cache bringing in the next line before we actually need it, to hide that latency.
And the article describes how there are various code paths for big blocks, short blocks, aligned, unaligned, etc.
`rep movsb` shouldn't be thought of as repeatedly copying single bytes from source to destination, but rather as a `memcpy` instruction with a weird name due to history. If it's used frequently enough to be worth optimizing (and it seems like it is) then Intel and AMD can easily recognize it as such and make their hardware perform the operation as efficiently as possible.
I wrote an (actually) fast memcpy() in assembly. There are more problems then just different size-s. Hardest problem is actually alignment, particularly 1 byte unaligned buffers.
A couple things (if hn doesn't autoformat): 1. SSE2 non-temporal MOV (one that doesn't go through cache) is basically required when going over ~cache_size. 2. One byte unaligned (or any odd number) to cpu alignment buffers need to have the first n bytes copied first to get to that alignment (16byte for SSE; also couple bytes at the end). 3. Buffers not aligned to each other are the biggest mess. SSSE3 actually helps with that as it can shuffle bytes around, but i found it easier (and probably faster) to shift-or the things.
There's probably more that i forgot. I lost the original code but i have a half-arsed version somewhere around here if someone wants to see. It is only for 8 or 16 bytes aligned buffers (to each other) but the grand structure is there.
PS To those saying "REP MOV* is faster": In theory, yes, but no, not really.
And you want to unroll each case, because speed is the goal.
The longer the alignment size (4-byte? 8-byte? 16-byte?) the more permutations of start and end conditions there are. E.g. moving from a 15-byte aligned source to a 3-byte aligned destination. Maybe 256 combinations? Only 1 of which is zero-aligned source and destination (full-register move) which is all most folks think of writing to begin with.
Add in cache-line size considerations, and overlap possibilities (are we moving a string in-place left or right by 7 characters?) Can the bus do unaligned loads? Stores? How do they compare in performance to multiple single-byte transfers? So many things can affect performance, each case becomes a heuristic. After all the theorizing, testing is the only real metric.
I've dived down this rabbit hole before. Wrote a test app to move 0-128 bytes from source aligned 0-128 to destination aligned 0-128. The memcpy I was testing on a RISC architecture hit 11 bugs (processor faults, incorrect result) before I was through.
Its refreshing to find somebody else with an appreciation for the depth of this problem. Thanks for posting, gens!
It is, but in performance critical code there is never any reason to use unaligned buffers or copying byte sizes not a multiple of the machine's register size. E.g you can have a memcpy_fast() which demands correct alignment, and the standard memcpy() which is slower but has no such requirements.
BugMeNot has you covered: http://bugmenot.com/view/codeproject.com
Why not write in assembly then? And this sounds like a recipe for slower code when using a different compiler or a different version of the same compiler:
>The way instructions are ordered also prevents the compiler from making some `assumptions`!
If he had indeed meant that, he should have written
x = ~x + 1;
GCC, even with optimisation turned off, will turn that into a negl instruction.In any case, there are unknowns here. Perhaps you should be less hasty in declaring people's work 'pointless' and telling others what they 'should' have done?
The advantage of this construct is that you can use the flags set by the increment to test for loop termination, rather than needing an additional comparison. The problem in C is that the compiler tends to "unoptimize" it for you after seemingly innocuous changes.
Anyway, the loop ends up looking something like this:
char *end = start + size;
size_t neg = -size;
do {
register = *(end + neg);
process(register);
neg += advance;
} while (neg);
For certain loops this construct shaves off 1 cycle per iteration, at the cost of ~1 cycle of initial setup cost. I'm wagering that he was doing something similar, but just kept the name "size" after the negation, rather than switching to "neg" as I did here.I don't understand your optimization. ZF is also set for dec, so why not just use dec and not negate the size?