Skip to content

Commit 02cc34c

Browse files
committed
build: reduce linux release binary size by 87%
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
1 parent 5af08de commit 02cc34c

File tree

5 files changed

+46
-9
lines changed

5 files changed

+46
-9
lines changed

.cargo/config

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[target."x86_64-unknown-linux-gnu"]
2+
# Compressing debug information can yield hundreds of megabytes of savings.
3+
# The Rust toolchain does not currently perform dead code elimination on
4+
# debug info.
5+
#
6+
# See: https://github.com/rust-lang/rust/issues/56068
7+
# See: https://reviews.llvm.org/D74169#1990180
8+
rustflags = ["-C", "link-arg=-Wl,--compress-debug-sections=zlib-gabi"]

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
target
1313
miri-target
14-
/.cargo
1514
.mtrlz.log
1615
**/*.rs.bk
1716
.netlify

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ members = [
2323
]
2424

2525
[profile.release]
26-
debug = true
26+
# Emit only the line info tables, not full debug info, in release builds, to
27+
# substantially reduce the size of the debug info. Line info tables are enough
28+
# to correctly symbolicate a backtrace, but do not produce an ideal interactive
29+
# debugger experience. This seems to be the right tradeoff for release builds:
30+
# it's unlikely we're going to get interactive access to a debugger in
31+
# production installations, but we still want useful crash reports.
32+
debug = 1
2733

2834
[patch.crates-io]
2935
# Waiting on a release with this commit:

bin/lint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ copyright_files=$(grep -vE \
6060
-e '(^|/)\.gitmodules$' \
6161
-e '(^|/)go\.sum$' \
6262
-e '(^|/)Cargo\.toml$' \
63+
-e '^\.cargo/config$' \
6364
-e '^Cargo\.lock$' \
6465
-e '^deny\.toml$' \
6566
-e '^netlify\.toml$' \

misc/python/mzbuild.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,15 @@ def xcargo_target_dir(root: Path) -> Path:
9898
return root / "target" / "x86_64-unknown-linux-gnu"
9999

100100

101-
def xstrip(root: Path) -> str:
101+
def xbinutil(tool: str) -> str:
102102
if sys.platform == "linux":
103-
return "strip"
103+
return tool
104104
else:
105-
return "x86_64-unknown-linux-gnu-strip"
105+
return f"x86_64-unknown-linux-gnu-{tool}"
106+
107+
108+
xobjcopy = xbinutil("objcopy")
109+
xstrip = xbinutil("strip")
106110

107111

108112
def docker_images() -> Set[str]:
@@ -157,13 +161,32 @@ def run(self, root: Path, path: Path) -> None:
157161
# down CI, since we're packaging these binaries up into Docker
158162
# images and shipping them around. A bit unfortunate, since it'd be
159163
# nice to have useful backtraces if the binary crashes.
160-
runv([xstrip(root), path / self.bin])
164+
runv([xstrip, path / self.bin])
165+
else:
166+
# Even if we've been asked not to strip the binary, remove the
167+
# `.debug_pubnames` and `.debug_pubtypes` sections. These are just
168+
# indexes that speed up launching a debugger against the binary,
169+
# and we're happy to have slower debugger start up in exchange for
170+
# smaller binaries. Plus the sections have been obsoleted by a
171+
# `.debug_names` section in DWARF 5, and so debugger support for
172+
# `.debug_pubnames`/`.debug_pubtypes` is minimal anyway.
173+
# See: https://github.com/rust-lang/rust/issues/46034
174+
runv(
175+
[
176+
xobjcopy,
177+
"-R",
178+
".debug_pubnames",
179+
"-R",
180+
".debug_pubtypes",
181+
path / self.bin,
182+
]
183+
)
161184

162185
def depends(self, root: Path, path: Path) -> List[bytes]:
163186
# TODO(benesch): this should be much smarter about computing the Rust
164187
# files that actually contribute to this binary target.
165188
return super().depends(root, path) + git_ls_files(
166-
root, "src/**", "Cargo.toml", "Cargo.lock"
189+
root, "src/**", "Cargo.toml", "Cargo.lock", ".cargo"
167190
)
168191

169192

@@ -216,7 +239,7 @@ def run(self, root: Path, path: Path) -> None:
216239
with open(path / "tests" / "manifest", "w") as manifest:
217240
for (executable, slug, crate_path) in tests:
218241
shutil.copy(executable, path / "tests" / slug)
219-
runv([xstrip(root), path / "tests" / slug])
242+
runv([xstrip, path / "tests" / slug])
220243
manifest.write(f"{slug} {crate_path}\n")
221244
shutil.move(str(path / "testdrive"), path / "tests")
222245
shutil.copy(
@@ -229,7 +252,7 @@ def depends(self, root: Path, path: Path) -> List[bytes]:
229252
# TODO(benesch): this should be much smarter about computing the Rust
230253
# files that actually contribute to this binary target.
231254
return super().depends(root, path) + git_ls_files(
232-
root, "src/**", "Cargo.toml", "Cargo.lock"
255+
root, "src/**", "Cargo.toml", "Cargo.lock", ".cargo"
233256
)
234257

235258

0 commit comments

Comments
 (0)