Superoptimization is the process where a compiler automatically finds the optimal sequence for a loop-free sequence of instructions.
Compilers are good at making mechanical transformations that make code faster. Unfortunately, most of them are not as good at non-trivial reasoning, so they can’t invent a completely novel approach to solving your problem.
When designing a library for floating-point calculations, I had to implement a function that takes a float
This can be implemented with bit twiddling, but the straightforward algorithm breaks for powers of two, so I had to insert an if:
if f32::from_bits(x.to_bits() & !f32::MANTISSA_MASK) == x { // is x a power of two?
return x; // x itself is the lowest bit
} else {
return x - f32::from_bits(x.to_bits() & (x.to_bits() - 1)); // x, minus x without lowest bit
}
This if really annoyed me. It’s slowed down for the common case, and branches could be mispredicted. I wanted to find a formula that automatically handled powers of two, but try as I might, I couldn’t find anything convincingly optimal.
So I made a non-orthodox play.
What if there was a way to find such a formula automatically? No, not by spawning an LLM swarm, but by brute-forcing all possible formulas. LLMs can miss stuff or overindex on popular patterns; brute-force can’t.
But how do we check if a formula is correct? Theorem provers are too slow, but we can design a few representative test cases and hope that whatever formula we find is correct. And if it has a counterexample, just add it to the list of test cases and repeat.
This is not as insane as it sounds: by this moment I had already implemented an algorithm with
There are
But we also need to choose the inputs to each operation, which can be either the input value float binary representation, and not completely random ones, reducing the list to
To implement the search efficiently, I build up the formula set inductively.
Formulas with
for n_ops in 1..N_OPS {
let mut new_exprs = FxHashSet::default();
for a_ops in 0..n_ops {
let b_ops = n_ops - 1 - a_ops;
let a_exprs = &exprs_per_op_count[a_ops];
let b_exprs = &exprs_per_op_count[b_ops];
for a_expr in a_exprs {
for b_expr in b_exprs {
for op in OPS {
// New expression: `a_expr op b_expr`
new_exprs.insert(Expr {
values: core::array::from_fn(|i| {
op(a_expr.values[i], b_expr.values[i])
}),
});
}
}
}
}
}
Induction allows us to prune duplicates. For example,
To save time and memory, I avoid deduplication on the last iteration and output all matching candidates. Memory usage matters because I had
So, did this exhaustive search find anything interesting? Yes! There were several formulas with
But this approach has a limitation: it doesn’t take into account that calculations can be reused. So, for example, if x & 1 is used in two places, it will count that as two operations, not one. I was hoping that one of the
let a = x.to_bits() & !f32::MANTISSA_MASK; // x without mantissa bits
return f32::from_bits(x.to_bits() & (a - x.to_bits())) - f32::from_bits(a);
It doesn’t make much sense at first glance, and it’s not supposed to.
The reason why only this formula can reuse a is the subexpression a - x.to_bits(). It’s equivalent to -(x & f32::MANTISSA_MASK), which is how other formulas refer to it. All formulas compute x & !f32::MANTISSA_MASK and x & f32::MANTISSA_MASK, but this one rewrites the latter as x - (x & f32::MANTISSA_MASK), and then merges that with negation to save one operation!
I was flabbergasted when I realized it found such a clever rewrite. You don’t need an LLM to be amazed by what computers can invent! And it doesn’t destroy industries.
Anyway, this code now lives in my work-in-progress FPU emulator and I’m proud of it.