Would it be legal for a C(++?) compiler to implement this optimization?
An unexpected optimisation can introduce a side channel (most commonly timing). This one would be safe, but "how do you tell a compiler which ones [not] to use" is a whole topic by itself.
The radix 2^51 trick (2017)
41–50 of 86 posts
Re: The radix 2^51 trick (2017)
#42It looks more or less like this:
__m256i s = _mm256_add_epi64(a, b);
const __m256i all_ones = _mm256_set1_epi64x(~0);
int g = _mm256_cmpgt_epu64_mask(a, s);
int p = _mm256_cmpeq_epu64_mask(s, all_ones);
int carries = ((g
The throughput even seems to be better: https://godbolt.org/z/e7zETe8xYIt's trivial to change this to do 512 bit addition where the improvement will be even more significant.
Re: The radix 2^51 trick (2017)
#43Earlier quoted context omitted.
Setting both to 2^63 means your original 256-bit numbers were 2^255, thus the addition would overflow no matter what intermediate encoding you’re using.
Sure, then set one to 2^62 and the other to -2^62 (namely: 0b1100..00). It's overflow as far as unsigned arithmetic is concerned, but not in the case of signed arithmetic. That said, when you're dealing with 256-bit integers, you're almost assuredly not working with signed arithmetic.
Re: The radix 2^51 trick (2017)
#44Re: The radix 2^51 trick (2017)
#45Re: The radix 2^51 trick (2017)
#46> Aside: Why 13 bits instead of 12? For our purposes, we’re going to ignore the carries in the most significant limb, allowing numbers to wrap when they overflow past 2256 - 1 (just like how unsigned addition works in C with normal size integer types). As a result, we can assign 52 bits to the most significant limb and ignore the fact that it will run out of room for carries before the other limbs do. Why not give th…
I think one goal is to use 5 64 bit registers to do 256 bit math. That means using 256/5 = 51.2 bits of each word. That's probably some kind of ideal if you want 256bit math, but not optimal if you're writing a generic big-int library. In the old days you'd want to use exactly one byte for the carry(s) because we didn't have barrel shifters to do arbitrary bit shifts efficiently. In that case I'd use 56 bits of the 64 to get nice byte alignment.
This is all quite relevant for RISC-V since the ISA does not have flags.
Re: The radix 2^51 trick (2017)
#47Someone working entirely on x86_64 very nicely demonstrates that RISC-V is not wrong to omit the carry flag.
Also, there is another way to do this while keeping 64 bit limbs. All variables uint64_t. s0 += a0; s1 += a1; s2 += a2; s3 += a3; c0 = s0 The key insight here is that unless the sum at a particular limb position is all 1s the carry out from that position DOES NOT DEPEND on the carry in to that limb position, but only on whether the original add in that position produces a carry. If the sum is all 1s the the carry out…
if (s1 == -1)
c1 = c0;
if (s2 == -1)
c2 = c1;
These can become conditional moves on x86. I've often thought RISC-V should have implemented an IF instruction instead of compare and branch. IF would cause the next instruction to be executed conditionally while not needing a flag register at the ISA level. They could have required only branch and jump to be conditional, but it turns out conditional mov, load, and store are all very useful in real code.Re: The radix 2^51 trick (2017)
#48Someone working entirely on x86_64 very nicely demonstrates that RISC-V is not wrong to omit the carry flag.
There remain many frequently-encountered cases when carry-save addition is worse than addition using add-with-carry. Neither of the 2 multi-word addition algorithms can replace the other, both have their use cases, so ADC/SBB instructions are included in any decent ISA, because the cost of adding them is negligible. A dedicated flag register is not necessary, some ISAs store the carry/borrow flags in general-purpose…
That's absurd. A better way is to ensure that your algorithms don't overflow. Detecting an overflow just means your code has to STOP which is usually not safe. It'd be insane to have conditionally executed code trying to figure out how to handle an overflow anywhere in code. Another problem is that flags are not even accessible from any language higher level then ASM. From a C perspective there are no flags.
Re: The radix 2^51 trick (2017)
#49Re: The radix 2^51 trick (2017)
#50I'm seriously doubtful that adc is inherently slower than add on a modern CPU other then the data hazard introduced by the carry bit. I realize the point of the article is the data hazard so this is a really minor nit.
uops.info has latency for both (Alder Lake) at 1 cycle but throughput (lower is better) * for add is 0.20 (ie 5 per cycle) * for adc is 0.50 (ie 2 per cycle) so it does seem correct. This seems to be a consequence of `add` being available on ports 0, 1, 5, 6, & B, whereas `adc` is only available on ports 0 & 6 So yes as an individual instruction it’s no worse, but even non-dependent instructions will be worse for OoO…