Skip to content

perf(binary): unpack packed values through a byte-pair table - #1214

Merged
jlucaso1 merged 4 commits into
mainfrom
bench/binary-small-stanza-decode
Aug 6, 2026
Merged

perf(binary): unpack packed values through a byte-pair table#1214
jlucaso1 merged 4 commits into
mainfrom
bench/binary-small-stanza-decode

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Decoder is entirely pub(crate), so the only way to use this crate's parser is unmarshal_ref, which hands back the whole tree. A consumer that projects the node straight into another representation, as the WASM bridge serving Baileys does, builds that tree only to take it apart. The proposal behind this work was a visitor API letting such a caller read a decoded node without materializing anything, on the same grounds as #1211 and #1213.

The measurement refused the visitor and found something better. The tree turns out to cost almost nothing, so removing it was never worth much. But the benchmark written to evaluate it exposed a path nobody was measuring, and profiling that path found a real 15% of a small-stanza decode sitting in plain sight. That fix helps every caller, including this repository's client, which is what a visitor never would have done.

Measurement

bench_unmarshal_ack is a 30-byte ack whose encoded bytes carry JID_PAIR, NIBBLE_8 and HEX_8 at once; bench_unmarshal_fanout repeats the packed and AD_JID decodes across eight devices.

To be precise about what that adds, since an earlier revision of this description overstated it: create_small_node already reaches read_jid_pair and the nibble half of read_packed. What no bench reached is the hex half, because the large fixture's lowercase abcdef classifies as raw bytes rather than HEX_8. So the ack adds hex coverage plus a combination and frequency that match the wire, not two uncovered paths.

bench time allocations
bench_unmarshal_ack (new, 30 B) 114.9 ns 1 (168 B)
bench_unmarshal_small (existing) 88.7 ns -
bench_unmarshal_fanout (new) 1.338 us 45 (2.9 KB)
bench_unmarshal_large (existing) 1.301 us 50

Figures are fastest, pinned to one core. Allocation counts come from a heap profiler run separately, since it perturbs timing.

Why the visitor was refused. The ack allocates exactly once, for its Box<[(NodeStr, ValueRef)]> of three attributes. Nothing else, because the decoder already does most of what a visitor would: NodeStr::Borrowed keeps frame strings borrowed, ValueRef::Jid holds components rather than rebuilding the JID string, and unpacked hex and nibble values fit inline in a CompactString without hitting the heap. An isolated malloc plus free of 168 bytes measures 4.37 ns against the 113 ns decode, so that single allocation is 3.9%. That independently reproduces on x86-64 native what a wasm profile had already put at under 4% for dlmalloc.

What a visitor could still take is that one allocation plus repositioning roughly 33 bytes of writes for the reconstructed values. The unpacking and UTF-8 validation underneath are protocol work it moves rather than removes. Against a 70 ns gap in 217, it does not close.

What this does not cover. These are native x86-64 numbers; the profile that motivated the proposal is wasm32, where absolute costs differ. It also does not measure a visitor, because none was built: constructing the API to discard it was not worth it once the allocation count came back at one. If someone wants to reopen this, the number to beat is 4%.

Two further suspicions were checked and did not hold: read_packed already has a SIMD path, and its 254-byte stack buffer produces no memset (LLVM elides it, confirmed in the disassembly).

What the profile found

With the ack benchmark in place, perf puts read_packed at 30% of the decode, well above the 7.7% a wasm profile had shown. The cause is that its SIMD path only handles 16-byte chunks, and real packed values never reach one: a 13-digit phone number packs to 7 bytes, a 20-character id to 10. Every packed value on a small stanza takes the scalar loop, which does two shifts, two lookups and two bounds-checked stores per input byte.

A 256-entry table mapping each byte to its two output characters turns that into one load and one 2-byte store. Nibble values 12, 13 and 14 encode nothing, so they are marked in the table and fall back to the scalar walk, which still reports which half was bad.

baseline with the table delta
bench_unmarshal_ack 114.9 ns 98.3 ns -14.5%
bench_unmarshal_fanout 1.338 us 1.249 us -6.7%
bench_unmarshal_small (no packed values) 88.7 ns 87.3 ns noise
bench_unmarshal_large (no packed values) 1.301 us 1.300 us unchanged
instructions, 5M ack decodes 9.375 G 7.925 G -15.5%

