[Dwarf-discuss] Revisiting two-level line tables Dwarf proposal
Suprateeka R Hegde
srhegde@nvidia.com
Sat Oct 31 07:26:23 GMT 2026
NVIDIA has been using this in production for years now. Thanks for some
of these numbers.
--
Supra
On 31-Oct-2026 04:40 am, Zequan Wu via Dwarf-discuss wrote:
> *External email: Use caution opening links or attachments*
>
>
> Hi Cary,
>
> I've prototyped two-level line tables in LLVM, following issue
> 140906.1 [1] and the wiki page [2], and I'd like to share some data
> and ask whether the proposal could be revisited.
>
> Motivation: today, when the optimizer merges instructions from
> different source locations, the merged instruction gets line 0.
> Sample-based profiling (AutoFDO) then loses the samples for those
> source locations. What we want is for one instruction to map to
> several source locations, each with its own inline stack, and to get
> all of that from the line table alone. Two-level line tables provide
> exactly this. At the LLVM Dev Meeting, Eric Christopher mentioned that
> some concerns were raised when this proposal was discussed before.
> Cary, could you share what they were? I'd like to know whether they
> still apply, and what a revised proposal would need to address.
>
> To show what the prototype emits, here's a small demo:
> ```
> $ cat -n demo.c
> 1 void foo(int);
> 2
> 3 static inline void work(int c) {
> 4 if (c)
> 5 foo(1);
> 6 else
> 7 foo(1);
> 8 }
> 9
> 10 void caller(int c) {
> 11 work(c);
> 12 }
>
> $ llvm-dwarfdump --debug-line demo.o
> ...
> file_names[ 0]:
> name: "demo.c"
> dir_index: 0
> md5_checksum: 20924601724a32ef530cc082992f3600
> subprograms[ 1]:
> name: "caller"
> decl_file: 0
> decl_line: 10
> subprograms[ 2]:
> name: "work"
> decl_file: 0
> decl_line: 3
> actuals_table_offset: 0x00000017
>
> Logicals Table:
> Index Line Column File Discriminator Context Subprogram Flags
> ------ ------ ------ ------ ------------- ------- ------------------
> -------------
> 1 11 3 0 0 1 (caller)
> 2 5 5 0 0 1 2 (work)
> is_stmt prologue_end
> 3 7 5 0 0 1 2 (work)
> is_stmt prologue_end
>
> Actuals Table:
> Address OpIndex Log ISA Flags
> ------------------ ------- ------ --- -------------
> 0x0000000000000000 0 2 0
> 0x0000000000000000 0 3 0
> 0x000000000000000a 0 3 0 end_sequence
> ```
> Debug info size on clang itself
>
> I built upstream clang (llvm-project c1d907e151c9, X86 target only,
> RelWithDebInfo, -O2, DWARF 5) twice with the prototype compiler: once
> with plain DWARF 5 (merged locations get line 0) and once with
> -gdwarf-two-level-line-tables. Nothing else differs. After removing
> the debug sections, the two binaries are byte-identical. Sizes below
> are uncompressed.
>
> With -g: file size 1,686.1 MB -> 2,010.6 MB (+19.2%) .debug_line 89.0
> MB -> 242.4 MB (2.72x) .debug_line_str 0.3 MB -> 160.5 MB all .debug_*
> 1,511.3 MB -> 1,835.8 MB (+21.5%)
> With -gmlt: file size 500.0 MB -> 670.8 MB (+34.2%) .debug_line 88.0
> MB -> 219.8 MB (2.50x) .debug_line_str 0.3 MB -> 35.4 MB all .debug_*
> 325.3 MB -> 496.1 MB (+52.5%)
> Inside the two-level .debug_line, the logicals tables take 83% of the
> bytes with both -g and -gmlt (201.4 MB and 182.3 MB), the actuals
> tables 10-11% (24.5 MB in both), and the headers, i.e. file tables
> plus subprogram lists, 6-7% (16.5 MB and 13.0 MB).
>
> Function names are a large part of the growth. To rebuild the inline
> stack from .debug_line/.debug_line_str alone, the subprogram list
> stores each function's name and declaration line. Those names repeat
> what is already in .symtab and .debug_info. Counting the strings in
> .debug_line_str plus the subprogram-list entries in the .debug_line
> headers, names account for about 54% of the total growth with -g (175
> MB of 324 MB; about 929K unique mangled linkage names, 170 bytes on
> average). With -gmlt they account for about 26% (45 MB of 171 MB). At
> -gmlt, clang omits linkage names unless -fdebug-info-for-profiling is
> used (as AutoFDO builds do), so in this build the list stores short names.
>
> On the other hand, with -gmlt the line table alone now carries
> everything a profiler or symbolizer needs: all source locations of an
> instruction and their inline stacks. A build could therefore keep only
> .debug_line and .debug_line_str. In this experiment those two sections
> total 255.2 MB, less than the 325.3 MB of all debug sections in
> today's -gmlt build. In that setup the names in .debug_line_str are no
> longer duplicates of .debug_info. (Consumers would have to build their
> own address index, e.g. by parsing all of .debug_line and recording
> the address range of each sequence. Today they reach the line table
> for an address through the compile unit in .debug_info or
> .debug_aranges. My prototype doesn't do this yet.)
>
> The other DWARF sections also grow slightly (.debug_info +0.7%,
> .debug_loclists +3%, .debug_rnglists +7%). This is because the
> two-level build has 157,710 more DW_TAG_inlined_subroutine entries
> (8,745,925 -> 8,903,635, +1.8%). With line 0, a merged instruction
> moves to the common ancestor scope, so an inlined call whose
> instructions were all merged disappears from .debug_info. With
> two-level tables the instruction keeps its original scope, so those
> inlined calls are recorded.
>
> With zstd-compressed debug sections (-gz=zstd), the -g file grows from
> 653.3 MB to 758.3 MB (+16.1%): .debug_line 27.0 -> 106.1 MB and
> .debug_line_str 0.05 -> 20.9 MB.
>
> Since the original proposal is from 2014, I think the encoding could
> also be made more compact. In a sample of 39 compile units from the
> clang build above, 40% of logical rows are call-site rows, which exist
> only to be referenced as a Context and which no address refers to.
> DW_LNS_inlined_call, which sets Context and Subprogram, precedes 73%
> of logical rows and accounts for 37% of the logicals table's bytes. My
> prototype writes the call-site rows first and then the other rows in
> the order of their first use, which mostly follows addresses. Code
> from different inline frames is interleaved in the machine code, so
> the (Context, Subprogram) pair changes on most rows.
>
> The proposal doesn't require any particular order of logical rows,
> because actuals rows and Context refer to them by number. So, as an
> experiment, I grouped the logical rows of the same inline frame
> together, with no format change. This shrinks .debug_line by about 12%:
>
> -g: .debug_line 242.4 MB -> 213.1 MB (-12.1%) logicals 201.4 -> 152.9
> MB, actuals 24.5 -> 43.8 MB -gmlt: .debug_line 219.8 MB -> 194.4 MB
> (-11.6%)
>
> The actuals tables grow because the row numbers they refer to no
> longer mostly increase by one. Compared with plain DWARF 5,
> .debug_line is then 2.40x with -g (file +17.5%) and 2.21x with -gmlt
> (file +29.1%).
>
> I'm happy to share the prototype, the measurements, and my scripts,
> and to help with a revised proposal.
>
> Thanks,
> Zequan
>
> [1] https://dwarfstd.org/issues/140906.1.html
> <https://dwarfstd.org/issues/140906.1.html>
> [2] https://wiki.dwarfstd.org/TwoLevelLineTables.md
> <https://wiki.dwarfstd.org/TwoLevelLineTables.md>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.dwarfstd.org/pipermail/dwarf-discuss/attachments/20261031/de35975f/attachment.htm>
More information about the Dwarf-discuss
mailing list