Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trivial dependencies on large crates pull in massive amounts of debuginfo #56068

Open
rocallahan opened this issue Nov 19, 2018 · 19 comments
Open
Labels
A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) C-enhancement Category: An issue proposing an enhancement or a PR with one. I-compiletime Issue: Problems and improvements with respect to compile times. I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@rocallahan
Copy link

extern crate rusoto_core;
const REGION: rusoto_core::Region = rusoto_core::Region::UsEast1;
fn main() {
    println!("Hello, region {:?}", &REGION);
}

rusoto_core::Region is a very simple enum type. This doesn't run any interesting code. The resulting Linux debug-build binary is 41MB. If I replace &REGION with 0 the binary is 7.5MB.

readelf -a shows that .text is 370,558 bytes. .debug_info is 10,541,677 bytes. The other debug sections account for most of the rest.

Inspecting the debuginfo shows DWARF compilation units for rusoto_core and lots of its dependencies that are entirely dead code. For example:

COMPILE_UNIT<header overall offset = 0x0034b008>:
< 0><0x0000000b>  DW_TAG_compile_unit
                    DW_AT_producer              clang LLVM (rustc version 1.30.1 (1433507eb 2018-11-07))
                    DW_AT_language              DW_LANG_Rust
                    DW_AT_name                  /home/roc/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.33/src/lib.rs
                    DW_AT_stmt_list             0x001534c8
                    DW_AT_comp_dir              /home/roc/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.33
                    DW_AT_GNU_pubnames          yes(1)
                    DW_AT_low_pc                0x00000000
                    DW_AT_ranges                0x000a7120
                ranges: 89 at .debug_ranges offset 684320 (0x000a7120)
                        [ 0] <offset pair low-off: 0x00000001 addr 0x00000001 high-off: 0x00000001 addr 0x00000001>
                        [ 1] <offset pair low-off: 0x00000001 addr 0x00000001 high-off: 0x00000001 addr 0x00000001>
                        [ 2] <offset pair low-off: 0x00000001 addr 0x00000001 high-off: 0x00000001 addr 0x00000001>
                        [ 3] <offset pair low-off: 0x00000001 addr 0x00000001 high-off: 0x00000001 addr 0x00000001>

... followed by 85 more identical ranges. These all indicate empty code ranges; all code for this CU has been stripped by the linker. However, this CU still has a ton of debuginfo for types and for functions. E.g.:

< 4><0x00001802>          DW_TAG_subprogram
                            DW_AT_low_pc                0x00000000
                            DW_AT_high_pc               <offset-from-lowpc>262
                            DW_AT_frame_base            len 0x0001: 57: DW_OP_reg7
                            DW_AT_linkage_name          _ZN91_$LT$core..slice..Iter$LT$$u27$a$C$$u20$T$GT$$u20$as$u20$core..iter..iterator..Iterator$GT$4next1
7hc6f1932985554eebE
                            DW_AT_name                  next<serde_json::value::Value>
                            DW_AT_decl_file             0x00000005 /home/roc/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.33/libcore/slice/m
od.rs
                            DW_AT_decl_line             0x00000978
                            DW_AT_type                  <0x000007b0>
< 5><0x00001820>            DW_TAG_formal_parameter
                              DW_AT_location              len 0x0002: 9128: DW_OP_fbreg 40
                              DW_AT_name                  self
                              DW_AT_decl_file             0x00000003 /home/roc/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.33/src/lib.rs
                              DW_AT_decl_line             0x00000001
                              DW_AT_type                  <0x00002f94>
< 5><0x0000182e>            DW_TAG_inlined_subroutine
                              DW_AT_abstract_origin       <0x00001a7d>
                              DW_AT_low_pc                0x00000000
                              DW_AT_high_pc               <offset-from-lowpc>123
                              DW_AT_call_file             0x00000005 /home/roc/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.33/libcore/slice/mod.rs
                              DW_AT_call_line             0x00000982
< 6><0x00001842>              DW_TAG_formal_parameter
                                DW_AT_location              len 0x0002: 9138: DW_OP_fbreg 56
                                DW_AT_abstract_origin       <0x00001a97>
< 6><0x0000184a>              DW_TAG_formal_parameter
                                DW_AT_location              len 0x0003: 91c000: DW_OP_fbreg 64
                                DW_AT_abstract_origin       <0x00001aa2>
< 6><0x00001853>              DW_TAG_lexical_block
                                DW_AT_low_pc                0x00000000
                                DW_AT_high_pc               <offset-from-lowpc>36
< 7><0x00001860>                DW_TAG_variable
                                  DW_AT_location              len 0x0003: 91d000: DW_OP_fbreg 80
                                  DW_AT_abstract_origin       <0x00001aae>
