My first superoptimizer
austinhenley.com
austinhenley.com
[1] https://github.com/zwegner/x86-sat
[2] https://people.cs.umass.edu/~aabhinav/Publications/Unbounded...
(The other two wins listed are "recognize the Hacker's Delight trick and replace it with the straight-forward implementation a human would write", which I like.)
My favorite trick when cutting down the search space of possible programs is to generate DAGs directly instead of an instruction sequence. This way, it's easier to write rules that avoid nonsensical programs and to some extent avoid generating multiple equivalent programs.
For example to avoid programs that overwrite calculations you make sure that each node in the generated DAG has something that depends on it.
In a suitable environment the DAG can then be executed directly without lowering to a sequence of instructions.
https://en.wikipedia.org/wiki/Binary_decision_diagram
(i first read about BDDs in Knuth's, "The Art of Computer Programming", Volume 4, Fascicle 1, then spent an enjoyable few weeks going down a rabbit hole of building a boolean function to decide if an input 2d grid of bits was a maze or not, then uniformly sampling mazes from it...)
Of course. The superoptimizer is too slow to use in the compiler itself.
-O4
Extreme optimization. GCC uses AI and quantum
computation to warp space-time and invent
entirely new paradigms to make your programs 2% faster.
Warning: May destroy your current universe.Related: I work on Souper (https://github.com/google/souper).
Feel free to reach out if anyone has questions!
It's a shame it's a bit hard to actually build these days. Its target is people working on compilers, not end users. But there are some odd cases where I'd really like to use it myself, like trying to get speedup in llama.cpp.
Are llama.cpp (and similar targets) made into standalone C++ source files without dependencies doing the heavy lifting?
One can now compile against cuBLAS and other GPU libraries to help take some of the load, but the core of the project is indeed to run on the CPU and do all of the work without dependencies.
Massalin (1987) [2] calls the first phase "probabilistic execution" and claims that nearly all of the functions which pass the PE test also pass the more rigorous Boolean verification test. Can you give any insight into the benefits of TT over more "automated" optimizations? I am curious if MLTT/HoTT is more suitable for certain compiler optimizations or offers additional expressive power for proving equivalence, or is the benefit mostly ergonomics?
Supercompilation is good at removing unused scaffolding and indirection, e.g. for code that's written defensively/flexibly, supporting a bunch of fallbacks, override hooks, etc. A common problem with supercompilation is increasing code size, since it replaces many calls to a single general-purpose function/method, with many inlined versions (specialised to various extents).
Superoptimisation is "bottom up": generating small snippets of code from scratch, stopping when it finds something that behaves the same as the original code.
It is based on the exhaustive search with backtracking, kind of what Prolog is best for, and given a function that takes an input vector of integers, it attempts to find the most optimal instruction sequence that produces an output integer.
First try - get every permutation, feed to some stable code-oriented LLM, ask to explain code and measure number of words.
To avoid generating useless things like "++--", you could have the optimizer generate instructions that are translated to BrainF operations. So instead of ">>>--", the optimizer would generate "Move +3; Add -2".
on another hand, there are many opportunities for optimization when compiling brainfuck to an actual real world instruction set
e.g. suppose in your program you want to load the integer 36 into the current cell
a naive way to do this is to increment 36 times, e.g. `++++++++++++++++++++++++++++++++++++`.
if you can't guarantee that the initial value of the current cell is zero, then you could zero it first: `[-]++++++++++++++++++++++++++++++++++++`
if we're optimizing for code size, not instruction count, we could express this as a more complex, compact program: `[-]>[-]++++++[-<++++++>]<` -- assuming it is OK to use the cell one to the right of the pointer as working storage. this will run slower than the less compact naive program as it executes more instructions.
if compiling to some reasonable instruction set, there's likely a single non-BF instruction that lets you load the constant 36 into a register.
extra credit: implementing the optimising brainfuck-to-something compiler in brainfuck itself
It won’t be efficient (but who cares about that in a true Turing machine?), but you can do binary/octal/decimal/whatever, using a series of cells each containing a number in the right range.
As an optimization, you can postpone any carries until you want to compare or print such numbers. Addition would ‘just’ loop over the cells in the numbers to be added, adding cells pairwise. For example, binary 11001 + 10101 would yield 21102.
Then you could try to superoptimize yourself a solution and compare how close the evolved program was to the superoptimized program.
Could also be interesting with other esoteric languages like befunge or malbolge.
Compared to deep learning, genetic algoritms in general feels like childs play!
As I understand it, there are recent advances in SAT solvers giving practical algorithms for much larger numbers of terms, but I'm much behind the state of the art at this point.
When you find a truly optimal solution, you end up with the least satisfying proof of correctness: "Here's this this inscrutable formula; I verified that it produces the right output for all possible inputs."
It also doesn't seem particularly interesting because it doesn't allow the programs to get input. Obviously that makes things much more difficult wrt to proving program equivalence.
In most cases, you just slice the program to isolate pure computation, and just optimize that.
Most traditional compiler optimizations stick to that as well, the exceptions to this rule are carefully engineered.
I have a simple example, starting from a “generate all possible combinations” python function, here: https://tech.davis-hansson.com/p/price-wars.html
To solve the superoptimization problem, imagine every possible memory state as a node in a graph, and each instruction as an edge between two states, the superoptimized program is the shortest path between the starting state and the end state.
You can even use A* which is very similar to Dijkstra's algorithm, but with a sense of direction, materialized by a heuristic function that estimates how close we are to the goal. Here, a simple heuristic could be the number of memory locations that are different to the goal divided by two, that because each instruction changes at most two memory locations.
Of course, dynamic programming is a space-time trade-off, so memory consumption is something to consider.
There's a feature coming to Copilot where you can select code in-editor and prompt on it directly.
ChatGPT: Good evening! Superoptimization, in the context of computer programming, refers to the process of finding the optimal or most efficient implementation of a piece of code for a given task or problem. It involves automatically searching through all possible combinations of instructions and their arrangements to produce the most efficient code in terms of speed, memory usage, or other specific criteria.
Superoptimization is typically performed by specialized tools or algorithms known as superoptimizers. These tools analyze the code and exhaustively explore the space of all possible code sequences, considering both the individual instructions and their order. The goal is to find the shortest or fastest sequence of instructions that achieves the desired output or behavior.
Superoptimization is a computationally expensive process since it involves searching a vast solution space. Due to the exponential nature of the problem, it is often only feasible for small code snippets or performance-critical sections of code. Additionally, superoptimization may not always be practical or necessary because modern compilers and optimization techniques can often generate highly efficient code automatically.
Superoptimization techniques have been primarily used in areas such as compiler design, code generation, and optimizing low-level code for specific architectures. They can be beneficial in situations where manual optimization is not feasible or when there is a need for extreme performance optimizations in constrained environments.
Superoptimization has been around for well over a decade.