The two benches without packed values not moving is the control: the gain comes from the path it should. Instruction counts are from perf stat, which varies by under 1000 across 9 billion, so the -15.5% is exact rather than estimated. Wall time gains less than instruction count does, because the decode is latency-bound rather than throughput-bound; on wasm, where the consumer runs, instructions weigh more.

Why this is not #1211 or #1213

Those opened a door for a consumer with a legitimately different profile, at no cost to anyone who ignores it. This would have too, and it carries none of what sank #1212: no global state, no bound, no eviction, no policy. The criterion that did apply from #1212 applies here as well, and is worth saying plainly: there is no internal beneficiary. This repository's client wants the tree and uses all of it.

That criterion alone would not have been decisive, since #1213 has no internal beneficiary either and was still right to land. What decided it was the size of the prize.

What stays

The benches. They are tracked by CodSpeed from here on, so a regression in read_jid_pair or read_packed becomes visible instead of silent, for this repository's client and for any consumer. The 22 ns between bench_unmarshal_ack and bench_unmarshal_small is the path that had no coverage.

Validation

cargo fmt --all
cargo nextest run --profile ci -p wacore-binary                  # 132 passed
cargo nextest run --profile ci -p wacore-binary --features simd  # 132 passed
cargo clippy -p wacore-binary --all-targets -- -D warnings
cargo bench -p wacore-binary --bench binary_benchmark

No existing test or bench was edited. packed_equivalence pins the decode across the SIMD boundary and the invalid-nibble error, and passes against both the old and the new decoder, so it fixes behavior rather than implementation. Full matrix left to CI.

Follows #1211, #1212 and #1213.

The two unmarshal benches decode nodes with neither a JID nor a hex id,
so read_jid_pair and read_packed, which dominate a small-stanza profile,
were measured by nothing. bench_unmarshal_ack fixes that: a 30-byte ack
whose bytes carry JID_PAIR, NIBBLE_8 and HEX_8, which costs 113 ns
against 90 ns for the existing small node. That 22 ns of decode path had
no coverage at all. bench_unmarshal_fanout adds the repeated shape, eight
devices at 1.35 us.

Measured while evaluating a visitor API that would decode without
building the tree, and the benches are what survived. The tree turns out
to cost almost nothing: with a heap profiler, the ack allocates exactly
once, 168 bytes for its attribute array, because the decoder already
borrows frame strings and keeps ValueRef::Jid lazy rather than rebuilding
the JID. An isolated malloc plus free of that size is 4.37 ns, 3.9% of
the decode, which independently reproduces on x86-64 what a wasm profile
had put at under 4%. There is no tree cost to remove.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Performance
    • Improved packed-value decoding for faster processing of hexadecimal and nibble-encoded data.
  • Bug Fixes
    • Invalid packed values now produce clearer validation errors while preserving existing safeguards.
  • Tests
    • Expanded coverage for odd-length values, SIMD boundaries, and round-trip decoding.
    • Added validation checks for malformed packed values and broader device-related decoding scenarios.

Walkthrough

Packed hex and nibble decoding now uses lookup tables. Tests cover round trips across SIMD boundaries and invalid values. Binary benchmarks add ACK and device-fanout payloads.

Changes

Packed decoding optimization

Layer / File(s) Summary
Lookup-table packed decoding
wacore/binary/src/decoder.rs
Adds lookup tables for hexadecimal and nibble output. Decoding copies precomputed pairs and preserves invalid-nibble errors.
Packed decoding equivalence tests
wacore/binary/tests/packed_equivalence.rs
Tests round trips across SIMD boundaries, odd lengths, and invalid nibble values.
ACK and fanout benchmark coverage
wacore/binary/benches/binary_benchmark.rs
Adds ACK and device-fanout fixtures, marshaled-data setup, and unmarshal benchmarks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: performance

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main optimization: byte-pair table decoding for packed values.
Description check ✅ Passed The description directly explains the packed-value optimization, benchmark coverage, measurements, validation, and omitted visitor API.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bench/binary-small-stanza-decode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces scalar packed-value expansion with byte-pair lookup tables and adds focused benchmarks and equivalence tests.

  • Adds precomputed lookup tables for hex and nibble unpacking.
  • Adds ack and device-fanout decoding benchmarks.
  • Adds round-trip and malformed-nibble regression coverage.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
