# Fast Permutation Compression

*All code variations can be found at https://github.com/egonelbre/exp/blob/master/permutation/code.go.*

Given a permutation what is the best way to convert it into a smaller data-structure?

Let’s say you have a permutation of 12 items. A trivial approach would be to store each item in a separate byte. This of course works, but can we do better?

```
1
``` | type Permutation [12]byte |

It’s easy to see that the maximum number we need to store is 12, but the byte can store 256 entries. By fitting it into 4 bits, it would be possible to store it in `12*4 = 48 bits`

.

1 2 3 4 5 6 7 | func PackNybble(perm Permutation) uint64 { r := uint64(0) for _, v := range perm { r = r<<4 + uint64(v) } return r } |

We can actually do better, by encoding using base 12 directly, instead of rounding it to 16.

1 2 3 4 5 6 7 | func PackBase12(perm Permutation) uint64 { r := uint64(0) for _, v := range perm { r = r*12 + uint64(v) } return r } |

This will end up using `log2(12^12) = 43.0195 bits`

. We know that there are exactly `12!`

permutations. This would mean that in the perfect world, we could pack all of this into `log2(12!)=28.835bits`

.

Some basic permutation theory gives us that we can assign a unique index to each permutation and convert it to and back. One of such numbering is Lehmer code.

I found one description of the algorithm for converting a permutation to code from here https://www.researchgate.net/figure/The-Lehmer-code-A-complete-translation-from-permutation-to-decimal-by-way-of-the_fig1_230831447

*We can imagine this as reducing the problem at each stage to a smaller problem. At first there are 12! possibilities for using the index. We know that for permutation of size 11, there are 11! of them. This means when we partition the permutations into 12 equal chunks, based on some criteria, figure out which chunk it belongs to and then multiply that by 11! then we get our first reduction. Each next step won’t spill over to the next partition, because they are smaller than 11!.*

The implementation required a little tinkering, but wasn’t anything too complicated.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | func Code(perm [12]byte) uint64 { r := uint64(0) for min := byte(0); min < 11; min++ { z := 0 n := base - min for i, v := range perm[:n] { if v == min { z = i break } } copy(perm[z:], perm[z+1:n]) r += uint64(z) * fact(11 - min) } return r } |

It really gives the smallest encoding possible, but compared to the just packing we have worse performance. I implemented few variations, to see whether it would be possible to improve upon it. However the first variant was pretty good already.

1 2 3 4 5 6 | BenchmarkCopy 199 ns/op BenchmarkCount 312 ns/op BenchmarkTable 284 ns/op BenchmarkTable2 524 ns/op BenchmarkPackNybble 17.3 ns/op BenchmarkPackNybbleUnroll2 4.94 ns/op |

So, it’s ~40 times slower than just packing. Can we do better?

Notice that we are able to pack the whole permutation into a uint64 with very low cost.

The core of the loop is the following:

1 2 3 | z := find the index of min in the permutation remove z from the permutation r += z * min! |

Bit-parallel search is a good way to find that index. Bit-parallel is easier to show in an example, let’s take a permutation `[1,2,3,0]`

and lets search for number `2`

.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | decimal 1 2 3 0 ---- ---- ---- ---- packed 0001 0010 0011 0000 mask 1101 1101 1101 1101 f1 := packed ^ mask 1101 1111 1110 1101 f2 := f1 >> 2 11 0111 1111 1111 f3 := f1 & f2 0001 0111 1110 1101 f4 := f3 >> 1 000 1011 1111 0110 f5 := f4 & f3 0000 0001 1110 0100 f5 := f4 & f3 0000 0001 1110 0100 onemask 1 1 1 1 s := f5 & onemask 0000 0001 0000 0000 .... .... |

At step 03 we see the bit representation of that packed sequence. First we need a mask to detect number `2`

at all positions at the same time. We can use the complement of number `2`

and use xor for that. This leaves us with a result where only the matching nybble contains all ones. This can be seen as the result `f1`

.

Now we need to find the index, where all the numbers are one. For this we can and them all together using steps 7 to 10. Finally we need to remove all the other bits that aren’t relevant with Step 14.

Our result is the number of zeros after the number. Note that this result still needs to be divided by 4 to get the final result. Once we translate this idea into code, it looks roughly like this:

1 2 3 4 5 6 | mask := masks[min] filtered := perm ^ mask filtered &= filtered >> 2 filtered &= filtered >> 1 filtered &= onemask z4 := byte(bits.TrailingZeros64(filtered)) |

We can now need a way to remove that element from the sequence:

1 2 3 | upper := (perm >> (z4 + 4)) << z4 lower := perm &^ (^uint64(0) << z4) perm = upper | lower |

Here we construct the upper part, by cutting off the bottom part. And then get the lower part by masking out all the upper parts. Then we combine them back together.

Benchmarking the code gives us a speed up from ~200ns to ~120ns.

1 2 | BenchmarkCopy 199 ns/op BenchmarkCopyBit 123 ns/op |

The final (uncensored) code was this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func CodeCopyBit(perm [base]byte) int { return codebit(uint64(PackNybbleUnroll(perm))) } func codebit(perm uint64) int { r := 0 for min, mask := range masks { filtered := perm ^ mask filtered &= filtered >> 2 filtered &= filtered >> 1 filtered &= maskstarts z := byte(bits.TrailingZeros64(filtered)) upper := (perm >> (z + 4)) << z lower := perm &^ (fullmask << z) perm = upper | lower r += int(z/4) * reverse_factorial[min] } return r } |

When there aren’t many elements then using bit-parallel approaches can get rid of branches and reduce cycles. Of course, when we have much larger sequences, then this approach becomes more cumbersome to write and might also end up slower.

This probably can be made even faster by unrolling the whole loop. However, it’s sufficient for this time.

*PS: When there are very few elements then this whole thing can be replaced by a lookup table.*

Robert Clausecker suggested even better approaches in Reddit. His descriptions in StackOverflow are quite nice. His benchmarks are here https://github.com/fuzxxl/permcode.

1 2 3 4 | BenchmarkCopy 205.0 ns/op BenchmarkCopyBit 126.0 ns/op BenchmarkShuffle 69.2 ns/op BenchmarkShuffleUnroll 39.2 ns/op |

His best approach is doing a Fisher-Yates shuffle in reverse, which is brilliant. We can think of permuting an array `[0, 1, 2, 3 .. 11]`

as picking random number and eliminating them. By finding the “random” numbers used to generate that we get a sequence, where each “maximum random” is smaller than the previous. So maximally we can only get a sequence `[11, 10, 9, 8 .. 0]`

. We can convert that into a number using factoriadics (each element multiplied by the factorial).