A Branchless UTF-8 Decoder
nullprogram.com
nullprogram.com
Also, none of the compilers I tried at https://godbolt.org/ were able to combine the 4 s[0...3] 8-bit reads into one 32-bit read, which is somewhat disappointing. They generated 4 separate byte-read instructions, one for each s[0..3]. Yes, I know it could be unaligned, but this is x86 where it would still be faster than 4 separate reads. You would need to do this instead (and sacrifice endianness/alignment-independence):
uint32_t v = *(uint32_t*)s;
*c = (uint32_t)(v & masks[len]) << 18;
*c |= (uint32_t)((v>>8) & 0x3f) << 12;
*c |= (uint32_t)((v>>16) & 0x3f) << 6;
*c |= (uint32_t)((v>>24) & 0x3f) << 0;
*c >>= shiftc[len];
We're still left with a bunch of shifts, ands, and ors, the purpose of which is to essentially "compact" the 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx bitstring into one 21-bit codepoint by removing the bits in the middle. All the compilers I tried weren't so clever as to do this (assuming eax = v from above after masking, and cl = slightly modified shiftc[len]) at any optimisation level: bswap eax
mov ebx, eax
and ebx, 00FF00FFh
lea ebx, [ebx + ebx*2]
add eax, ebx
shr eax, 2
shl ax,4
shr eax, cl
I played around with optimising UTF-8 decoding a while ago, and the above resembles one of the fastest and smallest ways to "compact" the 4 bytes into a codepoint.Of course, this bit string manipulation would be much faster and trivial in hardware (essentially rearranging wires), which makes me want a LODSUTF8 instruction that operates like LODSB but reads a whole codepoint instead, and sets CF on error...
You have to worry about memory access violations too.
More likely, it could just not execute that instruction or any instruction that depends on it, because that's the fastest way to preserve undefined behavior.
That's not quite how it works, or at least not what usually happens. Optimizers don't look for undefined behaviour to then do whatever, they look for the set of defined behaviours and then try to generate the cheapest code that behaves correctly in all those cases--and if that means that undefined cases do weird stuff, that's an acceptable side effect.
That in particular means that the potential for undefined behaviour to occur at runtime does not make the program undefined. If you ask the user to input a number and you then use that number as an index into some array without any input validation, the user could enter an out-of-range index, in which case the behaviour of the program would be undefined. But if the user inputs a valid index, the program must not exhibit any undefined behaviour.
Therefore, yes, if any of the bytes are not inside any valid C object, then that would produce undefined behaviour. Which is precisely why the compiler would be allowed to combine the loads into one 32-bit load: As the code, if it takes that path, is going to load all four bytes, and accessing a byte outside any objects would result in undefined behaviour, the compiler may assume that all accesses are within valid objects, and therefore loading them all in one is correctness-preserving.
> if any of the bytes are not inside any valid C object, then that would produce undefined behaviour
The compiler might also assume that every one of these loads will be exactly 4 bytes apart. Even worse, it might know that int32_t allocations will always be aligned, so it will assume that every load will be 4-byte-aligned. It then might output code that loses track of the bottom two bits, or SSE instructions that only work with certain alignments.
This isn't theoretical, this exact kind of bug has happened before, on a normal non-malicious compiler: http://pzemtsov.github.io/2016/11/06/bug-story-alignment-on-... https://news.ycombinator.com/item?id=12889855
It's true that the buffer overrun issue is the same for either version; I misread what was being replaced. But the alignment issue is specific to this version, and can't be ignored.
"One important consequence of always reading four bytes is that the caller must zero-pad the buffer to at least four bytes. In practice, this means padding the entire buffer with three bytes in case the last character is a single byte."
In other words, both the byte-by-byte and dord-at-a-time code assume none of the bytes being read will be at an invalid address.
I did this recently in C++ and it worked fine: cast a uint8_t* to a uint32_t* and read data as 32 bit ints. I had to use reinterpret_cast in C++ though and of course I had to swap words to get the correct ordering.
The correct way to do an unaligned read is memcpy.
What the compiler cannot glean from casts are alias relationships, which are important for ensuring the order of operations are correctly maintained. As long as you're not mutating aliased objects, it's irrelevant. But if you are mutating aliased objects, you usually should derive the alias through a union with an in-scope definition. That way the compiler, by backtracking the derivation of the reference through the union, will recognize that the two objects of different types alias and ensure that loads and stores are properly ordered.
Another way to signal alias relationships relevant to UTF-8 decoding is through pointers to char or unsigned char. Those types can legally alias anything, and the compiler must take note. Sometimes it's useful to cast an address to, e.g., (char *) even when using memcpy(), even though it's unnecessary in C for the immediate expression because of automatic void pointer conversions. I can't think of a concrete example at the moment to explain the point, but it's something to keep in mind. Note that int8_t and uint8_t are not necessarily the same type as char or unsigned char, even when they have the same representation, and thus aren't required to express to the compiler the possibility of aliasing with other types (that is, mutating a uint8_t object doesn't necessarily tell the compiler that another object of type uint32_t might have been mutated, while mutating an unsigned char object would have in the same context).
https://blog.regehr.org/archives/959
(edited to better blog post by same author)
C++11 § 5.2.10, paragraph 7 says:
An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2>(static_cast<cv void>(v)) if both T1 and T2 are standard-layout types (3.9) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified
Deferencing a pointer of incorrect alignment will generate traps on many architectures.
Nonetheless, I have taken the advice in this thread and converted my code to use std::memcpy instead.
As far as I know, aliasing rules are nothing but new. You can cast _to_ char, but that's pretty the sole cast allowed in C [note: void is a kind of neutral, transitional type, and is not a cast per se]
; Parameters: RDI - pointer to input character
; Return value: RAX - Unicode codepoint, or -1 for error
lea r8, [encoding_table] ; Load pointer to table base using rip-relative addressing
mov edx, [rdi] ; unaligned (over)read of 32-bit utf8 codepoint
mov ebx, edx ; Copy to EBX to construct our table index
and ebx, 0xf8 ; Mask off the (potential) data bits
; Now we want to convert the top 5 bits into an index into a table of three * 32-bit entries
; Currently EBX = index * 8, we need index * 12, so we'll divide by two and then multiply by 3
shr ebx, 1
lea ebx, [ebx + ebx * 2]
bswap edx ; Get the codepoint into big-endian representation
pext ecx, edx, [ebx + 4] ; extract padding bits using mask
pext eax, edx, [ebx] ; extract data bits
cmp ecx, [ebx + 8] ; check padding bits
cmovnz eax, -1
retq
encoding_table:
; 00000xxx - 01111xxx
times 16 dd 0x7F, 0x80, 0x00
; 10000xxx - 10111xxx (invalid)
times 8 dd 0, 0xFFFFFFFF, 0 ; The padding will never match here, forcing an error return
; 11000xxx - 11011xxx (two bytes) - padding value 11010
times 4 dd 0x1F3F, 0xE0C0, 0x1A
; 11100xxx - 11101xxx (three bytes) - padding value 1110 1010
times 2 dd 0x0F3F3F, 0xF0C0C0, 0xEA
; 11110xxx (four bytes) - padding value 111 1010 1010
dd 0x073F3F3F, 0xF8C0C0C0, 0x7AAAAhttps://en.m.wikipedia.org/wiki/Bit_Manipulation_Instruction...
http://www.felixcloutier.com/x86/PEXT.html
The only unfortunate thing is that it was introduced quite recently in terms of x86 history (instead of being present early on but just microcoded), so earlier software can't take advantage of it nor any subsequent improvements, and uses a pretty complex encoding (VEX) only usable in protected mode. But if anything, this is another datapoint in the argument in favour of CISC --- try doing this on a MIPS, RISC-V or even ARM! With less powerful ISAs, even something as simple as the "shl ax, 4" (shift the lower 16 bits only) turns into a multi-instruction sequence of masking and combining.
RISC generally tries to keep instructions as generic/useful as possible to keep silicon small.
This suboperation does an N-bit version of the total operation: takes N bits of the mask, the corresponding N bits of the input, and produces an S-bit result, R. (S is the population count of that part of the mask.)
Suppose N=4. Then for a 64-bit value, you can do 16 of these in parallel, and you've got 16 intermediate results, R0...Rf, and 16 result sizes, S0...Sf. Then combine them. Overall result = R0 | R1<<S0 | R2<<S0+S1 | R3<<S0+S1+S2 | ... | Rf<<S0+S1+S2+...+Se.
I've no idea whether this is actually easy to implement this way, but at least it requires no loop ;)
----
12-bit example where N=4.
Mask is %000101011000, and value is %lkjihgfedcba. (I've labelled each bit of the value by its position, since that's the key part.)
Intermediate results: R0=%000d, S0=1. R1=%00ge, S1=2. R2=%000i, S2=1.
R0's contribution to the overall result = R0 = %d.
R1's = R1<<S0 = R1<<1 = %ge0.
R2's = R2<<S0+S1 = R<<3 = %i000.
So the overall result is %000d|%ge0|%i000 = %iged.
Performance seems to be quite good compared to the implementations from the article (577.333 MB/s vs the author's implementation's 410 MB/s on my hardware)
MSB .... byte1... byte2... LSB
00000xxx 00xxxxxx 00xxxxxx 00xxxxxx
How does multiplying (byte1 | LSB) * 3 help? I'm asking about lea ebx, [ebx + ebx*2], which is a shortcut for multiplying by 3.
Genuinely lost here, the 6 bits of byte1 and the 6 bits of LSB are separated by enough 0 bits that a multiplication by 3 won't mix them up. But within the 6 bits of byte1 and within the 6 bits of LSB, the multiplication will mean each bit is the sum of itself, the lower order bit, and any carried bits. What is that for?
Thanks for any tips!
Spoiler: https://pastebin.com/msjkXkKd
Edit: I fiddled around a bit and came up with:
The nullprogram.com version is 0x185 bytes (gcc 4.8.4 x86_64 -O3; objdump -s -j .text -j .rodata)
I switched to __builtin_clz (with cross platform support). I hesitate to put in your assembly code, though it would work great! My version still sticks to C and not inline assembly.
My version takes 0xeb bytes.
Benchmarking with clang-802.0.42 -O3 -march=native on a Core i7-4770HQ @ 2.20GHz:
branchless: 490.666725 MB/s, 0 errors
0xeb small: 352.000042 MB/s, 0 errors
Hoehrmann: 337.333374 MB/s, 0 errors
With gcc 4.8.4 -O3 -march=native on a Xeon E5-1650v3 @ 3.50GHz: branchless: 436.000052 MB/s, 0 errors
0xeb small: 345.333375 MB/s, 0 errors
Hoehrmann: 412.000049 MB/s, 0 errors
[1] https://stackoverflow.com/questions/19527897/how-undefined-a...clang-802.0.42 -O3 -march=native on a Core i7-4770HQ @ 2.20GHz:
branchless: 490.666725 MB/s, 0 errors
0xe9 small: 380.000045 MB/s, 0 errors
Hoehrmann: 337.333374 MB/s, 0 errors
With gcc 4.8.4 -O3 -march=native on a Xeon E5-1650v3 @ 3.50GHz: branchless: 436.000052 MB/s, 0 errors
0xe9 small: 405.333382 MB/s, 0 errors
Hoehrmann: 412.000049 MB/s, 0 errors
https://pastebin.com/j7T3bDwRMy original benchmark read the file as a stream — no guarantee that you'll get whole characters in each read — so I modified it a bit to read the whole file into memory, parse it, and print out the number of characters decoded and an XOR "hash". Here's what happened (looking at the "user" line of `time` output):
- Bjoern Hoehrmann’s decoder: 2.159s
- This post’s decoder: 2.754s (27.6% slower)
- Naive decoder: 1.453s (32.7% faster)
I'm curious whether this test makes sense and, if so, why there's such a big difference. Currently my best guess is the data cache pressure mentioned in twoodfin's comment.
I guess I should write a blog post, too.
Naive parser: https://gist.github.com/s4y/7c95f1ebeb2c069cfb09db3c3251eca3
Benchmarks: https://gist.github.com/s4y/344a355f8c1f99c6a4cb2347ec4323cc
It'd be interesting to see the results where you have randomly shuffled characters of all lengths, although it's likely to be something you see in reality.
Replacing read, test sequence end, buffer, drop buffer if it exceeds certain length without sequence end but record sequence start, seek until end sequence is found, and process that mess in a safe, passably-efficient manner with memory mapped files made my cross-platform tac a breeze to write: https://neosmart.net/blog/2017/a-high-performance-cross-plat...
Mmap doesn't work with pipes, which hugely reduces the utility of this kind of program.
Which is why you don't read to the end (until you have an EOF). You can just sidestep the partial sequence problem by getting more data before there's potential for a partial sequence to show up. The longest possible sequence is 4 bytes, so as long as you ensure you have 4 bytes available, partial sequences are impossible on well formed input.
This probably helps a bit for speed.
DFA-based decoder: ~400 MB/s
Naive decoder with ASCII: ~1.2 GB/s
Naive decoder with with most Western languages: ~1.0 GB/s
Naive decoder with with Russian, Chinese, Japanese: ~800 MB/s
Branch prediction of modern CPUs is so good that it's hard to beat the naive if-else chain with real-world data. Interestingly, I got the worst performance with heavily accented Eastern European languages like Czech where performance dropped to 600-700 MB/s.Modern x86 CPUs. ARM branch prediction is much worse, unfortunately.
The same is true for long vs. short functions and the instruction cache.
#define VALIDTAB (0x7f00ffff)
#define LENTAB (0x000000003a550000)
inline bool isvalidutf8start(char c)
{ return((VALIDTAB >> (c>>3)) & 1); }
inline unsigned utf8length(char c)
{ return(1+((LENTAB >> ((c>>3)*2)) & 2)); }
The 32-entry table of lengths can be coded up as a 64-bit value. The
"is valid" table is stored as a separate constant. inline unsigned utf8length(char c)
{ return(1+((LENTAB >> ((c>>3)*2)) & 3)); } #define LENTAB (0x3a55000000000000)The thing is, that decoder could be already branchless. The core of the algorithm is:
*codep = (*state != UTF8_ACCEPT) ?
(byte & 0x3fu) | (*codep << 6) :
(0xff >> type) & (byte);
Which can be compiled with a conditional move (CMOV) instead of a branch. In fact:> With GCC 6.3.0 on an i7-6700, my decoder is about 20% faster than the DFA decoder in the benchmark. With Clang 3.8.1 it’s just 1% faster.
There's a chance that when inlined into the benchmark Clang uses a CMOV while GCC uses a branch. As he correctly points out, his benchmark is the worst case for branch prediction:
> The even distribution of lengths greatly favors a branchless decoder. The random distribution inhibits branch prediction.
*c = (uint32_t)(s[0] & masks[len]) << 18;
*c |= (uint32_t)(s[1] & 0x3f) << 12;
*c |= (uint32_t)(s[2] & 0x3f) << 6;
*c |= (uint32_t)(s[3] & 0x3f) << 0;
*c >>= shiftc[len];
/* Accumulate the various error conditions. */
*e = (*c < mins[len]) << 6;
*e |= ((*c >> 11) == 0x1b) << 7; // surrogate half?
*e |= (s[1] & 0xc0) >> 2;
*e |= (s[2] & 0xc0) >> 4;
*e |= (s[3] ) >> 6;
*e ^= 0x2a; // top two bits of each tail byte correct?
*e >>= shifte[len];
Plus, all those loads are not making it easy for the processor to pipeline.And
*e = (*c < mins[len]) << 6;
is really a CMOV in disguise :)I don't see how using CMOV would be appropriate there --- I'd compile it as something more like this:
; eax = *c, ebx = mins[len]
cmp eax, ebx
sbb eax, eax
and eax, 64
; eax = (*c < mins[len]) << 6However, CMOV is single cycle latency in Kaby Lake. Also, I don’t know if those data dependencies wouldn’t also cause similar problems for resolving a conditional branch.
It may be possible to construct a scenario where CMOV is marginally slower but I think we can put Linus Torvalds’ CMOV rant in the bit bucket. Intel recommends CMOV with a proviso. Assembly/Compiler Coding Rule 2:
Use the SETCC and CMOV instructions to eliminate
unpredictable conditional branches where possible.
Do not do this for predictable branches.
The Intel C Compiler uses it. X = flag ? X : 0;
X = test ? X : 0;
Vs X |= flag;
X |= test;
The latter is more obviously branch less. How exactly the two perform probably requires some testing.And somewhat more general point, since we're writing C and not asm, depends how much you want to depend on the compiler to use the right instructions. To backtrack on my point a bit, whether CMOV gets used is an important consideration.
> There's a chance that when inlined into the benchmark Clang uses a CMOV
And a chance it won't. :)
https://github.com/nmap/nmap/blob/b7a5a6/ncat/scripts/httpd....
It's pretty straightforward. He uses a lookup table for the first byte ("lengths") and then later checks that subsequent bytes have the correct continuation prefix (0xc0 mask / 0x2a value).
I think it checks all of the same conditions as your Lua code.
Of course, any examples in Lua of different approaches to UNIX directory listing via HTTP are appreciated. Still a Lua noob.
At any rate the caller needs to check for errors coming out of this function (the int *e parameter) and zero termination, so there will be conditional jumps somewhere in the use of this thing. (And reading the code it seems to assume s[3] is safe to dereference...)
> unsigned char *next = s + len + !len;
> Originally this expression was the return value, computed at the very end of the function. However, after inspecting the compiler’s assembly output, I decided to move it up, and the result was a solid performance boost. That’s because it spreads out dependent instructions. With the address of the next code point known so early, the instructions that decode the next code point can get started early.
If there's no change in the actual logic, I feel like the compiler should be able to move up expressions in order to make them available earlier. I wonder whether this was tested/inspected with some optimizations turned off.
It gives a glimpse into the mind of the chess master (or optimiser) even when you yourself don't possess these skills.
Now? For a long time x86/amd64 performance is quite unpredictable because the CPUs are so complex.
It is not like chess. More like Poker because more uncertainty.
Optimizing doesn't need to reach the optimal, just improve.
> It is not like chess. More like Poker because more uncertainty.
I would say the uncertainty is similar to chess. It isn't feasible to know the best option in any given situation, but it is deterministic.
I also find that people have way too much faith in omnipotence of compilers. If you care about performance, you _have_ to go down to the assembly level and ensure that the compiler did the right thing, at least in the hotspots. Oftentimes it does not, and you have to change your higher level code to make it cooperate.
Well, that's only true because programmers define functions in terms of types that are not sufficiently specific wrt their runtime invariants; in a dependently-typed language you could have an optimizing compiler that makes use of type-level information to do better optimizations. For instance, in a dependently-typed language, you could have a type of integers divisible by 11, and it will have no runtime overhead with respect to a machine integer. Of course, this comes with a cost: if you want to use such specific types, you have to prove to the compiler that the data it is getting really does satisfy the property (and yes, you can do such proofs even with data that is only available at runtime, by simply implementing a runtime check).
Compilers should view operations more like a dependency graph, laying out operations with an interleaving that maximizes multi-issue, while keeping register pressure in check. In my mind, if the human can significantly change the output of the assembly by moving one line, the compiler optimizations may be turned off.
How to actually do that has been an active research topic for 30 years or so. So far, that research hasn't yielded anything that is simple, efficient, and effective enough to be included in general-purpose compilers. Last time I looked at LLVM's backend, it used to schedule for minimal register pressure because spills are more expensive in general, and because out-of-order execution will undo your careful scheduling anyway. It also has a second scheduler after register allocation, but at that point it's hard to move stuff, especially on x86.
On this particular code, manual scheduling paid off, partly because humans, unlike compilers, can simply try different variants and keep the best. Think of it as survivorship bias.
Edit: BTW, if you actually compile the code from Github and compare the generated assembly to a version where the computation of next is moved to the end, you'll see that moving it to the end causes GCC to allocate a stack frame (on x86-64), which it doesn't do in the manually optimized one.
*c >>= shiftc[len]; could probably be replaced by switch(len) and would result in equal code.
My guess is that the speed increase comes from the trailing 3 bytes potential overread and the somewhat odd error handling, not the lookups.
It's useful when there is some assurance that the UTF-8 is error-free. E.g. tdata bundled with the program or otherwise trusted.
*e = (*c < mins[len]) << 6;Why would you decode utf8?
> This week I took a crack at writing a branchless UTF-8 decoder: a function that decodes a single UTF-8 code point from a byte stream…
2. It's not entirely branch less. Consistent branching is equally important.