< 5><0x0000186b>            DW_TAG_template_type_parameter
                              DW_AT_type                  <0x00000064>
                              DW_AT_name                  T

All DW_AT_low_pcs of DW_TAG_subprogram, DW_TAG_lexical_block and DW_TAG_inlined_subroutine in this CU are zero. There are a lot of CUs like this.

@rocallahan
Copy link
Author

It seems to me that this debuginfo should have been garbage collected at some point, by the linker if not before, but for some reason that isn't happening. rustc 1.30.1 (1433507eb 2018-11-07), cargo 1.30.0 (a1a4ad372 2018-11-02). Regular cargo build with no special settings, just cargo new --bin bloat and in Cargo.toml

[dependencies]
rusoto_core = "0.35"

@rocallahan
Copy link
Author

@tromey you might be interested in this.

@rocallahan
Copy link
Author

Also, GNU ld version 2.29.1-23.fc28.

@rocallahan
Copy link
Author

Similar results with LLD 7.0.0.

@rocallahan
Copy link
Author

And similar results with gold.

It seems to me that the object files that don't have any code eventually included in the binary should be skipped by the linker, with their debuginfo not linked in at all.

@rocallahan
Copy link
Author

It appears that because Rust divides code up into codegen units (i.e. object files) somewhat arbitrarily, as soon as you pull one from a crate, you end up having to link them all as you transitively resolve undefined symbols. Then it's up to --gc-sections to eliminate unused code, but apparently --gc-sections doesn't know about .debug_info etc. And, because the decisions about which object files to link happen before any --gc-sections processing, the debuginfo for all object files gets linked.

@rocallahan
Copy link
Author

In that case, probably the best option here would be to teach the linker's --gc-sections implementation to drop all debuginfo sections for any object file that has had all its code and data sections discarded.

@rocallahan
Copy link
Author

This issue has been discussed before, e.g. http://sourceware-org.1504.n7.nabble.com/Re-Debuggin-info-for-unused-sections-td112685.html.

I guess this isn't a high priority for C/C++ libraries since developers can divide them up manually into separate object files so that not all object files need to be linked into every application. So implementing the above optimization, though it needs to be done in the linker, would mainly be for the benefit of Rust.

@rocallahan
Copy link
Author

In that case, probably the best option here would be to teach the linker's --gc-sections implementation to drop all debuginfo sections for any object file that has had all its code and data sections discarded.

I implemented this in LLD. It works well for some of my binaries, and for the testcase here, but only shrank the overall size of my built binaries from 2.4G to 2.0G, so I'm not sure whether it's worth the effort to push upstream...

@rocallahan
Copy link
Author

Let's give it a go. https://reviews.llvm.org/D54747

@tromey tromey added the A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) label Nov 20, 2018
@Diggsey
Copy link
Contributor

Diggsey commented Dec 18, 2018

I have the exact same issue, and it's really problematic because it results in up 600MB of memory usage when generating a backtrace, for only 50MB of debug info, for a binary that is only 643KB in size when stripped!

That's literally 1000x increase in memory usage 😱

dtzWill pushed a commit to llvm-mirror/lld that referenced this issue Apr 10, 2019
Patch by Robert O'Callahan.

Rust projects tend to link in all object files from all dependent
libraries and rely on --gc-sections to strip unused code and data.
Unfortunately --gc-sections doesn't currently strip any debuginfo
associated with GC'ed sections, so lld links in the full debuginfo from
all dependencies even if almost all that code has been discarded. See
rust-lang/rust#56068 for some details.

Properly stripping debuginfo for discarded sections would be difficult,
but a simple approach that helps significantly is to mark debuginfo
sections as live only if their associated object file has at least one
live code/data section. This patch does that. In a (contrived but not
totally artificial) Rust testcase linked above, it reduces the final
binary size from 46MB to 5.1MB.

Differential Revision: https://reviews.llvm.org/D54747

git-svn-id: https://llvm.org/svn/llvm-project/lld/trunk@358069 91177308-0d34-0410-b5e6-96231b3b80d8
llvm-git-migration pushed a commit to llvm/llvm-project that referenced this issue Apr 10, 2019
Patch by Robert O'Callahan.

Rust projects tend to link in all object files from all dependent
libraries and rely on --gc-sections to strip unused code and data.
Unfortunately --gc-sections doesn't currently strip any debuginfo
associated with GC'ed sections, so lld links in the full debuginfo from
all dependencies even if almost all that code has been discarded. See
rust-lang/rust#56068 for some details.

