Live data from Hacker News

The radix 2^51 trick (2017)

chosenplaintext.ca

41–50 of 86 posts

Re: The radix 2^51 trick (2017)

#41
post #27
post #5

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 C++ standard doesn't forbid introducing side channels, so the answer to the question is yes.

Re: The radix 2^51 trick (2017)

#42
With AVX512 (and to a lesser extent with AVX2) one can implement 256 bit addition pretty efficiently with the additional benefit of fitting more numbers in registers.

It 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/e7zETe8xY

It'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)

#43
post #40
post #35

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

...so? They don't care about top limb overflow, at all. That's the point.

Re: The radix 2^51 trick (2017)

#44
post #22

Someone working entirely on x86_64 very nicely demonstrates that RISC-V is not wrong to omit the carry flag.

This is all downstream of C omitting the carry flag, which means in practice it's very rarely used for the purpose of a carry.

C does, however, now have _BitInt

Re: The radix 2^51 trick (2017)

#45
It's funny that carries don't just make addition difficult to parallelize. Binary addition without carry is XOR. XOR subset sum - find a subset whose XOR gives the desired target - is in P, but proper subset sum with carry is NP-complete.

Re: The radix 2^51 trick (2017)

#46
post #2

> 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…

>> Why not give the top limb 64 bits and the other four limbs 48 bits each, then?

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)

#47

Someone 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…

I think you want to write:

  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)

#48

Someone 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…

>> because the software workaround for detecting integer overflow, which is mandatory for any program that claims to be written in a safe way, lowers the attainable performance much more than the workarounds for not having carry

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)

#50
post #4
post #3

I'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…

Intel is also supposed to introduce the new APX instructions which include a bunch of instructions that duplicate existing ones but don't set any flags. The only plausible reason to add these is for performance reasons.
Post reply on HN