Importance of compiler optimizations
- A large fraction of most CS courses, programming, data structures, algorithms, operating systems, etc., is about efficiency and optimization.
- Optimizations can be crudely categorized as: algorithm-level (study in data structures and algorithms courses), system-level (study in OS and other systems courses), and code-level optimizations (do not really study them, but a large fraction of developers spend their time on this).
- Compiler optimizations is a meta-level, or a higher-level subject, that aims is to automate these optimizations. This is an ambitious, yet desirable, pursuit.
- Most compiler optimizations today only target code-level optimizations. Code-level optimizations are often closely dependent on the characteristics of the underlying hardware. And so, most compiler developers are usually engaged in code-level optimization discussions, not the grander possibilities mentioned above.
- The root of code-level optimizations is due to the occurrence of redundancy in machine architecture and ISA --- the same function can be implemented in multiple ways. We can draw a venn diagram of all the implementations that may implement a function "f"; the job of an optimizer is to pick an implementation that is better than many others. Ideally, we would want the best possible implementation, but that is a very hard problem to achieve, not just automatically, but even manually. And so, we currently just pick an implementation that is in the neighborhood of the original implementation, but is expected to be better. The number of implementations that are left unexplored is huge, and a very powerful compiler should be able to explore and pick among all of them.
- In fact, if we order the equivalent programs from slowest to fastest, there are an infinite number of programs that are slower than the naive implementation, but only a finite number of programs that are faster than the naive implementation. Yet, the finite number is intractably large.
- In that perspective, compiler optimizations has been a very important area for the past 40 years, and in my view, it will continue to remain a very important area going forward.
Example program
Used the following program to illustrate the effect of optimizations.
//counts the number of ones in an array of booleans
sum=0;
for (i = 0; i < n; i++) {
if (a[i]) sum++;
}
return sum;
Why is profile-guided optimization not the norm?
- Given the large improvements that are possible, why do we not provide the compiler with all the workload information?
- For example, every mature project has a test-suite of unit and integration tests, a compiler should accept the command to run these tests, and then identify the best possible optimization?
- While compilers support this today, this is not the de-facto method of usage, in fact, very few projects use the compiler in this mode.
- The reason is that compilers are incredibly complex, and they are rapidly increasing in complexity. The profile-guided optimization support, while improving, remains weak today. That is not to say that we will not likely see more adoption of profile-guided optimization in the future, e.g., ten years from now, as the support becomes more mature.
History and future of compilers
- Recently, Linus Torvalds was asked to comment on how LLMs have made coding 10x easier. He retorted that compilers made coding 1000x easier, and nobody talks about that.
- In the early days of computing, most programs, e.g., mainframe server, were written in assembly. People would carefully write assembly functions, maintain docs on which register stores which values for which function, create assembly-level abstractions to be able to manage the complexity, and so on.
- Higher-level PLs and compilers were thought-of as tools for amateur programmers, who do not care about efficiency and performance. But it soon became the de-facto way of programming thereafter, with very few assembly programmers left today. Today's compilers can be thought-of as matching or exceeding the generated code quality of handwritten code.
- One of the key features of a compiler is our trust in its correctness. A compiler is expected to produce executable code that behaves exactly as the source code, and almost nobody inspects the executable code to check for correctness. This is one major reason why compilers improve productivity by 1000x, future solutions that try to meet this adoption and productivity bar, must also focus heavily on correctness.
- Before 1990s, compilers were developed by individual teams (usually within companies), e.g., a team of twenty people. After 1990s, open source movements gathered stream, and so hundreds of developers started contributing to an ecosystem like GCC. While it was at around 100K SLOC in 1990, today it is at 20 MLOC, growing at 2MLOC every three years.
- This complexity is due to the addition of more optimization logic --- solving a larger chunk of the problem we discussed above. Even after all this, large swaths of the problem space remain untouched.
- I think there is a way to make progress on this problem, using a combination of AI and FM.
Overview of a compiler
- While COL728 is not a pre-requisite to this course, it certainly provides a perspective for this course.
- Made up of parser (to generate Abstract Syntax Tree), Semantic Analyzer (for type checking), IR generation, Optimization over the IR, and finally Code generation and optimization to executable code.
- This code will mostly concern itself with the optimization at the IR level, and the code generation and optimization to executable code, although optimizations are present at all stages.
- Types are constraints imposed by a program on her own code, because the programmer does not trust himself or herself to always write correct code; and so the compiler checks that the code maintains the constraints specified by the programmer. These include built-in types (int, float, ..), and user-defined types (classes, enumerations, ..).
- In addition to correctness, types help with optimization opportunity, as they inform the compiler about these constraints, e.g., if a compiler knows that a member variable of a class is private, and has access to all the member functions, it can optimize much better.