Properly stripping debuginfo for discarded sections would be difficult,
but a simple approach that helps significantly is to mark debuginfo
sections as live only if their associated object file has at least one
live code/data section. This patch does that. In a (contrived but not
totally artificial) Rust testcase linked above, it reduces the final
binary size from 46MB to 5.1MB.

Differential Revision: https://reviews.llvm.org/D54747

llvm-svn: 358069
@sanxiyn
Copy link
Member

sanxiyn commented May 14, 2019

It seems LLD patch is merged. How can I take advantage of this today?

@rocallahan
Copy link
Author

Build LLD master and use it. LLD is pretty easy to build.

@pnkfelix
Copy link
Member

pnkfelix commented Jul 4, 2019

It was unfortunately reverted back in May (shortly after @rocallahan last comment above...)

@zephyrus00jp
Copy link

zephyrus00jp commented Aug 22, 2019

I am creating patches for Thunderbird mail client (originally from Mozilla) from time to time when I notice a bug here and there.

I have a nagging problem since early this year.
Since early this summer, I occasionally run out of my 50GB partition (in a VM setting.) for storing binary object only. This has not happened before.
I found that among the binary directory, the incremental compilation cache of rust is huge.
But I suspect EACH object of rust compilation is large on its own.
The cause of the problem seems to be this Rust's debug info bloat.

On my local PC, I see that the file directory size (with the sizes of
subdirectories added to it) is as follows:

MOZ_OBJ is 45.8 GB large. This is the top-most directory for storing binary object files.
(as of now: I had to stop TB compilation due
to the 50 GB smallish partition overflow where this directory is located.)
MOZ_OBJ/x86_64-unknown-linux-gnu/ is 33.9 GB large .
MOZ_OBJ/x86_64-unknown-linux-gnu/debug/ is 33.9 GB large.
I think /debug/ is because I create full debug version of TB locally.
MOZ_OBJ/MOZ_OBJ/x86_64-unknown-linux-gnu/debug/incremental is 26.5 GB large.

I have learned of this issue of large debug info from a discussion in a mozilla mailing list.
https://groups.google.com/forum/#!topic/mozilla.dev.platform/EnZbltbwUeE

It would be super if we can reduce the size of debug info (.dwo) for rust object files.
Can't the compiler itself reduce the debug info a bit more intelligently?

Thank you for the great package otherwise.
Mozilla developers seem to be creating a lot of libraries in rust at a frantic pace.

TIA

benesch added a commit to benesch/materialize that referenced this issue Apr 18, 2020
Our Linux release binary was hilariously large, weighing in at nearly
800MB (!). Nearly all of the bloat was from DWARF debug info:

    $ bloaty materialized -n 10
        FILE SIZE        VM SIZE
     --------------  --------------
      24.5%   194Mi   0.0%       0    .debug_info
      24.1%   191Mi   0.0%       0    .debug_loc
      13.8%   109Mi   0.0%       0    .debug_pubtypes
      10.1%  79.9Mi   0.0%       0    .debug_pubnames
       8.8%  70.0Mi   0.0%       0    .debug_str
       8.3%  66.3Mi   0.0%       0    .debug_ranges
       4.4%  35.3Mi   0.0%       0    .debug_line
       3.1%  24.8Mi  66.3%  24.8Mi    .text
       1.8%  14.4Mi  25.1%  9.39Mi    [41 Others]
       0.6%  4.79Mi   0.0%       0    .strtab
       0.4%  3.22Mi   8.6%  3.22Mi    .eh_frame
     100.0%   793Mi 100.0%  37.4Mi    TOTAL

