Many of us have considered it a long-term goal to make whole-module optimizations the default compilation mode for Release builds. I believe that we are ready and that we should enable WMO by default. WMO solves real problems but comes at a cost. Let’s discuss this and make a decision.
Today, each swift file is compiled and optimized independently. Compiling each file independently makes our compile times relatively fast because multiple files are optimized in parallel on different CPU cores, and only files that have been modified need to be recompiled. Both Debug and Release builds use this technique.
However, the current compilation mode is not that great for the performance of the generated code. File boundaries are optimization barriers. Swift users found that when they develop generic data structures (like Queue<T>) the performance of these data structures was not ideal, because the compiler was not able to specialize the generics into concrete types (turn T into an Int for all uses of Queue<T>). Our recommendation was to either copy the generic data structures into the files that use them or enable WMO.
With WMO all of the files in the module are optimized together and the optimizer is free to specialize generics and inline functions across file boundaries. This compilation mode is excellent for performance, but it comes at a cost. Naturally, WMO builds take longer because the compiler can’t parallelize the build on multiple cores. Also, every change in a single files makes the compiler re-optimize the whole program and not only the file what was modified.
We’ve been working to improve our WMO builds in a number of ways. Erik (@eeckstein) found that we spent most of our compile time in code generation optimizations (such as instruction selection and register allocations) inside LLVM, and was able to split the Swift module into multiple units that LLVM can compile in parallel. He also implemented cacheing at the LLVM level that improved the performance of incremental builds. Unrelated to the work on WMO, we also improved the speed of the optimizer by tuning our optimization pipeline and the different optimizations - and in the last three months we improved the overall compile time of optimized builds by ~10%.
Another concern was the increase in code size. In WMO mode we are free to specialize generics and inline code between files. These optimizations can increase the size of the generated program significantly. We also made a huge progress on this front. We were able to reduce the size of the Swift dylib that builds with WMO from 4.6MB in January to 3.7MB today.
We should not enable WMO for Debug builds. We don’t do any major optimizations on Debug builds and compiling things in WMO will only slow the compile times and not provide any speedups. We strive to make Swift Debug builds as fast as scripts (and intend to allow people to use #!/bin/swift), so unjustified reduction in compile time is unacceptable.
To summarize, WMO is critical to the performance of optimized Swift programs. WMO increases code size and compile times but we’ve made excellent progress and we’ll continue to invest in these areas. I believe that considering the inherit tradeoffs, enabling WMO is a good idea.
Please let me know what you think.