<div dir="ltr">Hi Cary,<br><br>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.<br><br>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.<br><br>To show what the prototype emits, here's a small demo:<br>```<br>$ cat -n demo.c<br>     1 void foo(int);<br>     2<br>     3 static inline void work(int c) {<br>     4  if (c)<br>     5    foo(1);<br>     6  else<br>     7    foo(1);<br>     8 }<br>     9<br>    10 void caller(int c) {<br>    11  work(c);<br>    12 }<br><br>$ llvm-dwarfdump --debug-line demo.o<br>...<br>file_names[  0]:<br>           name: "demo.c"<br>      dir_index: 0<br>   md5_checksum: 20924601724a32ef530cc082992f3600<br>subprograms[  1]:<br>           name: "caller"<br>      decl_file: 0<br>      decl_line: 10<br>subprograms[  2]:<br>           name: "work"<br>      decl_file: 0<br>      decl_line: 3<br>actuals_table_offset: 0x00000017<br><br>Logicals Table:<br>Index  Line   Column File   Discriminator Context Subprogram         Flags<br>------ ------ ------ ------ ------------- ------- ------------------ -------------<br>     1     11      3      0             0         1 (caller)<br>     2      5      5      0             0       1 2 (work)           is_stmt prologue_end<br>     3      7      5      0             0       1 2 (work)           is_stmt prologue_end<br><br>Actuals Table:<br>Address            OpIndex Log    ISA Flags<br>------------------ ------- ------ --- -------------<br>0x0000000000000000       0      2   0<br>0x0000000000000000       0      3   0<br>0x000000000000000a       0      3   0 end_sequence<br>```<br>Debug info size on clang itself<br><br>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.<br><br>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%)<br>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%)<br>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).<br><br>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.<br><br>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.)<br><br>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.<br><br>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.<br><br>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.<br><br>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%: <br><br>-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%)<br><br>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%).<br><br>I'm happy to share the prototype, the measurements, and my scripts, and to help with a revised proposal.<br><br>Thanks,<br>Zequan<br><br>[1] <a href="https://dwarfstd.org/issues/140906.1.html">https://dwarfstd.org/issues/140906.1.html</a><br>[2] <a href="https://wiki.dwarfstd.org/TwoLevelLineTables.md">https://wiki.dwarfstd.org/TwoLevelLineTables.md</a></div>