From echristo@gmail.com Thu Nov 5 22:31:54 2026 From: echristo@gmail.com (Eric Christopher) Date: Thu, 5 Nov 2026 14:31:54 -0800 Subject: [Dwarf-discuss] Revisiting two-level line tables Dwarf proposal In-Reply-To: <67ffd0d6-11ad-462f-a61e-e6242d98b311@nvidia.com> References: <67ffd0d6-11ad-462f-a61e-e6242d98b311@nvidia.com> Message-ID: +ayermolovich@nvidia.com as well as having a standardized version of these sounds great and he's up for helping as well. :) And yes, thanks for following up on this Zequan! :) -eric On Sat, Oct 31, 2026 at 2:27?AM Suprateeka R Hegde via Dwarf-discuss < dwarf-discuss@lists.dwarfstd.org> wrote: > 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 > [2] https://wiki.dwarfstd.org/TwoLevelLineTables.md > > > -- > Dwarf-discuss mailing list > Dwarf-discuss@lists.dwarfstd.org > https://lists.dwarfstd.org/mailman/listinfo/dwarf-discuss > -------------- next part -------------- An HTML attachment was scrubbed... URL: