Problem
The current interfaces for building/editing/querying DIExpressions are fragmented, low-level, and require throwaway work which leaves “orphaned” uniqued nodes in LLVMContext.
Survey of Status Quo
Operations over DIExpressions are generally implemented in a functional style, because a DIExpression is always uniqued and so immutable. However, this has a few downsides:
- Operations which would be more efficiently implemented as mutation cannot be.
- The input to every operation has to be
uniqued, as that’s the only way to get aDIExpression *. This costs both time and space (which is not reclaimed until theLLVMContextis destroyed).- As a corollary, composing operations back-to-back means creating intermediate
DIExpression *s that are never actually used. - In my testing this isn’t a huge deal in most cases, and even 40-50% “wasted” expressions can represent almost nothing in terms of e.g. RSS, but if we are improving the interface it should address this inefficiency.
- As a corollary, composing operations back-to-back means creating intermediate
- There is no clear “home” for operations, with some being free functions, some members, some
staticmembers.
When these costs are high enough (or the author won’t tolerate them) we get functions which work on a mutable proxy for a DIExpression, e.g. SmallVectorImpl<uint64_t> &.
As an example of the issue, Introduce DIExpression::foldConstantMath() by rastogishubham · Pull Request #71718 · llvm/llvm-project optimizes large expressions, but still has to unique the unoptimized expressions first, only to orphan the intermediate DIExpression (which then lives in the LLVMContext forever).
There is also no abstraction over the vector-of-uint64_t representation, even though all of the operations actually in use in LLVM could readily be statically typed. This may be more or less critical to others, but if we’re fixing the other issues it seems like something to address. Hiding as much as reasonable also means further optimizations can be done without touching the whole codebase.
Prior Art
There is already DIExpression::ExprOperand which improves the vector-of-uint64_t situation with about as little overhead as possible.
There is even a TODO on that type:
TODO: Store arguments directly and change \a DIExpression to store a range of these. This RFC is effectively trying to set us on a path where we can make good on that TODO.
Proposal
I tried to look for patterns in the current code and extract types whose interfaces allow the current implementations to drop-in largely unchanged whenever possible.
At a high level there are two kinds of operations related to DIExpression in the codebase today:
- “Query” operations, which don’t yield a new expression. These include some which yield subexpressions like
DIExpression::getExtOps. - “Edit” operations which yield a new expression.
These are mapped to immutable+non-owning (DIExprRef) and mutable+owning (DIExprBuf) types, respectively. I see these as analogous to the LLVM SmallVector/ArrayRef, the Java String/StringBuilder, the Rust &str/String, etc.
To represent a single operation, there is a variant-like type DIOp::Op. Its alternative types are all in the DIOp namespace, and have PascalCase-ified names corresponding to DW_OP_* ops already in use e.g. DIOp::PlusUConst(uint64_t). This is not a std::variant because it is not possible to pack the “tag” with the data members of the alternative types, and because it isn’t possible to meaningfully derive from std::variant in C++17.
Currently the DIExprRef is 4-quadwords, but could be optimized to 2-quadwords. It works in terms of an iterator over a range of uint64_ts, where the iterator lazily constructs DIOp::Ops.
While iterating, if an unknown operation is encountered, the iterator yields a range-like DIOp::Escape over the remainder of the underlying vector. This seemed preferable to the Error-based fallible-iterator pattern, which is surprisingly difficult to get right, and which asks more of each call-site. This does leak the underlying representation, and I’m not actually sure what use the range would be, so maybe just a unit-type DIOp::Error could be used here instead.
The DIExprBuf is as large as an LLVMContext * and two SmallVector<uint64_t, 0>, as it allows for remembering the context of the input expression, and it bakes in double-buffering which is common to many edit operations. It currently does not handle the single-operation case optimally, as it eagerly copies the source expression into a buffer at construction. I believe this can be optimized behind the current interface so I didn’t worry about it for the initial version.
A couple of additional constraints I’ve placed on this version are:
- Leave the at-rest
DIExpressionrepresentation alone. Always get back tovector<uint64_t>. This decision was made to make an incremental transition easier. Downstream users can continue to use the old interfaces initially, tests in terms ofDIExpressions can remain as-is and validate the correctness of the new APIs, and the performance characteristics of the new approach will be easier to reason about relative to the current implementation. - Keep
DIOp::Opno larger than 2-quadwords. Working with value types is convenient, and along with some other restrictions this should allow the type to be passed around in register classes in SysV/Itanium. This also opens up the possibility of switching the underlying encoding over to justSmallVector<DIOp::Op>without a significant increase in memory usage.
Request
I have been spending a long time iterating on this without any feedback, so I wanted to share it in its current state and request high-level thoughts/feedback. The code is currently at [WIP][NFC] Start to hide details of DIExpression by slinder1 · Pull Request #161716 · llvm/llvm-project · GitHub and the compile-time-tracker is under at slinder1/perf/diexpr-ref-buf. The current numbers aren’t ideal, but I don’t think the performance issues are intrinsic to the approach.
One final note, there are a few different approaches taken in various places, such as some implementations in DIExprRef/DIExprBuf being essentially lifted from the current implementations and some being in terms of the FromUIntIterator. I left the different approaches side-by-side so those reviewing can compare; I’m not sure which is best, and I think it depends on whether we intend to end up with a SmallVector<DIOp::Op> like representation for DIExpression or not.