The radix 2^51 trick (2017)
chosenplaintext.ca
chosenplaintext.ca
One interesting fact about carry save adders is that the carry part of the register can be extended in order to avoid any carrying between words for very long periods of time. Instead of using 53 bits to represent 52 bits, use 60 bits, and now you can perform 256 sequential additions with no carrying between words before the carry part saturates and needs to be handled.
Somewhat surprisingly, it's even possible to use this construction in the context of a quantum computation [2][3]. The reason it's surprising is because tacking on additional registers like this can act as an information leakage mechanism (which is an error in a quantum computation). For analogy, you can imagine that if you computed a public key using this trick that you would not be comfortable handing an attacker the registers storing your public key before you removed the carry padding that made the computation faster. But it turns out that if you just initialize the carry padding randomly, subtracting out of the next word at the start to ensure everything sums to the correct result at the end, then you can show that the information leakage and chance-of-bad-overflow is exponentially suppressed in the length of the carry padding. Currently the most efficient known way to implement Shor's algorithm uses this technique [4].
1: https://en.wikipedia.org/wiki/Carry-save_adder
2: https://arxiv.org/abs/1905.08488
If you're going to wrap, you could go all the way and assign 64 bits to the most significant limb; that way you save 12 bits which you can spread to the other four limbs. You can go from {52, 51, 51, 51, 51} to {64, 48, 48, 48, 48}, so you have a spare 16 bits instead of 13 bits.
using 64 bit registers, but only using 51 bits, has no overflow. Using 64 bit adds in 64 bit registers overflows, making the carry updates more costly.
So 52/51/51/51/51 seems like the more generally useful choice
This approach is efficient when you know you have to add a bunch of numbers together. You add them up, and then normalize.
The example can be, e.g. doing long multiplication, or adding all numbers in a list.
(Just repeating what was said in the parent comment for emphasis)
https://en.wikipedia.org/wiki/Redundant_binary_representatio...
https://en.wikipedia.org/wiki/Carry-save_adder
http://lux.dmcs.pl/csII/ca2_RedundantNS.pdf
I have an arbitrary precision arithmetic library in my mind that represents numbers as streams of digits starting from the most significant digit. A redundant numeric representation could avoid unbounded backtracking (think flipping between 0.9999... and 1.0000... in a regular representation) and could guarantee forward progress for the generated digits.
14*14
140 + 14*4
140 + 4(16) <-- 4 tens + 16 ones
18(16)
196 14*14
7*7*2*2 [b/c 14 = 2*7]
49*2*2
98*2
196
For whatever reason, my brain likes using twos (and powers of it), so I play to that strong spot wherever possible.EDIT: as noted below, I made a mistake (7·7=49,not 47,) and carried it halfway through my solution. Oops.
14 * 14
(7 * 2) * (7 * 2)
7 * 7 * 2 * 2
49 * 2 * 2
98 * 2
196
And on my side, 49 * 2 quickly resolves to "a hundred minus 2" (because 49 * 2 = (50 - 1) * 2), and pretty much the same for 196 (200 - 4).Thanks for noticing!
17 * 17
170 + 17*7
170 + 07(49)
1(14)(49)
24(49)
289The “normalized” representation, called Zeckendorf representation, perfectly packs error correction into the binary, as it ensures no number will ever have consecutive ones.
It also has applications in data compression.
https://github.com/constructor-igor/cudafy/blob/11cdd4def4e7...
s = a ^ b; // sum bits
r = a & 0x7f7f7f7f; // clear msbs
t = b & 0x7f7f7f7f; // clear msbs
s = s & 0x80808080; // msb sum bits
r = r + t; // add without msbs, record carry-out in msbs
r = r ^ s; // sum of msb sum and carry-in bits, w/o carry-out mov W, E
mov V, D
mov U, C
mov T, B
shr W, 51
shr V, 51
shr U, 51
shr T, 51
add D, W
add C, V
add B, U
add A, T
which does seem like it could be parallelized.No they're not. Let's say B's non-carry bits are all 1. If you carry anything from C, that will affect B's carry bits.
It would be an incredibly rare case, so branch prediction will always get it right, and the cost of a branch nearly nill.
Not sure you'd gain any speed boost though.
Here's a toy example adding three-bit numbers, where all letters are bits (0 or 1) and you can normally only add one bit at a time.
a b c
+ d e f
-------
g h i j
Instead of doing j = c ^ f
j_carry = c & f
i = j_carry ^ (b ^ e)
in two steps, we can condense it to j = c ^ f
i = (c & f) ^ (b ^ e)
And similarly, we can get g and h combinationally from the input: # "j_carry AND one of b, e"
i_carry = (b & e) | ( (c & f) & (b | e) )
h = i_carry ^ (a ^ d)
h = ( (b & e) | ((c & f) & (b | e)) ) ^ (a ^ d)
g = (a & d) | (i_carry & (a | d))
g = (a & d) | ((b & e) | ( (c & f) & (b | e)) & (a | d))
So we end up directly computing g = (a & d) | ((b & e) | ( (c & f) & (b | e)) & (a | d))
h = ( (b & e) | ((c & f) & (b | e)) ) ^ (a ^ d)
i = (c & f) ^ (b ^ e)
j = c ^ f
All of these innermost combinations like (a & d) you can get just by AND / OR of the original inputs.But as you suggest (I think), then you need to combine these further to actually get results. In theory they're all parallelizable (if you picture this as tree), but I don't see a good way to do that quickly. Though I'm no assembly expert so maybe I'm missing something.
I guess thinking in terms of gate delays doesn't really help much at this abstraction level.
For instance here are the first few natural numbers in this system: 1,2,3,4,5,6,7,8,9,A,11,12...
I first read about them in Chris Okasaki's purely functional data structures book, they have some applications in terms of designing data structures (e.g. combining perfect-powers-of-two sized heaps to build heaps of arbitrary size).
They're conceptually interesting too, being a positional number system with no redundancy (exactly one way to represent each number) and without 0. Since the concept of a 0 digit is not required, one could argue that it's conceptually easier for e.g. a classical Roman mathematician to learn this than the positional system we do use today.
Also, excel's system for numbering columns uses this - A, B, C, ... X, Y, Z, AA, AB ...
You'd still need some cleverness if your goal is to add just one giant bignum (as opposed to doing lots of independent add-with-carry sums).
Brilliant and tremendously interesting!
Useful for if/when I write a fast super-long integer math library in the future (yes, I know GNU already has one, but it's always interesting to know/understand tricks like this for >64-bit, AKA "too-long-to-fit-in-a-single-register" integers...)
Because it does look a little bit as if it was engineered for that particular situation, and if that was the case, with this situation now in practice being solved much more efficiently without a single 'adc' instruction, the opcode might suddenly be completely useless bloat - which of course still had to be maintained forever, even though nobody uses it in practice, because it's part of the spec.
Furthermore, the article is talking about adding many numbers, which is just one of the possible use cases. As far as I can tell, adc is still useful for adding a pair of 128-bit numbers on a 64-bit CPU for example.
The carry flag gets set by all sorts of instructions other than addition and subtraction (comparisons, bit shifts, multiplication, etc) so this is more useful than you might think.
A one-instruction variant I've seen that gets you the negated value of the carry flag is sbb rax, rax. This doesn't depend on the previous value of rax in a mathematical sense, though I'm unsure if it depends in an out-of-order sense; that is, if it's recognized as a zeroing idiom (or rather, zeroing or all-one-ing in this case). Probably not.
EDIT: did a quick test, and sbb rax, rax etc are not recognized to be a zeroing idiom, at least on my old Haswell. So it's still one instruction, but has a dependency on the previous register value.
You should be able to break the dependency and avoid the partial register stall by doing: movzx eax, ax
See: https://stackoverflow.com/questions/41573502/why-doesnt-gcc-... ; https://software.intel.com/en-us/forums/intel-isa-extensions... ; https://www.agner.org/optimize/microarchitecture.pdf (section 6.8)
https://link.springer.com/article/10.1007/s11432-015-5411-x https://arxiv.org/pdf/1411.5949.pdf
You aren't gaining performance by reducing carries, you are gaining performance by running these additions in parallel.
The ALU is doing the addition in binary. Even though you add padding to the beginning of the number chunks it's still a binary number. When you recombine the chunks of numbers you will be doing all those carries you feel like you got for free.
Next time, I'll keep from reading tech articles while running on too little sleep.
Thanks for your understanding.
What I am saying is the total number of carry operations is conserved, even though they are delayed.
There is no performance gain if you only add two numbers, even though that's the setup the article discusses for 90% of its length. It seems that this is leading to some confusion.