[RFC] Function-Local Metadata: IR and API changes

tl;dr following on from our (Sony’s) prior proposal to use function-local storage for source location metadata, we propose a fully-defined storage model, and a new textual IR representation for the new format:

; Old
%c = add i32 %a, %b, !dbg !10
!10 = !DILocation(line: 10, column: 4, scope: !9, inlinedAt: !11)
; New
%c = add i32 %a, %b, !!dbgLoc(line: 10, column: 4, scope: !9, inlinedAt: 2)

Background

The motivation for changing the underlying representation of source locations (i.e. DILocation metadata) is given in the prior RFC, but to briefly summarize: there are significant inefficiencies in the current metadata model for source locations, and we could see great savings in terms of overall memory usage and number of allocations by using a function-local format for source location information. Changing the in-memory IR format strongly pushes us to change the textual IR format accordingly: converting between these formats is not free, it would add significant overhead to printing/parsing, and would also not be a faithful representation of the in-memory IR.

Design

The design proposed in this RFC extends and modifies the prior proposal, although the basic concept is the same. The storage model is more fully fleshed out as “function-local metadata” (FLMD), and is used to store source locations and loop metadata (!llvm.loop). In exchange for more indirection, this format compresses source location information further than the initial design by deduplicating source locations across inlined function instances, which greatly reduces the cost of inlining - often a significant contributor to expensive source location information in optimized debug builds.

While this project will change the debug location API and the textual IR output, it should have no effect on final compiler output. We are storing the same information in a different shape, which reduces overhead and enables some deduplication. All existing API methods will either continue to work as-is or have trivial replacements (see below). Further explanation is given in the design document found here.

API changes

This change unavoidably changes the API for dealing with source locations, as storage is no longer based on Metadata, and references are now integers rather than pointers, which means any code that previously created debug line information is affected.

  • Existing code will work without modification, although it will be relatively inefficient (as we end up paying the cost of both representations), and existing methods that convert between DILocation and DebugLoc will be deprecated.
  • DILocation itself will be converted into a “wrapper” class that references function-local metadata, and DILocation::get will write to FLMD storage.
  • Loop metadata will similarly be made into a function-local metadata type, and any code that accesses it as an MDNode will be deprecated.
  • In almost all cases, every instance of DILocation can be trivially replaced with DebugLoc.

Updating downstream/consumer code (e.g. frontends, or forks of LLVM) should be quite straightforward; as mentioned above, existing code will continue to work (but will be marked deprecated), and the vast majority of updates are a simple replacement of all DILocation* variables with DebugLoc variables, and all static DILocation method calls with DebugLoc method calls. Practically, >90% of updates can be performed with text substitution, and the remainder should still be trivial (e.g. replacing a deprecated method with a substitute). New API additions (e.g. builder methods) will also be added, which will allow for more efficient use of FLMD.

IR changes

The textual IR must also change; the key points of the change to the textual IR are:

  • A new syntax is provided for function-local metadata (FLMD): !!<type>,
  • !dbg !<num> attachments are replaced with (mostly) in-line printed location information, e.g. !!dbgLoc(line: 10, column: 4, scope: !9, inlinedAt: 2),
  • !llvm.loop !<num> attachments are replaced with a FLMD reference, e.g. !!loop 4,
  • A new metadata type is created to store FLMD, aptly named !DIFunctionLocalMetadata; this type is always distinct, and each instance is associated with exactly one concrete Function,
  • inlinedAt and loop values are integer indices, referring to array entries in the current function’s !DIFunctionLocalMetadata.
  • A flag, --use-deprecated-function-local-metadata, causes DILocation and llvm.loop metadata to be printed out in their current form, to smooth over the transition for tooling.

Example

Presented below is a short example showing how each old IR element translates to the newly proposed IR. One cost is that the in-line representation adds visual clutter - the !!dbgLoc attachment often ranges from 1-2x the original length of the lines where it is present, which makes it harder to read the original program. On the other hand, printing the debug location information in-line makes it much easier to cross reference the IR with the original source code. The flag --output-compact-debug-locations is available, which removes some of the annotative comments and uses indirection to reduce the printed size of dbgLocs, at the cost of making source locations harder to read, especially when inlined. To give a better view of this change in practice, some more extensive examples are provided here.

