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.
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:
- The value whose bits are being permuted.
- The bits that are being permuted.
- The indices of those bits.
- The bits of those indices.
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.
Actually we don’t need to go deeper.
The building blocks
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 a,b,c where a locates a QWORD in the 512-bit register, b locates a byte within that QWORD, and c locates a bit within that byte.
Here are some nice building blocks (given fairly arbitrary names):
- Pf(a,b,c) = a,b,f(c) aka “right GF2P8AFFINEQB”, where f is any mapping from a 3-bit integer to a 3-bit integer. This building block can be implemented with _mm512_gf2p8affine_epi64_epi8(input, _mm512_set1_epi64(f_as_a_reversed_matrix), 0)
- Qf(a,b,c) = a,f(c),~b aka “left GF2P8AFFINEQB”, where ~b is a 3-bit inversion, equivalent to 7 - b. f 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 _mm512_gf2p8affine_epi64_epi8(_mm512_set1_epi64(f_as_a_matrix), input, 0)
- Sg(a,b,c) = g(a,b),c aka