-
Notifications
You must be signed in to change notification settings - Fork 7
Improve performance and documentation #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Deserializing a vector can now be 7-10x faster, and a nested vec can be 7x faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR tightens documentation, adds CI for linting/formatting/coverage, improves serialization/deserialization performance, and expands tests/benchmarks to better exercise the BCS API.
Changes:
- Adds detailed error and API documentation to core serialization/deserialization functions, and exposes a new
to_bytes_with_capacityhelper. - Introduces several performance-oriented changes (optimized ULEB128 encoding/decoding, direct primitive writes, and richer benchmarks) plus stricter lint configuration.
- Extends test coverage with many new tests (including new error paths and helper functions) and sets up GitHub Actions workflows for CI, coverage, and rustdoc deployment.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/serde.rs |
Loosens clippy in test code, refactors an option expectation, and adds a large suite of tests to cover error paths, helpers, seed-based APIs, and max-length behavior. |
src/test_helpers.rs |
Documents and updates assert_canonical_encode_decode to take &T, improving ergonomics and avoiding unnecessary moves. |
src/ser.rs |
Enhances docs, adds to_bytes_with_capacity, documents error conditions, optimizes ULEB128 output and primitive serialization, adds an is_human_readable helper, and tweaks WriteCounter. |
src/de.rs |
Documents deserialization behavior and error conditions, introduces faster byte-reading helpers, optimizes ULEB128 decoding, and annotates many methods with #[inline] for performance. |
src/lib.rs |
Re-exports the new to_bytes_with_capacity function from the crate root. |
rustfmt.toml |
Adds a rustfmt configuration to standardize formatting across the crate. |
benches/bcs_bench.rs |
Replaces the simple map benchmark with structured serialize/deserialize benchmarks over primitives, structs, strings, vectors, and maps, and exercises to_bytes_with_capacity. |
Cargo.toml |
Configures crate-level Rust and Clippy lints (forbidding unsafe_code, enabling all/pedantic, and allowing specific cast lints). |
.github/workflows/rustdoc.yml |
Modernizes the rustdoc workflow to run on main/master, use the dtolnay/rust-toolchain action, cache builds, and deploy docs to GitHub Pages with warnings-as-errors for rustdoc. |
.github/workflows/ci.yml |
Expands CI into separate jobs for fmt, clippy, tests, coverage (with cargo llvm-cov and Codecov), docs, and explicit MSRV checking on Rust 1.78. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.0 = self | ||
| .0 | ||
| .checked_add(len) | ||
| .ok_or_else(|| std::io::Error::other("WriteCounter reached max value"))?; |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::io::Error::other is only available on newer Rust versions, but this crate explicitly tests a minimum supported Rust version of 1.78 in the CI workflow; using this constructor will cause the MSRV job to fail to compile. To keep the library compatible with the configured MSRV, construct the error via std::io::Error::new(std::io::ErrorKind::Other, ...) instead of Error::other.
| .ok_or_else(|| std::io::Error::other("WriteCounter reached max value"))?; | |
| .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "WriteCounter reached max value"))?; |
| fn test_io_error_conversion() { | ||
| use std::io; | ||
|
|
||
| let io_err = io::Error::other("test"); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
io::Error::other is only available on newer Rust versions, but this crate's CI explicitly checks a minimum supported Rust version of 1.78; this test will not compile under the configured MSRV. To keep the tests building on the MSRV, construct the error using io::Error::new(io::ErrorKind::Other, ...) instead of Error::other.
| let io_err = io::Error::other("test"); | |
| let io_err = io::Error::new(io::ErrorKind::Other, "test"); |
Adds linting, formatting, some performance increases, and more documentation.
Summary
The optimizations primarily targeted deserialization hot paths, resulting in significant improvements for all deserialization workloads while maintaining equivalent serialization performance.
Benchmark Results
Deserialization Performance
u64simple_structcomplex_structvec_u64/10vec_u64/100vec_u64/1000vec_u64/10000btree_map_2000Serialization Performance
u64simple_structcomplex_structvec_u64/1000btree_map_2000Note: Serialization variance is high due to allocator behavior; differences are within noise margin.
Optimizations Applied
1. Bulk Byte Reading (
read_bytes)Before: Integer parsing read bytes one at a time using repeated
next()calls.After: A new
read_bytes(n)method usessplit_atto read multiple bytes in a single operation.Impact: Eliminates per-byte bounds checking overhead for multi-byte reads.
2. ULEB128 Fast Path
Before: All ULEB128 values went through a loop, even single-byte values.
After: Single-byte values (0-127) are handled with a fast path that skips the loop entirely.
Impact: Sequence lengths and enum variant indices are typically small, making this fast path hit rate very high.
3. Inline Hints on Hot Paths
Added
#[inline]attributes to frequently-called methods:peek(),next(),read_bytes()parse_bool(),parse_u8(),parse_u16(),parse_u32(),parse_u64(),parse_u128()parse_u32_from_uleb128(),parse_length()deserialize_*trait methodsImpact: Allows the compiler to inline these small functions, reducing call overhead and enabling further optimizations.
4. Direct Array Conversion
Before: Manual byte-by-byte array construction.
After: Using
try_into().unwrap()for direct slice-to-array conversion.Impact: The compiler can optimize this pattern better than manual indexing.
5. Serialization ULEB128 Optimization
Similar fast-path optimization for ULEB128 encoding during serialization:
6. Additional Serialization Improvements
to_bytes_with_capacity()for pre-allocating output bufferssort_bywithsort_unstable_byfor map key sorting (stability not needed for unique keys)Benchmark Environment
Running Benchmarks
To reproduce these results:
To compare against a baseline:
Conclusion
The optimizations delivered significant deserialization improvements (3-8x faster depending on workload) while maintaining equivalent serialization performance. This is particularly impactful for applications that deserialize more than they serialize, which is common in blockchain and networking contexts where BCS is typically used.
The key insight is that deserialization spends most of its time in tight loops reading bytes. By optimizing byte reading patterns and adding fast paths for common cases, we achieved substantial performance gains without any API changes or unsafe code.