llvm / llvm/llvm-project

[CodeGen][ELF] Unused string literals from gapped switches are not eliminated by --gc-sections

Open
#199,194 1 comment 0 reactions 0 assignees View on GitHub
llvm:codegen missed-optimization
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

**Motivation**
When building "bluez" with LLVM vs GCC for AArch64, the binaries built by LLVM vs GCC are having increased size in .rodata growth +~56KB per each binary (like `sdptool` or `l2ping`). This causing total +1.8MB of `.rodata` increase for all binaries from bluez. The problem strictly affects environments where `ld.bfd` is a mandatory constraint (due to platform-specific policies), making switching to `lld` impossible for this configuration.

**Test-case**
Synthetic reproducer demonstrating the root cause:
```
$ cat foo.h
#ifndef __FOO_H
#define __FOO_H

const char *foo();
const char *goo(int i);

#endif /* __FOO_H */

$ cat foo.c
#include "foo.h"

const char *foo() {
return "FOO";
}

#define CASE_X1(n) case n: return "qwertyqwertyabracadabra" #n;
#define CASE_X4(n) CASE_X1(n) CASE_X1(n+1) CASE_X1(n+2) CASE_X1(n+3)
#define CASE_X16(n) CASE_X4(n) CASE_X4(n+4) CASE_X4(n+8) CASE_X4(n+12)
#define CASE_X64(n) CASE_X16(n) CASE_X16(n+16) CASE_X16(n+32) CASE_X16(n+48)
#define CASE_X256(n) CASE_X64(n) CASE_X64(n+64) CASE_X64(n+128) CASE_X64(n+192)
#define CASE_X1024(n) CASE_X256(n) CASE_X256(n+256) CASE_X256(n+512) CASE_X256(n+768)

const char *goo(int i) {
switch (i) {
CASE_X1024(0)
CASE_X1024(10000) // having the 1024..9999 gap
default: return "def";
}
}

$ cat main.c
#include
#include "foo.h"

int main() {
printf("foo:%s\n", foo());
return 0;
}
```

Building for AArch64:
```
$ clang -O2 -ffunction-sections -fdata-sections -c foo.c -o foo.o
$ clang -O2 -ffunction-sections -fdata-sections -c main.c -o main.o
$ SYSROOT=`aarch64-linux-ld.bfd --print-sysroot`
$ aarch64-linux-ld.bfd -dynamic-linker $SYSROOT/lib/ld-linux-aarch64.so.1 $SYSROOT/usr/lib/crt1.o -lc --gc-sections main.o foo.o -o main.x
```

**Analysis**
For this gapped switch, LLVM groups all strings into a single `.rodata.str1.1` section:
```
$ aarch64-linux-objdump -h foo.o | grep rodata
3 .rodata.goo 00005620 ...
4 .rodata.str1.1 00013208 ...
```

`main.c` only references `foo()`. However, because all strings are in the same section, the linker cannot eliminate the unused literals from `goo()`, resulting in a bloated `.rodata`:
```
$ aarch64-linux-objdump -h main.x | grep rodata
11 .rodata 00013210 ... <--- 76KB
```

In a contiguous switch case (replace `10000`->`1024` in foo.c), a jump table is generated, and LLVM emits each string literal into its own unique section (`.rodata..L.str.X`):
```
3 .rodata.str1.1 00000008 ...
4 .rodata..L.str.1 00000019 ...
5 .rodata..L.str.2 0000001b ...
...
2050 .rodata..L.str.2047 0000002c ...
2051 .rodata..L.str.2048 0000002c ...
2052 .rodata..Lswitch.table.goo.rel 00002000 ...
```
So that linker could optimize-out unnecessary global string sections:
```
$ aarch64-linux-objdump -h main.x | grep rodata
11 .rodata 00000010 ... <--- 16B
```

For the gapped switch, the front-end assigns the `unnamed_addr` attribute to string globals, enabling LLVM to mark them as mergeable and optimize within the module. The AsmPrinter then merges everything into `.rodata.str1.1`, which prevents the linker from tracking and eliminating unused literals in the final binary.

GCC handles this differently by grouping strings per-function, which is sufficient for `--gc-sections` to work:
```
$ aarch64-linux-objdump -h foo.o | grep rodata
3 .rodata.foo.str1.8 00000004 ...
5 .rodata.goo.str1.8 00014d84 ...
$ aarch64-linux-objdump -h main.x | grep rodata
11 .rodata 0000000c ... <--- 12B
```

**Proposed approach**
Introduce a new `-fstring-sections` flag, working similar to `-fdata-sections`, but forcely enabling `EmitUniqueSection` in the `getELFSectionNameForGlobal()` for mergeable strings, placing each global string into its own unique section with a mangled name.

Rough PoC patch:
```diff
--- a/llvm/lib/CodeGen/TargetLoweringObjectFileImpl.cpp
+++ b/llvm/lib/CodeGen/TargetLoweringObjectFileImpl.cpp
@@ -638,7 +638,7 @@ getELFSectionNameForGlobal(const GlobalObject *GO, SectionKind Kind,
const MachineJumpTableEntry *JTE) {
SmallString<128> Name =
getSectionPrefixForGlobal(Kind, TM.isLargeGlobalValue(GO));
- if (Kind.isMergeableCString()) {
+ if (Kind.isMergeableCString() && !TM.getStringSections()) {
// We also need alignment here.
// FIXME: this is getting the alignment of the character, not the
// alignment of the global!
@@ -949,6 +949,8 @@ MCSection *TargetLoweringObjectFileELF::SelectSectionForGlobal(
EmitUniqueSection = TM.getDataSections();
}
EmitUniqueSection |= GO->hasComdat();
+ if (Kind.isMergeableCString())
+ EmitUniqueSection |= TM.getStringSections();
return selectELFSectionForGlobal(getContext(), GO, Kind, getMangler(), TM,
Used.count(GO), EmitUniqueSection, Flags,
&NextUniqueID);
```

Any thoughts on this approach? If this looks reasonable, I'll clean it up and open a PR.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.