Current LLVM IR
; Simple DILocation
%exitcond.not = icmp eq i32 %inc, %i, !dbg !24
; Inlined DILocation
%mul.i = mul nsw i32 %call.i, %x.addr.013, !dbg !32
; DILocation + Loop attachment
br i1 %exitcond.not, label %cleanup2, label %for.body, !dbg !25, !llvm.loop !26

!20 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 9, type: !13, scopeLine: 9, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, keyInstructions: true)
!24 = !DILocation(line: 10, column: 21, scope: !20, atomGroup: 2, atomRank: 1)
!25 = !DILocation(line: 10, column: 3, scope: !20, atomGroup: 3, atomRank: 1)
!26 = distinct !{!26, !27, !28, !29}
!27 = !DILocation(line: 10, column: 3, scope: !20)
!28 = !DILocation(line: 16, column: 3, scope: !20)
!29 = !{!"llvm.loop.mustprogress"}
!31 = distinct !DILocation(line: 11, column: 16, scope: !20)
!32 = !DILocation(line: 6, column: 12, scope: !12, inlinedAt: !31)
New LLVM IR (Function-Local Metadata)
; DILocation information is printed in-line.
; To make the repesentation a bit less cluttered, the atom-related fields are compressed into one.
%exitcond.not = icmp eq i32 %inc, %i, !!dbgLoc(line: 10, column: 21, scope: !20, atom: [2, 1])

; DILocation information is printed in-line, with an "inlinedAt" field that references the function-local metadata object.
; Line ends with a comment that can be grepped to find the function-local metadata entry corresponding to the inlinedAt location.
%mul.i = mul nsw i32 %call.i, %x.addr.013, !!dbgLoc(line: 6, column: 12, scope: !12, inlinedAt: 0) ; inlinedAt: foo@inlinedCallLoc@0

; Loop metadata is not printed in-line, instead printed as an index which references the function-local metadata object.
; Again, the line ends with a greppable comment to help lookup the function-local metadata entry.
br i1 %exitcond.not, label %cleanup2, label %for.body, !!dbgLoc(line: 10, column: 3, scope: !20, atom: [3, 1]), !!loop 0 ; loop: foo@loops@0

; New "functionLocalMetadata" field on !DISubprogram.
!20 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 9, type: !13, scopeLine: 9, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, keyInstructions: true, functionLocalMetadata: !38)
!29 = !{!"llvm.loop.mustprogress"}
; Each DIFunctionLocalMetadata is distinct and may contain arrays of `inlinedCallLocs` and `loops`.
; Each entry in these arrays is printed on a separate line, and ends with a line comment that matches the line comments for "users" of those entries.
!38 = distinct !DIFunctionLocalMetadata(
  ; Each inlinedCallLoc has a dbgLoc and a reference to the inlined function's DIFunctionLocalMetadata.
  ; An inlinedCallLoc may itself be inlined; if so, the inlinedAt index always refers to an index in *this* DIFunctionLocalMetadata.
  inlinedCallLocs: [
    !!inlinedCallLoc(dbgLoc: !!dbgLoc(line: 11, column: 16, scope: !20), inlinedFLMD: !37) ; foo@inlinedCallLoc@0
  ],
  loops: [
    !!loop(start: !!dbgLoc(line: 10, column: 3, scope: !20), end: !!dbgLoc(line: 16, column: 3, scope: !20), properties: !29) ; foo@loops@0
  ]
)
3 Likes

Thank you for the detailed proposal; I think this looks pretty good and I especially love the inlined debug locations.

  • On DILoc: I expect you want to store this inline in the instruction. What happens if (a) an instruction is removed from a function – what does SrcLocIdx mean then? – and (b) inserted in a different function?

  • Does this affect the bitcode serialization?

  • How is uniqueing done? And how are these deduplicated across inlined functions instances (or do you mean only deduplicated across instances in the same function)? What is the trade-off between uniqueing vs. living with possible duplicates? (I imagine the benefit of guaranteeing equality iff references are equal is low?)

  • Where is the reference of the !!loop FLMD stored if this is no longer regular metadata? In CondBrInst?

  • What is the benefit of a DIFunctionLocalMetadata MDNode over storing the members directly in Function? Given that we often either have loop metadata (opt builds) or debug info (O0g builds), it is probably always present? (I feel like I’m missing something here.)