This patch gets a handle on this by attacking the problem
from several angles:

  1. We instruct the linker to compress debug info sections. Most of the
     debug info is redundant and compresses exceptionally well. Part of
     the reason we didn't notice the issue is because our Docker images
     and gzipped tarballs were relatively small (~150MB).

  2. We strip out the unnecessary `.debug_pubnames` and
     `.debug_pubtypes` from the binary. This works around a known Rust
     bug (rust-lang/rust#46034).

  3. We ask Rust to generate less debug info for release builds,
     limiting it to line info. This is enough information to symbolicate
     a backtrace, but not enough information to run an interactive
     debugger. This is usually the right tradeoff for a release build.

    $ bloaty materialized -n 10
         VM SIZE                         FILE SIZE
     --------------                   --------------
       0.0%       0 .debug_info        31.9Mi  33.8%
      70.5%  25.0Mi .text              25.0Mi  26.5%
       0.0%       0 .debug_str         7.54Mi   8.0%
       0.0%       0 .debug_line        6.36Mi   6.7%
       9.4%  3.33Mi [38 Others]        5.36Mi   5.7%
       0.0%       0 .strtab            4.71Mi   5.0%
       0.0%       0 .debug_ranges      3.55Mi   3.8%
       8.8%  3.11Mi .eh_frame          3.11Mi   3.3%
       0.0%       0 .symtab            2.87Mi   3.0%
       6.0%  2.12Mi .rodata            2.12Mi   2.2%
       5.4%  1.92Mi .gcc_except_table  1.92Mi   2.0%
     100.0%  35.5Mi TOTAL              94.4Mi 100.0%

One issue remains unsolved, which is that Rust/LLVM cannot currently
garbage collect DWARF that refers to unused symbols/types. The actual
symbols get cut from the binary, but their debug info remains. Follow
rust-lang/rust#56068 and LLVM D74169 [0] if curious. I tested with the
aforementioned lld patch (and none of the other changes) and it cut the
binary down to 300MB. With the other changes, the savings are less
substantial, but probably another 10MB to be had.

[0]: https://reviews.llvm.org/D74169
benesch added a commit to benesch/materialize that referenced this issue Apr 18, 2020
Our Linux release binary was hilariously large, weighing in at nearly
800MB (!). Nearly all of the bloat was from DWARF debug info:

    $ bloaty materialized -n 10
        FILE SIZE        VM SIZE
     --------------  --------------
      24.5%   194Mi   0.0%       0    .debug_info
      24.1%   191Mi   0.0%       0    .debug_loc
      13.8%   109Mi   0.0%       0    .debug_pubtypes
      10.1%  79.9Mi   0.0%       0    .debug_pubnames
       8.8%  70.0Mi   0.0%       0    .debug_str
       8.3%  66.3Mi   0.0%       0    .debug_ranges
       4.4%  35.3Mi   0.0%       0    .debug_line
       3.1%  24.8Mi  66.3%  24.8Mi    .text
       1.8%  14.4Mi  25.1%  9.39Mi    [41 Others]
       0.6%  4.79Mi   0.0%       0    .strtab
       0.4%  3.22Mi   8.6%  3.22Mi    .eh_frame
     100.0%   793Mi 100.0%  37.4Mi    TOTAL

This patch gets a handle on this by attacking the problem
from several angles:

  1. We instruct the linker to compress debug info sections. Most of the
     debug info is redundant and compresses exceptionally well. Part of
     the reason we didn't notice the issue is because our Docker images
     and gzipped tarballs were relatively small (~150MB).

  2. We strip out the unnecessary `.debug_pubnames` and `.debug_pubtypes`
     sections from the binary. This works around a known Rust bug
     (rust-lang/rust#46034).

  3. We ask Rust to generate less debug info for release builds,
     limiting it to line info. This is enough information to symbolicate
     a backtrace, but not enough information to run an interactive
     debugger. This is usually the right tradeoff for a release build.

    $ bloaty materialized -n 10
        FILE SIZE       VM SIZE
     --------------   --------------
      33.8%  31.9Mi     0.0%       0  .debug_info
      26.5%  25.0Mi    70.5%  25.0Mi  .text
       8.0%  7.54Mi     0.0%       0  .debug_str
       6.7%  6.36Mi     0.0%       0  .debug_line
       5.7%  5.36Mi     9.4%  3.33Mi  [38 Others]
       5.0%  4.71Mi     0.0%       0  .strtab
       3.8%  3.55Mi     0.0%       0  .debug_ranges
       3.3%  3.11Mi     8.8%  3.11Mi  .eh_frame
       3.0%  2.87Mi     0.0%       0  .symtab
       2.2%  2.12Mi     6.0%  2.12Mi  .rodata
       2.0%  1.92Mi     5.4%  1.92Mi  .gcc_except_table
     100.0%  94.4Mi   100.0%  35.5Mi  TOTAL

One issue remains unsolved, which is that Rust/LLVM cannot currently
garbage collect DWARF that refers to unused symbols/types. The actual
symbols get cut from the binary, but their debug info remains. Follow
rust-lang/rust#56068 and LLVM D74169 [0] if curious. I tested with the
aforementioned lld patch and the resulting binary is even small, at
71MB, so there's another 25MB of savings to be had there. (That patch on
its own, without the other changes, cuts the ~800MB binary to a ~300MB
binary, so it's an impressive piece of work. Unfortunately it also
increases link time by 15-25x.)

[0]: https://reviews.llvm.org/D74169
@jonas-schievink jonas-schievink added C-enhancement Category: An issue proposing an enhancement or a PR with one. I-compiletime Issue: Problems and improvements with respect to compile times. I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 18, 2020
@benesch
Copy link
Contributor

benesch commented Apr 18, 2020

There's an LLVM patch out that adds a --gc-debuginfo flag to lld that seems to have exactly the desired effect: https://reviews.llvm.org/D74169

Looks to be a ways away from landing, but I tested it on a relatively large Rust project and it was able to reduce an 800MB binary to a 300MB binary (with full debug info). Details in a comment here: https://reviews.llvm.org/D74169#1990180

benesch added a commit to benesch/materialize that referenced this issue Apr 18, 2020
Our Linux release binary was hilariously large, weighing in at nearly
800MB (!). Nearly all of the bloat was from DWARF debug info:

    $ bloaty materialized -n 10
        FILE SIZE        VM SIZE
     --------------  --------------
      24.5%   194Mi   0.0%       0    .debug_info
      24.1%   191Mi   0.0%       0    .debug_loc
      13.8%   109Mi   0.0%       0    .debug_pubtypes
      10.1%  79.9Mi   0.0%       0    .debug_pubnames
       8.8%  70.0Mi   0.0%       0    .debug_str
       8.3%  66.3Mi   0.0%       0    .debug_ranges
       4.4%  35.3Mi   0.0%       0    .debug_line
       3.1%  24.8Mi  66.3%  24.8Mi    .text
       1.8%  14.4Mi  25.1%  9.39Mi    [41 Others]
       0.6%  4.79Mi   0.0%       0    .strtab
       0.4%  3.22Mi   8.6%  3.22Mi    .eh_frame
     100.0%   793Mi 100.0%  37.4Mi    TOTAL

This patch gets a handle on this by attacking the problem
from several angles:

  1. We instruct the linker to compress debug info sections. Most of the
     debug info is redundant and compresses exceptionally well. Part of
     the reason we didn't notice the issue is because our Docker images
     and gzipped tarballs were relatively small (~150MB).

  2. We strip out the unnecessary `.debug_pubnames` and `.debug_pubtypes`
     sections from the binary. This works around a known Rust bug
     (rust-lang/rust#46034).

  3. We ask Rust to generate less debug info for release builds,
     limiting it to line info. This is enough information to symbolicate
     a backtrace, but not enough information to run an interactive
     debugger. This is usually the right tradeoff for a release build.

    $ bloaty materialized -n 10
        FILE SIZE       VM SIZE
     --------------   --------------
      33.8%  31.9Mi     0.0%       0  .debug_info
      26.5%  25.0Mi    70.5%  25.0Mi  .text
       8.0%  7.54Mi     0.0%       0  .debug_str
       6.7%  6.36Mi     0.0%       0  .debug_line
       5.7%  5.36Mi     9.4%  3.33Mi  [38 Others]
       5.0%  4.71Mi     0.0%       0  .strtab
       3.8%  3.55Mi     0.0%       0  .debug_ranges
       3.3%  3.11Mi     8.8%  3.11Mi  .eh_frame
       3.0%  2.87Mi     0.0%       0  .symtab
       2.2%  2.12Mi     6.0%  2.12Mi  .rodata
       2.0%  1.92Mi     5.4%  1.92Mi  .gcc_except_table
     100.0%  94.4Mi   100.0%  35.5Mi  TOTAL

One issue remains unsolved, which is that Rust/LLVM cannot currently
garbage collect DWARF that refers to unused symbols/types. The actual
symbols get cut from the binary, but their debug info remains. Follow
rust-lang/rust#56068 and LLVM D74169 [0] if curious. I tested with the
aforementioned lld patch and the resulting binary is even small, at
71MB, so there's another 25MB of savings to be had there. (That patch on
its own, without the other changes, cuts the ~800MB binary to a ~300MB
binary, so it's an impressive piece of work. Unfortunately it also
increases link time by 15-25x.)

[0]: https://reviews.llvm.org/D74169
@zephyrus00jp
Copy link

zephyrus00jp commented Apr 19, 2020 via email

@khuey
Copy link
Contributor

khuey commented Aug 25, 2022

This continues to be an enormous problem. I have a binary that's 1% useful stuff and 99% debug info, almost all of which is for dead code. And that --gc-debuginfo LLD thing hasn't had any activity in almost two years.

@athre0z
Copy link
Contributor

athre0z commented Dec 2, 2022

Agreed. The majority of DWARF debug records in even a hello world application are dead code with bogus PC ranges all starting at 0. This is not only bloating the binary, but also simply wrong and requires anyone parsing Rust DWARF to special case it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) C-enhancement Category: An issue proposing an enhancement or a PR with one. I-compiletime Issue: Problems and improvements with respect to compile times. I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

10 participants