| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
@erikcorry I don't mind this but the reason I don't do it is that then we get bug reports due to sanitizers. So to make it usable, I think we need to have code to quiet the sanitizers... but I am totally open to discussion and I would not block your PR. |
Sorry, something went wrong.
|
Looks like I broke it on Windows with the custom version of popcount. |
Sorry, something went wrong.
We could tell the sanitizer to ignore this function, but perhaps it's a better fix to fall back to the scalar version when the sanitizer is enabled. This way the sanitizer can still find bugs in the rest of the program, for example passing an invalid length parameter to our function. Of course if we do this we have to be 100% sure we don't have an actual bug in our implementation :-) |
Sorry, something went wrong.
| // The built-in version of _mm256_movemask_epi8 returns an int, but the | ||
| // instruction actually returns a zero-extended 64 bit value. The compiler | ||
| // will do silly sign-extend and top-half-zeroing instructions because of this. | ||
| // This version doesn't lie to the compiler about the result size. |
There was a problem hiding this comment.
I don't think we need these asm coded functions.
Sorry, something went wrong.
|
what is the exact sanitizer error you get? |
Sorry, something went wrong.
|
What we do elsewhere in the simdutf code is to not worry about alignment, read the pointer, and handle the tail with scalar code (except with ISAs such as AVX-512 where that part can be done using scalar code. In some instances, we do use the alignment trick, but not to read out of bounds... that is, we still have a scalar tail. Line 6 in d56ea3e Well, you do end up doing this scalar fixing at the end in any case. I have a very long held view that optimizing for alignment is not usually important:
For the 'find' function above (see first link), optimizing the alignment is likely beneficial... It might also be beneficial in the case of this function, given how simple it is... but I submit to you that we want to avoid the out-of-bound reads unless we can demonstrate tangible benefits. Consider that even if we exclude the function from LLVM's sanitizers, for example, this will not free us from other sanitizers like valgrind and friends. There are others as well. I don't think we can ever be sure that we can detect the sanitizer. So there is some kind of cost here, and we need to make sure that it is really worth it to pay. My instinct here is that the OOB behavior is unwarranted. (But I'd love to be proven wrong.) As an aside, this implementation that you are doing can probably be done with our abstraction layer (see the first link in this comment). |
Sorry, something went wrong.
Confused by this comment. There's no scalar fixing in this change, it's all done with SIMD. |
Sorry, something went wrong.
For a test with a 5-byte buffer starting at 0xf00baa500 we read the 32 bytes from 0xf00baa500-0xf00baa1f. So we are going over the end of the buffer. But we don't count those bytes, and this can't cause a segfault because we are reading an aligned area that includes the buffer. Nobody has a 16 byte page size. It's a legit sanitizer error, it's just harmless in the absence of sanitizers. |
Sorry, something went wrong.
To be clear I don't think the alignment makes things faster for the bulk of a large operation, it just enables us to do the whole operation without falling back on a scalar loop at the end. |
Sorry, something went wrong.
|
You have a scalar patch up at // Fix the end: subtract any over-counted bytes past 'end'. It is relatively cheap, but still needed. |
Sorry, something went wrong.
|
OK wasn't counting that as 'scalar' because it subtracts all the bytes in one parallel operation rather than using a loop. |
Sorry, something went wrong.
|
Closing this because I don't have time to benchmark on small inputs, where it might be expected to help, and the disadvantages around sanitizers are clear. For reference, here's the generated code. |
Sorry, something went wrong.
Co-authored-by: Erik Corry <erik@arbat.com>
This avoids zero-extend in the inner loop. Since we are accumulating the result in a 64 bit register we want to keep it all 64 bit clean.
Port the AVX2 binary_length_from_base64 function to use AVX-512 instructions for the icelake implementation. Key differences from AVX2: - Process 64 bytes per iteration instead of 32 - Use _mm512_cmpgt_epi8_mask which returns __mmask64 directly - Use _mm_popcnt_u64 for popcount - Guard against overshoot=0 case to avoid UB from shifting by 64 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
I reopened and I'm trying to simplify. For Haswell the simple version with the scalar cleanup is just as good. Running on a 128k input file. This is the Icelake speed on 0d55bdd no asm. With the latest e1a01e6 I only get: With the completely unaligned Icelake version 21bc1f1 I get miserable: I'm not sure why this is. |
Sorry, something went wrong.
|
The latest two versions are miscompiled for some reason - perhaps there's a bug in my code. Looking at this it's not surprising that it loses some speed. Can't work out what is going on. |
Sorry, something went wrong.
you have UB in the expressions aligned_ptr += 64; and aligned_ptr < end since it is illegal to step a pointer after the hypothetical element one past the end. |
Sorry, something went wrong.
|
The slowest version is the one with no aligned pointers so I don't think that's the issue. |
Sorry, something went wrong.
| while (ptr + 64 <= end) { | ||
| __m512i data = _mm512_load_si512(reinterpret_cast<const __m512i *>(ptr)); | ||
| uint64_t mask = _mm512_cmpgt_epi8_mask(data, spaces); | ||
| count += __builtin_popcountll(mask); |
There was a problem hiding this comment.
| count += __builtin_popcountll(mask); | |
| count += count_ones(mask); |
Sorry, something went wrong.
There was a problem hiding this comment.
(We define our own bit manip functions for portability issues. Got to love Microsoft.)
Sorry, something went wrong.
|
I don't know the performance of your machine. But if you try something like this... ./build/benchmarks/base64/benchmark_base64 -d somebase64file -f simdutf::icelake You might get... I don't know... Let us say 20 GBs/s. Ok. Suppose you compute the length at 80 GB/s... That's two passes for a net speed of 1/(1/80 +1/20) or 16 GB/s. That's reasonable, right? No matter how we square this, doing two passes over the data is going to be slower. This cannot be helped. But if your penalty is, say, 25%... Then that's a reasonable trade-off. |
Sorry, something went wrong.
The actual numbers for my Icelake (actually Sapphire Rapids) are 18GB/s and 130-140GB/s, so it's not too bad. Something like a 13% penalty on a 128k input for having two passes, where the first one is very simple. The latest version without UB (I think) is the fastest, but I think it's just a coincidence. Whenever I change something the compiler changes its strategy. I can't work out what the compiler is doing, but it's far from straightforward, and not faster than the always aligned sanitizer-hostile version was. Without the scalar prologue section to get to an aligned point, the AVX512 version doesn't go nearly as fast (80GB/s only). Icelake GB/s | gcc-13.3 -O3 | clang-20 -O3 Version | | 21bc1f1d No alignment | 80.95 | 81.77 4d957af4 Aligned | 108.90 | 109.49 6e83fc84 Aligned no UB | 119.21 | 141.17 0d55bddf Aligned OOB reads | 128.71 | 133.69 82604ca9 With inline ASM | 123.84 | 130.65 |
Sorry, something went wrong.
|
I can review later. |
Sorry, something went wrong.
|
We’ll get this done |
Sorry, something went wrong.
|
I had a quick stab at using the generic instead of the custom version for icelake and haswell, but I got a bit lost in the include changes needed, so I abandoned that, but I'd still be interested to see what the performance penalty is for using the generic implementation. |
Sorry, something went wrong.
|
@erikcorry I'm travelling but I will work later on this, to help finish it up. The purpose is not replace the intrinsic code, but rather to scale our implementation over the many kernels. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
binary_length_from_base64 for Haswell.
Description
This is a stab at an implementation for Haswell.
It's a bit unusual in that it doesn't have a scalar clean-up loop at the end. Instead, it reads too far and then subtracts the extra bytes again. Because all reads are aligned it can't fault.
(Sadly there is then a scalar search for '=' padding afterwards.)
Unfortunately , this sort of implementation is probably not acceptable, since it confuses asan. What I like about it is that it's almost branch free and should be pretty fast even for small inputs. It doesn't work with the sanitizer build though :-(
This was done with help from Claude. Transcript is at https://gist.github.com/erikcorry/e786ca8689fcacb30661deabb7258617
Type of change