Improving Compilation Times in Common Lisp with Macro Refactoring
Learn how macro design affects compilation times in Common Lisp projects.
― 6 min read
Table of Contents
Common Lisp is a programming language that allows developers to create their own tools and extensions through the use of Macros. However, when not designed carefully, these macros can lead to big problems, including slow compilation times. This article looks at a specific case where a large system faced significant delays when compiling its code and how the issues related to certain macros were fixed.
The Problem
In a large Common Lisp project, which we will refer to as "System X," the compilation process was taking an excessive amount of time. When the project was fully recompiled, it took over 33 minutes. This is particularly problematic because software developers often need to make quick changes and see the effects of those changes. If the recompilation takes too long, it can disrupt their workflow.
Upon investigation, it was found that several macros were causing this delay. The main issue was that these macros could lead to an exponential increase in the amount of code generated when they were expanded. This is called "exponential code blowup."
Understanding Macros
Macros in Common Lisp allow developers to write code that generates other code. This means you can create patterns that reduce repetition and improve clarity. However, if a macro is poorly designed, it can end up generating a lot more code than intended, especially when multiple layers of macros are involved. For instance, when a macro is used inside another macro, each usage can multiply the amount of code produced.
For example, let’s say you have a macro that is meant to wrap around some code but can be used multiple times in a nested fashion. Each time it is used, it might repeat parts of itself in a way that makes the final output much larger than the original code. This is what happened in System X.
The Impact of Slow Compilation
When the compilation time exceeds a reasonable limit, developers tend to avoid recompilation during their daily coding. Instead, they rely on automated systems to compile their changes overnight. This can result in changes going untested for longer periods, leading to bugs that crop up only after significant alterations have been made.
In large projects, tracking down which macros or changes caused an increase in compilation time can be a daunting task. It becomes difficult to pinpoint the source of issues because changes are often grouped together. The difficulties compound in projects with many people working on different parts of the codebase.
Identifying the Causes
To tackle the issues in System X, the specific macros responsible for the delay were examined. In one case, it was found that a certain macro was defined in a way that led to exponential expansions every time it was called. These expansions contained duplicated code due to how the macro was structured.
Solutions for Refactoring
Two main strategies were used to improve the situation.
First Strategy: Using Local Functions
The first method involved breaking up the problematic macros by introducing local functions. Instead of having the macro duplicate its code, a local function was created to handle the repeated parts of the code. This means that rather than repeating code multiple times, the macro would call this local function, significantly reducing the amount of code generated during compilation.
By organizing the code this way, the expansion size and complexity were lowered. The result was that the new version of the macro recompiled much faster. This approach helps to keep the macro understandable and avoids the pitfalls of generating too much code.
Second Strategy: Using PROGV
The second approach focused on using the progv special form instead of local functions. This method aimed to unify the code by creating a common structure for the macros, which would manage the context in which they operate. The challenge here was to ensure that the right values were assigned to certain variables at different times in the macro’s execution.
By rendering the use of macros simpler, the progv method also aimed to reduce the code's overall size. This approach helped to eliminate issues that arose from using multiple different branches within the macro.
Results of the Refactoring
After implementing these changes, the project saw a significant reduction in compilation time, dropping it from more than 33 minutes to around 5 minutes. This was not only a big time saver for developers but also improved the performance of the system as a whole.
The size of the compiled files also decreased dramatically, which is crucial for managing resources and loading times. The success of these techniques demonstrated how essential it is to carefully design macros in programming to avoid long-term issues.
Lessons Learned
From this experience, several important takeaways emerged:
Design Macros Carefully: When creating macros, it is important to consider their long-term impact on the codebase, especially in large projects. Poorly designed macros can lead to significant overhead down the line.
Test Incrementally: Regular testing of code changes helps in identifying performance issues early. By compiling frequently and checking for problems, potential issues can be addressed before they grow too large.
Monitor Compilation Times: Developers should keep an eye on how long their Compilations take. If times start to creep up, it may be time to revisit the macro design and refactor as needed.
Be Aware of Nesting: Using nested macros can create confusion and lead to larger code output. It's essential to manage how these macros call each other to prevent unnecessary code multiplication.
Collaboration and Communication: Working in teams requires good communication about code changes and their effects. Keeping everyone informed helps in addressing issues collaboratively.
Conclusion
This case study on System X illustrates the complexities involved with using macros in programming, particularly in Common Lisp. By understanding the implications of macro design, implementing effective refactoring strategies, and maintaining open lines of communication among developers, it is possible to create efficient code that avoids many of the pitfalls associated with exponential code expansion. The changes implemented not only improved the compilation time but also set a precedent for future development practices in similar projects.
Title: Notes on Refactoring Exponential Macros in Common Lisp
Abstract: I recently consulted for a very big Common Lisp project having more than one million lines of code (including comments). Let's call it "System X" in the following. System X suffered from extremely long compilation times; i.e., a full recompile took about 33:17 minutes on a 3.1 GHz MacBook Pro Intel Core i7 with SSD and 16 GBs of RAM, using ACL 10.1. It turns out that a number of macros were causing an exponential code blowup. With these macros refactored, the system then recompiled in 5:30 minutes - a speedup by a factor of ~ 6. In this experience report, I will first illuminate the problem, and then demonstrate two potential solutions in terms of macro refactoring techniques. These techniques can be applied in related scenarios.
Authors: Michael Wessel
Last Update: 2023-05-04 00:00:00
Language: English
Source URL: https://arxiv.org/abs/2305.02991
Source PDF: https://arxiv.org/pdf/2305.02991
Licence: https://creativecommons.org/licenses/by/4.0/
Changes: This summary was created with assistance from AI and may have inaccuracies. For accurate information, please refer to the original source documents linked here.
Thank you to arxiv for use of its open access interoperability.