3 ms·
You're not thinking in terms of SIMD. If you load 5 bytes into a 128-bit register (16-bytes) you'll have 5-bytes of data + 11-bytes of garbage. Perform the ca
by dragontamer 4y ago
You're not thinking in terms of SIMD.
If you load 5 bytes into a 128-bit register (16-bytes) you'll have 5-bytes of data + 11-bytes of garbage.
Perform the calculation over all 16-bytes. Yes, this makes 11-bytes of garbage at the end. Once you're done, just write back the first 5 bytes and you're set.
The 11-bytes of garbage are "free". They didn't cost you anything.
> Getting the data from RAM is the main bottleneck for short strings
Your L1 and L2 cache-lines are 64-byte minimum read / write anyway (actually, some systems were 128-byte minimum IIRC). A 16-byte read is literally smaller than what your cache does on the path into your registers.
EDIT: More importantly, modern CPUs only have 2x or 3x load/store units. Meaning a modern CPU can only perform ~2ish load/stores per clock tick. The SSE (16-byte / 128-bit) read / write to L1 cache will perform the same speed as a 8-byte /64-bit read/write to L1 cache in practice.
- fanf2 4y agoIn fact the code that I left out of the blog post does almost exactly what you suggest :-) It’s the “load 5 bytes” and “store 5 bytes” that I don’t have a good solution for. At the moment I am using memmove() and relying on the compiler developers to have better ideas about optimizing it than I do… The bottleneck comes from the number of instructions and the branchiness, not the data bandwidth. I briefly considered playing games with overlong reads, but then asan gave me a slap and I reconsidered the path to wisdom.
- dragontamer 4y ago> It’s the “load 5 bytes” and “store 5 bytes” that I don’t have a good solution for. You build that out of "load 16 bytes" and "store 16 bytes" operations. register = [news.YCOMBINATOR] isValid = [00000FFFFFFFFFFF]; // "F" is really 0xFF, but you get the gist // isValid was generated because you were focusing on the YCOMBINATOR part // of the DNS entry: news . YCOMBINATOR . com. YCOMBINATOR there to focus // upon the current data being considered, while "news." and "com" should // remain untouched register = (register | ((register >= 'A' & register <='Z') << 5)); register = register & isValid; toWrite = writeLocation & (~isValid); toWrite = toWrite | register; writeLocation = toWrite; If you're doing things in-place, then writeLocation could be a copy of "register" from the 1st step.
- dzaima 4y agoDoesn't work if you don't have read access past (or before) the segment you want. And, as GP says, asan won't like reading/writing out-of-bounds (even if temporarily) either. Probably very unlikely to happen, but that's not really something you want to be betting on.
- dragontamer 4y agoI get it now. Thanks for explaining. That particular problem is solved by rounding up to the SIMD-width for all SIMD-allocations. Ex: 13-size gets rounded up to 16-sized alloc. 25-size gets rounded up to a 32-sized alloc. This, in combination of "processing of garbage" and "Valid" flags, should cover all the cases. EDIT: At least, for the "dynamic" strings that are loaded into memory. Someone elsewhere has brought up compiler strings, which seem complicated to me (I'm not sure if we have any alignment guarantees, and therefore are forced to process strings sequentially, one byte at a time if they're from the compiler).
- janwas 4y agoI agree padding is a good solution when you can arrange it. In Highway we also use the blending/bit-select you mention: https://github.com/google/highway/blob/master/hwy/ops/generic_ops-inl.h#L90 https://github.com/google/highway/blob/master/hwy/ops/generi... If HWY_MEM_OPS_MIGHT_FAULT, which is true if using ASAN or AVX2 (because AMD reserves the right to fault even if mask=0), we actually use a scalar loop. I'm curious if anyone has data indicating that a jump table is faster, enough to make a difference?
- dragontamer 4y ago> I'm curious if anyone has data indicating that a jump table is faster, enough to make a difference? Anything that touches the branch predictors on these modern, crazy CPUs, is going to be too complicated to really understand from a performance perspective! I'm pretty sure that jump tables / if statements all touch the branch predictor. Things will be high performance if the branch is consistently predicted correctly, but good luck figuring that out without just running the darn program on a CPU.
- dragontamer 4y ago> branchiness Memorize the following concept / pattern. int select(int choice, int A, int B){ return (A & (~choice)) | (B & (choice)); } You can convert any "if(bool)" statement into the above 4 assembly instructions: two ands, an or, and a not. This performs a 32-bit way parallel "choice" operation. For example, "choice = 0x00000000" will choose all bits from A. "choice = 0xFFFFFFFF" will choose all bits from B. SIMD because "choice = 0xFFFF0000" will choose the first 16-bits from B, and the last-16 bits from A. ------ Any data-operation can be "simulated" with enough code operations, and vice versa. Obviously, use code when code is needed, and data when data is needed. But sometimes, you need to perform a "code/data" switcheroo and the above pattern helps. EDIT: It doesn't always work. The combinational explosion of data may prevent such a technique from working in the general case. But still, its a good first step and "most if statements" are simple enough to work with the above pattern.
- fanf2 4y agoFor loading the rag end of a string, the branchiness looks like while (len-- > 0) { /* load */ } or switch (len) { /* 8 cases */ } https://en.wikipedia.org/wiki/Duff%27s_device https://en.wikipedia.org/wiki/Duff%27s_device
- nwallin 4y agoSSE4.1 added `blend` which does this for most data types. The lone exception is 32 bit integers for whatever reason, which wasn't added until AVX2.
- Const-me 4y ago> It’s the “load 5 bytes” and “store 5 bytes” that I don’t have a good solution for Strings in C are null terminated. You can avoiding loading exactly 5 bytes. For a string of length 4 load 4 bytes, for a string of length 5 load 6 bytes, you’ll read 5 characters of the payload + the terminating '\0'. If you code C++, the standard library was fortunately designed with C interop in mind, strings form the standard library have c_str() method. About how to do that efficiently, a good way is a jump table. Not all compilers are smart enough to compile switch into a table, but many of them are. Modern GCC usually does the right thing, take a look: https://godbolt.org/z/sK7jcoooa https://godbolt.org/z/sK7jcoooa (untested) Partial stores are pretty similar. Compare the vector of bytes for b == `\0`, use _mm_movemask_epi8(), then _tzcnt_u32() or _BitScanForward() to find the length, then in the switch use various extract instructions: _mm_cvtsi128_si32, _mm_cvtsi128_si64, _mm_extract_epi16, and/or _mm_extract_epi32.