For a while, I have tried to find ways (known or new) to do stuff with Gray codes. As some of you probably know, I already tried to implement an algorithm to add two Gray codes without having to convert them to a regular binary representation, perform a regular addition and convert the result back to a Gray code.
The first time, I tried to implement a bitwise algorithm described by Harold Lucal and then to optimize it. Since even the most optimized code I could come with was still slower than the naive double-conversion solution, I decided to start over from scratch, namely, to observe patterns in Gray codes and find a new way to perform an addition. Before going any further, let me describe the notation used in the rest of the question and how some mathematical symbols are (ab)used to represent common bitwise operations:
- \$\oplus\$ is used to represent a bitwise XOR operation.
- \$\ll\$ is used to represent a left shift.
- \$\gg\$ is used to represent a right shift.
- \$parity(n)\$ is used to compute the parity of a Gray code \$n\$, which happends to correspond to the parity of the number of set bits in its representation (you can find an implementation in the older question).
Here are the most interesting things I observed and used to devise the new addition algorithm:
A Gray power of \$2\$ corresponds to two bits set followed by clear bits, except for \$1\$. That one is easy to observe: with the usual integer representation, a power of \$2\$ corresponds to a single bit set and the formula to convert an integer \$n\$ to its corresponding Gray code is \$(n \gg 1) \oplus n\$.
For any Gray code \$n\$, \$2n = (n \ll 1) \oplus parity(n)\$.
For two Gray codes \$a\$ and \$b\$, if \$a\$ is a power of \$2\$ and \$a > b\$, then \$a \oplus b = a + b\$.
For two Gray codes \$a = 2^n\$ and \$b\$, if \$a \leq b < 2^{n+1}\$, then \$a \oplus b = b - a\$ (derived from the preceding observation).
Additionally, I will frequently use what I call the « base » or « base2 » of a Gray code (I don't have a better name - if you have one, please let me know), which is, for a Gray code \$a\$, the Gray code \$2^n\$ such as \$2^n \leq a < 2^{n+1}\$ (e.g. If \$a = 7\$, then \$base(a) = 4\$). I use it with an exception for \$0\$ where \$base(0) = 0\$ instead of \$1\$.
So here is the pseudocode for the new addition algorithm. I use the gray_code implementation from the older question - you can find an up-to-date version on GitHub:
function add(a: gray, b: gray) -> gray if base(a) = base(b) then: if a = b then: return 2 * a else: return 2 * base(a) + (a - base(a)) + (b - base(b)) else: if a < b then: swap(a, b) tmp := (a - base(a)) + b if base(a) > base(tmp) then: return base(a) + tmp else: # Here, base(a) = base(tmp) return 2 * base(a) + (tmp - base(a)) As you can see (can you?), the principle is to add things when it is simple (when \$a = b\$ or when \$a = 2^n\$) and to break numbers into powers of \$2\$ otherwise, then try to add them. Here is the recursive C++14 implementation of the algorithm, with the correct bitwise operations to implement the simple cases of addition, subtraction and multiplication by \$2\$ (when reading it, don't forget that I use gray_code<Unsigned> everywhere, and that every + is actually a recursive call, not a regular integer addition):
template<typename Unsigned> auto operator+(gray_code<Unsigned> lhs, gray_code<Unsigned> rhs) -> gray_code<Unsigned> { auto lhs_base = base2(lhs); auto rhs_base = base2(rhs); if (lhs_base == rhs_base) { if (lhs == rhs) { return (lhs << 1u) ^ is_odd(lhs); } return (lhs_base << 1u) ^ ((lhs_base ^ lhs) + (lhs_base ^ rhs)); } if (lhs_base.value < rhs_base.value) { std::swap(lhs, rhs); std::swap(lhs_base, rhs_base); } if (lhs == lhs_base) { return lhs ^ rhs; } auto tmp = (lhs ^ lhs_base) + rhs; if (base2(tmp).value < lhs_base.value) { return lhs_base ^ tmp; } return (lhs_base << 1u) ^ (lhs_base ^ tmp); } And here is the implementation of base2 and of the function isomsb (isolate most significant bit) used to implement it:
// Isolate the most significant bit template<typename Unsigned> auto isomsb(gray_code<Unsigned> x) -> gray_code<Unsigned> { for (std::size_t i = 1u ; i <= std::numeric_limits<Unsigned>::digits / 2u ; i <<= 1u) { x |= x >> i; } return x & ~(x >> 1u); } // Return the greatest power of 2 not higher than // x where x and the power of 2 are encoded in Gray // code auto base2(gray_code<Unsigned> x) -> gray_code<Unsigned> { auto msb = isomsb(x); return msb | (msb >> 1u); } Unfortunately, this new addition algorithm is even slower than the one from my previous question (not by an order of magnitude, but almost by half of one). So, I have several questions:
Correctness: do you see any flaw in the algorithm? Consider that it does not handle overflow and is not meant to. I tested it to add Gray codes whose results are in the range \$[0, 2^{32}[\$ and it passed all the tests. I think the algorithm is correct, but I didn't prove it.
Speed: do you see any way to improve the algorithm so that it runs faster on average? Currently, the only optimization is the dropped
is_oddwhen doubling a power of \$2\$ (a base) since every power of \$2\$ is known to be even by definition (except \$1\$ which is handled separately).
isomsbfunction is already using a bithack found on such a site. It boils down to 5 iterations for a 32-bits integer and 6 iterations for a 64-bits integer. I like it since it's generic and pretty fast. It seems that the compiler uses the fact that the bound is known at compile-time to unroll the loop, so it probably ends up with ~13 instructions for a 32-bits integer. \$\endgroup\$