1 Like

What happens if (a) an instruction is removed from a function – what does SrcLocIdx mean then? – and (b) inserted in a different function?

The result would be an invalid location - but this is already the case, as the verifier rejects a !DILocation in a function where the top of the location’s inlining chain doesn’t match the current function’s DISubprogram (with the only exception being if the function has no DISubprogram). In general, if an instruction is spliced from one function to another then the code responsible for the splicing should know how to update the debug location - it is possible to simply “copy” to the new function, although this is rarely desirable.

Does this affect the bitcode serialization?

Yes, and it should have a positive effect: when reading we can load stored FLMD indices directly into each instruction without needing to reference the metadata section.

How is uniqueing done?

We can only unique source locations by searching the SrcLoc array for an existing copy, which is expensive. However, this search would only need to happen when we create a new source location, which we generally don’t do after the frontend finishes - hence the desire to add a builder (likely extending DIBuilder) to speed this up in the frontend.

And how are these deduplicated across inlined functions instances?

The deduplication comes from each inlinedCallLoc holding a reference to the inlined function’s DIFunctionLocalMetadata, which we use to lookup source locations. When we inline one function into another, we only need to create/copy inlinedCallLoc entries - all of the inlined instructions will lookup their source location in the associated DIFunctionLocalMetadata, so we don’t need to copy any SrcLocs when we inline.

What is the trade-off between uniqueing vs. living with possible duplicates?

There is some performance benefit to having object equality == reference equality, though I’ve not measured it. However, we don’t want to be forced to create new SrcLoc entries every time we create a column-0 or line-0 entry from an existing SrcLoc - doing this would get expensive quite fast in optimized debug builds. The best way to avoid this would be to, after creating the initial set of SrcLocs in the frontend, pre-generate all line/column 0 entries that LLVM might want, and sort+unique the resulting array to make looking up these locations fast.

Where is the reference of the !!loop FLMD stored if this is no longer regular metadata? In CondBrInst?

I actually hadn’t considered storing it in CondBrInst, which is an interesting idea, though I don’t think it’s guaranteed that !llvm.loop can only appear on a condbr. The plan for loop metadata is to use a map in DIFunctionLocalMetadata similar to the existing metadata map; I’ve not determined what the most efficient type would be for this, but I believe the end result should be more efficient in terms of space and performance than using the global value-to-metadata map. If this turned out not to be the case though, it would still be possible to keep loop metadata as Metadata.

What is the benefit of a DIFunctionLocalMetadata MDNode over storing the members directly in Function?

The main reason is that a Function might cease to exist, but we still need to keep its DIFunctionLocalMetadata alive - this can happen quite easily if a function is inlined everywhere.

1 Like

I was a bit confused on why loop metadata is part of this proposal. I assume the answer is “loop metadata can contain debug locations”.

However, if we’re changing loop metadata to not actually be metadata anymore, we may want to consider resolving another long-standing issue and move it from the terminator of the loop latch to the loop header block (so stored in BasicBlock rather than CondBrInst). This was always blocked on lack of support for basic block metadata, but if it’s not metadata anymore…

1 Like

NB: I somewhat recently removed this map, the context now stores a vector of metadata attachments, which form a linked list in this vector, and an Instruction/GlobalValue references the head of the list with an index.

A map might not be a good idea: If an Instruction is removed, who updates the function-local map? I think for frequently used metadata (!dbg, !tbaa), allocating storage directly in the Instruction is good, and for infrequently used metadata, we use the storage in the context. I’d rather not have new maps from instruction without a clear benefit.

Semi-related: What probably also deserves a better representation more is !tbaa (or, more general, everything that getAAMetadata() gets). This is obviously a lot less function-local and a separate project, but maybe if you build infrastructure, it’d be great if it could also be used for this purpose and/or for some similar representation on the module-level.

That’s more or less correct - including loop metadata in the design is a small part of the proposal, but since loop metadata is tied to DILocation, it made sense to fold it in. Including FLMD that can be attached to BasicBlocks may be viable - I’m not familiar with prior attempts to attach normal metadata to basic blocks, but it’s possible this will make it easier to get around any restrictions.

