tag:blogger.com,1999:blog-14659869424355382082024-10-09T15:20:17.529-07:00Bits, Math and Performance(?)50% bits, 50% math (allegedly)Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comBlogger45125tag:blogger.com,1999:blog-1465986942435538208.post-922073610521154752024-08-22T06:01:00.000-07:002024-08-22T06:01:33.416-07:00Enumerating identities, part 2<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>Part 2 of <a href="https://bitmath.blogspot.com/2024/04/enumerating-all-mathematical-identities.html">Enumerating all mathematical identities (in fixed-size bitvector arithmetic with a restricted set of operations) of a certain size</a>.</p>
<p>To recap, the approach in part 1 was broadly:</p>
<ul>
<li>Use a CEGIS-style loop to find a pair of expressions that are equal for all possible inputs.</li>
<li>After finding such a pair, block it from being found again.</li>
<li>Repeat until no more pairs can be found.</li>
</ul>
<p>One weakness (in addition to the other weaknesses) of that approach is that the "block list" keeps growing as more identities are found. Here is an alternative approach that does not use an ever-growing block list:</p>
<ul>
<li>Interpreting the raw bit-string that represents a pair of expressions as an integer, find the <i>lowest</i> pair of equivalent expressions.</li>
<li>After finding the lowest pair, interpreted as the number <tt>X</tt>, set the lower bound for the next pair to <tt>X + 1</tt>.</li>
<li>Repeat until no more pairs can be found.</li>
</ul>
<h2>Finding the lowest pair of expressions</h2>
<p>SAT by itself does not try to find the <i>lowest</i> solution, nor does the CEGIS-loop built on top of it. But we can use the CEGIS-loop as an oracle to answer the question: is there any solution in (a restricted part of) the search space, and that lets us do a bitwise binary search - the "find one bit at the time" variant of binary search, typically discussed in a <a href="https://muscar.eu/shar-binary-search-meta.html">completely different context</a>. Bitwise binary search maps well to SAT, not directly of course, but in the following way:</p>
<ol>
<li>Initialize the <tt>prefix</tt> to an empty list.</li>
<li>If the <tt>prefix</tt> has the same size as a pair of expressions, directly return the result of the CEGIS-oracle.</li>
<li>Extend the <tt>prefix</tt> with <tt>false</tt>.</li>
<li>Ask the CEGIS-oracle if there is a solution that starts with the <tt>prefix</tt>, if there is, go back to step 2.</li>
<li>Otherwise, turn the <tt>false</tt> at the end of the <tt>prefix</tt> into <tt>true</tt>, then go back to step 2.</li>
</ol>
<p>Asking a SAT solver for a solution that starts with a given <tt>prefix</tt> is easy and tends to make the SAT instance easier to solve (especially when the <tt>prefix</tt> is long), this only involves forcing some variables (the ones covered by the <tt>prefix</tt>) to be true or false, using single-literal clauses.</p>
<p>A very useful optimization can be done in step 4: when the CEGIS-oracle says there is a solution with the current <tt>prefix</tt>, the solution may have some extra zeroes <i>after</i> the <tt>prefix</tt> which we can use directly to extend the <tt>prefix</tt> for free. That saves a lot of SAT solves when the solutions tend to have a lot of zeroes in them, which in my case they do, due to extensive use of one-hot encoding.</p>
<h2>Encoding the lower bound</h2>
<p>There is a neat way to encode that an integer must be greater-than-or-equal-to some given constant, which I haven't seen people talk about (perhaps it's part of the folklore?), using only <tt>popcnt(lower_bound)</tt> clauses. The idea here is that for every bit that's set in the bound, at least one of the following bits must be set in the solution: that bit itself, or any more-significant bit that is zero in the lower bound. That only takes one clause to encode, a clause containing the variable that corresponds to the set bit, and the variables corresponding to the zeroes to the left of that set bit.</p>
<h2>Code</h2>
<p>For concreteness, here's how I implemented constraining solutions to conform to the <tt>prefix</tt>, it's really simple:</p>
<pre>// set the prefix (used by binary search)
for (size_t i = 0; i < prefix.size(); i++)
{
if (prefix[i])
s.addClause(allprogbits[i]);
else
s.addClause(~allprogbits[i]);
}</pre>
<p>Here's how I set the lower bound:</p>
<pre>// if there is a lower bound (used by binary search), enforce it
if (!lower_bound.empty())
{
vector<Lit> cl;
for (size_t i = 0; i < lower_bound.size(); i++) {
if (lower_bound[i]) {
cl.push_back(allprogbits[i]);
s.addClause(cl);
cl.pop_back();
}
else
{
cl.push_back(allprogbits[i]);
}
}
}</pre>
<p>And binary search (with the optimization to keep the extra zeroes that the solver gives for free) looks like this:</p>
<pre>optional<pair<vector<InstrB>, vector<InstrB>>> find_lowest(vector<bool>& prefix,
vector<vector<int>>& inputs)
{
do {
if (prefix.size() == progbits) {
return format_progbits(synthesize(prefix, inputs),
inputcount, lhs_size, rhs_size);
}
else {
prefix.push_back(false);
auto f = synthesize(prefix, inputs);
if (f.has_value()) {
// if the next bits are already zero in the solution, keep them zero
auto bits = *f;
while (prefix.size() < progbits && !bits[prefix.size()])
prefix.push_back(false);
continue;
}
prefix.pop_back();
prefix.push_back(true);
}
} while (true);
}</pre>
<p>The full code of my implementation of this idea is <a href="https://gitlab.com/haroldaptroot/aimkissat">available on gitlab</a>. It's a bit crap but at least it should show any detail that you may still be wondering about. In the code I also constrain expressions to not be "a funny way to write zero" and to not be "a complicated way to do nothing", otherwise a lot of less-interesting identities would be generated.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-73786634725148586542024-06-18T04:14:00.000-07:002024-06-21T20:31:21.198-07:00Sorting the nibbles of a u64<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>I was reminded (on mastodon) of this nibble-sorting technique (it could be adapted to other element sizes), which I apparently had only vaguely tweeted about in the past. It deserves a post, so here it is.</p>
<p>Binary LSD radix sort can be expressed as a sequence of stable-partitions, first stable-partitioning based on the least-significant bit, then on the second-to-least-significant bit and so on.</p>
<p>In modern x86, <a href="https://www.felixcloutier.com/x86/pext">pext</a> essentially implements half of a stable partition, only the half that moves a subset of the elements down towards lower indices. If we do that twice, the second time with an inverted mask, and shift the subset of elements where the mask is set left to put it at the top, we get a gadget that partitions a u64 based on a mask:</p>
<pre>(pext(x, mask) << popcount(~mask)) | pext(x, ~mask)</pre>
<p>This is sometimes called the <a href="http://programming.sirrida.de/bit_perm.html#sag">sheep-and-goats operation</a>.</p>
<p>For radix sort the masks that we need are, in order, the least significant bit of each element, each broadcasted to cover the whole corresponding element, then the same thing but with the second-to-least-signficant bit and so on. One way to express that is by shifting the whole thing right to put the bit that we want to broadcast in the the least significant position of the element, and then multiplying by 15 to broadcast that bit into every bit of the element. Different compilers handled that multiplication by 15 differently (there are alternative ways to express that).</p>
<pre>static ulong sort_nibbles(ulong x)
{
ulong m = 0x1111111111111111;
ulong t = (x & m) *15;
x = (Bmi2.X64.ParallelBitExtract(x, t) << BitOperations.PopCount(~t)) |
Bmi2.X64.ParallelBitExtract(x, ~t);
t = ((x >> 1) & m) * 15;
x = (Bmi2.X64.ParallelBitExtract(x, t) << BitOperations.PopCount(~t)) |
Bmi2.X64.ParallelBitExtract(x, ~t);
t = ((x >> 2) & m) * 15;
x = (Bmi2.X64.ParallelBitExtract(x, t) << BitOperations.PopCount(~t)) |
Bmi2.X64.ParallelBitExtract(x, ~t);
t = ((x >> 3) & m) * 15;
x = (Bmi2.X64.ParallelBitExtract(x, t) << BitOperations.PopCount(~t)) |
Bmi2.X64.ParallelBitExtract(x, ~t);
return x;
}</pre>
<p>It's easy to extend this to a key-value sort. Hypothetically you could use that key-value sort to invert a permutation (sorting the values 0..15 by the permutation), but you can do <a href="https://bitmath.blogspot.com/2023/04/not-transposing-16x16-bitmatrix.html">much better with AVX512</a>.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-68625313861819572822024-06-04T17:22:00.000-07:002024-06-04T23:47:56.510-07:00Sharpening a lower bound with KnownBits information<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>I have <a href="http://bitmath.blogspot.com/2013/04/improving-bounds-when-some-bits-have.html">written about this before</a>, but that was a long time ago, I've had a lot more practice with <a href="https://bitmath.blogspot.com/2023/07/propagating-bounds-through-bitwise.html">similar things</a> since then. This topic came up on Mastodon, inspiring me to give it another try. Actually the title is a bit of a lie, I will be using MinΓ©'s <a href="https://www-apr.lip6.fr/~mine/publi/article-mine-wing12.pdf">bitfield domain</a> in which we have bitvectors <tt>z</tt> indicating the bits that can be zero and <tt>o</tt> indicating the bits that can be one (as opposed to bitvectors which indicate the bits known to be zero and the bits known to be one respectively, or bitvectors <tt><sup>0</sup>b</tt> and <tt><sup>1</sup>b</tt> as used in the paper linked below where <tt><sup>1</sup>b</tt> has bits set that are known to be set and <tt><sup>0</sup>b</tt> has bits unset that are known to be unset). The exact representation doesn't really matter.</p>
<p>The problem, to be clear, is that suppose we have a lower bound on some variable, along with some knowledge about its bits (knowing that some bits have a fixed value, which others do not), for example we may know that a variable is even (its least significant bit is known to be zero) and at least 5. "Sharpening" the lower bound means increasing it, if possible, so that the lower bound "fits" the knowledge we have about the bits. If a value is even and at least 5, it is also at least 6, so we can increase the lower bound.</p>
<p>As a more recent reference for an algorithm that is better than my old one, you can read <a href="https://cea.hal.science/cea-01795779">Sharpening Constraint Programming approaches for Bit-Vector Theory</a>.</p>
<p>As that paper notes, we need to find the highest bit in the current lower bound that doesn't "fit" the KnownBits (or <tt>z, o</tt> pair from the bitfield domain) information, and then either:</p>
<ul>
<li>If that bit was not set but should be, we need to set it, and reset any lower bits that are not required to be set (lower bound must go <i>up</i>, but only as little as possible).</li>
<li>If that bit was set but shouldn't be, we need to reset it, and in order to do that we need to set a higher bit that wasn't set yet, and also reset any lower bits that are not required to be set.</li>
</ul>
<p>So far so good. What that paper doesn't tell you, is that these are essentially the same case, and we can do:</p>
<ul>
<li>Starting from the highest "wrong" bit in the lower bound, find the lowest bit that is unset but could be set, set it, and clear any lower bits that are not required to be set.</li>
</ul>
<p>That mostly sounds like the second case, what allows the original two cases to be unified is the fact that the bit we find is the same as the bit that needs to be set in the first case too.</p>
<p>As a reminder, <tt>x & -x</tt> is a common technique used to extract or isolate the lowest set bit aka <a href="https://www.felixcloutier.com/x86/blsi"><tt>blsi</tt></a>. It can also be written as <tt>x & (~x + 1)</tt>, and if we change the 1 to some other constant, we can use this technique to find the lowest set bit but starting from some position that is not necessarily the least significant bit. So if we start from <tt>highestSetBit(~low & o)</tt>, we find the bit we're looking for. Actually the annoying part is <tt>highestSetBit</tt>. Putting the rest together, we may get an implementation like this:</p>
<pre>uint64_t sharpen_low(uint64_t low, uint64_t z, uint64_t o)
{
uint64_t m = (~low & ~z) | (low & ~o);
if (m) {
uint64_t target = ~low & o;
target &= ~target + highestSetBit(m);
low = (low & -target) | target;
low |= ~z;
}
return low;
}</pre>
<p>The branch on <tt>m</tt> is a bit annoying, but on the plus side it means that the input of <tt>highestSetBit</tt> is always non-zero. Zero is otherwise a bit of an annoying case to handle in <tt>highestSetBit</tt>. In modern C++, you can use <a href="https://en.cppreference.com/w/cpp/numeric/bit_floor"><tt>std::bit_floor</tt></a> for <tt>highestSetBit</tt>.</p>
<p>Sharpening the upper bound is symmetric, it can be implemented as <tt>~sharpen_low(~high, o, z)</tt> or you could push the bitwise flips "inside" the algorithm and do some algebra to cancel them out.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-60576033055522188462024-06-03T00:23:00.000-07:002024-06-03T00:28:17.652-07:00Multiplying 64x64 bit-matrices with GF2P8AFFINEQB<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>This is a relatively simple use of <tt>GF2P8AFFINEQB</tt>. By itself <tt>GF2P8AFFINEQB</tt> essentially multiplies two 8x8 bit-matrices (but with transpose-flip applied to the second operand, and an extra XOR by a byte that we can set to zero). A 64x64 matrix can be seen as a block-matrix where each block is an 8x8 matrix. You can also view this as taking the ring of 8x8 matrices over GF(2), and then working with 8x8 matrices with elements from that ring. All we really need to do is write an 8x8 matrix multiplication and let <tt>GF2P8AFFINEQB</tt> take care of the complicated part.</p>
<p>Using the "full" 512-bit version of <tt>VGF2P8AFFINEQB</tt> from AVX-512, one <tt>VGF2P8AFFINEQB</tt> instructions performs 8 of those products. A convenient way to use them is by broadcasting an element (which is really an 8x8 bit-matrix but let's put that aside for now) from the left matrix to all QWORD elements of a ZMM vector, and multiplying that by a row of the right matrix. That way we end up with a row of the result, which is nice to work with: no reshaping or horizontal-SIMD required. XOR-ing QWORDs together horizontally could be done relatively reasonably with another <tt>GF2P8AFFINEQB</tt> trick, which is neat but avoiding it is even better. All we need to do to compute a row of the result (still viewed as an 8x8 matrix) is 8 broadcasts, 8 <tt>VGF2P8AFFINEQB</tt>, and XOR-ing the 8 results together, which doesn't take 8 <tt>VPXORQ</tt> because <tt>VPTERNLOGQ</tt> can XOR <i>three</i> vectors together. Then just do this for each row of the result.</p>
<p>There are two things that I've skipped so far. First, the built-in transpose-flip of <tt>GF2P8AFFINEQB</tt> needs to be cancelled out with a flip-transpose (unless explicitly working with a matrix in a weird format is OK). Second, working with an 8x8 block-matrix is mathematically "free" by imagining some dotted lines running through the matrix, but in order to get the right data into <tt>GF2P8AFFINEQB</tt> we have to actually rearrange it (again: unless the weird format is OK).</p>
<p>One way to implement a flip-transpose (ie the inverse of the bit-permutation that <tt>GF2P8AFFINEQB</tt> applies to its second operand) is by reversing the bytes in each QWORD and then left-multiplying (in the second of <tt>GF2P8AFFINEQB</tt>-ing with constant as the first operand) by a flipped identity matrix, which as a QWORD looks like: <tt>0x0102040810204080</tt>. Reversing the bytes in each QWORD could be done with a <tt>VPERMB</tt>, there are other ways, but we're about to have a <tt>VPERMB</tt> anyway.</p>
<p>Rearranging the data between a fully row-major layout and an 8x8 matrix in which each element is an 8x8 bit-matrix is easy, that's just an 8x8 transpose after all, so just <tt>VPERMB</tt>. That's needed both for the inputs and the output. The input that is the right-hand operand of the overall matrix multiplication also needs to have a byte-reverse applied to each QWORD, the same <tt>VPERMB</tt> that does that transpose can also do that byte-reverse.</p>
<p>Here's one way to put that all together:</p>
<pre>array<uint64_t, 64> mmul_gf2_avx512(const array<uint64_t, 64>& A, const array<uint64_t, 64>& B)
{
__m512i id = _mm512_set1_epi64(0x0102040810204080);
__m512i tp = _mm512_setr_epi8(
0, 8, 16, 24, 32, 40, 48, 56,
1, 9, 17, 25, 33, 41, 49, 57,
2, 10, 18, 26, 34, 42, 50, 58,
3, 11, 19, 27, 35, 43, 51, 59,
4, 12, 20, 28, 36, 44, 52, 60,
5, 13, 21, 29, 37, 45, 53, 61,
6, 14, 22, 30, 38, 46, 54, 62,
7, 15, 23, 31, 39, 47, 55, 63);
__m512i tpr = _mm512_setr_epi8(
56, 48, 40, 32, 24, 16, 8, 0,
57, 49, 41, 33, 25, 17, 9, 1,
58, 50, 42, 34, 26, 18, 10, 2,
59, 51, 43, 35, 27, 19, 11, 3,
60, 52, 44, 36, 28, 20, 12, 4,
61, 53, 45, 37, 29, 21, 13, 5,
62, 54, 46, 38, 30, 22, 14, 6,
63, 55, 47, 39, 31, 23, 15, 7);
array<uint64_t, 64> res;
__m512i b_0 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[0]));
__m512i b_1 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[8]));
__m512i b_2 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[16]));
__m512i b_3 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[24]));
__m512i b_4 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[32]));
__m512i b_5 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[40]));
__m512i b_6 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[48]));
__m512i b_7 = _mm512_permutexvar_epi8(tpr, _mm512_loadu_epi64(&B[56]));
b_0 = _mm512_gf2p8affine_epi64_epi8(id, b_0, 0);
b_1 = _mm512_gf2p8affine_epi64_epi8(id, b_1, 0);
b_2 = _mm512_gf2p8affine_epi64_epi8(id, b_2, 0);
b_3 = _mm512_gf2p8affine_epi64_epi8(id, b_3, 0);
b_4 = _mm512_gf2p8affine_epi64_epi8(id, b_4, 0);
b_5 = _mm512_gf2p8affine_epi64_epi8(id, b_5, 0);
b_6 = _mm512_gf2p8affine_epi64_epi8(id, b_6, 0);
b_7 = _mm512_gf2p8affine_epi64_epi8(id, b_7, 0);
for (size_t i = 0; i < 8; i++)
{
__m512i a_tiles = _mm512_loadu_epi64(&A[i * 8]);
a_tiles = _mm512_permutexvar_epi8(tp, a_tiles);
__m512i row = _mm512_ternarylogic_epi64(
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(0), a_tiles), b_0, 0),
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(1), a_tiles), b_1, 0),
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(2), a_tiles), b_2, 0), 0x96);
row = _mm512_ternarylogic_epi64(row,
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(3), a_tiles), b_3, 0),
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(4), a_tiles), b_4, 0), 0x96);
row = _mm512_ternarylogic_epi64(row,
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(5), a_tiles), b_5, 0),
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(6), a_tiles), b_6, 0), 0x96);
row = _mm512_xor_epi64(row,
_mm512_gf2p8affine_epi64_epi8(_mm512_permutexvar_epi64(_mm512_set1_epi64(7), a_tiles), b_7, 0));
row = _mm512_permutexvar_epi8(tp, row);
_mm512_storeu_epi64(&res[i * 8], row);
}
return res;
}</pre>
<p>When performing multiple matrix multiplications in a row, it may make sense to leave the intermediate results in the format of an 8x8 matrix of 8x8 bit-matrices. The <tt>B</tt> matrix needs to be permuted anyway, but the two <tt>_mm512_permutexvar_epi8</tt> in the loop can be removed. And obviously, if the same matrix is used as the <tt>B</tt> matrix several times, it only needs to be permuted once. You may need to manually inline the code to convince your compiler to keep the matrix in registers.</p>
<h2>Crude benchmarks</h2>
<p>A very boring conventional implementation of this 64x64 matrix multiplication may look like this:</p>
<pre>array<uint64_t, 64> mmul_gf2_scalar(const array<uint64_t, 64>& A, const array<uint64_t, 64>& B)
{
array<uint64_t, 64> res;
for (size_t i = 0; i < 64; i++) {
uint64_t result_row = 0;
for (size_t j = 0; j < 64; j++) {
if (A[i] & (1ULL << j))
result_row ^= B[j];
}
res[i] = result_row;
}
return res;
}</pre>
<p>There are various ways to write this slightly differently, some of which may be a bit faster, that's not really the point.</p>
<p>On my PC, which has a 11600K (Rocket Lake) in it, <tt>mmul_gf2_scalar</tt> runs around 500 times as slow (in terms of the time taken to perform a chain of dependent multiplications) as the AVX-512 implementation. Really, it's that slow - but that is partly due to my choice of data: I mainly benchmarked this on random matrices where each bit has a 50% chance of being set. The AVX-512 implementation does not care about that at all, while the above scalar implementation has thousands (literally) of branch mispredictions. That can be fixed without using SIMD, for example:</p>
<pre>array<uint64_t, 64> mmul_gf2_branchfree(const array<uint64_t, 64>& A, const array<uint64_t, 64>& B)
{
array<uint64_t, 64> res;
for (size_t i = 0; i < 64; i++) {
uint64_t result_row = 0;
for (size_t j = 0; j < 64; j++) {
result_row ^= B[j] & -((A[i] >> j) & 1);
}
res[i] = result_row;
}
return res;
}</pre>
<p>That was, in my benchmark, already about 8 times as fast as the branching version, if I don't let MSVC auto-vectorize. If I do let it auto-vectorize (with AVX-512), this implementation becomes 25 times as fast as the branching version. This was not supposed to be a post about branch (mis)prediction, but be careful out there, you might get snagged on a branch.</p>
<p>It would be interesting to compare the AVX-512 version against established finite-field (or GF(2)-specific) linear algebra packages, such as FFLAS-FFPACK and M4RI. Actually I tried to include M4RI in the benchmark, but it ended up being 10 times as slow as <tt>mmul_gf2_branchfree</tt> (when it is auto-vectorized). That's bizarrely bad so I probably did something wrong, but I've already sunk 4 times as much time into getting M4RI to work <i>at all</i> as it took to write the code which this blog post is really about, so I'll just accept that I did it wrong and leave it at that.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-89608891609532302102024-05-28T20:23:00.000-07:002024-05-29T01:32:08.480-07:00Implementing grevmul with GF2P8AFFINEQB<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>As a reminder, <tt>grev</tt> is <a href="https://programming.sirrida.de/bit_perm.html#general_reverse_bits">generalized bit-reversal</a>, and performs a bit-permutation that corresponds to XORing the indices of the bits of the left operand by the number given in the right operand. A possible (not very suitable for actual use, but illustrative) implementation of <tt>grev</tt> is:</p>
<pre>def grev(bits, k):
return bits[np.arange(len(bits)) ^ k]</pre>
<p><tt>grevmul</tt> (see also <a href="https://bitmath.blogspot.com/2023/05/grevmul.html">this older post</a> where I list some of its properties) can be defined in terms of <tt>grev</tt> (hence the name<sup><a href="#note1">[1]</a></sup>), with <tt>grev</tt> replacing the left shift in a carryless multiplication. But let's do something else first. An interesting way to look at multiplication (plain old multiplication between natural numbers) is as:</p>
<ol>
<li>Form the Cartesian product of the inputs, viewed as arrays of bits.</li>
<li>For each pair in the Cartesian product, compute the AND of the two bits.</li>
<li>Send the AND of each pair with index <tt>(i, j)</tt> to the bin <tt>i + j</tt>, to be accumulated with the function <tt>(+)</tt>.</li>
<li>Resolve the carries.</li>
</ol>
<p>Carryless multiplication works mostly the same way except that accumulation is done with XOR, which makes step 4 unnecessary. <tt>grevmul</tt> also works mostly the same way, with accumulation also done with XOR, but now the pair with index <tt>(i, j)</tt> is sent to the bin <tt>i XOR j</tt>.</p>
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;}
.tg .tg-0lax{text-align:left;vertical-align:top}
</style>
<table class="tg"><thead>
<tr>
<th class="tg-0lax"></th>
<th class="tg-0lax">+</th>
<th class="tg-0lax">^</th>
</tr></thead>
<tbody>
<tr>
<td class="tg-0lax">+</td>
<td class="tg-0lax">imul</td>
<td class="tg-0lax">clmul</td>
</tr>
<tr>
<td class="tg-0lax">^</td>
<td class="tg-0lax">???</td>
<td class="tg-0lax">grevmul</td>
</tr>
</tbody>
</table>
<p>That won't be important for the implementation, but it may help you think about what <tt>grevmul</tt> does, and this will be on the test. OK there is no test, but you can test yourself by explaining why <tt class="nobr">(popcnt(a & b) & 1) == (grevmul(a, b) & 1)</tt>, based on reasoning about the Cartesian-product-algorithm for <tt>grevmul</tt>.</p>
<h2>Implementing <tt>grevmul</tt> with <tt>GF2P8AFFINEQB</tt></h2>
<p>Some of you may have already seen a version of the code that I am about to discuss, although I have made some changes since. Nothing serious, but while thinking about how the code worked, I encountered some opportunities to make polish it up.</p>
<p><tt>GF2P8AFFINEQB</tt> computes, for each QWORD (so for now, let's concentrate on one QWORD of the result), the product of two bit-matrices, where the left matrix comes from the first input and the right matrix is the transpose-flip (or flip-transpose, however you prefer to think about it) of the second input. You can think of it as a mutant of the <tt>mxor</tt> operation found in Knuth's MMIX instruction set, and from here on I will use the name <tt>bmatxor</tt> for the version of this operation that simply multiplies two 8x8 bit-matrices, with none of that transpose-flip business<sup><a href="#note2">[2]</a></sup>. There is also a free XOR by a constant byte thrown in at the end, which can be safely ignored by setting it to zero. The transpose-flip however need to be worked around or otherwise taken into account. You may also want to read <a href="https://www.corsix.org/content/abusing-gf2p8affineqb-indices-into-bits">
(Ab)using gf2p8affineqb to turn indices into bits</a> first for some extra familiarity with / alternate view of <tt>GF2P8AFFINEQB</tt>.</p>
<p>To implement <tt>grevmul(a, b)</tt>, we need some linear combination (controlled by <tt>b</tt>) of permuted (by <tt>grev</tt>) versions of <tt>a</tt> (the roles of <tt>a</tt> and <tt>b</tt> can be swapped since <tt>grevmul</tt> is commutative, which the Cartesian product-based algorithm makes clear). <tt>GF2P8AFFINEQB</tt> is all about linear combinations, but it works with 8x8 matrices, not 64x64 (put that in AVX-4096) which would have made it really simple. Fortunately, we can slice a <tt>grevmul</tt> however we want.</p>
<p>Now to start in the middle, let's say we have 8 copies of a byte (a byte of <tt>b</tt>), with the <tt>i</tt>'th copy <tt>grev</tt>'ed by <tt>i</tt>, concatenated into a QWORD that I will call <tt>m</tt>. <tt>bmatxor(a, m)</tt> would (thinking of a matrix multiplication AB as forming linear combinations of rows of B), for each row of the result, form a linear combination (controlled by <tt>a</tt>) of <tt>grev</tt>'ed copies of the byte from <tt>b</tt>. That may seem like it would be wrong, since every byte of <tt>a</tt> is done separately and uses the same <tt>m</tt>, so it's "missing" a <tt>grev</tt> by 8 for the second byte, 16 for the third byte, etc. But if <tt>x</tt> is a byte (not if and <i>only</i> if, just regular "if"), then <tt>grev(x, 8 * i)</tt> is the same as <tt>x << (8 * i)</tt> and the second byte is indeed already in the second position anyway, so we get this for free. Thus, <tt>mxor(a, m)</tt> would allow us to <tt>grevmul</tt> a 64-bit number by an 8-bit number. If we could just do that 8 times (for each byte of <tt>b</tt>) and combine the results, we're done.</p>
<p>But we don't have <tt>bmatxor</tt>, we have <tt>GF2P8AFFINEQB</tt> with its built-in transpose-flip, and that presents a choice: either put another <tt>GF2P8AFFINEQB</tt> before, or after, the "main" <tt>GF2P8AFFINEQB</tt> to counter that built-in transpose. Not the whole transpose-flip, let's put the flip aside for now. There is a small reason to favour the "extra <tt>GF2P8AFFINEQB</tt> after the main one" order, namely that that results in <tt>GF2P8AFFINEQB(m, broadcast(a))</tt> (as opposed to <tt>GF2P8AFFINEQB(broadcast(a), m)</tt>) and when <tt>a</tt> comes from memory it can be loaded and broadcasted directly with a <tt>{1to8}</tt> broadcasted memory operand. That option would not be available if <tt>a</tt> was the first operand of the <tt>GF2P8AFFINEQB</tt>. This is a small matter, but we have to make the choice somehow, and there seems to be no difference aside from this.</p>
<p>At this point there are two (and a half) pieces of the puzzle left: forming <tt>m</tt>, and horizontally combining 8 results.</p>
<h2>Forming <tt>m</tt></h2>
<p>If we would first form 8 copies of a byte of <tt>b</tt> and then try to <tt>grev</tt> those by their respective indices, that would be hard. But doing it the other way around is easy, broadcast <tt>b</tt> into each QWORD of a 512-bit vector, <tt>grev</tt> each QWORD by its index, then transpose the whole vector as an 8x8 matrix of bytes. Actually for constant-reuse (loading fewer humongous vector constants is rarely bad) and because of the built-in transpose-<i>flip</i> it turns out slightly better to transpose-flip that 8x8 matrix of bytes as well, that doesn't cost any more than just transposing it.</p>
<p><tt>grev</tt>-ing each QWORD by its index is easy, perhaps easier than it sounds. A <tt>grev</tt> by 0..7 only rearranges the bits within each byte, which is easy to do with <a href="https://bitmath.blogspot.com/2023/09/permuting-bits-with-gf2p8affineqb.html">a single <tt>GF2P8AFFINEQB</tt> with a constant as the second operand (a "P" step)</a>.</p>
<p>An 8x8 transpose-flip is just a <tt>VPERMB</tt> by some index vector.<sup><a href="#note3">[3]</a></sup></p>
<h2>Combining the results</h2>
<p>After the "middle" step, if we went with the "extra <tt>GF2P8AFFINEQB</tt> before the main <tt>GF2P8AFFINEQB</tt>"-route, we would have <tt>bmatxor(a, m)</tt> in each QWORD (with a different <tt>m</tt> per QWORD), which need to be combined. If we were implementing a plain old integer multiplication, the value in the QWORD with the index <tt>i</tt> would be shifted left by <tt>8 * i</tt> and then all of them would be summed up. Since we're implementing a <tt>grevmul</tt>, that value is <tt>grev</tt>'ed by <tt>8 * i</tt> (which is just some byte permutation) and the resulting QWORDs are XORed.</p>
<p>If we go with the "extra <tt>GF2P8AFFINEQB</tt> after the main <tt>GF2P8AFFINEQB</tt>"-route, which I chose, then there is a <tt>GF2P8AFFINEQB</tt> to do before we can start combining QWORDs. We really only need it to un-transpose the result of the "main" <tt>GF2P8AFFINEQB</tt>, the rest is just a byte-permutation and we're about to do a <tt>VPERMB</tt> anyway (if we choose the cool way of XORing the QWORDs together), <i>but</i> there is a neat opportunity here: if we re-use the same set of bit-matrices that was used to <tt>grev</tt> <tt>b</tt> by 0..7, in addition to the transpose that we wanted we also permute the bytes such that we <tt>grev</tt> each QWORD by <tt>8 * i</tt> as a bonus.</p>
<p>Now we could just XOR-fold the vector 3 times to end up with the XOR of all eight QWORDs, and that would be a totally valid implementation, but it would also be boring. Alternatively, if we transpose the vector as an 8x8 matrix of bytes again (it can be a transpose-flip, so we get to reuse the same index vector as before), then the i'th byte of each QWORD would be gathered in the i'th QWORD of the transpose and <tt>bmatxor(0xFF, vector)</tt> would XOR together all bytes with the same index and give us a vector that has one byte of the final result per QWORD, easily extractable with <tt>VPMOVQB</tt>. We still don't have <tt>bmatxor</tt> though, we have <tt>bmatxor</tt> with an extra transpose-flip, which can be countered the usual way, with yet another <tt>GF2P8AFFINEQB</tt>.</p>
<p>As yet another alternative<sup><a href="#note4">[4]</a></sup>, after similar transpose trickery <tt>bmatxor(vector, 0xFF)</tt> would also XOR together bytes with the same index but leave the result in a form that can be extracted with <tt>VPMOVB2M</tt>, which puts the result in a mask register but it's not too bad to move it from there to a GPR.</p>
<h2>The code</h2>
<p>In case anyone makes it this far, here is one possible embodiment<sup><a href="#note5">[5]</a></sup> of the algorithm described herein:</p>
<pre>uint64_t grevmul_avx512(uint64_t a, uint64_t b)
{
uint64_t id = 0x0102040810204080;
__m512i grev_by_index = _mm512_setr_epi64(
id,
grev(id, 1),
grev(id, 2),
grev(id, 3),
grev(id, 4),
grev(id, 5),
grev(id, 6),
grev(id, 7));
__m512i tp_flip = _mm512_setr_epi8(
56, 48, 40, 32, 24, 16, 8, 0,
57, 49, 41, 33, 25, 17, 9, 1,
58, 50, 42, 34, 26, 18, 10, 2,
59, 51, 43, 35, 27, 19, 11, 3,
60, 52, 44, 36, 28, 20, 12, 4,
61, 53, 45, 37, 29, 21, 13, 5,
62, 54, 46, 38, 30, 22, 14, 6,
63, 55, 47, 39, 31, 23, 15, 7);
__m512i m = _mm512_set1_epi64(b);
m = _mm512_gf2p8affine_epi64_epi8(m, grev_by_index, 0);
m = _mm512_permutexvar_epi8(tp_flip, m);
__m512i t512 = _mm512_gf2p8affine_epi64_epi8(m, _mm512_set1_epi64(a), 0);
t512 = _mm512_gf2p8affine_epi64_epi8(grev_by_index, t512, 0);
t512 = _mm512_permutexvar_epi8(tp_flip, t512);
t512 = _mm512_gf2p8affine_epi64_epi8(_mm512_set1_epi64(0x8040201008040201), t512, 0);
t512 = _mm512_gf2p8affine_epi64_epi8(t512, _mm512_set1_epi64(0xFF), 0);
return _mm512_movepi8_mask(t512);
}</pre>
<h2>The end</h2>
<p>As far as I know, no one really uses <tt>grevmul</tt> for anything, so being able to compute it somewhat efficiently (more efficiently than a naive scalar solution at least) is not immediately useful. On the other hand, if an operation is not known to be efficiently computable, that may preclude its use. But the point of this post is more to show something neat.</p>
<p>Originally I had found the sequence of <tt>_mm512_gf2p8affine_epi64_epi8(m, _mm512_set1_epi64(a), 0)</tt> and <tt>_mm512_gf2p8affine_epi64_epi8(grev_by_index, t512, 0)</tt> as a solution to <tt>grevmul</tt>-ing a QWORD by a (constant) byte, using a SAT solver (shout-out to togasat for being easy to add to a project - though admittedly I eventually bit the bullet and switched to MiniSAT). That formed the starting point of this investigation / puzzle-solving session. It may be possible to pull a more complete solution out of the void with a SAT/SMT based technique such as CEGIS, perhaps the bitwise-bilinear nature of <tt>grevmul</tt> can be exploited (I used the bitwise-linear nature of <tt>grevmul</tt>-by-a-constant in my SAT-experiments to represent the problem as a composition of matrices over GF(2)).</p>
<p>Almost half of the steps of this algorithm are <i>some</i> kind of transpose, which has also been the case with some other SIMD algorithms that I recently had a hand in. I used to think of a transpose as "not really doing anything", barely worth the notation when doing linear algebra, but maybe I was wrong.</p>
<hr/>
<p>
<span id="note1">[1] Maybe it's less "the name" and more "what I decided to call it". I'm not aware of any established name for this operation.</span><br/>
<span id="note2">[2] This makes it sound more negative than it really is. The transpose-flip often needs to be worked around when we don't want it, but that's not <i>that</i> bad. Having no easy access to a transpose would be much worse to work around when we <i>do</i> need it. Separate <tt>bmatxor</tt> and <tt>bmattranspose</tt> instructions would have been nice.</span><br/>
<span id="note3">[3] <tt>GF2P8AFFINEQB</tt> trickery is nice, but when I recently wrote some AVX2 code it was <tt>VPERMB</tt> that I missed the most.</span><br/>
<span id="note4">[4] Can we stop with the alternatives and just pick something?</span><br/>
<span id="note5">[5] No patents were read in the development of this algorithm, nor in the writing of this blog post.</span><br/>
</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-75414570882661971942024-04-04T20:29:00.000-07:002024-08-22T08:03:31.110-07:00Enumerating all mathematical identities (in fixed-size bitvector arithmetic with a restricted set of operations) of a certain size<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>Once again a boring-but-specific title, I don't want to clickbait the audience after all. Even so, let's get some "disclaimers" out of the way.</p>
<ul>
<li>"All" includes both boring and interesting identities. I tried to remove the most boring ones, so it's no longer truly <i>all</i> identities, but still a lot of boring ones remain. The way I see it, the biggest problem which the approach that I describe in this blog post has, is generating too much "true but boring" junk.</li>
<li>This approach is, as far as I know, absolutely limited to fixed-size bitvectors, but that's what I'm interested in anyway. To keep things reasonable, the size should be small, which does result in some differences with eg the arithmetic of 64-bit bitvectors. Most of the results either directly transfer to larger sizes, or generalize to larger sizes.</li>
<li>The set of operations is restricted to those that are cheap to implement in CNF SAT, that is not a hard limitation but a practical one.</li>
<li>"Of a certain size" means we have to pick in advance the number of operations on both sides of the mathematical identity, and then only identities with exactly that number of operations (counted in the underlying representation, which may represent a seemingly larger expression if the result of an operation is used more than once) are found. This can be repeated for any size we're interested in, but this approach is not very scalable and tends to run out of memory if there are too many identities of the requested size.</li>
</ul>
<p>The results look something like this. Let's say we want "all" identities that involve 2 variables, 2 operations on the left, and 0 operations (ie only a variable) on the right. The result would be the following, keep in mind that a bunch of redundant and boring identities are filtered out.</p>
<pre>(a - (a - b)) == b
(a + (b - a)) == b
((a + b) - a) == b
(b | (a & b)) == b
(a ^ (a ^ b)) == b
(b & (a | b)) == b
// Done in 0s. Used a set of 3 inputs.</pre>
<p>Nothing too interesting so far, but then we didn't ask for much. Here are a couple of selected "more interesting" (but not by any means new or unknown) identities that this approach can also enumerate:</p>
<pre>((a & b) + (a | b)) == (a + b)
((a | b) - (a & b)) == (a ^ b)
((a ^ b) | (a & b)) == (a | b)
((a & b) ^ (a | b)) == (a ^ b)
((~ a) - (~ b)) == (b - a)
(~ (b + (~ a))) == (a - b)
</pre>
<p>Now that the expectations have been set accurately (hopefully), let's get into what the approach is.</p>
<h2>The Approach</h2>
<p>The core mechanism I use is CounterExample-Guided Inductive Synthesis (CEGIS) based on a SAT solver. Glucose worked well, other solvers can be used. Rather than asking CEGIS to generate a snippet of code that performs some specific task however, I ask it to generate two snippets that are equivalent. That does not fundementally change how it operates, which is still a loop of:</p>
<ol>
<li>Synthesize code that does the right thing for each input in the set of inputs to check.</li>
<li>Check whether the code matches the specification. If it does, we're done. If it doesn't, add the counter-example to the set of inputs to check.</li>
</ol>
<p>Both synthesis and checking could be performed by a SAT solver, but I only use a SAT solver for synthesis. For checking, since 4-bit bitvectors have so few combinations, I just brute force every possible valuation of the variables.</p>
<p>When a pair of equivalent expressions has been found, I add its negation as a single clause to prevent the same thing from being synthesized again. This is what enables pulling out one identity after the other. In my imagination, that looks like Thor smashing his mug and asking for another.</p>
<img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgzmGZ4-8HvEnkBHh5WtPokAjO8jHckEQpkRe3iwkXXE5T6YmjQG-M4mVOZo57dT3JvBJsBmBZhACBnP4U2zSfWHYSIMux91cDG1Et_C-iBHAYAzxFjyLP6khcLwLYs3VQqaOJoMDyqwXFdVManxFwzdVnUXEgtyjh7-m0jJdxH5dYq0g2QOj8sCn4AIc/s1600/7BZn.gif"/>
<p>Solving for <i>programs</i> may seem odd, the trick here is to represent a program as a sequence of instructions that are constructed out of boolean variables, the SAT solver is then invoked to solve for those variables.</p>
<p>The <a href="https://gitlab.com/haroldaptroot/aimglucose">code is available on gitlab</a>.</p>
<h2>Original motivation</h2>
<p>The examples I gave earlier only involve "normal" bitvector arithmetic. Originally what I set out to do is discover what sorts of mathematical identities are true in the context of <i>trapping</i> arithmetic (in which subtraction and addition trap on signed overflow), using the rule that two expressions are equivalent if and only if they have the same behaviour, in the following sense: for all valuations of the variables, the two expressions either yield the same value, or they both trap. That rule is also implemented in the linked source.</p>
<p>Many of the identities found in that context involve a trapping operation that can never actually trap. For example the trapping subtraction (the <tt>t</tt>-suffix in <tt>-t</tt> indicates that it is the trapping version of subtraction) in <tt class="nobr">(b & (~ a)) == (b -t (a & b))</tt> cannot trap (boring to prove so I won't bother). But the "infamous" (among who, maybe just me) <tt class="nobr">(-t (-t (-t a))) == (-t a)</tt> is also enumerated and the sole remaining negation <i>can</i> trap but does so in exactly the same case as the original three-negations-in-a-row (namely when <tt>a</tt> is the bitvector with only its sign-bit set). Here is a small selection of nice identities that hold in trapping arithmetic:</p>
<pre>((a & b) +t (a | b)) == (a +t b)
((a | b) -t (a & b)) == (a ^ b)
((~ b) -t (~ a)) == (a -t b)
(~ (b +t (~ a))) == (a -t b)
(a -t (a -t b)) == (a - (a -t b)) // note: one of the subtractions is a non-trapping subtraction
</pre>
<h2>Future directions</h2>
<p>A large source of boring identities is the fact that if <tt class="nobr">f(x) == g(x)</tt>, then also <tt class="nobr">f(x) + x == g(x) + x</tt> and <tt class="nobr">f(x) & x == g(x) & x</tt> and so on, which causes "small" identities to show up again as part of larger ones, without introducing any new information, and multiplied in myriad ways. If there was a good way to prevent them from being enumerated (it would have to be sufficiently easy to state in terms of CNF SAT clauses, to prevent slowing down the solver too much), or to summarize the full output, that could make the output of the enumeration more human-digestible.</p>
<p>There is a <a href="https://bitmath.blogspot.com/2024/08/enumerating-identities-part-2.html">part 2</a> for this post.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-47535596598687734072024-03-09T23:43:00.000-08:002024-03-10T18:13:48.555-07:00The solutions to ππππππ(π‘) < ππ£πππ(π‘) and why there are Fibonacci[n] of them below 2βΏ<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p><a href="http://haroldbot.nl/?q=popcnt%28x%29+%3C+tzcnt%28x%29"><tt>popcnt(x) < tzcnt(x)</tt></a> asks the question "does <tt>x</tt> have fewer set bits than it has trailing zeroes". It's a simple question with a simple answer, but cute enough to think about on a Sunday morning.<a href="#note1">[1]</a></p>
<p>Here are the solutions for 8 bits, in order: 0, 4, 8, 16, 24, 32, 40, 48, 64, 72, 80, 96, 112, 128, 136, 144, 160, 176, 192, 208, 224<a href="#note2">[2]</a></p>
<p>In case you find decimal hard to do read (as I do), here they are again in binary: 00000000, 00000100, 00001000, 00010000, 00011000, 00100000, 00101000, 00110000, 01000000, 01001000, 01010000, 01100000, 01110000, 10000000, 10001000, 10010000, 10100000, 10110000, 11000000, 11010000, 11100000</p>
<p>Simply staring at the values doesn't do much for me. To get a better handle on what's going on, let's recursively (de-)construct the set of <tt>n</tt>-bit solutions.</p>
<p>The most significant bit of an <tt>n</tt>-bit solution is either 0 or 1:</p>
<ol start="0">
<li>If it is 0, then that bit affects neither the <tt>popcnt</tt> nor the <tt>tzcnt</tt> so removing it must yield an <tt>(n-1)</tt>-bit solution.</li>
<li>If it is 1, then removing it along with the least significant bit (which must be zero, there are no odd solutions since their <tt>tzcnt</tt> would be zero) would decrease the both <tt>popcnt</tt> and the <tt>tzcnt</tt> by 1, yielding an <tt>(n-2)</tt>-bit solution.</li>
</ol>
<p>This "deconstructive" recursion is slightly awkward. The constructive version would be: you can take the <tt>(n-1)</tt>-bit solutions and prepend a zero to them, and you can take the <tt>(n-2)</tt>-bit solutions and prepend a one and append a zero to them. However, it is less clear then (to me anyway) that those are the <i>only</i> <tt>n</tt>-bit solutions. The "deconstructive" version starts with all <tt>n</tt>-bit solutions and splits them into two obviously-disjoint groups, removing the possibility of solutions getting lost or being counted double.</p>
<p>The <tt>F(n) = F(n - 1) + F(n - 2)</tt> structure of the number of solutions is clear, but there are different sequences that follow that same recurrence that differ in their base cases. Here we have 1 solution for 1-bit integers (namely zero) and 1 solution for 2-bit integers (also zero), so the base cases are 1 and 1 as in the Fibonacci sequence.</p>
<p>This is probably all useless, and it's barely even bitmath.</p>
<hr>
<p>
<span id="note1">[1] Or whenever, but it happens to be a Sunday morning for me right now.</span><br>
<span id="note2">[2] This sequence does not seem to be on the OEIS at the time of writing.</span><br>
</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-12582448048020276132024-01-17T03:06:00.000-08:002024-01-21T04:22:04.869-08:00Partial sums of popcount<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>The partial sums of <a href="https://en.wikipedia.org/wiki/Hamming_weight">popcount</a>, aka <a href="https://oeis.org/A000788">A000788: Total number of 1's in binary expansions of 0, ..., n</a> can be computed fairly efficiently with some mysterious code found through its OEIS entry (see the link <a href="https://oeis.org/A000788/a000788.txt">Fast C++ function for computing a(n)</a>): (reformatted slightly to reduce width)</p>
<pre>unsigned A000788(unsigned n)
{
unsigned v = 0;
for (unsigned bit = 1; bit <= n; bit <<= 1)
v += ((n>>1)&~(bit-1)) +
((n&bit) ? (n&((bit<<1)-1))-(bit-1) : 0);
return v;
}</pre>
<p>Knowing what we (or I, anyway) know from computing the <a href="https://bitmath.blogspot.com/2021/06/partial-sums-of-blsi-and-blsmsk.html">partial sums of blsi and blsmsk</a>, let's try to improve on that code. "Improve" is a vague goal, let's say we don't want to loop over the bits, but also not just unroll by 64x to do this for a 64-bit integer.</p>
<p>First let's split this thing into the sum of an easy problem and a harder problem, the easy problem being the sum of <tt>(n>>1)&~(bit-1)</tt> (reminder that <tt>~(bit-1) == -bit</tt>, unsigned negation is safe, UB-free, and does exactly what we need, even on hypothetical non-two's-complement hardware). This is the same thing we saw in the partial sum of blsi, bit <tt>k</tt> of <tt>n</tt> occurs <tt>k</tt> times in the sum, which we can evaluate like this:</p>
<pre>uint64_t v =
((n & 0xAAAA'AAAA'AAAA'AAAA) >> 1) +
((n & 0xCCCC'CCCC'CCCC'CCCC) << 0) +
((n & 0xF0F0'F0F0'F0F0'F0F0) << 1) +
((n & 0xFF00'FF00'FF00'FF00) << 2) +
((n & 0xFFFF'0000'FFFF'0000) << 3) +
((n & 0xFFFF'FFFF'0000'0000) << 4);</pre>
<p>The harder problem, the contribution from <tt>((n&bit) ? (n&((bit<<1)-1))-(bit-1) : 0)</tt>, has a similar pattern but more annoying in three ways. Here's an example of the pattern, starting with <tt>n</tt> in the first row and listing the values being added together below the horizontal line:</p>
<pre>00100011000111111100001010101111
--------------------------------
00000000000000000000000000000001
00000000000000000000000000000010
00000000000000000000000000000100
00000000000000000000000000001000
00000000000000000000000000010000
00000000000000000000000000110000
00000000000000000000000010110000
00000000000000000000001010110000
00000000000000000100001010110000
00000000000000001100001010110000
00000000000000011100001010110000
00000000000000111100001010110000
00000000000001111100001010110000
00000000000011111100001010110000
00000000000111111100001010110000
00000001000111111100001010110000
00000011000111111100001010110000</pre>
<ol>
<li>Some anomalous thing happens for the contiguous group of rightmost set bits.</li>
<li>The weights are based not on the column index, but sort of dynamic based on the number of set bits ...</li>
<li>... to the <i>left</i> of the bit we're looking at. That's significant, "to the right" would have been a lot nicer to deal with.</li>
</ol>
<p>For problem 1, I'm just going to state without proof that we can add 1 to <tt>n</tt> and ignore the problem, as long as we add <tt>n & ~(n + 1)</tt> to the final sum. Problems 2 and 3 are more interesting. If we had problem 2 but counting the bits to the <i>right</i> of the bit we're looking at, that would have nice and easy, instead of <tt>(n & 0xAAAA'AAAA'AAAA'AAAA)</tt> we would have <tt>_pdep_u64(0xAAAA'AAAA'AAAA'AAAA, n)</tt>, problem solved. If we had a "pdep but from left to right" (aka <a href="http://programming.sirrida.de/bit_perm.html#c_e"><tt>expand_left</tt></a>) named <tt>_pdepl_u64</tt> we could have done this:</p>
<pre>uint64_t u =
((_pdepl_u64(0x5555'5555'5555'5555, m) >> shift) << 0) +
((_pdepl_u64(0x3333'3333'3333'3333, m) >> shift) << 1) +
((_pdepl_u64(0x0F0F'0F0F'0F0F'0F0F, m) >> shift) << 2) +
((_pdepl_u64(0x00FF'00FF'00FF'00FF, m) >> shift) << 3) +
((_pdepl_u64(0x0000'FFFF'0000'FFFF, m) >> shift) << 4) +
((_pdepl_u64(0x0000'0000'FFFF'FFFF, m) >> shift) << 5);</pre>
<p>But as far as I know, <s>that requires bit-reversing the inputs</s> (see the update below) of a normal <tt>_pdep_u64</tt> and bit-reversing the result, which is not so nice at least on current x64 hardware. Every ISA should have a Generalized Reverse operation like the <tt>grevi</tt> instruction which used to be in the drafts of the RISC-V Bitmanip Extension prior to version 1.</p>
<h3>Update:</h3>
<p>It turned out there <i>is</i> a reasonable way to implement <tt>_pdepl_u64(v, m)</tt> in plain scalar code after all, namely as <tt>_pdep_u64(v >> (std::popcount(~m) & 63), m)</tt>. The <tt>& 63</tt> isn't meaningful, it's just to prevent UB at the C++ level.</p>
<p>This approach turned out to be more efficient than the AVX512 approach, so that's obsolete now, but maybe still interesting to borrow ideas from. Here's the scalar implementation in full:</p>
<pre>uint64_t _pdepl_u64(uint64_t v, uint64_t m)
{
return _pdep_u64(v >> (std::popcount(~m) & 63), m);
}
uint64_t partialSumOfPopcnt(uint64_t n)
{
uint64_t v =
((n & 0xAAAA'AAAA'AAAA'AAAA) >> 1) +
((n & 0xCCCC'CCCC'CCCC'CCCC) << 0) +
((n & 0xF0F0'F0F0'F0F0'F0F0) << 1) +
((n & 0xFF00'FF00'FF00'FF00) << 2) +
((n & 0xFFFF'0000'FFFF'0000) << 3) +
((n & 0xFFFF'FFFF'0000'0000) << 4);
uint64_t m = n + 1;
int shift = std::countl_zero(m);
m = m << shift;
uint64_t u =
((_pdepl_u64(0x5555'5555'5555'5555, m) >> shift) << 0) +
((_pdepl_u64(0x3333'3333'3333'3333, m) >> shift) << 1) +
((_pdepl_u64(0x0F0F'0F0F'0F0F'0F0F, m) >> shift) << 2) +
((_pdepl_u64(0x00FF'00FF'00FF'00FF, m) >> shift) << 3) +
((_pdepl_u64(0x0000'FFFF'0000'FFFF, m) >> shift) << 4) +
((_pdepl_u64(0x0000'0000'FFFF'FFFF, m) >> shift) << 5);
return u + (n & ~(n + 1)) + v;
}</pre>
<p>Repeatedly calling <tt>_pdepl_u64</tt> with the same mask creates some common-subexpressions, they could be manually factored out but compilers do that anyway, even MSVC only uses one actual <tt>popcnt</tt> instruction (but MSVC, annoyingly, <a href="https://godbolt.org/z/hxescdE44">actually performs the meaningless <tt>& 63</tt></a>).</p>
<h2>Enter AVX512</h2>
<p>Using AVX512, we could more easily reverse the bits of a 64-bit integer, there are various ways to do that. But just using that and then going back to scalar <tt>pdep</tt> would be a waste of a good opportunity to implement the whole thing in AVX512, <tt>pdep</tt> and all. The trick to doing a <tt>pdep</tt> in AVX512, if you have several 64-bit integers that you want to <tt>pdep</tt> with the same mask, is to transpose 8x 64-bit integers into 64x 8-bit integers, use <tt>vpexpandb</tt>, then transpose back. In this case the first operand of the <tt>pdep</tt> is a constant, so the first transpose is not necessary. We still have to reverse the mask though. Since <tt>vpexpandb</tt> takes the mask input in a mask register and we only have one thing to reverse, <a href="https://lemire.me/blog/2023/06/29/dynamic-bit-shuffle-using-avx-512/">this trick to bit-permute integers</a> seems like a better fit than Wunk's <a href="https://wunkolo.github.io/post/2020/11/gf2p8affineqb-bit-reversal/">whole-vector bit-reversal</a> or some variant thereof.</p>
<p>I sort of glossed over the fact that we're supposed to be bit-reversing relative to the most significant set bit in the mask, but that's easy to do by shifting left by <tt>std::countl_zero(m)</tt> and then doing a normal bit-reverse, so in the end it still comes down to a normal bit-reverse. The result of the <tt>pdep</tt>s have to be shifted right by the same amount to compensate.</p>
<p>Here's the whole thing: (note that this is less efficient than the updated approach without AVX512)</p>
<pre>uint64_t partialSumOfPopcnt(uint64_t n)
{
uint64_t v =
((n & 0xAAAA'AAAA'AAAA'AAAA) >> 1) +
((n & 0xCCCC'CCCC'CCCC'CCCC) << 0) +
((n & 0xF0F0'F0F0'F0F0'F0F0) << 1) +
((n & 0xFF00'FF00'FF00'FF00) << 2) +
((n & 0xFFFF'0000'FFFF'0000) << 3) +
((n & 0xFFFF'FFFF'0000'0000) << 4);
// 0..63
__m512i weights = _mm512_setr_epi8(
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63);
// 63..0
__m512i rev = _mm512_set_epi8(
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63);
uint64_t m = n + 1;
// bit-reverse the mask to implement expand-left
int shift = std::countl_zero(m);
m = (m << shift);
__mmask64 revm = _mm512_bitshuffle_epi64_mask(_mm512_set1_epi64(m), rev);
// the reversal of expand-right with reversed inputs is expand-left
__m512i leftexpanded = _mm512_permutexvar_epi8(rev,
_mm512_mask_expand_epi8(_mm512_setzero_si512(), revm, weights));
// transpose back to 8x 64-bit integers
leftexpanded = Transpose64x8(leftexpanded);
// compensate for having shifted m left
__m512i masks = _mm512_srlv_epi64(leftexpanded, _mm512_set1_epi64(shift));
// scale and sum results
__m512i parts = _mm512_sllv_epi64(masks,
_mm512_set_epi64(7, 6, 5, 4, 3, 2, 1, 0));
__m256i parts2 = _mm256_add_epi64(
_mm512_castsi512_si256(parts),
_mm512_extracti64x4_epi64(parts, 1));
__m128i parts3 = _mm_add_epi64(
_mm256_castsi256_si128(parts2),
_mm256_extracti64x2_epi64(parts2, 1));
uint64_t u =
_mm_cvtsi128_si64(parts3) +
_mm_extract_epi64(parts3, 1);
return u + (n & ~(n + 1)) + v;
}</pre>
<p>As for <tt>Transpose64x8</tt>, you can find out how to implement that in <a href="https://bitmath.blogspot.com/2023/09/permuting-bits-with-gf2p8affineqb.html">Permuting bits with GF2P8AFFINEQB</a>.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-77691032379647309192023-09-23T16:33:00.005-07:002024-07-22T22:36:36.529-07:00Permuting bits with GF2P8AFFINEQB<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>It's no secret that GF2P8AFFINEQB can be tricky to think about, even in the restricted context of bit-permutations. Thinking about more than one step (such as more than one GF2P8AFFINEQB back-to-back, or GF2P8AFFINEQB flanked by byte-wise shuffles) is just too much. Or perhaps you can do it, tell me your secret.</p>
<p>A good way for mere mortals to reason about these kinds of permutations, I think, is to think in terms of the bits of the indices of the bits that are really being permuted. So we're 4 levels deep:</p>
<ol>
<li>The value whose bits are being permuted.</li>
<li>The bits that are being permuted.</li>
<li>The indices of those bits.</li>
<li>The bits of those indices.</li>
</ol>
<p>This can get a little confusing because a lot of the time the operation that will be performed on the bits of those indices is a permutation again, but they don't have to be, another classic example is that a rotation corresponds to add/subtracting a constant to the indices. Just keep in mind that we're 4 levels deep the entire time.</p>
<div style="margin: auto;"><img alt="" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2O3nYzf-SioFbHLCZEn6KMJjtr9hzSrrWgad5TShU6ZLebPiBUgmtkSKZweuPubnsPWtUkYngBvIH1EEWrEcMj0nQ0LMWaUPjy7CTiKd_j6O4uEcp2X1icA0PI4JK2eqGM7bJ1ZSGC4TqDNKnYFlRaPE7vyh06hZdXtS7-JWpXTo4XWnBEqeybucacCU/s1600/we%20need%20to%20go%20deeper.jpg"/><p>Actually we don't need to go deeper.</p></div>
<h2>The building blocks</h2>
<p>Assuming we have 512 bits to work with, the indices of those bits are 0..511: 9-bit numbers. We will split that into 3 groups of 3 bits, denoted <tt>a,b,c</tt> where <tt>a</tt> locates a QWORD in the 512-bit register, <tt>b</tt> locates a byte within that QWORD, and <tt>c</tt> locates a bit within that byte.</p>
<p>Here are some nice building blocks (given fairly arbitrary names):</p>
<ul>
<li><tt>P<sub>f</sub>(a,b,c) = a,b,f(c)</tt> aka "right GF2P8AFFINEQB", where <tt>f</tt> is any mapping from a 3-bit integer to a 3-bit integer. This building block can be implemented with <tt>_mm512_gf2p8affine_epi64_epi8(input, _mm512_set1_epi64(f_as_a_reversed_matrix), 0)</tt></li>
<li><tt>Q<sub>f</sub>(a,b,c) = a,f(c),~b</tt> aka "left GF2P8AFFINEQB", where <tt>~b</tt> is a 3-bit inversion, equivalent to <tt>7 - b</tt>. <tt>f</tt> can often be the identity mapping, swapping the second and third groups of bits is useful on its own (the "bonus" inversion can be annoying to deal with). This building block can be implemented with <tt>_mm512_gf2p8affine_epi64_epi8(_mm512_set1_epi64(f_as_a_matrix), input, 0)</tt></li>
<li><tt>S<sub>g</sub>(a,b,c) = g(a,b),c</tt> aka Shuffle, where <tt>g</tt> is any mapping from a 6-bit integer to a 6-bit integer. This building block can be implemented with <tt>_mm512_permutexvar_epi8(g_as_an_array, input)</tt>, but in some cases also with another instruction that you may prefer, depending on the mapping.</li>
</ul>
<p><tt>S</tt>, though it doesn't touch <tt>c</tt>, is quite powerful. As a couple of special cases that may be of interest, it can be used to swap <tt>a</tt> and <tt>b</tt>, invert <tt>a</tt> or <tt>b</tt>, or do a combined swap-and-invert.</p>
<p>We could further distinguish:</p>
<ul>
<li><tt>S64<sub>f</sub>(a,b,c) = f(a),b,c</tt> aka VPERMQ. This building block can be implemented with, you guessed it, VPERMQ.</li>
<li><tt>S8<sub>f</sub>(a,b,c) = a,f(b),c</tt> aka PSHUFB. This building block can be implemented with, you guessed it, PSHUFB. PSHUFB allows a bit more freedom than is used here, the mapping could be from 4-bit integers to 4-bit integers, but that's not nice to think about in this framework of 3 groups of 3 bits.</li>
</ul>
<h2>Building something with the blocks</h2>
<p>Let's say that we want to take a vector of 8 64-bit integers, and transpose it into a vector of 64 8-bit integers such that the k'th bit of the n'th uint64 ends up in the n'th bit of the k'th uint8. In terms of the bits of the indices of the bits (I swear it's not as confusing as it sounds) that means we want to build something that maps <tt>a,b,c</tt> to <tt>b,c,a</tt>. It's immediately clear that we need a <tt>Q</tt> operation at some point, since it's the only way to swap some other groups of bits into the 3rd position. But if we start with a <tt>Q</tt>, we get <tt>~b</tt> in the 3rd position while we need <tt>a</tt>. We can solve that by starting with an <tt>S</tt> that swaps <tt>a</tt> and <tt>b</tt> while also inverting <tt>a</tt> (I'm not going to bother defining what that looks like in terms of an index mapping function, just imagine that those functions are whatever they need to be in order to make it work):</p>
<div>
<tt>S<sub>f</sub>(a,b,c) = b,~a,c</tt><br/>
<tt>Q<sub>id</sub>(b,~a,c) = b,c,a</tt><br/>
</div>
<p>Which translates into code like this:</p>
<pre>__m512i Transpose8x64(__m512i x)
{
x = _mm512_permutexvar_epi8(_mm512_setr_epi8(
56, 48, 40, 32, 24, 16, 8, 0,
57, 49, 41, 33, 25, 17, 9, 1,
58, 50, 42, 34, 26, 18, 10, 2,
59, 51, 43, 35, 27, 19, 11, 3,
60, 52, 44, 36, 28, 20, 12, 4,
61, 53, 45, 37, 29, 21, 13, 5,
62, 54, 46, 38, 30, 22, 14, 6,
63, 55, 47, 39, 31, 23, 15, 7), x);
__m512i idmatrix = _mm512_set1_epi64(0x8040201008040201);
x = _mm512_gf2p8affine_epi64_epi8(idmatrix, x, 0);
return x;
}</pre>
<p>Now let's say that we want to do the inverse of that, going back from <tt>b,c,a</tt> to <tt>a,b,c</tt>. Again it's clear that we need a <tt>Q</tt>, but we have some choice now. We could start by inverting the <tt>c</tt> in the middle first:</p>
<div>
<tt>S8<sub>f1</sub>(b,c,a) = b,~c,a</tt><br/>
<tt>Q<sub>id</sub>(b,~c,a) = b,a,c</tt><br/>
<tt>S<sub>f2</sub>(b,a,c) = a,b,c</tt><br/>
</div>
<p>Which translates into code like this:</p>
<pre>__m512i Transpose64x8(__m512i x)
{
x = _mm512_shuffle_epi8(x, _mm512_setr_epi8(
7, 6, 5, 4, 3, 2, 1, 0,
15, 14, 13, 12, 11, 10, 9, 8,
23, 22, 21, 20, 19, 18, 17, 16,
31, 30, 29, 28, 27, 26, 25, 24,
39, 38, 37, 36, 35, 34, 33, 32,
47, 46, 45, 44, 43, 42, 41, 40,
55, 54, 53, 52, 51, 50, 49, 48,
63, 62, 61, 60, 59, 58, 57, 56));
__m512i idmatrix = _mm512_set1_epi64(0x8040201008040201);
x = _mm512_gf2p8affine_epi64_epi8(idmatrix, x, 0);
x = _mm512_permutexvar_epi8(_mm512_setr_epi8(
0, 8, 16, 24, 32, 40, 48, 56,
1, 9, 17, 25, 33, 41, 49, 57,
2, 10, 18, 26, 34, 42, 50, 58,
3, 11, 19, 27, 35, 43, 51, 59,
4, 12, 20, 28, 36, 44, 52, 60,
5, 13, 21, 29, 37, 45, 53, 61,
6, 14, 22, 30, 38, 46, 54, 62,
7, 15, 23, 31, 39, 47, 55, 63), x);
return x;
}</pre>
<p>Or we could start with a <tt>Q</tt> to get the <tt>a</tt> out of the third position, then use an <tt>S</tt> to swap the first and second positions and a <tt>P</tt> to invert <tt>c</tt> (in any order).</p>
<div>
<tt>Q<sub>id</sub>(b,c,a) = b,a,~c</tt><br/>
<tt>S<sub>f1</sub>(b,a,~c) = a,b,~c</tt><br/>
<tt>P<sub>f2</sub>(a,b,~c) = a,b,c</tt><br/>
</div>
<p>Which translates into code like this:</p>
<pre>__m512i Transpose64x8(__m512i x)
{
__m512i idmatrix = _mm512_set1_epi64(0x8040201008040201);
x = _mm512_gf2p8affine_epi64_epi8(idmatrix, x, 0);
x = _mm512_permutexvar_epi8(_mm512_setr_epi8(
0, 8, 16, 24, 32, 40, 48, 56,
1, 9, 17, 25, 33, 41, 49, 57,
2, 10, 18, 26, 34, 42, 50, 58,
3, 11, 19, 27, 35, 43, 51, 59,
4, 12, 20, 28, 36, 44, 52, 60,
5, 13, 21, 29, 37, 45, 53, 61,
6, 14, 22, 30, 38, 46, 54, 62,
7, 15, 23, 31, 39, 47, 55, 63), x);
x = _mm512_gf2p8affine_epi64_epi8(x, idmatrix, 0);
return x;
}</pre>
<p>I will probably keep using a SAT solver to solve the masks (using the same techniques as in <a href="https://bitmath.blogspot.com/2023/04/not-transposing-16x16-bitmatrix.html">(Not) transposing a 16x16 bitmatrix</a>), but now at least I have a proper way to think about the <i>shape</i> of the solution, which makes it a lot easier to ask a SAT solver to fill in the specifics.</p>
<p>This framework could be extended with other bit-permutation operatations such as QWORD rotates, but that quickly becomes tricky to think about.</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-74502350382132905862023-07-02T02:26:00.002-07:002023-08-01T09:28:13.401-07:00Propagating bounds through bitwise operations<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
blockquote footer {
background-color: #F5F5F5;
clear: both;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>This post is meant as a replacement/recap of some work that I did over a decade ago on propagating bounds through bitwise operations, which was intended as an improvement over the implementations given in Hacker's Delight chapter 4, Arithmetic Bounds.</p>
<p>The goal is, given two variables <tt>x</tt> and <tt>y</tt>, with known bounds <tt class="nobr">a β€ x β€ b</tt>, <tt class="nobr">c β€ y β€ d</tt>, compute the bounds of <tt class="nobr">x | y</tt> and of <tt class="nobr">x & y</tt>. Thanks to De Morgan, we have the equations (most also listed in Hacker's Delight, except the last one)</p>
<ul>
<li><tt class="nobr">minAND(a, b, c, d) = ~maxOR(~b, ~a, ~d, ~c)</tt></li>
<li><tt class="nobr">maxAND(a, b, c, d) = ~minOR(~b, ~a, ~d, ~c)</tt></li>
<li><tt class="nobr">minXOR(a, b, c, d) = minAND(a, b, ~d, ~c) | minAND(~b, ~a, c, d)</tt></li>
<li><tt class="nobr">maxXOR(a, b, c, d) = maxOR(a, b, c, d) & ~minAND(a, b, c, d)</tt></li>
</ul>
<p>Everything can be written in terms of only <tt>minOR</tt> and <tt>maxOR</tt> and some basic operations.</p>
<h2><tt>maxOR</tt></h2>
<p>To compute the upper bound of the OR of <tt>x</tt> and <tt>y</tt>, what we need to do is find is the leftmost bit (henceforth the "target bit") such that it is both:</p>
<ol>
<li>set in both <tt>b</tt> and <tt>d</tt> (the upper bounds of <tt>x</tt> and <tt>y</tt>) and,</li>
<li>changing an upper bound (either one of them, doesn't matter, but never both) by resetting the target bit and setting the bits that are less significant, keeps it greater-or-equal than the corresponding lower bound.</li>
</ol>
<p>The explanation of why that works can be found in Hacker's Delight, along with a more of less direct transcription into code, but we can do better than a direct transcription.</p>
<p>Finding the leftmost bit that passes only the first condition would be easy, its the highest set bit in <tt class="nobr">b & d</tt>. The second condition is a bit more complex to handle, but still surprisingly easy thanks to one simple observation: the bits that can pass it, are precisely those bits that are at (or to the right of) the leftmost bit where the upper and lower bound differ. Imagine two numbers in binary, one being the lower bound and the other the upper bound. The number have some equal prefix (possibly zero bits long, up to <i>all</i> bits) and then if they differ, they must differ by a bit in the upper bound being 1 while the corresponding bit in the lower bound is 0. Lowering the upper bound by resetting that bit while setting all bits the right of it, cannot make it lower than the lower bound.</p>
<p>For one of the inputs, say <tt>x</tt>, the position at which that second condition start being <i>false</i> (looking at that bit and to the left of it) can be computed directly with <tt class="nobr">64 - lzcnt(a ^ b)</tt>. We actually need the maximum of that across both pairs of bounds, but there's no need to compute that for both bounds and then take the maximum, we can use this to let the <tt>lzcnt</tt> find the maximum automatically: <tt class="nobr">64 - lzcnt((a ^ b) | (c ^ d))</tt>.</p>
<p><a href="https://www.felixcloutier.com/x86/bzhi.html"><tt>bzhi(m, k)</tt></a> is an operation that resets the bits in <tt>m</tt> starting at index <tt>k</tt>. It can be emulated by shifting or masking, but an advantage of <tt>bzhi</tt> is that it is well defined for any relevant <tt>k</tt>, including when <tt>k</tt> is equal to the size of the integer in bits. <tt>bzhi</tt> is not strictly required here, but it is more convenient than "classic" bitwise operations, and available on most x64 processors today<sup><a href="#note1">[1]</a></sup>. Using <tt>bzhi</tt>, it's simple to take the position calculated in the previous paragraph and reset all the bits in <tt class="nobr">b & d</tt> that do not pass the second condition: <tt class="nobr">bzhi(b & d, 64 - lzcnt((a ^ b) | (c ^ d)))</tt>.</p>
<p>With that bitmask in hand, all we need to do is apply it to one of the upper bounds. We can skip the "reset the target bit" part, since that bit will be set in the <i>other</i> upper bound and therefore also in the result. It also does not matter which upper bound is changed, regardless of which bound we were conceptually changing. Let's pick <tt>b</tt> for no particular reason. Then in total, the implementation could be:</p>
<pre>uint64_t maxOR(uint64_t a, uint64_t b, uint64_t c, uint64_t d)
{
uint64_t index = 64 - _lzcnt_u64((a ^ b) | (c ^ d));
uint64_t candidates = _bzhi_u64(b & d, index);
if (candidates) {
uint64_t target = highestSetBit(candidates);
b |= target - 1;
}
return b | d;
}</pre>
<p>For the <tt>highestSetBit</tt> function you can choose any way you like to isolate the highest set bit in an integer.</p>
<h2><tt>minOR</tt></h2>
<p>Computing the lower bound of <tt class="nobr">x | y</tt> surprisingly seems to be more complex. The basic principles are similar, but this time bits are being reset in one of the lower bounds, and it <i>does</i> matter in which lower bound that happens. The computation of the mask of candidate bits also "splits" into separate candidates for each lower bound, unless there's some trick that I've missed. This whole "splitting" thing cannot be avoided by defining <tt>minOR</tt> in terms of <tt>maxAND</tt> either, because the same things happen there. But it's not too bad, a little bit of extra arithmetic. Anyway, let's see some code.</p>
<pre>uint64_t minOR(uint64_t a, uint64_t b, uint64_t c, uint64_t d)
{
uint64_t candidatesa = _bzhi_u64(~a & c, 64 - _lzcnt_u64(a ^ b));
uint64_t candidatesc = _bzhi_u64(a & ~c, 64 - _lzcnt_u64(c ^ d));
uint64_t target = highestSetBit(candidatesa | candidatesc);
if (a & target) {
c &= -target;
}
if (c & target) {
a &= -target;
}
return a | c;
}</pre>
<p>A Fun Fact here is that the target bit cannot be set in both bounds, opposite to what happens in <tt>maxOR</tt> where the target bit is always set in both bounds. You may be tempted to turn the second <tt>if</tt> into <tt>else if</tt>, but in my tests it was quite important that the <tt>if</tt>s are compiled into conditional moves rather than branches (which of the lower bounds the target bit is found in was essentially random), and using <tt>else if</tt> here apparently discourages compilers (MSVC at least) from using conditional moves.</p>
<p><tt>candidatesa | candidatesc</tt> can be zero, although that is very rare, at least in my usage of the function. As written, the code assumes that <tt>highestSetBit</tt> deals with that gracefully by returning zero if its input is zero. Branching here is (unlike in the two <tt>if</tt>s at the end of <tt>minOR</tt>) not a big deal since this case is so rare (and therefore predictable).</p>
<h2>Conclusion</h2>
<p>In casually benchmarking these functions, I found them to be a bit faster than the ones that I came up with over a decade ago, and significantly faster than the ones from Hacker's Delight. That basic conclusion probably translates to different scenarios, but the exact ratios will vary a lot based on how predictable the branches are in that case, on your CPU, and on arbitrary codegen decisions made by your compiler.</p>
<p>In any case these new versions look nicer to me.</p>
<p>There are probably much simpler solutions if the bounds were stored in bit-reversed form, but that doesn't seem convenient.</p>
<p>Someone on a certain link aggregation site asked about signed integers. As Hacker's Delight explains via a table, things can go wrong if one (or both) bounds cross the negative/positive boundary - but the solution in those cases is still easy to compute. The way I see it, the basic problem is that a signed bound that crosses the negative/positive boundary effectively encodes two different unsigned intervals, one starting at zero and one ending at the greatest unsigned integer, and the basic unsigned <tt>minOR</tt> and so on cannot (by themselves) handle those "split" intervals.</p>
<hr>
<p id="note1">[1] Sadly not all, some low-end Intel processors have AVX disabled, which apparently is done by disabling the entire VEX encoding and it takes out BMI2 as collateral damage.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-61235177564640760542023-06-12T09:02:00.002-07:002023-06-12T09:07:19.991-07:00Some ways to check whether an unsigned sum wraps<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>When computing <tt class="nobr">x + y</tt>, does the sum wrap? There are various ways to find out, some of them well known, some less. Some of these are probably totally unknown, in some cases deservedly so.</p>
<p>This is not meant to be an exhaustive list.</p>
<ul>
<li><h3><tt class="nobr">~x < y</tt></h3>
<p>A cute trick in case you don't want to compute the sum, for whatever reason.</p>
<p>Basically a variation of how <a href="https://wiki.sei.cmu.edu/confluence/display/c/INT30-C.+Ensure+that+unsigned+integer+operations+do+not+wrap">MISRA C</a> recommends checking for wrapping if you use the precondition test since <tt class="nobr">~x = UINT_MAX - x</tt>.</p>
</li>
<li><h3><tt class="nobr">(x + y) < x</tt></h3>
<h3><tt class="nobr">(x + y) < y</tt></h3>
<p>The Classic™. Useful for being cheap to compute: <tt class="nobr">x + y</tt> is often needed anyway, in which case this effectively only costs a comparison. Also recommended by MISRA C, in case you want to express the "has the addition wrapped"-test as a postcondition test.</p>
</li>
<li><h3><tt class="nobr">avg_down(x, y) <s 0</tt></h3>
<h3><tt class="nobr">avg_down(x, y) & 0x80000000 // adjust to integer size</tt></h3>
<p><tt class="nobr"><s</tt> is signed-less-than. Performed on unsigned integers here, so be it.</p>
<p><tt class="nobr">avg_down</tt> is the unsigned average rounded down. <tt class="nobr">avg_up</tt> is the unsigned average rounded up.</p>
<p>Since <tt class="nobr">avg_down</tt> is the sum (the full sum, without wrapping) shifted right by 1, what would have been the carry out of the top of the sum becomes the top bit of the average. So, checking the top bit of <tt class="nobr">avg_down(x, y)</tt> is equivalent to checking the carry out of <tt class="nobr">x + y</tt>.</p>
<p>Can be converted into <tt class="nobr">avg_up(~x, ~y) >=s 0</tt> through the equivalence <tt class="nobr">avg_down(x, y) = ~avg_up(~x, ~y)</tt>.</p>
</li>
<li><h3><tt class="nobr">(x + y) < min(x, y)</tt></h3>
<h3><tt class="nobr">(x + y) < max(x, y)</tt></h3>
<h3><tt class="nobr">(x + y) < avg(x, y)</tt></h3>
<h3><tt class="nobr">(x + y) < (x | y)</tt></h3>
<h3><tt class="nobr">(x + y) < (x & y)</tt></h3>
<h3><tt class="nobr">~(x | y) < (x & y)</tt></h3>
<p>Variants of The Classic™. They all work for <a href="https://bitmath.blogspot.com/2022/07/bit-level-commutativity-and-sum-gap.html">essentially the same reason</a>: addition is commutative, including at the bit-level. So if we have <tt class="nobr">(x + y) < x</tt>, then we also have <tt class="nobr">(x + y) < y</tt>, and together they imply that instead of putting <tt>x</tt> or <tt>y</tt> on the right hand side of the comparison, we could arbitrarily select one of them, or anything between them too. Bit-level commutativity takes care of the bottom three variants in a similar way.</p>
<p>Wait, is that a signed or unsigned <tt>min</tt>? Does <tt>avg</tt> round up or down or either way depending on the phase of the moon? It doesn't matter, all of those variants work and more.</p>
</li>
<li><h3><tt class="nobr">(x + y) != addus(x, y)</tt></h3>
<h3><tt class="nobr">(x + y) < addus(x, y)</tt></h3>
<p><tt class="nobr">addus</tt> is addition with unsigned saturation, meaning that instead of wrapping the result would be <tt>UINT_MAX</tt>.</p>
<p>When are normal addition and addition with unsigned saturation different? Precisely when one wraps and the other saturates. Wrapping addition cannot "wrap all the way back" to <tt>UINT_MAX</tt>, the highest result <i>when the addition wraps</i> is <tt class="nobr">UINT_MAX + UINT_MAX = UINT_MAX - 1</tt>.</p>
<p>When the normal sum and saturating sum are different, the normal sum must be the smaller of the two (it certainly couldn't be greater than <tt>UINT_MAX</tt>), hence the second variant.</p>
</li>
<li><h3><tt class="nobr">subus(y, ~x) != 0</tt></h3>
<h3><tt class="nobr">subus(x, ~y) != 0</tt></h3>
<h3><tt class="nobr">addus(~x, ~y) != UINT_MAX</tt></h3>
<p><tt class="nobr">subus</tt> is subtraction with unsigned saturation.</p>
<p>Strange variants of <tt class="nobr">~x < y</tt>. Since <tt class="nobr">subus(a, b)</tt> will be zero when <tt class="nobr">a <= b</tt>, it will be non-zero when <tt class="nobr">b < a</tt>, therefore <tt class="nobr">subus(y, ~x) != 0</tt> is equivalent to <tt class="nobr">~x < y</tt>.</p>
<p><tt class="nobr">subus(a, b) = ~addus(~a, b)</tt> lets us turn the <tt>subus</tt> variant into the <tt>addus</tt> variant.</p>
</li>
<li><h3><tt class="nobr">(x + y) < subus(y, ~x)</tt></h3>
<p>Looks like a cursed hybrid of <tt class="nobr">(x + y) < avg(x, y)</tt> and <tt class="nobr">subus(y, ~x) != 0</tt>, but the mechanism is (at least the way I see it) different from both of them.</p>
<p><tt class="nobr">subus(y, ~x)</tt> will be zero when <tt class="nobr">~x >= y</tt>, which is exactly when the sum <tt class="nobr">x + y</tt> would not wrap. <tt class="nobr">x + y</tt> certainly cannot be unsigned-less-than zero, so overall the condition <tt class="nobr">(x + y) < subus(y, ~x)</tt> must be false (which is good, it's supposed to be false when <tt class="nobr">x + y</tt> would not wrap).</p>
<p>In the other case, when <tt class="nobr">~x < y</tt>, we know that <tt class="nobr">x + y</tt> will wrap and <tt class="nobr">subus(y, ~x)</tt> won't be zero (and therefore cannot saturate). Perhaps there is a nicer way to show what happens, but at least under those conditions (predictable wrapping and no saturation) it is easy to do algebra:</p>
<ul style="list-style-type:none">
<li><tt class="nobr">(x + y) < subus(y, ~x)</tt></li>
<li><tt class="nobr">x + y - 2<sup>k</sup> < y - (2<sup>k</sup> - 1 - x)</tt></li>
<li><tt class="nobr">x + y - 2<sup>k</sup> < y - 2<sup>k</sup> + 1 + x</tt></li>
<li><tt class="nobr">x + y < y + 1 + x</tt></li>
<li><tt class="nobr">0 < 1</tt></li>
</ul>
<p>So the overall condition <tt class="nobr">(x + y) < subus(y, ~x)</tt> is true <i>IFF</i> <tt class="nobr">x + y</tt> wraps.</p>
</li>
<li><h3><tt class="nobr">~x < avg_up(~x, y)</tt></h3>
<p>Similar to <tt class="nobr">~x < y</tt>, but stranger. Averaging <tt>y</tt> with <tt class="nobr">~x</tt> cannot take a low <tt>y</tt> to above <tt class="nobr">~x</tt>, nor a high <tt>y</tt> to below <tt class="nobr">~x</tt>. The direction of rounding is important: <tt class="nobr">avg_down(~x, y)</tt> could take an <tt>y</tt> that's just one higher than <tt class="nobr">~x</tt> down to <tt class="nobr">~x</tt> itself, making it no longer higher than <tt class="nobr">~x</tt>. <tt class="nobr">avg_up(~x, y)</tt> cannot do that thanks to rounding up.</p>
</li>
</ul>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-17437326884631113852023-05-22T02:48:00.004-07:002024-05-29T01:30:31.502-07:00grevmul<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
blockquote footer {
background-color: #F5F5F5;
clear: both;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p><tt>grev</tt> (<a href="http://programming.sirrida.de/bit_perm.html#general_reverse_bits">generalized bit-reverse</a>) is an operation that implements bit-permutations corresponding to XOR-ing the indices by some value. It has been proposed to be part of the Zbp extension of RISC-V, with this reference implementation (source: <a href="https://github.com/riscv/riscv-bitmanip/releases/tag/v0.93">release v0.93</a>)</p>
<pre>uint32_t grev32(uint32_t rs1, uint32_t rs2)
{
uint32_t x = rs1;
int shamt = rs2 & 31;
if (shamt & 1) x = ((x & 0x55555555) << 1) | ((x & 0xAAAAAAAA) >> 1);
if (shamt & 2) x = ((x & 0x33333333) << 2) | ((x & 0xCCCCCCCC) >> 2);
if (shamt & 4) x = ((x & 0x0F0F0F0F) << 4) | ((x & 0xF0F0F0F0) >> 4);
if (shamt & 8) x = ((x & 0x00FF00FF) << 8) | ((x & 0xFF00FF00) >> 8);
if (shamt & 16) x = ((x & 0x0000FFFF) << 16) | ((x & 0xFFFF0000) >> 16);
return x;
}</pre>
<p><tt>grev</tt> looks in some ways similar to bit-shifts and rotates: the left and right operands have distinct roles with the right operand being a mask of <tt>k</tt> bits if the left operand has 2<sup>k</sup> bits<sup><a href="#note1">[1]</a></sup>.</p>
<p>Carry-less multiplication normally has a left-shift in it, <tt>grevmul</tt> is what you get when that left-shift is replaced with <tt>grev</tt>.</p>
<pre>uint32_t grevmul32(uint32_t x, uint32_t y)
{
uint32_t r = 0;
for (int k = 0; k < 32; k++) {
if (y & (1 << k))
r ^= grev32(x, k);
}
return x;
}</pre>
<p><tt>grevmul</tt> is, at its core, very similar to <tt>clmul</tt>: take single-bit products (logical AND) of every bit of the left operand with every bit of the right operand, then do some XOR-reduction. The difference is in which partial products are grouped together. For <tt>clmul</tt>, the partial products that contribute to bit <tt>k</tt> of the result are pairs with indices <tt>i,j</tt> such that <tt>i + j = k</tt>. For <tt>grevmul</tt>, it's the pairs with indices such that <tt>i ^ j = k</tt>. This goes back to <tt>grev</tt> permuting the bits by XOR-ing their indices by some value, and that value is <tt>k</tt> here.</p>
<p>Now that <tt>grevmul</tt> has been defined, let's look at some of its properties, comparing it to <tt>clmul</tt> and plain old <tt>imul</tt>.</p>
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;}
.tg .tg-3z1b{border-color:#000000;text-align:right;vertical-align:top}
.tg .tg-73oq{border-color:#000000;text-align:left;vertical-align:top}
</style>
<table class="tg">
<thead>
<tr>
<th class="tg-73oq"></th>
<th class="tg-3z1b">grevmul</th>
<th class="tg-3z1b">clmul</th>
<th class="tg-3z1b">imul</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tg-73oq">zero<sup><a href="#note2">[2]</a></sup></td>
<td class="tg-3z1b">0</td>
<td class="tg-3z1b">0</td>
<td class="tg-3z1b">0</td>
</tr>
<tr>
<td class="tg-73oq">identity</td>
<td class="tg-3z1b">1</td>
<td class="tg-3z1b">1</td>
<td class="tg-3z1b">1</td>
</tr>
<tr>
<td class="tg-73oq">commutative</td>
<td class="tg-3z1b">yes</td>
<td class="tg-3z1b">yes</td>
<td class="tg-3z1b">yes</td>
</tr>
<tr>
<td class="tg-73oq">associative</td>
<td class="tg-3z1b">yes</td>
<td class="tg-3z1b">yes</td>
<td class="tg-3z1b">yes</td>
</tr>
<tr>
<td class="tg-73oq">distributes over</td>
<td class="tg-3z1b">xor</td>
<td class="tg-3z1b">xor</td>
<td class="tg-3z1b">addition</td>
</tr>
<tr>
<td class="tg-73oq">op(x, 1 << k) is</td>
<td class="tg-3z1b">grev(x, k)</td>
<td class="tg-3z1b">x << k</td>
<td class="tg-3z1b">x << k</td>
</tr>
<tr>
<td class="tg-73oq">x has inverse if<br></td>
<td class="tg-3z1b">popcnt(x) & 1</td>
<td class="tg-3z1b">x & 1</td>
<td class="tg-3z1b">x & 1</td>
</tr>
<tr>
<td class="tg-73oq">op(x, x) is</td>
<td class="tg-3z1b">popcnt(x) & 1</td>
<td class="tg-3z1b"><a href="https://wunkolo.github.io/post/2020/05/pclmulqdq-tricks/#bit-spread">pdep(x, 0x55555555)</a><br></td>
<td class="tg-3z1b">xΒ²</td>
</tr>
</tbody>
</table>
<h2 id="inverse">What is the "grevmul inverse" of <tt>x</tt>?</h2>
<p>Time for some algebra. Looking just at the table above, and forgetting the actual definition of <tt>grevmul</tt>, can we say something about the solutions of <tt>grevmul(x, y) == 1</tt>? Surprisingly, yes.</p>
<p>Assuming we have some <tt>x</tt> with odd hamming weight (numbers with even hamming weight do not have inverses, so let's ignore them for now), we know that <tt>grevmul(x, x) == 1</tt>. The <a href="https://proofwiki.org/wiki/Inverse_in_Monoid_is_Unique">inverse in a monoid is unique</a> so <tt>x</tt> is not just some inverse of <tt>x</tt>, it is <i>the</i> (unique) inverse of <tt>x</tt>.</p>
<p>Since the "addition operator" is XOR (for which negation is the identity function), this is a non-trivial example of a ring in which <tt class="nobr">x = -x = x<sup>-1</sup></tt>, when <tt class="nobr">x<sup>-1</sup></tt> exists. Strange, isn't it?</p>
<p>We also have that <tt>f(x) = grevmul(x, c)</tt> (for appropriate choices of <tt>c</tt>) is a (non-trivial) involution, so it may be a contenter for the "middle operation" of an <a href="http://marc-b-reynolds.github.io/math/2019/08/20/InvFinalizer.html">involutary bit finalizer</a>, but probably useless without an efficient implementation.</p>
<p><s>I was going to write about implementing <tt>grevmul</tt> by an 8-bit constant with two <tt>GF2P8AFFINEQB</tt>s but I've had enough for now, maybe later.</s> E: see <a href="https://bitmath.blogspot.com/2024/05/implementing-grevmul-with-gf2p8affineqb.html">Implementing grevmul with GF2P8AFFINEQB</a> where I went ahead and implemented the whole thing, not only the "multiply by 8-bit constant" case.</p>
<hr>
<p id="note1">[1] The right operand of a shift is often called the shift <i>count</i>, but it can also be interpreted as a mask indicating some subset of shift-by-2<sup>i</sup> operations to perform. That interpretation is useful for example when implementing a shift-by-variable operation on a machine that only has a shift-by-constant instruction, following the same pattern as the reference implementation of <tt>grev32</tt>.</p>
<p id="note2">[2] This looks like a joke, but I mean that the numeric value 0 acts as the zero element of the corresponding semigroup. </p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-64198561125188993572023-04-12T17:11:00.006-07:002024-07-16T14:42:59.545-07:00(Not) transposing a 16x16 bitmatrix<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
blockquote footer {
background-color: #F5F5F5;
clear: both;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>Inverting a 16-element permutation may done like this:</p>
<pre>for (int i = 0; i < 16; i++)
inv[perm[i]] = i;</pre>
<p>Computing a histogram of 16 nibbles may done like this:</p>
<pre>for (int i = 0; i < 16; i++)
hist[data[i]] += 1;</pre>
<p>These different-sounding but already similar-looking tasks have something in common: they can be both be built around a 16x16 bitmatrix transpose. That sounds silly, why would anyone want to first construct a 16x16 bitmatrix, transpose it, and then do yet more processing to turn the resulting bitmatrix back into an array of numbers?</p>
<p>Because it turns out to be an efficiently-implementable operation, on some modern processors anyway.</p>
<p>If you know anything about the <a href="https://gist.github.com/animetosho/d3ca95da2131b5813e16b5bb1b137ca0">off-label application of <tt>GF2P8AFFINEQB</tt></a>, you may already suspect that it will be involved somehow (merely left-<tt>GF2P8AFFINEQB</tt>-ing by the identity matrix already results in some sort of 8x8 transpose, just horizontally mirrored), and it will be, but that's not the whole story.</p>
<p>First I will show not only how to do it with <tt>GF2P8AFFINEQB</tt>, but also how to find that solution programmatically using a SAT solver. There is nothing that fundamentally prevents a human from finding a solution by hand, but it seems difficult. Using a SAT solver to find a solution <i>ex nihilo</i> (requiring it to find both a sequence of instructions and their operands) is not that easy either (though that technique also exists). Thankfully, Geoff Langdale suggested a promising sequence of instructions:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">nibble value, so now we have 16 bitfields with 1 bit set. <br><br>At this point, what we really want is a 16x16 transpose, not 4 8x8 transposes, but I'm pretty sure we can fake it by using VPERMB to redistribute our bytes (probably first grouping all top 8 bytes into the first 128b ...</p>— Geoff Langdale (@geofflangdale) <a href="https://twitter.com/geofflangdale/status/1611662757665050625?ref_src=twsrc%5Etfw">January 7, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>The problem we have now (which the SAT solver will solve) is, under the constraint that for all <tt>X</tt>, <tt>f(X) = PERMB(GF2P8AFFINE(B, PERMB(X, A)), C)</tt> computes the transpose of <tt>X</tt>, what is a possible valuation of the variables <tt>A, B, C</tt>. Note that the variables in the SAT problem correspond to constants in the resulting code, and the variable in the resulting code (<tt>X</tt>) is quantified out of the problem.</p>
<p>If you know a bit about SAT solving, that "for all <tt>X</tt>" sounds like trouble, requiring either creating a set of constraints for every possible value of <tt>X</tt> (henceforth, concrete values of <tt>X</tt> will be known as "examples"), or some advanced technique such as CEGIS to dynamically discover a smaller set of examples to base the constraints on. Luckily, since we are dealing with a bit-permutation, there are simple and small sets of examples that together sufficiently constrain the problem. For a 16-bit permutation, this set of values could be used:</p>
<ul>
<li><tt>1010101010101010</tt></li>
<li><tt>1100110011001100</tt></li>
<li><tt>1111000011110000</tt></li>
<li><tt>1111111100000000</tt></li>
</ul>
<p>For a 256-bit permutation, a similar pattern can be used, where each of the examples has 256 bits and there would be 8 of them. Note that if you read the <i>columns</i> of the values, they list out the indices of the corresponding columns, which is no coincidence. Using that set of examples to constrain the problem with, essentially means that we assert that <tt>f</tt> when applied to the sequence 0..n-1 must result in the desired permutation. The way that I actually implemented this puts a column into one "abstract bit", so that it represents the index of the bit all in one place instead of spread out.</p>
<p>Implementing a "left <tt>GF2P8AFFINEQB</tt>" (multiplying a constant matrix on the left by a variable matrix on the right) in <a href="https://en.wikipedia.org/wiki/Conjunctive_normal_form">CNF</a>, operating on "abstract bits" (8 variables each), is relatively straight forward. Every (abstract) bit of the result is the XOR of the AND of some (abstract) bits, writing that down is mostly a chore, but there is one interesting aspect: the XOR can be turned into an OR, since we know that we're multiplying by a permutation matrix. In CNF, OR is simpler than XOR, and easier for the solver to reason through.</p>
<p><tt>VPERMB</tt> is more difficult to implement, given that the permutation operand is a variable (if it was a constant, we could just permute the abstract bits without generating any new constraints). To make it easier, I represent the permutation operand as a 32x32 permutation matrix, letting me create a bunch of simple ternary constraints of the form <tt>(Β¬P(i, j) β¨ Β¬A(j) β¨ R(i)) β§ (Β¬P(i, j) β¨ A(j) β¨ Β¬R(i))</tt> (read: if <tt>P(i, j)</tt>, then <tt>A(j)</tt> must be equal to <tt>R(i)</tt>). The same thing can be used to implement <tt>VPSHUFB</tt>, with additional constraints on the permutation matrix (to prevent cross-slice movement).</p>
<p>Running <a href= "https://gitlab.com/haroldaptroot/transposesolver16x16/-/blob/main/TransposeSolver16x16.cpp">that code</a>, at least on my PC at this time<a href="#note1"><sup>[1]</sup></a>, results in (with some whitespace manually added):</p>
<pre>__m256i t0 = _mm256_permutexvar_epi8(_mm256_setr_epi8(
14, 12, 10, 8, 6, 4, 2, 0,
30, 28, 26, 24, 22, 20, 18, 16,
15, 13, 11, 9, 7, 5, 3, 1,
31, 29, 27, 25, 23, 21, 19, 17), input);
__m256i t1 = _mm256_gf2p8affine_epi64_epi8(_mm256_set1_epi64x(0x1080084004200201), t0, 0);
__m256i t2 = _mm256_shuffle_epi8(t1, _mm256_setr_epi8(
0, 8, 1, 9, 3, 11, 5, 13,
7, 15, 2, 10, 4, 12, 6, 14,
0, 8, 1, 9, 3, 11, 5, 13,
7, 15, 2, 10, 4, 12, 6, 14));</pre>
<p>So that's it. That's the answer<a href="#note2"><sup>[2]</sup></a>. If you want to transpose a 16x16 bitmatrix, on a modern PC (this code requires AVX512_VBMI and AVX512_GFNI<a href="#note3"><sup>[3]</sup></a>), it's fairly easy and cheap, it's just not so easy to find this solution to begin with.</p>
<p>Using this transpose to invert a 16-element permutation is pretty easy, for example using <tt>_mm256_sllv_epi16</tt> to construct the matrix and <tt>_mm256_popcnt_epi16(_mm256_sub_epi16(t2, _mm256_set1_epi16(1)))</tt> (sadly there is no SIMD version of <tt>TZCNT</tt> .. yet) to convert the bit-masks back into indices. It may be tempting to try to use a mirrored matrix and leading-zero count, which AVX512 does offer, but it only offers the DWORD and QWORD versions <tt>VPLZCNTD/Q</tt>.</p>
<p>Making a histogram is even simpler, using only <tt>_mm256_popcnt_epi16(t2)</tt> to convert the matrix into counts. </p>
<h2>And for my next trick, I will now <i>not</i> transpose the matrix</h2>
<p>What if we didn't transpose that matrix. Does that even make sense? Well, at least for the two applications that I focused on, what we really need is not so much the transpose of the matrix, but any matrix such that:</p>
<ol>
<li>Every bit of the original matrix occurs exactly once in the result.</li>
<li>Each row of the result contains all bits from a particular column.</li>
<li>The permutation within each row is "regular" enough that we can work with it. We don't need this when making a histogram (as Geoff already noted in one of his tweets).</li>
</ol>
<p>There is no particular requirement on the order of the rows, any row-permutation we end up with is easy to undo.</p>
<p>The first two constraints leave plenty of options open, but the last constraint is quite vague. Too vague for me to do something such as searching for the best not-quite-transpose, so I don't promise to have found it. But here is <i>a</i> solution: rotate every row by its index, then rotate every column by its index.</p>
<p>At least, that's the starting point. Rotating the columns requires 3 rounds of blending a vector with cross-slice-permuted copy of that vector, and a <tt>VPERMQ</tt> sandwiched by two <tt>VPSHUFB</tt>s to rotate the last 8 columns by 8. That's a lot of cross-slice permuting, most of it can be avoided by modifying the overall permutation slightly:</p>
<ol>
<li>Exchange the off-diagonal quadrants.</li>
<li>Rotate each row by its index.</li>
<li>For each quadrant individually, rotate each column by its index.</li>
</ol>
<p>Here is some attempt at illustrating that process, feel free to <a href="#afterimages">skip past it</a></p>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfdPsR2jD09_YFXo49BXxK6TKn2yEnEiZXOQ8YniaiIXlIsd-hAK43pmt7OMl1iKLkYO9WrnBsP-GJGm-O6RxKvpxxHNby8w7OFpD1Sl9wBmKmpTwJSr5KMb8YKq8omaY9rtoLV4OW0ZPBf761pk17DnglJ2GwmzVhYVE1iRbz9pP0PdGCSoKUxBIG/s1600/M1.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="513" data-original-width="513" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfdPsR2jD09_YFXo49BXxK6TKn2yEnEiZXOQ8YniaiIXlIsd-hAK43pmt7OMl1iKLkYO9WrnBsP-GJGm-O6RxKvpxxHNby8w7OFpD1Sl9wBmKmpTwJSr5KMb8YKq8omaY9rtoLV4OW0ZPBf761pk17DnglJ2GwmzVhYVE1iRbz9pP0PdGCSoKUxBIG/s1600/M1.png"/></a></div><div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRoWeFTX8UNGGnz6DyeDuEv-AgW4iDwLXn4di-QRBGh71XIcJwgI1m5YFe5SfvSlzVW77uJ-A632DMwtEEFv11JXySdl7DF4aAbRCApsaHipP5pVeDKBet6hylsCiOnp8d7gmZl9cdYC2AZGuz6FZS07hdKYiyr7CpaheQ1NXsHDQAuJ1a746P0I6Y/s1600/M2.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="513" data-original-width="513" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRoWeFTX8UNGGnz6DyeDuEv-AgW4iDwLXn4di-QRBGh71XIcJwgI1m5YFe5SfvSlzVW77uJ-A632DMwtEEFv11JXySdl7DF4aAbRCApsaHipP5pVeDKBet6hylsCiOnp8d7gmZl9cdYC2AZGuz6FZS07hdKYiyr7CpaheQ1NXsHDQAuJ1a746P0I6Y/s1600/M2.png"/></a></div><div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt7g_o74IX00R9j3PWjdT3BftUpayzauVbymZHhKd0aiLT8lVlMYM7Jf1vwXLb5NGTeU0wunQyjLFq5dtXioNBW_SNp2bnYCJPmaEvwRh4IFyR_Nk3JiUGd3dMWwb9497zUrAxU9xTD7pEcst7THrCjDPlcgPiuygcBF0uVoNEQYF89fGVrzWfsa8r/s1600/M3.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="513" data-original-width="513" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt7g_o74IX00R9j3PWjdT3BftUpayzauVbymZHhKd0aiLT8lVlMYM7Jf1vwXLb5NGTeU0wunQyjLFq5dtXioNBW_SNp2bnYCJPmaEvwRh4IFyR_Nk3JiUGd3dMWwb9497zUrAxU9xTD7pEcst7THrCjDPlcgPiuygcBF0uVoNEQYF89fGVrzWfsa8r/s1600/M3.png"/></a></div><div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5scRDLbvJwPVxF5nZFK6vbGINKnWmcskh8Y1jhfBmWKyQK0VVZpjle2P8b6nsLQDZjUsExjOTYgyTD5vhFpxyFtUAZHDjvJUvDqR0yNbdZP6WtOlKgux-iVbADLoN2XTzUsEmmZ-9cWXsMw8y-ybNRWYTsDhfA0HlQuNJ8-j6ttWWC-joUZduOayq/s1600/M4.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="513" data-original-width="513" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5scRDLbvJwPVxF5nZFK6vbGINKnWmcskh8Y1jhfBmWKyQK0VVZpjle2P8b6nsLQDZjUsExjOTYgyTD5vhFpxyFtUAZHDjvJUvDqR0yNbdZP6WtOlKgux-iVbADLoN2XTzUsEmmZ-9cWXsMw8y-ybNRWYTsDhfA0HlQuNJ8-j6ttWWC-joUZduOayq/s1600/M4.png"/></a></div>
<p id="aferimages">These three steps are implementable in AVX2:</p>
<ol>
<li>Exchanging the off-diagonal quadrants can be done by gathering the quadrants into QWORDs, permuting them, and shuffling the QWORDs back into quadrants.</li>
<li>Rotating the rows can be done with <tt>VPMULLW</tt> (used as a variable shift-left), <tt>VPMULHUW</tt> (used as a variable shift-right), and <tt>VPOR</tt>.</li>
<li>Rotating the columns can be done by conditionally rotating the columns with odd indices by 1, conditionally rotating the columns that have the second bit of their index set by 2, and conditionally rotating the columns that have the third bit of their index set by 4. The rotations can be done using <tt>VPALIGNR</tt><a href="#note4"><sup>[4]</sup></a>, the conditionality can be implemented with blending, but since this needs to be bit-granular blend, it cannot be performed using <tt>VPBLENDVB</tt>.</li>
</ol>
<p>In total, here is how I <i>don't</i> transpose a 16x16 matrix with AVX2, hopefully there is a better way:</p>
<pre>__m256i nottranspose16x16(__m256i x)
{
// exchange off-diagonal quadrants
x = _mm256_shuffle_epi8(x, _mm256_setr_epi8(
0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15,
0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15));
x = _mm256_permute4x64_epi64(x, _MM_SHUFFLE(3, 1, 2, 0));
x = _mm256_shuffle_epi8(x, _mm256_setr_epi8(
0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15,
0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
// rotate every row by its y coordinate
__m256i shifts = _mm256_setr_epi16(
1 << 0, 1 << 1, 1 << 2, 1 << 3,
1 << 4, 1 << 5, 1 << 6, 1 << 7,
1 << 8, 1 << 9, 1 << 10, 1 << 11,
1 << 12, 1 << 13, 1 << 14, 1 << 15);
__m256i sll = _mm256_mullo_epi16(x, shifts);
__m256i srl = _mm256_mulhi_epu16(x, shifts);
x = _mm256_or_si256(sll, srl);
// within each quadrant independently,
// rotate every column by its x coordinate
__m256i x0, x1, m;
// rotate by 4
m = _mm256_set1_epi8(0x0F);
x0 = _mm256_and_si256(x, m);
x1 = _mm256_andnot_si256(m, _mm256_alignr_epi8(x, x, 8));
x = _mm256_or_si256(x0, x1);
// rotate by 2
m = _mm256_set1_epi8(0x33);
x0 = _mm256_and_si256(x, m);
x1 = _mm256_andnot_si256(m, _mm256_alignr_epi8(x, x, 4));
x = _mm256_or_si256(x0, x1);
// rotate by 1
m = _mm256_set1_epi8(0x55);
x0 = _mm256_and_si256(x, m);
x1 = _mm256_andnot_si256(m, _mm256_alignr_epi8(x, x, 2));
x = _mm256_or_si256(x0, x1);
return x;
}</pre>
<p>Using that not-transpose to invert a 16-element permutation takes some extra steps that, without AVX512, are about as annoying as not-transposing the matrix was.</p>
<ul>
<li>Constructing the matrix is more difficult. AVX2 has shift-by-variable, but not for 16-bit element.<a href="#note5"><sup>[5]</sup></a> There are various work-arounds, such as using DWORDs and then narrowing, of course (boring). Another (funnier) option is to duplicate every byte, add 0xF878 to every word, then use <tt>VPSHUFB</tt> in lookup-table-mode to index into a table of powers of two. Having added 0x78 to every low byte of every word, that byte will mapped to zero if it was 8 or higher, or otherwise two to the power of that byte. The high byte, having 0xF8 added to it, will be mapped to 0 if it was below 8, or otherwise to two to the power of that byte minus 8. As wild as that sounds, it is pretty fast, costing only 5 cheap instructions (whereas widening to DWORDs, shifting, and narrowing, would be worse than it sounds). Perhaps there is a better way.</li>
<li>Converting masks back into indices is more difficult due to the lack of trailing zero count, leading zero count, or even popcount. What AVX2 does have, is .. <tt>VPSHUFB</tt> again. We can multiply by an order-4 de Bruijn sequence and use <tt>VPSHUFB</tt> to map the results to the indices of the set bits.</li>
<li>Then we have indices, but since the rows and columns were somewhat arbitrarily permuted, they must still be mapped back into something that makes sense. Fortunately that's no big deal, a modular subtraction (or addition, same thing really) cancels out the row-rotations, and yet another <tt>VPSHUFB</tt> cancels out the strange order that the rows are in. Fun detail: the constants that are subtracted and the permutation are both <tt>0, 7, 6, 5, 4, 3, 2, 1, 8, 15, 14, 13, 12, 11, 10, 9</tt>.</li>
</ul>
<p>All put together:</p>
<pre>void invert_permutation_avx2(uint8_t *p, uint8_t *inv)
{
__m256i v = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i*)p));
// indexes to masks
v = _mm256_or_si256(v, _mm256_slli_epi64(v, 8));
v = _mm256_add_epi8(v, _mm256_set1_epi16(0xF878));
__m256i m = _mm256_shuffle_epi8(_mm256_setr_epi8(
1, 2, 4, 8, 16, 32, 64, 128,
1, 2, 4, 8, 16, 32, 64, 128,
1, 2, 4, 8, 16, 32, 64, 128,
1, 2, 4, 8, 16, 32, 64, 128), v);
// ???
m = nottranspose16x16(m);
// masks to indexes
__m256i deBruijn = _mm256_and_si256(_mm256_mulhi_epu16(m, _mm256_set1_epi16(0x9AF0)), _mm256_set1_epi16(0x000F));
__m128i i = _mm_packs_epi16(_mm256_castsi256_si128(deBruijn), _mm256_extracti128_si256(deBruijn, 1));
i = _mm_shuffle_epi8(_mm_setr_epi8(
0, 1, 2, 5, 3, 9, 6, 11, 15, 4, 8, 10, 14, 7, 13, 12), i);
// un-mess-up the indexes
i = _mm_sub_epi8(i, _mm_setr_epi8(0, 7, 6, 5, 4, 3, 2, 1, 8, 15, 14, 13, 12, 11, 10, 9));
i = _mm_and_si128(i, _mm_set1_epi8(0x0F));
i = _mm_shuffle_epi8(i, _mm_setr_epi8(0, 7, 6, 5, 4, 3, 2, 1, 8, 15, 14, 13, 12, 11, 10, 9));
_mm_storeu_si128((__m128i*)inv, i);
}</pre>
<p>To make a histogram, emulate <tt>VPOPCNTW</tt> <a href="http://0x80.pl/articles/sse-popcount.html">using, you guessed it, <tt>PSHUFB</tt></a>.</p>
<h2>The end</h2>
<p>This post is, I think, one of the many examples of how AVX512 can be an enormous improvement compared to AVX2 even when <i>not</i> using 512-bit vectors. Every step of every problem had a simple solution in AVX512 (even if it was not always easy to find it). With AVX2, everything felt "only barely possible".</p>
<p>"As complicated as it is, is this actually faster than scalar code?" Yes actually, but feel free to benchmark it yourself. The AVX2 version being somewhat more efficient than scalar code is not really the point of this post anyway. The AVX512 version is nice and efficient, I'm showing an AVX2 version mostly to show how hard it is to create it.<a href="#note6"><sup>[6]</sup></a></p>
<p>Transposing larger matrices with AVX512 can be done by first doing some quadrant-swapping (also used at the start of the not-transpose) until the bits that need to end up together in one 512-bit block are all in there, and then a <tt>VPERMB, VGF2P8AFFINEQB, VPERMB</tt> sequence with the right constants (which can be found using the techniques that I described) can put the bits in their final positions. But well, I <a href="https://gist.github.com/IJzerbaard/61d145c55143d56409a158350271e127">already did that</a>, so there you go.</p>
<p>A proper transpose can be done in AVX2 of course, for example using 4 rounds of quadrant-swapping. Implementations of that already exist so I thought that would be boring to talk about, but there is an interesting aspect to that technique that is often not mentioned: every round of quadrant-swapping can be seen as <a href="http://programming.sirrida.de/bit_perm.html#bit_index">exchanging two bits of the indices</a>. Swapping the big 8x8 quadrants swaps bits 3 and 7 of the indices, transposing the 2x2 submatrices swaps bits 0 and 4 of the indices. From that point of view, it's easy to see that the order in which the four steps are performed does not matter - no matter the order, the lower nibble of the index is swapped with the higher nibble of the index.</p>
<hr>
<p id="note1">[1] While MiniSAT (which this program uses as its SAT solver) is a "deterministic solver" in the sense of definitely finding a satifying valuation if there is one, it is not deterministic in the sense of guaranteeing that the same satisfying valuation is found every time the solver is run on the same input.</p>
<p id="note2">[2] Not the unique answer, there are multiple solutions.</p>
<p id="note3">[3] But not 512-bit vectors.</p>
<p id="note4">[4] Nice! It's not common to see a 256-bit <tt>VPALIGNR</tt> being useful, due to it not being the natural widening of 128-bit <tt>PALIGNR</tt>, but acting more like two <tt>PALIGNR</tt>s side-by-side (with the same shifting distance).</p>
<p id="note5">[5] Intel, why do you keep doing this.</p>
<p id="note6">[6] Also as an excuse to use <tt>PSHUFB</tt> for everything.</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-40075877436931549402023-04-09T16:54:00.003-07:002023-04-12T00:13:20.656-07:00Column-oriented row reduction<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
blockquote footer {
background-color: #F5F5F5;
clear: both;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<blockquote cite="https://en.wikipedia.org/wiki/Gaussian_elimination">In mathematics, Gaussian elimination, also known as row reduction, is an algorithm for solving systems of linear equations.
<footer>-- The Wikipedia entry of <a href="https://en.wikipedia.org/wiki/Gaussian_elimination">Gaussian elimination</a></footer></blockquote>
<p>The usual implementation of row reduction of a bit-matrix<a href="#note1"><sup>[1]</sup></a> takes an array of bit-packed rows, and applies row operations to the matrix in a row-wise manner. As a reminder, it works something like this:</p>
<ol>
<li>Find a row with a non-zero element<a href="#note2"><sup>[2]</sup></a> in the current column.</li>
<li>Swap it into position.</li>
<li>XOR that row into any other row that has a non-zero element in the current column.</li>
<li>Go to step 1 for the next column, if there is one.</li>
</ol>
<p>For the application that I had mind when writing this blog post<a href="#note3"><sup>[3]</sup></a>, step 2 is not necessary and will be skipped.<a href="#note4"><sup>[4]</sup></a></p>
<p>Note that at the single-bit-level, the XOR-logic of step 3 comes down to <tt>if M(i, j) and M(j, k) then flip M(i, k)</tt>. That is, in order for the XOR to flip <tt>M(i, k)</tt>, the corresponding row must have a 1 in the current column (condition A), and the row that was selected to pivot on must have a 1 in the column <tt>k</tt> (condition B).</p>
<h2>Turning it into a column-oriented algorithm</h2>
<p>In the row-oriented algorithm, condition A is handled by conditionally XOR-ing into only those rows that have a 1 in the current column, and condition B is handled by the XOR itself (which is a conditional bit-flip afterall). A column-oriented algorithm would do it the other way around, using XOR to implement condition B, and skipping columns for which condition A is false:</p>
<ol>
<li>Find a row that hasn't already been pivoted on, with a non-zero element in the current column.</li>
<li><i>Don't</i> swap it into position.</li>
<li>XOR each column that has a 1 in the row that we're pivoting on, with a mask formed by taking the current column and resetting the bit that corresponds to the pivot row.</li>
<li>If there are any rows left that haven't been pivoted on, go to step 1 for the next column.</li>
</ol>
<p>Step 1 may sound somewhat annoying, but it is really simple: <tt>AND</tt> the current column with a mask of the rows that have already been pivoted on, <tt>had</tt>, and use the old Isolate Lowest Set Bit<a href="#note5"><sup>[5]</sup></a> trick <tt>x & -x</tt> to extract a mask corresponding to the first row that passes both conditions.</p>
<p>Here is an example of what an implementation might look like (shown in C#, or maybe this is Java code with a couple of capitalization errors)</p>
<pre>static void reduceRows(long[] M)
{
long had = 0;
for (int i = 0; i < M.Length; i++)
{
long r = lowestSetBit(M[i] & ~had);
if (r == 0)
continue;
long mask = M[i] & ~r;
for (int j = 0; j < M.Length; j++)
{
if ((M[j] & r) != 0)
M[j] ^= mask;
}
had |= r;
}
}
static long lowestSetBit(long x)
{
return x & -x;
}</pre>
<h2>The stuff that goes at the end of the post</h2>
<p>While the column-oriented approach makes the step "search for a row that has a 1 in the current column" easy, transposing a matrix just to be able to do this does not seem worthwhile to me.</p>
<p>Despite the <tt>if</tt> inside the inner loops of both the row-oriented and the column-oriented algorithm, both of them can be accellerated using SIMD. The conditional code can easily be rewritten into arithmetic-based masking<a href="#note6"><sup>[6]</sup></a> or "real" masked operations (in eg AVX512). "But don't compilers autovectorize these days?" <a href="https://godbolt.org/z/eb3nzzqnr">Yes they do, but not always in a good way</a>, note that both GCC and Clang both used a masked store, which is worse than writing back some unchanged values with a normal store (especially on AMD processors, and also especially considering that the stored values will be loaded again soon). Rumour has it that there are Reasons™ for that quirk, but that doesn't improve the outcome.</p>
<hr>
<p id="note1">[1] Of course this blog post is not about floating point matrices, this is the bitmath blog.</p>
<p id="note2">[2] Sometimes known as 1 (one).</p>
<p id="note3">[3] XOR-clause simplification</p>
<p id="note4">[4] Row-swaps are particularly expensive to apply to a column-major bit-matrix, so perhaps this is just an excuse to justify the column-based approach. You decide.</p>
<p id="note5">[5] AKA BLSI, which AMD describes as "Isolate Lowest Set Bit", which makes sense, because well, that is what the operation does. Meanwhile Intel writes "Extract Lowest Set Isolated Bit" (isolated bit? what?) in their manuals.</p>
<p id="note6">[6] Arithmetic-based masking can also be used in scalar code.</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-85533500201869628532023-01-03T08:02:00.002-08:002023-04-30T18:30:29.102-07:00Weighted popcnt<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>Since I showed the code below on Twitter, and some people understood it and some didn't, I suppose I should explain how it works and what the more general technique is.</p>
<pre>// sum of indexes of set bits
int A073642(uint64_t n)
{
return __popcnt64(n & 0xAAAAAAAAAAAAAAAA) +
(__popcnt64(n & 0xCCCCCCCCCCCCCCCC) << 1) +
(__popcnt64(n & 0xF0F0F0F0F0F0F0F0) << 2) +
(__popcnt64(n & 0xFF00FF00FF00FF00) << 3) +
(__popcnt64(n & 0xFFFF0000FFFF0000) << 4) +
(__popcnt64(n & 0xFFFFFFFF00000000) << 5);
}</pre>
<p>A sum of several multi-digit numbers, such as <tt>12 + 34</tt>, can be rewritten to make the place-value notation explicit, giving <tt>1*10 + 2*1 + 3*10 + 4*1</tt>. Then the distributive property of multiplication can be used to group digits of equal place-value together, giving <tt>(1 + 3)*10 + (2 + 4)*1</tt>. I don't bring up something so basic as an insult to the reader, the basis of that possibly-intimidating piece of code really is this basic.</p>
<p>The masks 0xAAAAAAAAAAAAAAAA, 0xCCCCCCCCCCCCCCCC, etc, represent nothing more than the numbers 0..63, each written vertically in binary, starting with the least significant bit. Each column of bits contains its own index. AND-ing the masks with <tt>n</tt> leaves, in each column, either zero (if the corresponding bit of <tt>n</tt> was zero) or the index of that column.</p>
<p>The first <tt>popcnt</tt> then adds together all the digits of the "filtered" indexes with a place-value of 1, the second <tt>popcnt</tt> adds together the digits with a place-value of 2, and so on. The result of each <tt>popcnt</tt> is shifted to match the corresponding place-value, and all of them are added together.</p>
<h2>Generalizing the technique</h2>
<p>The numbers in the columns did not have to be 0..63, the columns can contain arbitrary numbers, even negative numbers. This gives some sort of weighted <tt>popcnt</tt>: rather than each bit having a weight of 1, we are free to choose an arbitrary weight for each bit.</p>
<p>In general, starting with some arbitrary numbers, interpret them as a binary matrix. Transpose that matrix. Every row of the transposed matrix is the mask that goes into the corresponding <tt>popcnt</tt>-step.</p>
<p>Some simplifications may be possible:</p>
<ul>
<li>If a row of the matrix is zero, the corresponding step can be left out.</li>
<li>If two or more rows are equal, their <tt>popcnt</tt>-steps can be merged into one with a weight that is the sum of the weights of the steps that are merged.</li>
<li>If a row is a linear combination of some set of other rows, the <tt>popcnt</tt> corresponding to that row can be computed as a linear combination of the <tt>popcnt</tt>s corresponding to those other rows.</li>
<li>If a row has exactly one bit set, its step can be implemented with an AND and a shift, without <tt>popcnt</tt>, by shifting the bit to its target position.</li>
<li>If <i>every</i> row has exactly one bit set, we're really dealing with a bit-permutation and there are better ways to implement them.</li>
</ul>
<p>For example, let's construct "the sum of squares of indexes of set bits" (with 1-indexed indexes) aka <a href="https://oeis.org/A003995">oeis.org/A003995</a>. Transposing the squares 1,2,4,9..4096 gives these masks:</p>
<pre>0x5555555555555555
0x0000000000000000
0x2222222222222222
0x1414141414141414
0x0d580d580d580d58
0x0335566003355660
0x00f332d555a66780
0x555a5b6666387800
0x66639c78783f8000
0x787c1f807fc00000
0x7f801fff80000000
0x7fffe00000000000
0x8000000000000000
</pre>
<p>The second mask is zero because a square cannot be congruent to 2 or 3 modulo 4. The last mask has exactly one bit set, so its step can be implemented without <tt>popcnt</tt>. Putting it together:</p>
<pre>int A003995(uint64_t n)
{
return __popcnt64(n & 0x5555555555555555) +
(__popcnt64(n & 0x2222222222222222) << 2) +
(__popcnt64(n & 0x1414141414141414) << 3) +
(__popcnt64(n & 0x0d580d580d580d58) << 4) +
(__popcnt64(n & 0x0335566003355660) << 5) +
(__popcnt64(n & 0x00f332d555a66780) << 6) +
(__popcnt64(n & 0x555a5b6666387800) << 7) +
(__popcnt64(n & 0x66639c78783f8000) << 8) +
(__popcnt64(n & 0x787c1f807fc00000) << 9) +
(__popcnt64(n & 0x7f801fff80000000) << 10) +
(__popcnt64(n & 0x7fffe00000000000) << 11) +
((n & 0x8000000000000000) >> 51);
}</pre>
<p>Aside from implementing strange sequences from the OEIS, possible uses of this technique may include</p>
<ul>
<li>Summing the face-values of a hand of cards. Unfortunately this cannot take "special combinations" into account, unlike <a href="https://jonathanhsiao.com/blog/evaluating-poker-hands-with-bit-math">other techniques</a>.</li>
<li>A basic evaluation of a board state in Reversi/Othello, based on weights for each captured square that differ by their position.</li>
<li>Determining the price for a pizza based on a set of selected toppings, I don't know, I'm grasping at straws here.</li>
</ul>
<hr>
<h2>Addendum</h2>
<p>What if the input is small, and the weights are also small? Here is a different trick that is applicable in some of those cases, depending on whether the temporary result fits in 64 bits. The trick this time is much simpler, or at least sounds much simpler: for bit <tt>i</tt> of weight <tt>w[i]</tt>, make <tt>w[i]</tt> copies of bit <tt>i</tt>, then <tt>popcnt</tt> everything.</p>
<p>A common trick to make <tt>k</tt> copies of only one bit is <tt>(bit << k) - bit</tt>. Assuming that <a href="https://www.felixcloutier.com/x86/pdep">pdep</a> exists and is efficient, that trick can be generalized to making different numbers of copies of different bits. The simplest version of that trick would sacrifice one bit of padding per input bit, which may be acceptable depending on whether that all still fits in 64 bits. For example A073642 with a 10-bit input would work:</p>
<pre>// requires: n is a 10-bit number
int A073642_small(uint32_t n)
{
uint64_t top = _pdep_u64(n, 0x0040100808104225);
uint64_t bot = _pdep_u64(n, 0x000020101020844b);
return __popcnt64(top - bot);
}</pre>
<p>That can be extended to an 11-bit input like this:</p>
<pre>// requires: n is an 11-bit number
int A073642_small(uint32_t n)
{
uint64_t top = _pdep_u64(n >> 1, 0x0040100808104225);
uint64_t bot = _pdep_u64(n >> 1, 0x000020101020844b);
return __popcnt64(top - bot | top);
}</pre>
<p>Or like this</p>
<pre>// requires: n is an 11-bit number
int A073642_small(uint32_t n)
{
uint64_t top = _pdep_u64(n, 0x0040100808104225) >> 1;
uint64_t bot = _pdep_u64(n, 0x008020101020844b) >> 1;
return __popcnt64(top - bot);
}</pre>
<p><tt>pdep</tt> cannot move bits to the right of their original position, in some cases (if there is no space for padding bits) you may need to hack around that, as in the example above. Rotates and right-shift are good candidates to do that with, and in general you may gather the bits with non-zero weights with <tt>pext</tt>.</p>
<p>This approach relies strongly on being able to efficiently implement the step "make <tt>w[i]</tt> copies of bit <tt>i</tt>", it is probably not possible to do that efficiently using only the plain old standard integer operations. Also, <tt>pdep</tt> is not efficient on all processors that support it, unfortunately making the CPUID feature flag for BMI2 useless for deciding which implementation to use.</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-21990154374045465592022-07-22T07:04:00.003-07:002022-07-22T07:04:40.523-07:00Bit-level commutativity and "sum gap" revisited<style type="text/css">
.nobr {
white-space: nowrap;
}
blockquote {
background-color: #EEE;
clear: both;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
<script type="text/javascript">
MathJax.Hub.Config({
"HTML-CSS": {
scale: 100
}
});
</script>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>In an earlier post, <a href="https://bitmath.blogspot.com/2017/12/bit-level-commutativity.html">bit-level commutativity</a>, it was shown that addition is not only invariant under swapping the operands, but also under swapping individual bits between the operands, provided that they are of equal place-value. For example the least significant bits of the operands of a sum could be exchanged without changing the value of the sum. Bit-level commutativity of addition implies that <tt class="nobr">A + B = (A & B) + (A | B)</tt>.</p>
<p>In <a href="https://bitmath.blogspot.com/2020/09/the-range-sum-cannot-fall-within.html">the range a sum cannot fall within</a>, it was shown that the "gap" which a sum cannot fall within can be widened from:
$$A + B \begin{cases}
\ge \max(A,B) & \text{ if addition does not carry} \\
\le \min(A,B) & \text{ if addition does carry}
\end{cases}\tag{Eq1}$$
To:
$$A + B \begin{cases}
\ge A\:|\:B & \text{ if addition does not carry} \\
\le A\:\&\:B & \text{ if addition does carry}
\end{cases}\tag{Eq2}$$
This makes the gap wider because <tt>A & B</tt> is often less than <tt>min(A, B)</tt>, and <tt>A | B</tt> is often greater than <tt>max(A, B)</tt>.
</p>
<p>To start with, let's assume that
$$A + B \begin{cases}
\ge A & \text{ if addition does not carry} \\
\le A & \text{ if addition does carry}
\end{cases}\tag{Eq3}$$
Since addition is commutative, the roles of <tt>A</tt> and <tt>B</tt> could be exchanged without affecting the truth of equation Eq3, so this must also hold:
$$A + B \begin{cases}
\ge B & \text{ if addition does not carry} \\
\le B & \text{ if addition does carry}
\end{cases}\tag{Eq4}$$
If <tt>A + B</tt> is greater than or equal to both <tt>A</tt> and <tt>B</tt>, then it must also be greater than and equal to <tt>max(A, B)</tt>, and a similar argument applies to the upper bound. Therefore, Eq1 follows from Eq3.
</p>
<p>Substituting <tt>A</tt> and <tt>B</tt> with <tt>A & B</tt> and <tt>A | B</tt> (respectively) in Eq3 and Eq4 gives
$$(A\:\&\:B) + (A\:|\:B) \begin{cases}
\ge (A\:\&\:B) & \text{ if addition does not carry} \\
\le (A\:\&\:B) & \text{ if addition does carry}
\end{cases}\tag{Eq5}$$
And
$$(A\:\&\:B) + (A\:|\:B) \begin{cases}
\ge (A\:|\:B) & \text{ if addition does not carry} \\
\le (A\:|\:B) & \text{ if addition does carry}
\end{cases}\tag{Eq6}$$
Picking the "does carry" case from Eq5 and the "does not carry" case from Eq6, and using that <tt class="nobr">A + B = (A & B) + (A | B)</tt>, gives Eq2.
</p>
</div>
Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-57376692920487784482021-07-10T09:43:00.004-07:002021-11-17T07:42:52.013-08:00Integer promotion does not help performance<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>There is a rule in the C language which roughly says that arithmetic operations on short integer types implicitly convert their operands to normal-sized integers, and also give their result as a normal-sized integer. For example in C: </p>
<blockquote style="background: #f9f9f9; border-left: 10px solid #ccc; padding: 0.5em 10px;">If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.</blockquote>
<p>Various other languages have similar rules. For example C#, the specification of which is not as jargon-infested as the C specifiction:</p>
<blockquote style="background: #f9f9f9; border-left: 10px solid #ccc; padding: 0.5em 10px;">In the case of integral types, those operators (except the ++ and -- operators) are defined for the int, uint, long, and ulong types. When operands are of other integral types (sbyte, byte, short, ushort, or char), their values are converted to the int type, which is also the result type of an operation.</blockquote>
<p>There may be various reasons to include such a rule in a programming language (and some reasons <i>not</i> to), but one that is commonly mentioned is that "the CPU prefers to work at its native word size, other sizes are slower". There is just one problem with that: it is based on an incorrect assumption about how the compiler will need to implement "narrow arithmetic".</p>
<p>To give that supposed reason the biggest benefit that I can fairly give it, I will be using MIPS for the assembly examples. MIPS completely lacks narrow arithmetic operations.</p>
<h2>Implementing narrow arithmetic as a programmer</h2>
<p>Narrow arithmetic is often required, even though various languages make it a bit cumbersome. C# and Java both demand that you explicitly convert the result back to a narrow type. Despite that, code that needs to perform several steps of narrow arithmetic is usually <i>not</i> littered with casts. The usual pattern is to do the arithmetic without intermediate casts, then only in the end use one cast just to make the compiler happy. In C, even that final cast is not necessary.</p>
<p>For example, let's reverse the bits of a byte in C#. This code was written by Igor Ostrovsky, in his blog post <a href="http://igoro.com/archive/programming-job-interview-challenge/">Programming job interview challenge</a>. It's not a unique or special case, and I don't mean that negatively: it's good code that anyone proficient could have written, a job well done. Code that senselessly casts back to <tt>byte</tt> after every step is also sometimes seen, perhaps because in that case, the author does not really understand what they are doing.</p>
<pre>// Reverses bits in a byte
static byte Reverse(byte b)
{
int rev = (b >> 4) | ((b & 0xf) << 4);
rev = ((rev & 0xcc) >> 2) | ((rev & 0x33) << 2);
rev = ((rev & 0xaa) >> 1) | ((rev & 0x55) << 1);
return (byte)rev;
}</pre>
<p>Morally, all of the operations in this function are really narrow operations, but C# cannot express that. A special property of this code is that none of the intermediate results exceed the limits of a <tt>byte</tt>, so in a language without integer promotion it could be written in much the same way, but without going through <tt>int</tt> for the intermediate results.</p>
<h2>Implementing narrow arithmetic as a compiler</h2>
<p>The central misconception that (I think) gave rise to the myth that integer promotion helps performance, is the assumption that without integer promotion, the compiler must implement narrow operations by inserting an explicit narrowing operation after every arithmetic operation. But that's not true, a compiler for a language that lacks integer promotion can use the same approach that programmers use to implement narrow arithmetic in languages that do have integer promotion. For example, what if two bytes were added together (from memory, and storing the result in memory) in a hypothetical language that lacks integer promotion, and what if that code was compiled for MIPS? The assumption is that it will cost an additional operation to get rid of the "trash bits", but it does not:</p>
<pre> lbu $2,0($4)
lbu $3,0($5)
addu $2,$2,$3
sb $2,0($4)</pre>
<p>The <tt>sb</tt> instruction does not care about any "trash" in the upper 24 bits, those bits simply won't be stored. This is not a cherry-picked case. Even if there were more arithmetic operations, in most cases the "trash" in the upper bits could safely be left there, being mostly isolated from the bits of interest by the fact that carries only propagate from the least significant bit up, never down. For example, let's throw in a multiplication by 42 and a shift-left by 3 as well for good measure:</p>
<pre> lbu $6,0($4)
li $3,42
lbu $2,0($5)
mul $5,$3,$6
addu $2,$5,$2
sll $2,$2,3
sb $2,0($4)</pre>
<p>What is true, is that before some operations, the trash in the upper bits must be cleared. For example before a division, shift-right, or comparison. That is not an exhaustive list, but the list of operations that require the upper bits to be clean is shorter (and have less "total frequency") than the list of operations that do not require that, see for example <a href="https://stackoverflow.com/q/34377711/555045">which 2's complement integer operations can be used without zeroing high bits in the inputs, if only the low part of the result is wanted?</a> "Before some operations" is not the same thing as "after every operation", but that still sounds like an additional cost. However, the trash-clearing operations that a compiler for a language that lacks integer promotion would have to insert are not <i>additional</i> operations: they are the same ones that a programmer would write explicitly in a language with integer promotion.</p>
<p>It may be possible to construct a contrived case in which a human would know that the high bits of an integer are clean, while a compiler would struggle to infer that. For example, a compiler may have more trouble reasoning about bits that were cancelled by an XOR, or worse, by a multiplication. Such cases are not likely to be the reason behind the myth. A more likely reason is that many programmers are not as familiar with basic integer arithmetic as they perhaps ought to be.</p>
<h2>So integer promotion is useless?</h2>
<p>Integer promotion may prevent accidental use of narrow operations where wide operations were intended, whether that is worth it is another question. All I wanted to say with this post, is that "the CPU prefers to work at its native word size" is a bogus argument. Even when it is true, it is irrelevant.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-42880317106887453982021-06-09T21:50:00.004-07:002021-11-17T07:43:06.567-08:00Partial sums of blsi and blsmsk<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p><a href="https://www.felixcloutier.com/x86/blsi">blsi</a> is an x86 operation which extracts the rightmost set bit from a number, it can be implemented efficiently in terms of more familiar operations as <tt>i&-i</tt>. <a href="https://www.felixcloutier.com/x86/blsmsk">blsmsk</a> is a closely related operation which extracts the rightmost set bit and also "smears" it right, setting the bits to the right of that bit as well. <tt>blsmsk</tt> can be implemented as <tt>i^(i-1)</tt>. The smearing makes the result of <tt>blsmsk</tt> almost (but not quite) twice as high as the result of <tt>blsi</tt> for the same input: <tt>blsmsk(i) = floor(blsi(i) * (2 - Ξ΅))</tt>.</p>
<p>The partial sums of <tt>blsi</tt> and <tt>blsmsk</tt> can be defined as <tt>b(n) = sum(i=1, n, blsi(i))</tt> and <tt>a(n) = sum(i=1, n, blsmsk(i))</tt> respectively. These sequences are on OEIS as <a href="https://oeis.org/A006520">A006520</a> and <a href="https://oeis.org/A080277">A080277</a>. Direct evaluation of those definitions would be inefficient, is there a better way?</p>
<h3>Unrecursing the recursive definition</h3>
<p>Let's look at the partial sums of <tt>blsmsk</tt> first. Its OEIS entry suggests the recursion below, which is already significantly better than the naive summation:</p>
<p><tt>a(1) = 1, a(2*n) = 2*a(n) + 2*n, a(2*n+1) = 2*a(n) + 2*n + 1</tt></p>
<p>A "more code-ish"/"less mathy" way to implement that could be like this:</p>
<pre>int PartialSumBLSMSK(int i) {
if (i == 1)
return 1;
return (PartialSumBLSMSK(i >> 1) << 1) + i;
}</pre>
<p>Let's understand what this actually computes, and then find an other way to do it. The overal big picture of the recursion is that <tt>n</tt> is being shifted right on the way "down", and the results of the recursive calls are being shifted left on the way "up", in a way that cancels each other. So in total, what happens is that a bunch of "copies" of <tt>n</tt> is added up, except that at the kth step of the recursion, the kth bit of <tt>n</tt> is reset.</p>
<p>This non-tail recursion can be turned into tail-recursion using a standard trick: turning the "sum on the way"-logic into "sum on the way down", by passing two accumulators in extra arguments, one to keep track of the sum, and an other to keep track of how much to multiply <tt>i</tt> by:</p>
<pre>int PartialSumBLSMSKTail(int i, int accum_a, int accum_m) {
if (i <= 1)
return accum_a + i * accum_m;
return PartialSumBLSMSKTail(i >> 1, accum_a + i * accum_m, accum_m * 2);
}</pre>
<p>Such a tail-recursive function is then simple to transform into a loop. Shockingly, GCC (even quite old versions) manages to compile <a href="https://godbolt.org/z/jTjz4vbG6">the original non-tail recursive function into a loop as well</a> without much help (just changing the left-shift into the equivalent multiplication), although some details differ.</p>
<p>Anyway, let's put in a value and see what happens. For example, if the input was <tt>11101001</tt>, then the following numbers would be added up:</p>
<pre>11101001 (reset bit 0)
11101000 (reset bit 1, it's already 0)
11101000 (reset bit 2)
11101000 (reset bit 3)
11100000 (reset bit 4)
11100000 (reset bit 5)
11000000 (reset bit 6)
10000000 (base case)</pre>
<p>Look at the columns of the matrix of numbers above, the column for bit 3 has four ones in it, the column for bit 5 has six ones in it. If bit <tt>k</tt> is set bit in <tt>n</tt>, there is a pattern that that bit is set in <tt>k+1</tt> rows.</p>
<h3>Using the pattern</h3>
<p>Essentially what that pattern means, is that <tt>a(n)</tt> can be expressed as the dot-product between <tt>n</tt> viewed as a vector of bits (weighted according to their position) (π·), and a constant vector (π¬) with entries 1, 2, 3, 4, etc, up to the size of the integer. For example for <tt>n=5</tt>, π· would be (1, 0, 4), and the dot-product with π¬ would be 13. A dot-product like that can be implemented with some bitwise trickery, by using bit-slicing. The trick there is that instead of multiplying the entries of π· by the entries of π¬ directly, we multiply the entries of π· by the least-significant bits of the entries of π¬, and then seperately multiply it by all the second bits of the entries of π¬, and so on. Multiplying every entry of π· at once by a bit of an entry of π¬ can be implemented using just a bitwise-AND operation.</p>
<p>Although this trick lends itself well to any vector π¬, I will use 0,1,2,3.. and add an extra <tt>n</tt> separately (this corresponds to factoring out the +1 that appears at the end of the recursive definition), because that way part of the code can be reused directly by the solution of the partial sums of <tt>blsi</tt> (and also because it looks nicer). The masks that correspond to the chosen vector π¬ are easy to compute: each <i>column</i> across the masks is an entry of that vector. In this case, for 32bit integers:</p>
<pre>c0 10101010101010101010101010101010
c1 11001100110011001100110011001100
c2 11110000111100001111000011110000
c3 11111111000000001111111100000000
c4 11111111111111110000000000000000</pre>
<p>The whole function could look like this:</p>
<pre>int PartialSumBLSMSK(int n)
{
int sum = n;
sum += (n & 0xAAAAAAAA);
sum += (n & 0xCCCCCCCC) << 1;
sum += (n & 0xF0F0F0F0) << 2;
sum += (n & 0xFF00FF00) << 3;
sum += (n & 0xFFFF0000) << 4;
return sum;
}</pre>
<p><tt>PartialSumBLSI</tt> works almost the same way, with its recursive formula being <tt style="white-space:nowrap;">b(1) = 1, b(2n) = 2b(n) + n, b(2n+1) = 2b(n) + n + 1</tt>. The +1 can be factored out as before, and the other part (<tt>n</tt> instead of <tt>2*n</tt>) is exactly half of what it was before. Dividing π¬ by half seems like a problem, but it can be done implicitly by shifting the bit-slices of the product to the right by 1 bit. There are no problems with bits being lost that way, because the least significant bit is always zero in this case (π¬ has zero as its first element).</p>
<pre>int PartialSumBLSI(int n)
{
int sum = n;
sum += (n & 0xAAAAAAAA) >> 1;
sum += (n & 0xCCCCCCCC);
sum += (n & 0xF0F0F0F0) << 1;
sum += (n & 0xFF00FF00) << 2;
sum += (n & 0xFFFF0000) << 3;
return sum;
}</pre>
<h3>Wrapping up</h3>
<p>The particular set of constants I used is very useful and appears in more tricks, such as <a href="https://branchfree.org/2018/05/22/bits-to-indexes-in-bmi2-and-avx-512/">collecting indexes of set bits</a>. They are the bitwise complements of a set of masks that Knuth (in The Art of Computer Programming volume 4, section 7.1.3) calls "magic masks", labeled Β΅<sub>k</sub>.</p>
<p>This post was inspired by <a href="https://stackoverflow.com/q/67854074/555045">this question on Stack Overflow</a> and partially based on my own answer and the answer of Eric Postpischil, without which I probably would not have come up with any of this, although I used a used a different derivation and explanation for this post.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-48282986168984613092020-09-26T06:27:00.006-07:002021-11-17T07:43:19.426-08:00The range a sum cannot fall within<style type="text/css">
.nobr {
white-space: nowrap;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>Throughout this post, the variables <tt>A</tt>, <tt>B</tt>, and <tt>R</tt> are used, with R defined as <tt>R = A + B</tt>, and <tt class="nobr">A β€ B</tt>. Arithmetic in this post is unsigned and modulo 2<sup>k</sup>. Note that <tt>A β€ B</tt> is not a restriction on the input, it is a choice to label the smaller input as <tt>A</tt> and the larger input as <tt>B</tt>. Addition is commutative, so this choice can be made without loss of generality.</p>
<h2><tt>R < A || R β₯ B</tt></h2>
<p>The sum is less than A <i>iff</i> the addition wraps (1), otherwise it has to be at least B (2).</p>
<ol>
<li><tt>B</tt> cannot be so high that the addition can wrap all the way up to or past <tt>A</tt>. To make <tt>A + B</tt> add up to <tt>A</tt>, <tt>B</tt> would have had to be 2<sup>k</sup>, which is one beyond the maximum value it can be. <tt class="nobr">R = A</tt> is possible only if <tt>B</tt> is zero, in which case <tt>R β₯ B</tt> holds instead.</li>
<li>Since <tt>A</tt> is at least zero, in the absence of wrapping there is no way to reduce the value below the inputs.</li>
</ol>
<p>Perhaps that all looks obvious, but this has a useful application: if the carry-out of the addition is not available, it can be computed via <tt class="nobr">carry = (x + y) < x</tt>, which is a relatively well-known trick. It does not matter which of <tt>x</tt> or <tt>y</tt> is the smaller or larger input, the sum cannot fall within the "forbidden zone" between them. The occasionally seen <tt class="nobr">carry = (x + y) < max(x, y)</tt> adds an unnecessary complication.</p>
<h2><tt>R < (A & B) || R β₯ (A | B)</tt></h2>
<p>This is a stronger statement, because <tt class="nobr">A & B</tt> is usually smaller than <tt>A</tt> and <tt class="nobr">A | B</tt> is usually greater than <tt>B</tt>.</p>
<p>If no wrapping occurs, then <tt class="nobr">R β₯ (A | B)</tt>. This can be seen for example by splitting the addition into a XOR and adding the carries separately, <tt class="nobr">(A + B) = (A ^ B) + (A & B) * 2</tt>, while bitwise OR can be decomposed similarly into <tt class="nobr">(A | B) = (A ^ B) + (A & B)</tt><sup>(see below)</sup>. Since there is no wrapping (by assumption), <tt class="nobr">(A & B) * 2 β₯ (A & B)</tt> and therefore <tt class="nobr">(A + B) β₯ (A | B)</tt>. Or, with less algebra: addition sometimes produces a zero where the bitwise OR produces a one, but then addition compensates doubly for it by carrying into the next position.</p>
<p>For the case in which wrapping occurs I will take a bit-by-bit view. In order to wrap, the carry out of bit <tt class="nobr">k-1</tt> must be 1. In order for the sum to be greater than or equal to <tt class="nobr">A & B</tt>, bit <tt class="nobr">k-1</tt> of the sum must be greater than or equal to bit <tt class="nobr">k-1</tt> of <tt class="nobr">A & B</tt>. That combination means that the carry <i>into</i> bit <tt class="nobr">k-1</tt> of the sum must have been 1 as well. Furthermore, bit <tt class="nobr">k-1</tt> of the sum can't be greater than bit <tt class="nobr">k-1</tt> of <tt class="nobr">A & B</tt>, at most it can be equal, which means bit <tt class="nobr">k-2</tt> must be examined as well. The same argument applies to bit <tt class="nobr">k-2</tt> and so on, until finally for the least-significant bit it becomes impossible for it to be carried into, so the whole thing falls down: by contradiction, <tt class="nobr">A + B</tt> must be less than <tt class="nobr">A & B</tt> when the sum wraps.</p>
<h2>What about <tt class="nobr">(A | B) = (A ^ B) + (A & B)</tt> though?</h2>
<p>The more obvious version is <tt class="nobr">(A | B) = (A ^ B) | (A & B)</tt>, compensating for the bits reset by the XOR by ORing exactly those bits back in. Adding them back in also works, because the set bits in <tt class="nobr">A ^ B</tt> and <tt class="nobr">A & B</tt> are <i>disjoint</i>: a bit being set in the XOR means that exactly one of the input bits was set, which makes their AND zero.</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-86301127161752818652020-08-03T06:22:00.001-07:002020-08-03T06:24:30.687-07:00Why does AND distribute over XOR<style>
blockquote {
background: #f9f9f9;
border-left: 10px solid #ccc;
margin: 1.5em 10px;
padding: 0.5em 10px;
quotes: "\201C""\201D""\2018""\2019";
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
blockquote p {
display: inline;
}
q {
background: #f9f9f9;
}
</style>
<div style="font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;">
<p>
AND distributes over XOR, unsurprisingly both from the left and right, that is:
</p>
<pre>x & y ^ z & y == (x ^ z) & y
x & y ^ x & z == x & (y ^ z)
a & c ^ a & d ^ b & c ^ b & d == (a ^ b) & (c ^ d)</pre>
A somewhat popular explanation for <i>why</i> is,
<blockquote cite="https://en.wikipedia.org/wiki/Exclusive_or#Properties">Conjunction and exclusive or form the multiplication and addition operations of a field GF(2), and as in any field they obey the distributive law.
<footer><a href="https://en.wikipedia.org/wiki/Exclusive_or#Properties">Wikipedia: Exclusive or#Properties</a></footer>
</blockquote>
<p>
Which is true and a useful way to think about it, but it is also the type of backwards explanation that relies on a concept that is more advanced than the thing which is being explained.
</p>
<h2>Diagrams with crossing lines</h2>
<p>
Let's represent an expression such as <tt>a & c ^ a & d ^ b & c ^ b & d</tt> by putting the variables on the left of every AND along the top of a grid, and the variables on the right of every AND along the side. Then for example the grid cell on the intersection between the column of <tt>a</tt> and the row of <tt>c</tt> corresponds to the term <tt>a & c</tt>. Further, let's draw lines for variables that are True, in this example all variables are True:
</p>
<img src="https://i.imgur.com/rvAKOhI.png" width="250"/>
<p>
The overall expression <tt>a & c ^ a & d ^ b & c ^ b & d</tt> counts the number of crossings, modulo 2. Rather than counting the crossings one by one, the number of crossings could be computed by counting how many variables along the top are True, how many along the side are True, and taking the product, again modulo 2. A sum modulo 2 is XOR and a product modulo 2 is AND, so this gives the equivalent expression <tt>(a ^ b) & (c ^ d)</tt>.
</p>
<p>
The simpler cases <tt>x & y ^ z & y</tt> and <tt>x & y ^ x & z</tt> correspond to 1x2 and 2x1 diagrams.
</p>
<h2>Diagrams with bites taken out of them</h2>
<p>
Such a diagram with a section of it missing can be dealt with by completing the grid and subtracting the difference. For example the unwieldy <tt>a & e ^ a & f ^ a & g ^ a & h ^ b & e ^ b & f ^ b & g ^ b & h ^ c & e ^ c & f ^ d & e ^ d & f</tt> (shown in the diagram below) is "incomplete", it misses the 2x2 square that corresponds to <tt>(c ^ d) & (g ^ h)</tt>. Completing the grid and subtracting the difference gives <tt>((a ^ b ^ c ^ d) & (e ^ f ^ g ^ h)) ^ ((c ^ d) & (g ^ h))</tt>, which <a href="http://haroldbot.nl/?q=a+%26+e+%5E+a+%26+f+%5E+a+%26+g+%5E+a+%26+h+%5E+b+%26+e+%5E+b+%26+f+%5E+b+%26+g+%5E+b+%26+h+%5E+c+%26+e+%5E+c+%26+f+%5E+d+%26+e+%5E+d+%26+f+%3D%3D+%28%28a+%5E+b+%5E+c+%5E+d%29+%26+%28e+%5E+f+%5E+g+%5E+h%29%29+%5E+%28%28c+%5E+d%29+%26+%28g+%5E+h%29%29">is correct</a>.
</p>
<img src="https://i.imgur.com/SfIxt6C.png" width="250"/>
<p>
This all has a clear connection to the FOIL method and its generalizations, after all <q>conjunction and exclusive or form the multiplication and addition operations of a field GF(2)</q>.
</p>
<p>
The same diagrams also show why AND distributes over OR (the normal, inclusive, OR), which could alternatively be explained in terms of the <a href="https://en.wikipedia.org/wiki/Two-element_Boolean_algebra">Boolean semiring</a>.
</p>
</div>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-36711275019684038902020-05-03T16:28:00.000-07:002020-05-03T16:42:18.175-07:00Information on incrementation<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
<script type="text/javascript">
MathJax.Hub.Config({
"HTML-CSS": {
scale: 100
}
});
</script>
<span style="font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;">
<h2>Defining <tt>increment</tt></h2>
<p>Just to avoid any confusion, the operation that this post is about is adding 1 (one) to a value: $$\text{increment}(x) = x + 1$$
Specifically, performing that operation in the domain of bit-vectors.</p>
<p>Incrementing is very closely related to <a href="https://bitmath.blogspot.com/2017/12/notes-on-negation.html">negating</a>. After all, <tt style="white-space: nowrap">-x = ~x + 1</tt> and therefore <tt style="white-space: nowrap">x + 1 = -~x</tt>, though putting it that way feel oddly reversed to me.</p>
<h2>Bit-string notation</h2>
<p>In bit-string notation (useful for analysing compositions of operations at the bit level), increment can be represented as: $$a01^k + 1 = a10^k$$</p>
<p>An "English" interpretation of that form is that an increment carries through the trailing set bits, turning them to zero, and then carries into the right-most unset bit, setting it.</p>
<p>That "do something special with the right-most unset bit" aspect of increment is the basis for various <a href="http://programming.sirrida.de/programming.html#rightmost_bits">right-most bit manipulations</a>, some of which were implemented in <a href="https://en.wikipedia.org/wiki/Bit_Manipulation_Instruction_Sets#TBM_(Trailing_Bit_Manipulation)">AMD Trailing Bit Manipulation (TBM)</a> (which has been discontinued).</p>
<p>For example, the right-most unset bit in <tt>x</tt> can be set using <tt style="white-space: nowrap">x | (x + 1)</tt>, which has a nice symmetry with the more widely known trick for unsetting the right-most set bit, <tt style="white-space: nowrap">x & (x - 1)</tt>.</p>
<h2>Increment by XOR</h2>
<p>As was the case with negation, there is a way to define increment in terms of XOR.
The bits that flip during an increment are all the trailing set bits and the right-most unset bit, the TBM instruction for which is <tt>BLCMSK</tt>.
While that probably does not seem very useful yet, the fact that <tt style="white-space: nowrap">x ^ (x + 1)</tt> takes the form of some number of leading zeroes followed by some number of trailing ones turns out to be useful.</p>
<p>Suppose one wants to increment a bit-reversed integer, a possible (and commonly seen) approach is looping of the bits from top the bottom and implementing the "carry through the ones, into the first zero" logic by hand. However, if the non-reversed value was <i>also</i> available (let's call it <tt>i</tt>), the bit-reversed increment could be implemented by calculating the number of ones in the mask as <tt style="white-space: nowrap">tzcnt(i + 1) + 1</tt> (or <tt style="white-space: nowrap">popcnt(i ^ (i + 1))</tt>) and forming a mask with that number of ones located at the desired place within an integer:
<pre>// i = normal counter
// rev = bit-reversed counter
// N = 1 << number_of_bits
int maskLen = tzcnt(i + 1) + 1;
rev ^= N - (N >> maskLen);</pre>
That may still not seem useful, but this enables an implementation of the <a href="https://en.wikipedia.org/wiki/Bit-reversal_permutation">bit-reversal permutation</a> (not a bit-reversal itself, but the permutation that results from bit-reversing the indices). The bit-reversal permutation is sometimes used to re-order the result of a non-auto-sorting Fast Fourier Transform algorithm into the "natural" order. For example,
<pre>// X = array of data
// N = length of X, power of two
for (uint32_t i = 0, rev = 0; i < N; ++i)
{
if (i < rev)
swap(X[i], X[rev]);
int maskLen = tzcnt(i + 1) + 1;
rev ^= N - (N >> maskLen);
}</pre>
This makes no special effort to be cache-efficient.</p>
</span>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-64281581720303151482019-10-10T07:43:00.000-07:002019-10-10T08:16:08.742-07:00Square root of bitwise NOT<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">
<p>The square root of bitwise NOT, if it exists, would be some function <i>f</i> such that <i>f(f(x)) = not x</i>, or in other words, <i>fΒ²(x) = not x</i>.
It is similar in concept to the <a href="https://en.wikipedia.org/wiki/Quantum_logic_gate#Square_root_of_NOT_gate_(%E2%88%9ANOT)">βNOT gate in Quantum Computing</a>, but in a different domain which makes the solution very different.</p>
<p>Before trying to find any specific <i>f</i>, it may be interesting to wonder what properties it would have to have (and lack).
<ul>
<li><i>f</i> must be bijective, because its square is bijective.</li>
<li><i>fΒ²</i> is an involution but <i>f</i> cannot be an involution, because its square would then be the identity.</li>
<li><i>f</i> viewed as a permutation (which can be done, because it has to be bijective) must be a <a href="https://en.wikipedia.org/wiki/Derangement">derangement</a>, if it had any fixed point then that would also be a fixed point in <i>fΒ²</i> and the <i>not</i> function does not have a fixed point.</li>
</ul>
</p>
<p><h2>Does <i>f</i> exist?</h2>
In general, a permutation has a square root if and only if the number of cycles of same even length is even. The <i>not</i> function, being an involution, can only consist of swaps and fixed points, and we already knew it has no fixed points so it must consist of only swaps. A swap is a cycle of length 2, so an even length. Since the <i>not</i> function operates on <i>k</i> bits, the size of its domain is a power of two, <i>2<sup>k</sup></i>. That almost always guarantees an even number of swaps, except when <i>k = 1</i>. So, the <i>not</i> function on a single bit has no square root, but for more than 1 bit there are solutions.</p>
<h2><i>f</i> for even k</h2>
<p>For 2 bits, the <i>not</i> function is the permutation <span>(0 3) (1 2)</span>. An even number of even-length cycles, as predicted. The square root can be found by interleaving the cycles, giving (0 1 3 2) or (1 0 2 3). In bits, the first looks like:</p>
<tt>
<table>
<tr><td>in</td><td>out</td></tr>
<tr><td>00</td><td>01</td></tr>
<tr><td>01</td><td>11</td></tr>
<tr><td>10</td><td>00</td></tr>
<tr><td>11</td><td>10</td></tr>
</table></tt>
<p>Which corresponds to swapping the bits and then inverting the lsb, the other variant corresponds to inverting the lsb first and then swapping the bits.</p>
<p>That solution can be applied directly to other even numbers of bits, swapping the even and odd bits and then inverting the even bits, but the square root is not unique and there are multiple variants. The solution can be generalized a bit, combining a step that inverts half of the bits with a permutation that brings each half of the bits into the positions that are inverted when it is applied twice, so that half the bits are inverted the first time and the <i>other</i> half of the bits are inverted the second time. For example for 32 bits, there is a nice solution in x86 assembly:
<pre style="background-color: #EBECE4">bswap eax
xor eax, 0xFFFF
</pre></p>
<h2><i>f</i> for odd k</h2>
<p>Odd k makes things less easy. Consider k=3, so (0 7) (1 6) (2 5) (3 4). There are different ways to pair up and interleave the cycles, leading to several distinct square roots:
<ol>
<li>(0 1 7 6) (2 3 5 4)</li>
<li>(0 2 7 5) (1 3 6 4)</li>
<li>(0 3 7 4) (1 2 6 5)</li>
<li>etc..</li>
</ol>
</p>
<tt>
<table>
<tr><td>in</td><td>1</td><td>2</td><td>3</td></tr>
<tr><td>000</td><td>001</td><td>010</td><td>011</td></tr>
<tr><td>001</td><td>111</td><td>011</td><td>010</td></tr>
<tr><td>010</td><td>011</td><td>111</td><td>110</td></tr>
<tr><td>011</td><td>101</td><td>110</td><td>111</td></tr>
<tr><td>100</td><td>010</td><td>001</td><td>000</td></tr>
<tr><td>101</td><td>100</td><td>000</td><td>001</td></tr>
<tr><td>110</td><td>000</td><td>100</td><td>101</td></tr>
<tr><td>111</td><td>110</td><td>101</td><td>100</td></tr>
</table></tt>
<p>These correspond to slightly tricky functions, for example the first one has as its three from lsb to msb: the msb but inverted, the parity of the input, and finally the lsb. The other ones also incorporate the parity of the input in some way.</p>
</p>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-60850388386975823762018-10-03T03:39:00.000-07:002018-10-03T03:52:47.090-07:00abs and its "extra" result<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
<script type="text/javascript">
MathJax.Hub.Config({
"HTML-CSS": {
scale: 100
}
});
</script>
<span style="font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;">
<p>The <tt>abs</tt> function has, in its usual (most useful) formulation, one extra value in its codomain than just "all non-negative values". That extra value is the most negative integer, which satisfies <tt>abs(x) == x</tt> despite being negative. Even accepting that the absolute value of the most negative integer is itself, it may still seem strange (for an operation that is supposed to have such a nice symmetry) that the size of the codomain is not exactly half of the size of the domain.</p>
<p>That there is an "extra" value in the codomain, and that it is specifically the most negative integer, may be more intuitively obvious when the action of <tt>abs</tt> on the number <s>line</s> circle is depicted as "folding" the circle symmetrically in half across the center and through zero (around which <tt>abs</tt> is supposed to be symmetric), folding the negative numbers onto the corresponding positive numbers: </br>
<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyM9q977rkb9nIA2c8x_JiN7d0r3-TlgTQamv4eQEz88wCbiIbVXtz9-kzEvvryoVFHaMAkrbnURsAEnxGGZ1mIgl8q5OjWr_0q2fks18nkI4L2uwQJqkPk72xwwxZqreif-aXXv9kDW0/s1600/number+circle+mirror.png"/></p>
<p>Clearly both zero and the most negative integer (which is also on the "folding line") stay in place in such a folding operation and remain part of the resulting half-circle. That there is an "extra" value in the codomain is the usual fencepost effect: the resulting half-circle is half the size of the original circle in some sense, but the "folding line" cuts through two points that have now become endpoints.</p>
<p>By the way the "ones' complement alternative" to the usual <tt>abs</tt>, let's call it <tt>OnesAbs(x) = x < 0 ? ~x : x</tt> (there is a nice branch-free formulation too) <i>does</i> have a codomain with a size exactly half of the size of its domain. The possible results are exactly the non-negative values. It has to pay for that by, well, not being the usual <tt>abs</tt>. The "folding line" for <tt>OnesAbs</tt> runs <i>between</i> points, avoiding the fencepost issue:</br>
<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyrphieVgjTXm39EhuI7yUJ9sC2rZfBPmyNStWmjjTRd0n9uVI26DeWQG7mcyTJCd1vEq2urU8Erimk86_UdSdNIhBbTeYCpiZv3yV8LA6Uwyw_oEN8yEzjoiaq2lFjJo-fC-A83DlM0E/s1600/number+circle+mirror+cpl.png" /></p>
</span>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-18537902208846599142018-08-21T13:06:00.000-07:002018-08-21T13:18:11.594-07:00Signed wrapping is meaningful and algebraically nice<span style="font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;">
<div>
<ul>
<li><a href="#c1">Signed wrapping is not wrong</a></li>
<li><a href="#c2">Signed wrapping is meaningful</a></li>
<li><a href="#c3">Signed wrapping is not inherent</a></li>
<li><a href="#c4">Signed wrapping is algebraically nice</a></li>
</ul>
</div>
<p>In this post I defend wrapping, a bit more opinionated than my other posts.
As usual I'm writing from the perspective that signed and unsigned integer types are a thin transparent wrapper around bit vectors, of course I am aware that they are often not used that way. That difference between their use and their actual nature is probably the source of the problems.</p>
<a name="c1"></a>
<h2>Signed wrapping is not wrong</h2>
<p>It is often said that when signed wraparound occurs, the result is simply wrong.
That is an especially narrow view to take, probably inspired by treating fixed-size bit vector arithmetic as if it is arithmetic in β€, which it is not.
Bit vector arithmetic can be viewed as arithmetic in β€ so long as no "overflow" occurs, but violating that condition does not make the result wrong, it makes the interpretation wrong.
</p>
<a name="c2"></a>
<h2>Signed wrapping is meaningful</h2>
<p>The wrapping works exactly the same as unsigned wrapping, it corresponds to taking the lowest k bits of the arbitrary precision result.
Such a truncation therefore gives you exactly k meaningful bits, it's just a slice of the result.
Some upper bits may be lost, they can be calculated if you need them.
If the whole result is meaningful, then part of it is too, namely <i>at least</i> under the interpretation of being "part of the result".</p>
<p>An other well known example of benign wrapping is the calculation of the average of two non-negative signed integers.
While <tt>(a + b) / 2</tt> gives inconvenient results when the addition "overflows", <tt>(uint)(a + b) / 2</tt> (using unsigned division) or <tt>(a + b) >>> 1</tt> (unsigned right shift as in Java) are correct even when the addition of two positive values results in a negative value. An other way to look at it is that there is no <i>unsigned</i> wrapping. Nominally the integers being added here are signed but that doesn't really matter. Casting the inputs to unsigned before adding them is a no-op that can be performed mentally.</p>
<p>Wrapping can also sometimes be cancelled with more wrapping.
For example, taking an absolute value with wrapping and casting the result to an unsigned type of the same width results in the actual absolute value without the funny <tt>int.MinValue</tt> edge case:</p>
<pre>(uint)abs(int.MinValue) =
(uint)abs(-2147483648) =
(uint)(-2147483648) =
2147483648</pre>
<p>This is <i>not</i> what <a href="https://docs.microsoft.com/en-us/dotnet/api/system.math.abs?view=netframework-4.7.2#System_Math_Abs_System_Int32_"><tt>Math.Abs</tt></a> in C# does, it throws, perhaps inspired by its signed return type.
On the other hand, Java's <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#abs-int-"><tt>Math.abs</tt></a> gets this right and leaves the reinterpretation up to the consumer of the result, of course in Java there is no uint32 to cast to but you can still treat that result <i>as if</i> it is unsigned.
Such "manual reinterpretation" is in general central to integer arithmetic, it's really about the bits, not the "default meaning" of those bits.</p>
<p>The principle of cancelling wrapping also has some interesting data structure applications.
For example, in a Fenwick tree or Summed Area Table, the required internal integer width is the desired integer width of any range/area-sum query that you actually want to make.
So a SAT over signed bytes can use an internal width of 16 bits as long as you restrict queries to an area of 256 cells or fewer, since 256 * -128 = -2<sup>15</sup> which still fits a signed 16 bit word.</p>
<p>An other nice case of cancelled wrapping is strength reductions like <tt>A * 255 = (A << 8) - A</tt>.
It is usually not necessary to do that manually, but that's not the point, the point is that the wrapping is not "destructive".
The overall expression wraps only <i>iff</i> <tt>A * 255</tt> wraps and even then it has exactly the same result.
There are cases in which the left shift experience "signed wrapping" but <tt>A * 255</tt> does not (for example, in 32 bits, A = 0x00800000), in those cases the subtraction also wraps and brings the result back to being "unwrapped".
That is not a coincidence nor an instance of two wrongs making a right, it's a result of the intermediate wrapped result being meaningful and wrapping being algebraically nice.</p>
<a name="c3"></a>
<h2>Signed wrapping is not inherent</h2>
<p>Signed and unsigned integers are two different ways to interpret bit vectors.
Almost all operations have no specific signed or unsigned version, only a generic version that does both.
There is no such thing as signed addition or unsigned addition, addition is just addition.
Operations that are actually different are:
<ul>
<li>Comparisons except equality</li>
<li>Division and remainder</li>
<li>Right shift, maybe, but arithmetic right shift and logical right shift can both be reasonably applied in both signed and unsigned contexts</li>
<li>Widening conversion</li>
<li>Widening multiplication</li>
</ul>
One thing almost all of these have in common is that they cannot overflow, except division of the smallest integer by negative one.
By the way I regard that particular quirk of division as a mistake since it introduces an asymmetry between dividing by negative one and multiplying by negative one.</p>
<p>The result is that the operations that can "overflow" are neither signed nor unsigned, and therefore do not overflow specifically in either of those ways.
If they can be said to overflow at all, when and how they do so depends on how they are being viewed by an outside entity, not on the operation itself.</p>
<p>The distinction between unsigned and signed wrapping is equivalent to imagining a "border" on the <a href="http://bitmath.blogspot.com/2017/08/visualizing-addition-subtraction-and.html">ring of integers</a> (not the mathematical Ring of Integers) either between 0 and -1 (unsigned) or between signed-smallest and signed-highest numbers, but <i>there is no border</i>. Crossing either of the imaginary borders does not mean nearly as much as many people think it means.</p>
<a name="c4"></a>
<h2>Signed wrapping is algebraically nice</h2>
<p>A property that wrapping arithmetic shares with arbitrary precision integer arithmetic, but not with trapping arithmetic, is that it obeys a good number of desirable algebraic laws.
The root cause of this is that β€/β€2<sup>k</sup> is a <a href="https://en.wikipedia.org/wiki/Ring_(mathematics)">ring</a>, and trapping arithmetic is infested with implicit conditional exceptions.
Signed arithmetic can largely be described by β€/β€2<sup>k</sup>, like unsigned arithmetic, since it is mostly a reinterpretation of unsigned arithmetic.
That description does not cover all operations or properties, but it covers the most important aspects.</p>
<p>Here is a small selection of laws that apply to wrapping arithmetic but not to trapping arithmetic:
<ul>
<li>-(-A) = A</li>
<li>A + -A = 0</li>
<li>A - B = A + -B</li>
<li>A + (B + C) = (A + B) + C</li>
<li>A * (B + C) = A * B + A * C</li>
<li>A * -B = -A * B = -(A * B)</li>
<li>A * (B * C) = (A * B) * C</li>
<li>A * 15 = A * 16 - A</li>
<li>A * multiplicative_inverse(A) = 1 (iff A is odd, this is something not found in β€ which has only two trivially invertible numbers, so sometimes wrapping gives you a new useful property)</li>
</ul>
Some laws also apply to trapping arithmetic:
<ul>
<li>A + 0 = A</li>
<li>A - A = 0</li>
<li>A * 0 = 0</li>
<li>A * 1 = A</li>
<li>A * -1 = -A</li>
<li>-(-(-A)) = -A</li>
</ul>
</p>
<p>The presence of all the implicit exceptional control flow makes the code very hard to reason about, for humans as well as compilers.</p>
<p>Compilers react to that by not optimizing as much as they otherwise would, since they are forced to preserve the exception behaviour.
Almost anything written in the source code must actually happen, and in the same order as originally written, just to preserve exceptions that are not even supposed to ever actually be triggered.
The consequences of that are often seen in Swift, where code using the <tt>&+</tt> operator is optimized quite well (including auto-vectorization) and code using the unadorned <tt>+</tt> operator can be noticeably slower.</p>
<p>Humans .. probably don't truly want trapping arithmetic to begin with, what they want is to have their code checked for unintended wrapping.
Wrapping is not a bug by itself, but <i>unintended</i> wrapping is.
So while canceling a "bare" double negation is not algebraically justified in trapping arithmetic, a programmer will do it anyway since the goal is not to do trapping arithmetic, but removing bad edge cases.
Statically checking for unintended wrapping would be a more complete solution, no longer relying on being lucky enough to dynamically encounter every edge case.
Arbitrary precision integers would just remove most edge cases altogether, though it would rely heavily on range propagation for performance, making it a bit fragile.</p>
<p>But anyway, wrapping is not so bad. Just often unintended.</p>
</span>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.comtag:blogger.com,1999:blog-1465986942435538208.post-44889289559536761212018-08-02T03:07:00.001-07:002019-01-07T13:28:56.721-08:00Implementing Euclidean division<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
<script type="text/javascript">
MathJax.Hub.Config({
"HTML-CSS": {
scale: 100
}
});
</script>
<span style="font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;">
<p>While implementing various kinds of division in <a href="http://haroldbot.nl">haroldbot</a>, I had to look up/work out how to implement different kinds of signed division in terms of unsigned division. The common truncated division (written as <tt>/s</tt> in this post and in haroldbot, <tt>/t</tt> in some other places) is natural result of using your intuition from β and writing the definition based on signs and absolute values, ensuring that the division only happens between non-negative numbers (making its meaning unambiguous) and that the result is an integer: $$\DeclareMathOperator{\sign}{sign} D /_s d = \sign(d)\cdot\sign(D)\cdot\left\lfloor\cfrac{\left|D\right|}{\left|d\right|}\right\rfloor$$ That definition leads to a plot like this, showing division by 3 as an example:<p><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoo889DAAJyXhc0SKL5g4T81a91B_J4HH-RoyXJgWJB0uWmPVKHBlUYfwv0lH4l5QCTLzEHhvRCYOk4bSGgcTIH6_H239PH7E-iBk6snJjqkSUDVBtaSRJIb2AYuui5t8AvQVJdAowDf8/s400/truncated_div_plot1.png" /><p/>
Of course the absolute values and sign functions create symmetry around the origin, and that seems like a reasonable symmetry to have. But that little plateau around the origin often makes the mirror at the origin a kind of barrier that you can run into, leading to the well-documented downsides of truncated division.</p>
<p>The alternative floored division and Euclidean division have a different symmetry, which does not lead to that little plateau, instead the staircase pattern simply continues:<p><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCzCJhT_pUeW8Y7eI3JNk9GZEKAorNbTyrRkKO9hVdKZ9Ohqf3kQp7875UmA68ZNqG3k1Ndu-JZyVCDbZ3GHLRUZgVklRweByoD_XPVHuo7lxm1WW5jV7fti_3HOPF1IUHj0D73-TeJPs/s400/euclidean_div_plot1.png" /></p>
The point of symmetry, marked by the red cross, is at (-0.5, -0.5). Flipping around -0.5 may remind you of bitwise complement, especially if you have read my earlier post <a href="http://bitmath.blogspot.com/2017/08/visualizing-addition-subtraction-and.html">visualizing addition, subtraction and bitwise complement</a>, and mirroring around -0.5 is no more than a conditional complement. So Euclidean division may be implemented with positive division as: $$\DeclareMathOperator{\sgn}{sgn} \DeclareMathOperator{\xor}{\bigoplus} D /_e d = \sign(d)\cdot(\sgn(D)\xor\left\lfloor\cfrac{D\xor\sgn(D)}{\left|d\right|}\right\rfloor)$$ Where the <tt>sgn</tt> function is -1 for negative numbers and 0 otherwise, and the circled plus is XOR. XORing with the <tt>sgn</tt> is a conditional complement, with the inner XOR being responsible for the horizontal component of the symmetry and the outer XOR being responsible for the vertical component.</p>
<p>It would have been even nicer if the symmetry of the divisor also worked that way, but unfortunately that doesn't quite work out. For the divisor, the offset introduced by mirroring around -0.5 would affect the size of the steps of the staircase instead of just their position.</p>
<p>The <tt>/e</tt> and <tt>%e</tt> operators are available in haroldbot, though like all forms of division the general case is really too hard, even for the circuit-SAT based truth checker (the BDD engine stands no chance at all).</p>
</span>Haroldhttp://www.blogger.com/profile/16934800558256607460noreply@blogger.com