FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Base64 length for Haswell and Icelake. by erikcorry · Pull Request #898 · simdutf/simdutf · GitHub

Base64 length for Haswell and Icelake. - #898

Open
erikcorry wants to merge 18 commits into
simdutf:yagiz/add-binary-length-base64from
erikcorry:erikcorry/base64-length-haswell
Open

Base64 length for Haswell and Icelake.#898
erikcorry wants to merge 18 commits into
simdutf:yagiz/add-binary-length-base64from
erikcorry:erikcorry/base64-length-haswell

Conversation

Copy link
Copy Markdown
Collaborator

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

  • Bug fix
  • [ x] Optimization
  • New feature
  • Refactor / cleanup
  • Documentation / tests
  • Other (please describe):

lemire commented Jan 5, 2026

Copy link
Copy Markdown
Member

@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.

Copy link
Copy Markdown
Collaborator Author

Looks like I broke it on Windows with the custom version of popcount.

Copy link
Copy Markdown
Collaborator Author

@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.

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 :-)

Comment thread src/haswell/avx2_base64.cpp Outdated
// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I don't think we need these asm coded functions.

Copy link
Copy Markdown
Collaborator

what is the exact sanitizer error you get?

lemire commented Jan 6, 2026

Copy link
Copy Markdown
Member

@erikcorry

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.

simdutf_really_inline const char *find(const char *start, const char *end,

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).

Copy link
Copy Markdown
Collaborator Author

Well, you do end up doing this scalar fixing at the end in any case.

Confused by this comment. There's no scalar fixing in this change, it's all done with SIMD.

Copy link
Copy Markdown
Collaborator Author

what is the exact sanitizer error you get?

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.

Copy link
Copy Markdown
Collaborator Author

I have a very long held view that optimizing for alignment is not usually important

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.

lemire commented Jan 6, 2026

Copy link
Copy Markdown
Member

@erikcorry

You have a scalar patch up at // Fix the end: subtract any over-counted bytes past 'end'. It is relatively cheap, but still needed.

Copy link
Copy Markdown
Collaborator Author

OK wasn't counting that as 'scalar' because it subtracts all the bytes in one parallel operation rather than using a loop.

Copy link
Copy Markdown
Collaborator Author

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.

https://godbolt.org/z/qKv4zx8bf

erikcorry closed this Jan 6, 2026
lemire and others added 15 commits January 6, 2026 13:20
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>
erikcorry reopened this Jan 6, 2026
erikcorry force-pushed the erikcorry/base64-length-haswell branch from 1c870b5 to e1a01e6 Compare January 6, 2026 20:52

Copy link
Copy Markdown
Collaborator Author

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.
simdutf::icelake_binary_length_from_base64 : 124.41 GB/s 18.03 % 4.41 GHz 0.04 c/b 0.17 i/b 4.92 i/c

With the latest e1a01e6 I only get:
simdutf::icelake_binary_length_from_base64 : 113.87 GB/s 15.96 % 4.17 GHz 0.04 c/b 0.13 i/b 3.52 i/c

With the completely unaligned Icelake version 21bc1f1 I get miserable:
simdutf::icelake_binary_length_from_base64 : 80.85 GB/s 20.12 % 4.11 GHz 0.05 c/b 0.13 i/b 2.53 i/c

I'm not sure why this is.

erikcorry changed the title Base64 length for Haswell with OOB, but safe reads. Base64 length for Haswell and Icelake. Jan 6, 2026

Copy link
Copy Markdown
Collaborator Author

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.

https://godbolt.org/z/qr88rf3x8

Copy link
Copy Markdown
Collaborator

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.

https://godbolt.org/z/qr88rf3x8

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.

Copy link
Copy Markdown
Collaborator Author

The slowest version is the one with no aligned pointers so I don't think that's the issue.

Comment thread src/icelake/icelake_base64.inl.cpp Outdated
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
count += __builtin_popcountll(mask);
count += count_ones(mask);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

(We define our own bit manip functions for portability issues. Got to love Microsoft.)

lemire commented Jan 7, 2026

Copy link
Copy Markdown
Member

@erikcorry

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.

lemire commented Jan 7, 2026

Copy link
Copy Markdown
Member

@anonrig
@erikcorry Please check #904

Copy link
Copy Markdown
Collaborator Author

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.

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

lemire commented Jan 8, 2026

Copy link
Copy Markdown
Member

I can review later.

lemire commented Jan 8, 2026

Copy link
Copy Markdown
Member

We’ll get this done

Copy link
Copy Markdown
Collaborator Author

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.

lemire commented Jan 10, 2026

Copy link
Copy Markdown
Member

@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.

anonrig force-pushed the yagiz/add-binary-length-base64 branch from 6211533 to 32c0869 Compare January 30, 2026 20:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants


Back | FazBrowse Home | New Git URL