Is there a plan to bump DEBUG_METADATA_VERSION? or is it no longer even relevant. I am guessing the latter, but wanted to double check.

I don’t think it would be necessary to do so; as far as I can tell, a DEBUG_METADATA_VERSION mismatch causes UpgradeDebugInfo to strip all debug info. It should be straightforward to directly upgrade from DILocation to function-local metadata, so we don’t need the stripping behaviour.

1 Like

Thanks, I think this proposal looks good to me!

Minor questions:

  • I am wondering why you chose the double !! introducer in the assembler syntax, since it is already a different keyword? Is it because ! is used to introduce MDNodes?
  • What’s your plan/timeline for removing the deprecated interfaces? It might be good to preannounce that we will keep this around for N LLVM releases and then remove it so we can actually follow through.

That’s correct. You only need to bump the metadata version if you cannot safely upgrade.

I am wondering why you chose the double !! introducer in the assembler syntax, since it is already a different keyword? Is it because ! is used to introduce MDNodes?

Mostly it’s down to the fact that by default, MDNode references (!<num>) have a different set of syntactic rules to FLMD references - generally we can insert any MDNode anywhere that an MDNode reference is accepted, while FLMD nodes are more limited in where they can appear; while semantically invalid MDNodes are rejected by the verifier, we can’t even parse improperly placed FLMD, so it made sense to distinguish it syntactically while maintaining its identity as a kind of metadata.

What’s your plan/timeline for removing the deprecated interfaces? It might be good to preannounce that we will keep this around for N LLVM releases and then remove it so we can actually follow through.

I don’t think we should need to keep them for long. The functions we really need/want to deprecate are those that implicitly/unnecessarily cast between DebugLoc and DILocation (because the operation is now costly), but we still allow explicit conversion between the two, so it should in theory be quite easy for downstream users to replace their uses of the deprecated functions; it could reasonably be as short as deprecate for one release, remove the next.

Flagging here since this RFC affects my RFC which is based on the current DebugLoc/DILocation/MDNode implementation: [RFC] Multi-level line table support in LLVM

1 Like

We can only unique source locations by searching the SrcLoc array for an existing copy, which is expensive. However, this search would only need to happen when we create a new source location, which we generally don’t do after the frontend finishes - hence the desire to add a builder (likely extending DIBuilder) to speed this up in the frontend.

Another one that comes to mind is uniquing during module linking?

(Not a blocker, but curious your thoughts).

1 Like

I’m not too familiar with exactly how the module link step works, but performing an extra uniquing step then should be fine. For both the frontend and for module linking, we can create temporary data structures to make uniquing and updating source locations much more efficient; the thing we really aim to avoid is having many scattered operations throughout the LLVM pipeline.

Solid, looking forward to seeing what it looks like :smiley:

I’m fast approaching a point where solving this might be reasonable - do you have any references for previous discussions on this, or otherwise an explanation for why this is preferable? It is probably not difficult, on a technical level, to attach FLMD to BasicBlocks; the cost from making this change would mainly be that (I assume) this would end up changing the output of the compiler in some cases, which means also testing that it’s changing the output correctly (as opposed to the DILocation changes which should produce identical binaries, which is relatively easy to verify).

There seem to be quite a few threads about the general problem (loop metadata on the latch being ambiguous and hard to preserve), but I wasn’t quickly able to find a lot of references for solving this by attaching the metadata to the header block. It’s mentioned in Loop latch info migrates to wrong loop. · Issue #49404 · llvm/llvm-project · GitHub (related to https://groups.google.com/g/llvm-dev/c/_q0oZJcHb9Q). I’m sure I’ve seen this in more places, but I can’t find them.

FWIW, I think it would be ideal if changing !loop representation could be done as a separate step from the remaining debug loc migration, to avoid too many changes at once. Not sure whether that’s feasible or not.

1 Like

For the record, having block metadata would allow fixing a substantial hole in the design of debug-info variable locations: variable assignments that happen on critical edges can’t be represented at all. There are examples out there where loop-preheader blocks are optimised until they’re empty aside from debug-records, and the block then gets erased (with that path becoming a critical edge), leaving nowhere for the debug-records to “live”. With block metadata we could record “there would be variable assignments on this path, but we don’t have a block to put them in”.

1 Like

Hi all,