wacore/binary/src/decoder.rs Replaces scalar hex and nibble expansion with pair-table copies while retaining invalid-nibble validation.
wacore/binary/tests/packed_equivalence.rs Adds packed-value round-trip coverage across chunk boundaries and checks rejection of invalid nibble values.
wacore/binary/benches/binary_benchmark.rs Adds representative ack and device-fanout decode benchmarks.

Reviews (4): Last reviewed commit: "test(binary): reject an invalid nibble f..." | Re-trigger Greptile

Comment thread wacore/binary/benches/binary_benchmark.rs
Comment thread wacore/binary/benches/binary_benchmark.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 487ed867d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/binary/benches/binary_benchmark.rs Outdated
Profiling the new ack benchmark put read_packed at 30% of a small-stanza
decode, far above where a wasm profile had it. The reason is that its
SIMD path only handles 16-byte chunks, and real packed values never reach
that: a 13-digit phone number packs to 7 bytes, a 20-character id to 10.
Every packed value on a small stanza takes the scalar loop, which does
two shifts, two lookups and two bounds-checked stores per input byte.

A 256-entry table mapping each byte to its two output characters makes
that one load and one 2-byte store. Nibble values 12, 13 and 14 encode
nothing, so they are marked in the table and fall back to the scalar
walk, which reports which half was bad, unchanged.

On the ack: 15.5% fewer instructions (9.375G to 7.925G over 5M decodes,
measured with perf) and 14.5% less wall time (114.9 ns to 98.3 ns). The
fanout drops 6.7%. The two benches without packed values do not move,
which is where the gain should come from and does.

The equivalence tests pass against both the old and the new decoder, so
they pin behavior rather than implementation.
@jlucaso1 jlucaso1 changed the title bench(binary): cover the small stanza the wire is actually made of perf(binary): unpack packed values through a byte-pair table Aug 6, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Confidence score: 4/5

  • In wacore/binary/benches/binary_benchmark.rs, the fanout benchmark comment and implementation diverge (read_jid_pair runs only for the parent to, while child device JIDs use read_ad_jid), which can mislead performance conclusions about parser hot paths—align the benchmark logic or update the comment so measured behavior matches the stated intent.
  • In wacore/binary/benches/binary_benchmark.rs (create_small_node docs), the comment currently understates exercised paths even though the fixture appears to hit read_jid_pair and read_packed; this can confuse future benchmark interpretation—reword the doc comment to reflect what the fixture actually covers.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="wacore/binary/benches/binary_benchmark.rs">

<violation number="1" location="wacore/binary/benches/binary_benchmark.rs:28">
P3: This doc comment claims create_small_node exercises neither read_jid_pair nor read_packed, but the existing fixture's JID (user@s.whatsapp.net) and numeric id (12345) already exercise these paths. Consider rewording to clarify this bench measures a more representative combination/frequency of already-covered operations rather than previously-uncovered paths, so CodSpeed results aren't misinterpreted.</violation>

<violation number="2" location="wacore/binary/benches/binary_benchmark.rs:37">
P2: The fanout benchmark does not repeat `read_jid_pair` per child as the comment claims: device JIDs take the `AD_JID`/`read_ad_jid` path, while only the single parent `to` value takes `read_jid_pair`. This makes the benchmark unsuitable for the stated repeated JID_PAIR coverage unless the fixture is changed to use bare JIDs, or the comment is corrected to describe the AD_JID path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread wacore/binary/benches/binary_benchmark.rs Outdated
Comment thread wacore/binary/benches/binary_benchmark.rs Outdated
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 6, 2026
Two claims in the fixture comments were wrong, both checked against the
encoded bytes rather than assumed this time.

create_small_node does reach read_jid_pair and read_packed: it encodes
JID_PAIR for user@s.whatsapp.net and NIBBLE_8 for the id 12345. What no
bench reached was the hex half of read_packed, because the large
fixture's lowercase abcdef classifies as raw bytes, not HEX_8. So the ack
adds hex coverage and a representative combination, not two uncovered
paths.

