This note records the application-level case study candidates evaluated during the revision. The goal was to find a non-string workload where a safe API call can be replaced with its unsafe counterpart, while preserving semantics and showing a measurable application-level effect.
All experiments were placed under e2e/, which is intentionally ignored by
Git. The code is therefore treated as temporary experimental material, while
this document records the setup and observed results.
RustScan is a real Rust port scanner. Its port planning logic contains a short application-level path:
PortStrategy::pick(..., ScanOrder::Random)
-> ports.shuffle(&mut rng)
This avoids network I/O by isolating the planning stage, while still using a real project component.
The experiment modeled RustScan's manual port-list randomization component:
manual port list -> Fisher-Yates shuffle -> randomized scan order
Two versions were compared:
safe: slice.swap(i, j)
unsafe: ptr::swap(slice.as_mut_ptr().add(i), slice.as_mut_ptr().add(j))
The unsafe version uses the same generated indices as the safe version. The
precondition is that both i and j are valid indices, which follows from
j in 0..=i and the reverse iteration over valid slice indices.
Property-based differential testing with Proptest was used. The test generates random port vectors and RNG seeds, then checks that the safe and unsafe planning results are identical.
Result:
cargo test --test differential
safe_and_unchecked_planning_match ... ok
Criterion benchmark results:
| Port Count | Safe Time | Unsafe Time | Safe/Unsafe |
|---|---|---|---|
| 1,024 | 5.1854 us | 5.2583 us | 0.986x |
| 8,192 | 43.407 us | 45.454 us | 0.955x |
| 65,535 | 358.13 us | 361.67 us | 0.990x |
This is a real and short application-level path, but it is not a good positive
case. The unsafe version is slightly slower. The likely reason is that the
compiler can already prove the indices safe in the simple Fisher-Yates loop,
so replacing swap with pointer-based swapping does not remove meaningful
runtime overhead.
Arti/Tor's circuit timeout estimator provides a more semantically meaningful
application-level path. The estimator computes timeout parameters from circuit
build-time history. In particular, it estimates the Pareto parameter Xm from
the most frequent histogram bins.
The relevant path is:
circuit build-time history
-> sparse histogram bins
-> k_smallest(n_modes)
-> itertools::k_smallest.rs::sift_down
-> heap.swap(...)
This is more natural than forcing the complete circuit construction path, because full circuit construction includes asynchronous runtime behavior, network effects, and unrelated protocol work.
The experiment implemented an Arti-style timeout-estimator component:
synthetic circuit build times
-> sparse histogram
-> select top modes via itertools::k_smallest
-> compute weighted Xm
Two vendored copies of itertools were used:
safe: k_smallest.rs::sift_down uses heap.swap(...)
unsafe: k_smallest.rs::sift_down uses ptr::swap(...)
Property-based differential testing with Proptest was used. The test generates
random build-time samples and mode counts, then checks that the safe and unsafe
estimators compute the same Xm.
Result:
cargo test --test differential
safe_and_unchecked_k_smallest_timeout_estimator_match ... ok
Criterion benchmark results:
| Workload | Safe Time | Unsafe Time | Safe/Unsafe |
|---|---|---|---|
| 1,000 samples, 10 modes | 6.7796 us | 6.8428 us | 0.991x |
| 8,000 samples, 10 modes | 51.792 us | 52.577 us | 0.985x |
| 65,536 samples, 10 modes | 422.55 us | 420.99 us | 1.004x |
| 65,536 samples, 64 modes | 420.75 us | 420.84 us | 1.000x |
| Wide 65,536 samples, 64 modes | 483.70 us | 477.59 us | 1.013x |
Arti is semantically the strongest case among the tested candidates. It gives
a realistic application-level interpretation for a heap-maintenance safe API
replacement. However, the performance benefit is small, with the best observed
case around 1.3%. The reason is that sift_down and its swap operation are
only a small part of the estimator; histogram construction and iterator logic
dominate the component cost.
The ring crate contains several buffer-partitioning paths based on
split_at_mut. Unlike string validation, this is still within the non-string
API scope and corresponds to a common low-level pattern: splitting a known
layout buffer into logically separate regions.
The selected pattern is adapted from ring/src/arithmetic/bigint/exp.rs:
state buffer
-> split_at_mut(m_len)
-> split_at_mut(m_len)
-> acc / base_cached / m_cached regions
This is not a full cryptographic operation. It isolates a buffer-processing component so that the effect of the safe/unsafe replacement is not hidden by large cryptographic primitive costs.
The experiment compares:
safe: split_at_mut(m_len)
unsafe: from_raw_parts_mut(ptr, mid) + from_raw_parts_mut(ptr.add(mid), len - mid)
The unsafe precondition is that m_len partitions each record into valid
non-overlapping regions. The benchmark processes repeated records with the
layout:
[acc | base_cached | m_cached]
Property-based differential testing with Proptest was used. The test generates different region sizes and record counts, then checks both the checksum and the final mutated buffer state.
Result:
cargo test --offline --test differential
safe_and_unchecked_split_processing_match ... ok
Criterion benchmark results:
| m_len | Safe Time | Unsafe Time | Safe/Unsafe |
|---|---|---|---|
| 1 | 21.412 us | 21.205 us | 1.010x |
| 2 | 41.270 us | 41.582 us | 0.993x |
| 4 | 80.020 us | 79.156 us | 1.011x |
| 8 | 144.59 us | 146.41 us | 0.988x |
| 16 | 278.66 us | 274.86 us | 1.014x |
| 32 | 255.38 us | 245.88 us | 1.039x |
This is the strongest positive candidate found so far. The best observed case
shows about 3.9% speedup. The result is still below the desired 5% threshold,
and the larger case showed noticeable outliers, so it should be presented
conservatively. Compared with Arti, this candidate has a stronger performance
signal but a weaker end-to-end application story because it is an isolated
buffer-processing component adapted from ring.
The candidates are ranked as follows:
| Candidate | Semantic Strength | Performance Signal | Recommendation |
|---|---|---|---|
| Arti timeout estimator | Strong | Weak, around 1% | Best semantic case |
| Ring buffer split | Medium | Best, up to about 3.9% | Best positive performance case |
| RustScan planning | Medium | Negative | Do not use as positive case |
For the paper, the most defensible strategy is:
Use Arti if the section prioritizes realistic application semantics.
Use ring-buffer-split if the section needs a conservative positive performance case.
Avoid claiming large end-to-end acceleration in either case.
Suggested wording for the ring case:
We further evaluate a buffer-processing component adapted from ring's BigInt
modular exponentiation implementation. The component repeatedly partitions a
known-layout working buffer into disjoint regions using split_at_mut. We replace
these checked splits with unchecked slice construction after validating the
layout precondition. Property-based differential tests confirm identical output
states. The transformed component shows a modest speedup, up to 3.9% in our
controlled workload, indicating that application-level benefits are possible
but remain workload-dependent.