Question Details

No question body available.

Tags

c++ assembly simd

Answers (8)

May 12, 2026 Score: 3 Rep: 380,787 Quality: Medium Completeness: 80%

On what CPU? 512-bit vectors are somewhat costly on some Intel CPUs; not a great job by Clang there, doing 8 bytes to 8 qwords because the source uses uint64t, but then doing the next 4 bytes scalar instead of with a YMM register. But that's version that runs faster on your CPU, so perhaps a Zen 5? GPR X/Y/ZMM has higher latency on AMD then Intel, I think even still on Zen, but both versions do that. Masked loads are efficient on AMD, and IIRC even masked stores aren't bad. (As long as it's AVX-512 masking, not AVX2 vpmaskmovd).

Benchmarking throughput or latency (i.e. is there a data dependency from the return value of one call to the args of the next call, like a pointer offset or something)? ** I don't see an attribute((noinline)) on either function, so I hope you put these in a separate file to stop them from optimizing away for compile-time constant args. Idiomatic way of performance evaluation?

As standalone functions, I'd have expected your pext function to be faster than the clunky code Clang made, at least on Intel and Zen 3 and newer (older Zen with extremely slow microcoded pext couldn't run this anyway since they don't support AVX-512).

Both versions could benefit from inlining so they can keep constants in registers across calls.

This would be the end of the answer to a question without a [mcve] of how you benchmarked them or on what CPU, except that there's some interesting stuff to say about how to implement that function much better.


I don't think it's worth using a masked vmovdqu8 load and shuffles instead of two plain loads directly into GPRs, of 8 and 4 bytes to get the total of 12 bytes. Modern CPUs have 3 or more load execution units which can handle scalar loads, but fewer shuffle units. (https://travisdowns.github.io/blog/2019/06/11/speed-limits.html#load-throughput-limit).

You don't need to subtract '0', that doesn't change the low 3 bits of '0' .. '7'. If you can assume correctness, you can just pext on the raw ASCII data. (After byte-reversing it with bswap or movbe.)

// 36-bit return value
// pure scalar GPR, doesn't need AVX-512.  Slow on Zen 2 and earlier AMD since it still uses PEXT.
uint64t parseOctalbmi2(const char message[12]) {
    const uint64t EXTRACTIONMASK = 0x0707070707070707;

uint64
t high = loadbei64(message + 0); uint64t low = (uint32t)loadbei32(message + 8); // stupid intrinsics API returns signed types, but only zero-extension is free high = pextu64(high, EXTRACTIONMASK); low = pextu64(low, EXTRACTIONMASK); // Use 64-bit operand-size to stop the compiler from foolishly materializing a narrower constant

return (high binary data is harder since you need to handle 0-9 vs. A-F. Octal doesn't have that, it's literally just bswap / pext if you aren't validating it. SIMD could be useful for validation to check that every byte is between '0' and '7', with subtract + unsigned-compare or compare+masked-compare with AVX-512, or with other tricks with only AVX2 where you only have signed-greater. Like maybe subtract '0', vpmaxub against set1_epi8(7), and vpcmpeqb to check that nothing wrapped to a high unsigned value, or was high to start with.)
May 12, 2026 Score: 3 Rep: 3,165 Quality: Low Completeness: 40%

You named your functions like parseOctalToBase10, however there is no conversion to decimal. You could consider it a conversion to binary as that's what the machine uses to represent numbers internally. Or like atoi just say it converts to a number / an integer.

May 12, 2026 Score: 2 Rep: 380,787 Quality: Medium Completeness: 80%

As we can see from the asm the OP showed, Clang 19 did auto-vectorize their version with a variable-count shift to put the bits at their final position in one go (vpsllvq zmm for 8x uint64t at once).

https://godbolt.org/z/x5xvKnPPc - It doesn't with your version, instead fully unrolling 12 scalar iterations. This is maybe even slower for 12 digits than Clang's clunky strategy, but could be good for fewer where vectorization isn't worth it.

I know this is only an "opinion based" / "discussion" question, but it's still a good idea to test your suggestion to see if it's actually better in practice. Your way is the simple way, and I'm a bit surprised Clang didn't see through the shifting, not even if we use unsigned char, or change the subtraction to &7 to make it a bitfield-pack with definitely no overlapping set bits in what we're adding. GCC16 goes scalar with yours or the OP's code, but in the OP's case needs a lot of shift instructions instead of being able to use LEA to shift-and-add.


Using narrower integers (32-bit or maybe only 16) in chunks could allow higher data density than 3 bits per 64. But that would require manually using uint32t. 12x3-bit = 36-bit, we can use uint32_t for all but the last 2 octal digits.

May 12, 2026 Score: 2 Rep: 4,009 Quality: Low Completeness: 50%

Your reference C implementation of this code is unnecessarily complicated. The normal way to do it is working from most significant digit first indexing message from 11 to 0 when the loop simplifies greatly to:

uint64t parseOctalToBase10(const char message[12]) { uint64t size = 0; int i=12; while (i--) { size = (size =0; i--) makes it much more likely that the compiler will see how to vectorise the code. Even the dumbest compiler can usually make decent code for this common canonical form (which is shared with Horner method for polynomial evaluation).
May 12, 2026 Score: 1 Rep: 40,851 Quality: Medium Completeness: 80%

I ran a benchmark to evaluate the performance of the two methods.

Please post the code of the benchmarks. Writing a benchmark for C/C++ can be a challenging task since the optimizer can over-optimize the code under test and remove a large part of the tested code.

So beware of this issue and provide your benchmarks. It may turn out that your implementation is faster and the benchmark was misleading because of over-optimization.
It would be best if you could use this site: https://quick-bench.com/ (sadly I was unable to force this site to compile your SIMD implementation).

update: I was able to compile your code on godbolt and run benchmark there. With my benchmarks version with explicit SIMD wins on gcc and clang. So it is possible my suspicions are correct: your benchmarks are over-optimized by a compiler.

Nitpicking: This function name parseOctalToBase10 is funny. It should be just parseOctal, since you are returning the actual value as represented in base 2 by hardware - there is no base 10 in this code. The conversion to base 10 happens when you convert this value back to a string.

May 12, 2026 Score: 0 Rep: 40,851 Quality: Low Completeness: 20%

Your version is faster then OP version.

May 12, 2026 Score: 0 Rep: 1 Quality: Low Completeness: 0%

Agreed, good catch. When naming I was focused on the output that I saw, rather than how the data was actually represented. :-)

May 12, 2026 Score: 0 Rep: 380,787 Quality: Low Completeness: 50%

That's with GCC, which doesn't vectorize either version.

(And Quick-Bench doesn't have an option to enable AVX2 or AVX-512, so we can't properly test its Clang 17.)

The OP is testing on Clang 19 with AVX-512 available, which like AVX2 has per-element-variable-count SIMD shifts. This is the key operation Clang uses to parallelize the i-dependent shifts in the original, feeding an OR reduction. (It's still far from optimal, of course, even without using pext.)