Skip to content

perf(kernel): QEMU sorts 42k guest symbols on every boot and never reads one - #76

Merged
aledbf merged 2 commits into
mainfrom
perf/strip-guest-kernel
Sep 7, 2026
Merged

perf(kernel): QEMU sorts 42k guest symbols on every boot and never reads one#76
aledbf merged 2 commits into
mainfrom
perf/strip-guest-kernel

Conversation

@aledbf

@aledbf aledbf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

QEMU's load_elf() reads the symbol table of the kernel it loads and qsort()s it, so it can name addresses for its own debugging. Nothing in this stack ever asks it: the guest resolves its own names through CONFIG_KALLSYMS, which lives in the loaded image data and has nothing to do with the ELF .symtab. The image is a 39.4 MB vmlinux of which 30.7 MB is PT_LOAD; most of the rest is that table.

How it was found

A perf profile of QEMU's main thread — once the binary had symbols to profile with. The production build is stripped, which is why an earlier profile of the same window named nothing but raw addresses:

1.58%  __memmove_avx_unaligned_erms   the copy into guest RAM
0.93%  msort_with_tmp.part.0          ┐ glibc qsort of the
0.52%  symcmp64                       ┘ guest's symbol table
0.67%  kernel_init_pages              host zeroing guest pages
0.56%  migrate_folio_unmap            ┐ host THP compaction
0.43%  suitable_migration_target      ┘

Why stripping is a clean cut

It removes nothing the machine loads:

before after
file 39.4 MB 34.3 MB
PT_LOAD 30.68 MB 30.68 MB (byte-identical)
PVH ELF notes present present

Measured

A/B/A/B, eight runs per arm, same shim/initrd/QEMU/image, host-quiet gated. Medians with [min–max]:

with symtab stripped
qemu_launch 36.1 [33.4–45.9] ms 24.3 [22.6–27.1] ms
guest_boot 92.0 ms 92.8 ms
first-accept 54.4 [52.2–58.5] ms 55.1 [53.7–56.1] ms
BOOT_TIMELINE total 128.6 [124.1–145.1] ms 118.0 [115.0–123.3] ms

−11.8 ms, entirely host-side. The guest boot is unchanged, as it must be — the bits it runs are the same. The saving lands before QMP answers, which is also why it shows up in qemu_launch and not anywhere in the kernel profile.

This is not a step toward bzImage

The same A/B settles that question: the transfer cost tracks PT_LOAD, not file size. 5.1 MB less file at constant PT_LOAD moved the pre-kernel window not at all (37.5 → 38.5 ms, noise). A compressed image would still expand to the same ~30 MB in guest RAM, paid as in-guest decompression instead of a host copy, and would need the linuxboot_dma option ROM this firmware set deliberately does not ship (Dockerfile.qemu:203).

Validation

  • Full integration suite passes on the stripped kernel.
  • /proc/kallsyms inside the guest still resolves (acpi_initialize_hp_context, …).
  • On the image the Dockerfile now produces: nm reports no symbols, readelf -n still finds the PVH notes, PT_LOAD is 30.68 MB.

🤖 Generated with Claude Code

aledbf and others added 2 commits September 7, 2026 02:10
Dockerfile.qemusym was scaffolding: the production QEMU is built stripped, so a
perf profile of it names only raw addresses, and the symbol table work this
branch removes was invisible until QEMU itself had symbols. It configures the
same build with --disable-strip --enable-debug-info, out of the shared source
and build caches so neither is disturbed.

It served its purpose and does not belong in the tree - the finding is in the
commit that strips the guest kernel, and the file is reproducible from
Dockerfile.qemu by swapping those two flags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fdjKufitSmytkon6wZDCb
@aledbf
aledbf merged commit 8dc4d21 into main Sep 7, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant