[llvm] [llvm-dwp] Replace MCStreamer with direct ELF writer for zero-copy output (PR #192112)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Fri Apr 17 22:54:56 PDT 2026


MaskRay wrote:

I spent some time with Claude Opus 4.7 to see how the ObjCopy implementation would pan out. 
https://github.com/MaskRay/llvm-project/commits/dwp-objcopy/ 

## pros

```
  Pros (ObjCopy path)

  Single ELF writer in tree. llvm/lib/ObjCopy/ELF/ is already the canonical non-MC ELF writer. llvm-objcopy, llvm-strip, llvm-dwarfutil all use it. Piggy-backing on it means the next time someone fixes an ELF bug (symbol versioning, compressed sections, large index tables,
  SHN_LORESERVE handling, etc.) llvm-dwp inherits the fix. Matches what dwblaikie and jh7370 asked for in the PR comments.

  Correctness for free.
  - 32/64 class and LE/BE endianness are templated on ELFT. The PR's writer hardcodes ELF64LE; I caught this as a blocking issue in review. My prototype derives it from the input file and the BE variant of compress.test actually exercises it.
  - SHF_COMPRESSED, oversized section count (SHN_LORESERVE/SHT_SYMTAB_SHNDX), and ABIVersion == ELFOSABI_GNU quirks are all handled by ELFWriter<ELFT>::finalize.
  - You get consistent alignment, proper sh_entsize, and section-header-string-table layout without hand-rolling.

  Smaller surface to review/maintain. The prototype deletes the whole BinaryFormat/ELFWriter.{h,cpp} and associated MC refactor (ELFObjectWriter.cpp changes). Net diff is smaller and doesn't perturb MC.

  Exposed a latent bug in the PR. Porting forced me to understand SectionData semantics, which is how I noticed the interleaving bug that made merge_v5.test hang. This wasn't a theoretical concern — the PR as submitted produces corrupt .debug_str_offsets.dwo for any multi-input run.

  Future extension point. Adding SHF_COMPRESSED output, --add-gnu-debuglink-style features, or building .dwps with multiple .debug_info contributions would be trivial since ObjCopy already knows how. A third writer in BinaryFormat would have to re-implement each.
```

## cons

```
### 1. Header plumbing
`llvm/lib/ObjCopy/ELF/ELFObject.h` is lib-private. The prototype reaches in via `#include "../ObjCopy/ELF/ELFObject.h"` — not shippable. A production change needs one of:
- **(a)** Promote relevant parts to `llvm/include/llvm/ObjCopy/ELF/` — widens ObjCopy's public API, which has been deliberately kept narrow.
- **(b)** Expose a narrow `Error writeDWP(Object&, raw_pwrite_stream&)` helper in ObjCopy's public API — cleaner but adds API surface for a single caller.
- **(c)** Move `writeELF` into `lib/ObjCopy/DWP.cpp` so DWP lives *inside* ObjCopy — awkward layering.

### 2. Extra data copy; perf impact unknown
ObjCopy's `ELFWriter::write()`:
```
Buf = WritableMemoryBuffer::getNewMemBuffer(totalSize);
// memcpy every section into Buf
Out.write(Buf->getBufferStart(), Buf->getBufferSize());
```
For the PR's 3.3 GB case that's 3.3 GB of anonymous mmap + a full-size memcpy. The PR's custom writer streams chunks straight to the output `raw_pwrite_stream` with zero extra copies for pass-through sections.

**Not yet benchmarked.** The PR claims 23.6s → 6.0s (3.9×) on a 3.3 GB file. Most of that win is escaping MC fragments/layout, not the final memcpy, so ObjCopy probably still delivers most of it — perhaps 2–3× instead of 3.9×. There is a standing TODO in ObjCopy source:
```cpp
// TODO: Implement direct writing to the output stream (without
//       intermediate memory buffer Buf).
```
Fixing that TODO benefits every ObjCopy caller.

### 3. `Section` is single-contents; accumulated DWP sections concat once
ObjCopy's `Section` holds a single `ArrayRef<uint8_t> Contents`. For DWP sections accumulated across many inputs (`.debug_info`, `.debug_abbrev` from N `.dwo`s) the prototype concatenates all chunks into a `std::vector<uint8_t>` before handing it to `OwnedDataSection`. That's one extra
 full-size copy per accumulated section versus the PR's chunk-list model.

Recovering zero-copy requires a new `SectionBase` subclass (`MultiChunkSection`) plus a `SectionVisitor::visit()` overload — a cross-cutting ObjCopy change, not a DWP-local one.

### 4. Dependency graph grows
`LLVMDWP` → `LLVMObjCopy` pulls in `LLVMMC` transitively (ObjCopy uses MC for `StringTableBuilder`). The PR's direction was opposite: *drop* `LLVMMC` + `AllTargets*` from the `llvm-dwp` tool to shrink binary size (an advertised bonus). The ObjCopy path gives that bonus back. In practic
e most distros symlink `llvm-dwp` alongside `llvm-objcopy` so combined binary size doesn't grow — but standalone builds lose the reduction.

### 5. Section-order / byte-layout drift
My prototype places `.shstrtab` at section index 1 (added first); the PR places it near the end. Both are valid ELF, but tools that diff `.dwp` files byte-for-byte (reproducibility checks, golden files) will see different output. Worth matching conventional placement before landing.

### 6. Complexity drift
ObjCopy's writer is ~3000 lines designed for full ELF round-tripping: segments, program headers, symbol tables, relocations, segment-offset preservation. `llvm-dwp` needs essentially none of that. Future ObjCopy refactors aimed at its main callers could break DWP subtly (e.g., changes
to how `OriginalOffset` is used during layout). Mitigation: targeted DWP-path unit test in ObjCopy.
```

https://github.com/llvm/llvm-project/pull/192112


More information about the llvm-commits mailing list