The fanout does not repeat read_jid_pair either. Device-qualified JIDs
encode as AD_JID, so what repeats per child is the packed decode and the
AD path.

Neither correction touches the measurement: read_packed is still 30% of
the ack decode and the table still removes 15.5% of its instructions.
@greptile-apps
greptile-apps Bot dismissed their stale review August 6, 2026 18:14

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 6, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Confidence score: 5/5

  • In wacore/binary/tests/packed_equivalence.rs, the invalid-nibble coverage currently uses only a single packed byte, so it misses the 16-byte chunk scalar fallback path called out for nibbles 12/13/14; this leaves a small regression risk that those lanes could behave differently without being caught by CI — add a multi-byte (>16-byte) invalid-nibble case to exercise that fallback path directly.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="wacore/binary/tests/packed_equivalence.rs">

<violation number="1" location="wacore/binary/tests/packed_equivalence.rs:37">
P3: The invalid-nibble test only ever feeds a single packed byte, so it exercises the remainder scalar path, never the 16-byte-chunk scalar fallback that the PR calls out for nibble 12/13/14. Consider adding a case with packed length >= 16 containing an invalid low nibble so the chunk double-pass validation is actually covered.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread wacore/binary/tests/packed_equivalence.rs Outdated
The invalid-nibble case fed a single packed byte, which only ever reaches
the scalar remainder. Under the simd feature, a bad nibble in the first
16 bytes takes a different route: the chunk fails its vector validation
and falls back to a scalar double pass. That route had no test.