Just an update, I’ll be opening the first reviews for this work fairly soon, and can post a bit more concretely about the changes to existing interfaces that this work will require - any questions or comments would be appreciated, so that I can address any fundamental concerns before opening the patches. The plan is to toggle between DILocation and FLMD source location storage with a CMake flag, such that both representations exist and work simultaneously in LLVM until we’re ready to fully replace DILocation; some of these interface changes exist to smooth over that transition period and may be further modified/simplified later on, and some are required changes for the FLMD model:

  • Context Argument - When creating DILocations via DILocation::get an LLVMContext& argument is required (as for all metadata). For function-local metadata we need a more specific context: the DIFunctionLocalMetadata associated with the function. To keep the interface identical whether FLMD is enabled or not, we use a DebugLocContext class which is implicitly constructed from a DebugLoc, Instruction*, or Function*, and traverses from there to find either the LLVMContext& or DIFunctionLocalMetadata* depending on whether FLMD is enabled.
  • Inlined Calls - The FLMD representation for inlined calls includes a reference to the inlined function itself (specifically its DIFunctionLocalMetadata*) rather than just the source location of the call. This means that creating a DebugLoc for an inlined call needs an extra argument, and thus we add a new pair of functions: DebugLoc::getDistinctInlinedCall for creating new inlined calls, and DebugLoc::getUniquedInlinedCall for the case where we merge inlined calls (in getMergedLocation); each of these functions take a DebugLocContext for the callee function as an extra argument.
  • getDebugLoc - The behaviour of Instruction::getDebugLoc is fundamentally different with FLMD. Instruction only stores half of a DebugLoc, specifically the indexes for its source information; the other half of a DebugLoc is a pointer to the function-local context, which contains the indexed data. Therefore, getDebugLoc() needs to call getFunction()->getMetadata(MD_flmd) to return its value, which also means that it will fail if called on an Instruction that is not already contained in a Function. There are other methods provided that can handle most of the operations needed without looking up the current function context, e.g. Instruction::setDebugLocFrom(Instruction *Other), or bool Instruction::hasDebugLoc(), which only need to examine/move the indexes stored in the Instruction.
    • Also, in the vast majority of cases where we call getDebugLoc(), we are calling it many times on different Instructions within a single Function. In these cases, it makes sense to get the function context once, and then reuse it (using an overload getDebugLoc(DebugLocContext)) to avoid the unnecessary function+metadata lookups!
  • Building Functions - For frontends constructing functions, the DIBuilder class has some new methods added for creating new function contexts, adding inlined calls to existing function contexts, and finalizing function contexts (to be called after the last source location is created). The frontend itself will need to track which function context is currently being emitted for each new source location; in Clang, for example, this is achieved by adding a “FnContextStack” to CGDebugInfo, which grows and shrinks as we start/stop emitting functions.
  • C API - Currently LLVM’s C API uses wrapped DILocation pointers for all debug location operations. To avoid breaking existing APIs, new methods will be provided that accept/return a wrapped DebugLoc instead (defined as a struct {uint64_t, LLVMMetadataRef}). The existing methods will continue to work, but using them will force LLVM to use some context-level data structures to handle conversion to FLMD, and new metadata nodes will need to be created and returned for every new location (which erases most of the performance benefits of this feature), potentially resulting in worse overall performance than current LLVM.

I’ll be posting a broader PSA soon, but particularly from the people in this thread (who are more technically involved with debug info than most contributors) I’d be interested in whether any of these interface changes sound problematic, either within LLVM or for downstream projects, or if anyone has any questions/suggestions regarding the interface design.

1 Like

Thanks for the detailed notes, sounds reasonable to me. Just two small questions:

  • Given that FLMD is going to be accessed rather often, would it be beneficial to store a pointer directly inside the function and special-case getMetadata(MD_flmd) (similar to what we do for MD_dbg for Instruction currently) (or maybe even assert that getMetadata(MD_flmd) is not used as such?)? getMetadata remains somewhat expensive and I’d prefer if we store very common metadata inline. The size cost of one pointer per function seems acceptable in non-debug builds.
  • I could’ve realized this ealier, but still: why is there a DI prefix in DIFunctionLocalMetadata? Not all info in there is debug-related (e.g. loops) and more data might follow.
1 Like