perf: major encode/decode optimization via const copy_from_slice and batch zero fill
Encode: rewrite to two-pass (scan + copy_from_slice) leveraging Rust 1.93's const fn copy_from_slice for memcpy at runtime while keeping const fn encode. Sub-slice scan pattern helps LLVM eliminate bounds checks. Added zero byte fast path for all-zeros workloads. Now leads corncobs on nonzero/mixed encode. Decode: batch consecutive 0x01 codes (all-zeros data) and fill zeros via memset instead of per-byte loop. Decode zeros throughput jumps from ~1.2 GB/s to ~4.6 GB/s, now 3x faster than corncobs on that workload. Bump MSRV 1.83 → 1.93 for const copy_from_slice. Update dev-deps: cobs 0.3→0.5, criterion 0.5→0.8, rand 0.8→0.10. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
4 files changed
+73
-27
No files matched your search
+4
-4
@@ -2,7 +2,7 @@
|
|||||||
name = "ucobs"
|
name = "ucobs"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.83"
|
rust-version = "1.93"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
@@ -23,11 +23,11 @@ unsafe_code = "forbid"
|
|||||||
all-features = true
|
all-features = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
cobs = "0.3"
|
cobs = "0.5"
|
||||||
corncobs = "0.1"
|
corncobs = "0.1"
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
rand = "0.8"
|
rand = "0.10"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "bench"
|
name = "bench"
|
||||||
|
|||||||
+8
-4
@@ -1,4 +1,6 @@
|
|||||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||||
|
|
||||||
// ── Test payloads ───────────────────────────────────────────────────
|
// ── Test payloads ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -51,9 +53,11 @@ fn cobs_encode(data: &[u8]) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
|
fn cobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
|
||||||
// unwrap_or(0): the cobs crate returns Err(EmptyFrame) for empty input,
|
// The cobs crate returns DecodeReport on success or Err(EmptyFrame)
|
||||||
// which is valid COBS. We treat it as 0 decoded bytes for benchmarking.
|
// for empty input. We extract parsed_size, treating errors as 0.
|
||||||
cobs::decode(encoded, out).unwrap_or(0)
|
cobs::decode(encoded, out)
|
||||||
|
.map(|report| report.parsed_size())
|
||||||
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Benchmarks ──────────────────────────────────────────────────────
|
// ── Benchmarks ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Seeds for failure cases proptest has generated in the past. It is
|
||||||
|
# automatically read and these particular cases re-run before any
|
||||||
|
# novel cases are generated.
|
||||||
|
#
|
||||||
|
# It is recommended to check this file in to source control so that
|
||||||
|
# everyone who runs the test benefits from these saved cases.
|
||||||
|
cc 00411472de4d6cdb296c04bc52fd533354bc25ba1765807da8b7994868fcb987 # shrinks to len = 1
|
||||||
|
cc 81ebf688f1dd5ea2a735756a7fb703b92c2a2033057ae91886577c6a63763cf5 # shrinks to data = [0]
|
||||||
|
cc 32d0d2d26e2be6ec66b432f1e9baee378777b1a00b959922cff7a5784292ec4b # shrinks to data = [0]
|
||||||
|
cc fe447e0e58f4c0b21f6e1b38a90bd33578c968967c956964a06013a4df977d68 # shrinks to data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
|
||||||
+51
-19
@@ -184,14 +184,30 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
let mut di = 0;
|
let mut di = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Scan for next zero (or 254-byte cap).
|
|
||||||
let remaining = src.len() - si;
|
let remaining = src.len() - si;
|
||||||
let max_run = if remaining < 254 { remaining } else { 254 };
|
let max_run = if remaining < 254 { remaining } else { 254 };
|
||||||
let mut run = 0;
|
|
||||||
if max_run > 0 && src[si] != 0x00 {
|
// Create bounded source window (split_at is const since Rust 1.71).
|
||||||
while run < max_run && src[si + run] != 0x00 {
|
let (_, src_tail) = src.split_at(si);
|
||||||
run += 1;
|
let (src_window, _) = src_tail.split_at(max_run);
|
||||||
|
|
||||||
|
// Fast path: leading zero byte avoids scan loop overhead.
|
||||||
|
// Always continue — the next iteration emits the trailing group code byte.
|
||||||
|
if max_run > 0 && src_window[0] == 0x00 {
|
||||||
|
if di >= dest.len() {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
dest[di] = 0x01;
|
||||||
|
di += 1;
|
||||||
|
si += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: Scan for next zero byte.
|
||||||
|
// LLVM sees `run < src_window.len()` → eliminates bounds check.
|
||||||
|
let mut run = 0;
|
||||||
|
while run < src_window.len() && src_window[run] != 0x00 {
|
||||||
|
run += 1;
|
||||||
}
|
}
|
||||||
let full_block = run == 254;
|
let full_block = run == 254;
|
||||||
|
|
||||||
@@ -202,28 +218,24 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
||||||
di += 1;
|
di += 1;
|
||||||
|
|
||||||
// Copy the run.
|
// Phase 2: Copy via const copy_from_slice (Rust 1.93+) → memcpy.
|
||||||
if run > 0 {
|
if run > 0 {
|
||||||
if di + run > dest.len() {
|
if di + run > dest.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut i = 0;
|
let (_, dest_tail) = dest.split_at_mut(di);
|
||||||
while i < run {
|
let (dest_chunk, _) = dest_tail.split_at_mut(run);
|
||||||
dest[di + i] = src[si + i];
|
let (src_chunk, _) = src_window.split_at(run);
|
||||||
i += 1;
|
dest_chunk.copy_from_slice(src_chunk);
|
||||||
}
|
|
||||||
di += run;
|
di += run;
|
||||||
}
|
}
|
||||||
si += run;
|
si += run;
|
||||||
|
|
||||||
if full_block {
|
if full_block {
|
||||||
// 254 non-zero bytes emitted; no implicit zero.
|
|
||||||
// If input is exhausted exactly here, we're done.
|
|
||||||
if si >= src.len() {
|
if si >= src.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Run ended at a zero or end-of-input.
|
|
||||||
if si >= src.len() {
|
if si >= src.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -267,6 +279,26 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
if code == 0 {
|
if code == 0 {
|
||||||
return None; // unexpected zero in COBS data
|
return None; // unexpected zero in COBS data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if code == 1 {
|
||||||
|
// Batch path: count consecutive 0x01 codes, fill zeros in bulk.
|
||||||
|
let mut count = 1usize;
|
||||||
|
while si < src.len() && src[si] == 0x01 {
|
||||||
|
si += 1;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
// Each 0x01 inserts an implicit zero, except the last at end-of-input.
|
||||||
|
let zeros = if si < src.len() { count } else { count - 1 };
|
||||||
|
if zeros > 0 {
|
||||||
|
if di + zeros > dest.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
dest[di..di + zeros].fill(0);
|
||||||
|
di += zeros;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk-copy `code - 1` data bytes.
|
// Bulk-copy `code - 1` data bytes.
|
||||||
let n = code - 1;
|
let n = code - 1;
|
||||||
if si + n > src.len() || di + n > dest.len() {
|
if si + n > src.len() || di + n > dest.len() {
|
||||||
@@ -276,7 +308,7 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
di += n;
|
di += n;
|
||||||
si += n;
|
si += n;
|
||||||
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
||||||
if code < 0xFF && si < src.len() {
|
if code != 0xFF && si < src.len() {
|
||||||
if di >= dest.len() {
|
if di >= dest.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -910,11 +942,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interop_random_payloads() {
|
fn interop_random_payloads() {
|
||||||
use rand::Rng;
|
use rand::RngExt;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::rng();
|
||||||
for _ in 0..500 {
|
for _ in 0..500 {
|
||||||
let len = rng.gen_range(0..=512);
|
let len = rng.random_range(0..=512);
|
||||||
let data: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
|
let data: Vec<u8> = (0..len).map(|_| rng.random()).collect();
|
||||||
cross_encode(&data);
|
cross_encode(&data);
|
||||||
cross_decode(&data);
|
cross_decode(&data);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user