A 20-byte frame with the bad nibble at byte 5 covers it. Both lengths
now pass with and without the feature, and against the pre-table decoder,
so the assertion is about the format rather than about either
implementation.
@greptile-apps
greptile-apps Bot dismissed their stale review August 6, 2026 18:24

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/binary/tests/packed_equivalence.rs`:
- Around line 36-59: Update an_invalid_nibble_is_still_rejected to pass both
short and long invalid frames through the alternate decoder entry point in
addition to unmarshal_ref. Assert that the alternate decoder rejects each frame
and includes the offending nibble in its error, preserving the existing checks
for unmarshal_ref.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3cce3522-2f85-4fd9-9e61-743863b61fd8

📥 Commits

Reviewing files that changed from the base of the PR and between f9cd9a1 and 374579d.

📒 Files selected for processing (1)
  • wacore/binary/tests/packed_equivalence.rs

Comment thread wacore/binary/tests/packed_equivalence.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Behavior-preserving performance refactor: packed hex/nibble decoding replaced by lookup tables, with new equivalence tests pinning boundary and invalid-nibble behavior. No public API, policy, or operational tradeoff changes; only internal decoder plus benchmarks/tests.

Re-trigger cubic

@codspeed-hq

codspeed-hq Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 8.06%

❌ 1 (👁 1) regressed benchmark
✅ 231 untouched benchmarks
🆕 4 new benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
🆕 Memory bench_unmarshal_ack N/A 168 B N/A
🆕 Memory bench_unmarshal_fanout N/A 2.8 KB N/A
🆕 Simulation bench_unmarshal_ack N/A 5.3 µs N/A
🆕 Simulation bench_unmarshal_fanout N/A 20.1 µs N/A
👁 Simulation identity_probe_misses[8] 2.9 µs 3.2 µs -8.06%

Comparing bench/binary-small-stanza-decode (f9cd9a1) with main (81c26b3)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@jlucaso1

jlucaso1 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@codspeedbot explain this regression

@codspeed-hq

codspeed-hq Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot explain this regression

identity_probe_misses[8] is not caused by this PR

Short version: that benchmark is nondeterministic run to run, the base run happened to land at the bottom of its range and the head run at the top. Nothing in this PR executes on that path.

What the number is made of

Simulation breakdown for identity_probe_misses[8]:

instructions memory access cache miss total
BASE (81c26b3) 1.165 µs 1.667 µs 0.083 µs 2.915 µs
HEAD (f9cd9a1) 1.187 µs 1.889 µs 0.094 µs 3.171 µs

The instruction count itself moved (+1.9%), so this is not a cache-model artifact of a runner swap — and CodSpeed's environment-diff section confirms this benchmark was compared on matching runners, unlike the wacore/binary, wacore/libsignal and wacore/appstate benches.

The same value already oscillates on main

That bench file, SignalStoreCache and ProtocolAddress have had no commits across the window below (git log 786c635..81c26b3 on those paths is empty), yet:

main commit identity_probe_misses[8]
#1209 786c635 3.003 µs
05c98a1 3.003 µs
9a78719 3.073 µs
#1210 37ae410 2.915 µs
#1211 0f78723 3.141 µs
#1212 2b60761 2.915 µs
#1213 81c26b3 (this PR's base) 2.915 µs
this PR 3.171 µs

A 7.8% spread on identical code. The head value sits at the top of a range main already visits.

Reproduced locally

I built and ran the bench under codspeed run --mode simulation on this branch and on 81c26b3, same machine, and read the raw callgrind counters for identity_probe_misses[8]:

  • base binary: Ir=10,643 Dr+Dw=3,649 D1mr=49
  • head binary: Ir=10,587 Dr+Dw=3,619 D1mr=45

Head is marginally cheaper on every counter. Then, running the unchanged head binary five times, it alternates between Ir=10,587 and Ir=10,643 — the two values I had attributed to head and base. The binary is fixed; the number is not.

Why it wobbles

SignalStoreCache.identities is a std::collections::HashMap<Arc<str>, _> with the default RandomState, so its SipHash keys are seeded per process. Bucket layout, probe length and the resulting data accesses differ every run. The bench's own DetHashMap precaution covers only the address_keyed_map_probe control map, not the cache under test. With 8 keys the per-iteration work is small enough that one extra probe or a differently placed group moves the total several percent; identity_probe_misses[64] amortizes it away and came in at +0.5%, and identity_probe_hits[8] was byte-identical between the two runs. Only the smallest, most seed-sensitive workload moved, which is the signature of noise rather than a change.

For completeness, this PR touches read_packed in wacore/binary/src/decoder.rs; signal_address_probe_benchmark never calls the decoder.

Suggestion

Acknowledge this one on CodSpeed. If you want the bench to stop generating these, the fix is to remove the seed dependence rather than the workload — letting SignalStoreCache be constructed with a fixed BuildHasher in benches would make [8] as reproducible as the rest of the suite. I did not make that change here since it means touching production store code, which is outside this PR.

Summary
  • Warming up the performance wizard...
  • Investigating the reported regression — Reproduced identity_probe_misses[8] locally on base and head: the benchmark is nondeterministic, the PR is not the cause
  • Handling comment — Comment handling finished successfully

Learnings updated. See what CodSpeed remembers about this repository.

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@jlucaso1
jlucaso1 merged commit 8756097 into main Aug 6, 2026
5 checks passed
@jlucaso1
jlucaso1 deleted the bench/binary-small-stanza-decode branch August 6, 2026 20:30
jlucaso1 added a commit that referenced this pull request Aug 6, 2026
NodeRef::content was Option<Box<NodeContentRef>>, so every node carrying
any content allocated, including the common leaf whose content is a
borrowed slice of the frame and allocates nothing itself. The box existed
only to keep the field pointer-sized. A device fanout pays one per node:
a native profile puts malloc, free and NodeRef's drop glue at 11.3% of
that decode, against 3.9% on an ack, which is why #1214 measuring only
the ack concluded the tree was nearly free.

Dropping the box does not create an infinite type, because the recursion
goes through Box<[NodeRef]>. NodeRef grows from 48 to 72 bytes, and the
memcpy that buys back does not show up: every benchmark improves, with
the gain tracking how many nodes carry content.

Allocations per decode: fanout 45 to 27, large 50 to 26, ack unchanged at
1 since it has no content. Time over 2M iterations, pinned: fanout -10.8%,
large -11.0%, ack -6.4%, small -5.4%. Instructions on the fanout drop
9.5% under callgrind.

The field is public, so this reaches 22 files outside wacore/binary. All
of it is as_deref() becoming as_ref(); no logic moves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant