Skip to content

obs-codec: AVC and HEVC header extraction writes straight into its bmalloc buffers (Fixes #1302) - #1415

Merged
zackees merged 2 commits into
obs-rust:masterfrom
vpacher:perf/1302-nal-extract-alloc
Oct 18, 2026
Merged

zackees merged 2 commits into
obs-rust:masterfrom
vpacher:perf/1302-nal-extract-alloc

Conversation

@vpacher

@vpacher vpacher commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Stacked: on #1414 (the first two commits). Review only the last two commits; I rebase this PR after #1414 merges. It needs #1414 only for the shared counting allocator in the tests (alloc_count.rs).

Fixes #1302

What and why

obs_extract_avc_headers and obs_extract_hevc_headers grew three Vecs (packet, header, SEI) unit by unit and then copied each one into a new bmalloc buffer: 5 Rust heap allocations for a normal 256-byte keyframe packet. That fixed cost made the 256-byte cases about 1.25x slower than C. The larger cases were already faster than C because of the faster start-code scan.

A second scan only to get the output sizes would double the scan cost, so the fix scans once:

  • nal::SplitUnits scans the start codes once. It adds up the three output sizes and keeps the ranges of the first 32 units on the stack (encoders write a few units per packet: parameter sets, SEI, slices). for_each gives the units again from those ranges. For a packet with more units it scans a second time, so it never allocates.
  • packet::write_split_units allocates each output with bmalloc at its final size and copies every unit straight into it. It panics instead of writing past an output when the units do not add up to the sizes. Empty outputs stay NULL with size 0.
  • avc::bucket and hevc::bucket name the unit classification that extract_headers used inline. split_headers keeps its Vec API for the tests.

No change to the C ABI or to any output byte (unit order, start codes, the packet/header/SEI split, bmalloc/bfree ownership).

RED → GREEN

  • RED commit: f9c7830. rust/obs-codec/tests/codec/nal_alloc.rs counts Rust heap allocations on the calling thread (the counter moves to alloc_count.rs, shared with obs-codec: AV1 output shims write straight into their bmalloc buffers (Fixes #1304) #1414's av1_alloc.rs: one test executable can have only one global allocator). 1/4 pass (the no-start-code guard): AVC and HEVC 5 allocations each for a normal packet, more than one for a packet with very many units.
  • GREEN: 57a11b7. 6/6, including a new test that compares the outputs for packets past the 32 stack-kept units with the C oracle byte for byte (AVC and HEVC, 64 KiB with thousands of units). The "very many units" test is tightened from at most one allocation to none. 83/83 obs-tests-codec (the AVC/HEVC parity proptests are unchanged), clippy -D warnings, cargo bench ... -- --test fixture checks. Tier 2 runs in PR CI.
  • Benchmarks: record.py options, only the extract/ cases, 5 paired passes before and after, same host (Apple M1 Max) and load. Median Rust/C and paired range:
Benchmark before after
avc/extract/256 1.280x (1.203-1.295) 0.839x (0.824-0.850)
hevc/extract/256 1.261x (1.249-1.280) 0.789x (0.751-0.790)
avc/extract/16384 0.382x (0.375-0.408) 0.302x (0.295-0.307)
hevc/extract/16384 0.397x (0.389-0.413) 0.302x (0.297-0.305)
av1/extract/256 (not changed here) 0.483x 0.464x
av1/extract/16384 (not changed here) 0.663x 0.640x

The 256-byte regression was stable on this host (the issue's wide ranges came from a shared host). Both cases are now below C.

Checklist

  • Base is master (or the description starts with a Stacked: note)
  • I claimed the issue, or no one else has an open PR for it
  • Characterization test (if the unit had none), then RED, then GREEN commits (parity and Tier 1 tests already cover the output bytes)
  • User-visible change: its line in docs/features.md is added or updated (not applicable: no user-visible change; the Rust libobs is not shipped by default)
  • (Optional) Local gate passed and the pushed head is stamped; otherwise PR CI runs the full suite
  • Warning or flag changes (-W..., ratchets): not applicable
  • Intentional differences from C listed in the parity test header and rust/README.md (ports): none

🤖 Generated with Claude Code

@vpacher
vpacher requested a review from zackees as a code owner October 18, 2026 09:41
@zackees
zackees added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 18, 2026
@zackees
zackees enabled auto-merge October 18, 2026 16:11
vpacher and others added 2 commits October 18, 2026 09:13
…ts (Refs obs-rust#1302)

RED: 1/4 (the no-start-code guard passes). obs_extract_avc_headers and
obs_extract_hevc_headers make 5 Rust heap allocations for a normal
256-byte keyframe packet (three growing Vecs, then a copy into bmalloc)
and more than one for a packet with very many units. The counting
global allocator moves from av1_alloc.rs to alloc_count.rs: one test
executable can have only one global allocator.

Benchmarks before the fix (Apple M1 Max, record.py options, extract/
only, 5 paired passes; median Rust/C and paired range):
  avc/extract/256     1.280x (1.203-1.295)
  hevc/extract/256    1.261x (1.249-1.280)
  avc/extract/16384   0.382x (0.375-0.408)
  hevc/extract/16384  0.397x (0.389-0.413)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alloc buffers (Fixes obs-rust#1302)

GREEN: 6/6 allocation tests (one added: packets past the stack-kept
units match the C oracle byte for byte), 83/83 obs-tests-codec with the
AVC/HEVC parity proptests unchanged, clippy -D warnings, bench fixture
checks.

- nal::SplitUnits scans the start codes once. It adds up the three
  output sizes and keeps the ranges of the first 32 units on the stack.
  for_each gives the units again from those ranges, or for a packet with
  more units from one more scan, so it never allocates.
- packet::write_split_units bmallocs each output at its final size and
  copies every unit straight into it. It panics instead of writing past
  an output when the units do not add up to the sizes.
- avc::bucket and hevc::bucket name the unit classification that
  extract_headers used inline; split_headers keeps its Vec API for the
  tests, pre-sized from SplitUnits.

The "very many units" test is tightened from at most one allocation to
none: the second scan replaces the heap fallback.

Benchmarks (Apple M1 Max, record.py options, extract/ only, 5 paired
passes, same load as the RED run; median Rust/C and paired range):
  avc/extract/256     1.280x -> 0.839x (0.824-0.850)
  hevc/extract/256    1.261x -> 0.789x (0.751-0.790)
  avc/extract/16384   0.382x -> 0.302x (0.295-0.307)
  hevc/extract/16384  0.397x -> 0.302x (0.297-0.305)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zackees
zackees force-pushed the perf/1302-nal-extract-alloc branch from 57a11b7 to 57b0965 Compare October 18, 2026 16:13
@zackees
zackees added this pull request to the merge queue Oct 18, 2026
Merged via the queue into obs-rust:master with commit b5fc888 Oct 18, 2026
23 checks passed
@vpacher
vpacher deleted the perf/1302-nal-extract-alloc branch October 18, 2026 17:26
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.

bug: codec: small AVC and HEVC header extraction regress against C

2 participants