Merge rust-bitcoin/rust-bitcoin#2759: Bench `base58` encoding and remove `SmallVec` to improve perf

4646690521 fix clippy lint by using resize instead of push (Riccardo Casatta)
deeb160b86 remove SmallVec (Riccardo Casatta)
e4b707ba83 add bench for base58::encode_check (Riccardo Casatta)

Pull request description:

  In a downstream app I've seen printing a descriptor is not a cheap operation, analyzing the flamegraph it seems base58 encoding of the xpub is the culprit

  ![image](https://github.com/rust-bitcoin/rust-bitcoin/assets/6470319/30883c6b-7627-4ad0-aa91-373f22393f26)

  This PR adds benches for the `encode_check` function, and add the changes gaining more boost, which is also good cause it removes code.

  Other attempts didn't provide enough benefit for inclusion but I report them here for knowledge.

  ```
  ## baseline

  running 2 tests
  test benches::bench_encode_check_50   ... bench:       8,760 ns/iter (+/- 113)
  test benches::bench_encode_check_xpub ... bench:      19,982 ns/iter (+/- 109)

  ## remove smallvec

  running 2 tests
  test benches::bench_encode_check_50   ... bench:       7,935 ns/iter (+/- 129)
  test benches::bench_encode_check_xpub ... bench:      18,076 ns/iter (+/- 184)

  ## increase smallvec to 128 (fits xpub)

  test benches::bench_encode_check_50   ... bench:       8,786 ns/iter (+/- 738)
  test benches::bench_encode_check_xpub ... bench:      20,006 ns/iter (+/- 2,611)

  ## avoid char-to-str by keeping str map

  test benches::bench_encode_check_50   ... bench:       7,895 ns/iter (+/- 88)
  test benches::bench_encode_check_xpub ... bench:      17,883 ns/iter (+/- 118)
  ```

  Gains are good (~10%), but I don't think they explains the 3ms to print a descriptor in wasm env,
  probably is the sha256 for the checksum is fast in cargo bench but slow in wasm env, but I didn't research on the topic.

ACKs for top commit:
  apoelstra:
    ACK 4646690521
  tcharding:
    ACK 4646690521

Tree-SHA512: 978bbe22c99bb455028d90532d59a076321e0c19105fc8335bd44cd84fbedda109083380df5c658b85121242c88d442994cfc58d141f3fc5daa66c27b1499329
This commit is contained in:
Andrew Poelstra 2024-05-11 14:01:32 +00:00
commit 89d6991cf1
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
1 changed files with 34 additions and 37 deletions

View File

@ -19,6 +19,9 @@
#[macro_use]
extern crate alloc;
#[cfg(bench)]
extern crate test;
#[cfg(feature = "std")]
extern crate std;
@ -28,7 +31,7 @@ pub mod error;
#[cfg(not(feature = "std"))]
pub use alloc::{string::String, vec::Vec};
use core::{fmt, iter, slice, str};
use core::{fmt, str};
#[cfg(feature = "std")]
pub use std::{string::String, vec::Vec};
@ -115,7 +118,9 @@ pub fn decode_check(data: &str) -> Result<Vec<u8>, Error> {
}
/// Encodes `data` as a base58 string (see also `base58::encode_check()`).
pub fn encode(data: &[u8]) -> String { encode_iter(data.iter().cloned()) }
pub fn encode(data: &[u8]) -> String {
encode_iter(data.iter().cloned())
}
/// Encodes `data` as a base58 string including the checksum.
///
@ -148,7 +153,7 @@ where
I: Iterator<Item = u8> + Clone,
W: fmt::Write,
{
let mut ret = SmallVec::new();
let mut ret = Vec::with_capacity(128);
let mut leading_zero_count = 0;
let mut leading_zeroes = true;
@ -173,9 +178,7 @@ where
}
// ... then reverse it and convert to chars
for _ in 0..leading_zero_count {
ret.push(0);
}
ret.resize(ret.len() + leading_zero_count, 0);
for ch in ret.iter().rev() {
writer.write_char(BASE58_CHARS[*ch as usize] as char)?;
@ -184,37 +187,6 @@ where
Ok(())
}
/// Vector-like object that holds the first 100 elements on the stack. If more space is needed it
/// will be allocated on the heap.
struct SmallVec<T> {
len: usize,
stack: [T; 100],
heap: Vec<T>,
}
impl<T: Default + Copy> SmallVec<T> {
fn new() -> SmallVec<T> { SmallVec { len: 0, stack: [T::default(); 100], heap: Vec::new() } }
fn push(&mut self, val: T) {
if self.len < 100 {
self.stack[self.len] = val;
self.len += 1;
} else {
self.heap.push(val);
}
}
fn iter(&self) -> iter::Chain<slice::Iter<T>, slice::Iter<T>> {
// If len<100 then we just append an empty vec
self.stack[0..self.len].iter().chain(self.heap.iter())
}
fn iter_mut(&mut self) -> iter::Chain<slice::IterMut<T>, slice::IterMut<T>> {
// If len<100 then we just append an empty vec
self.stack[0..self.len].iter_mut().chain(self.heap.iter_mut())
}
}
#[cfg(test)]
mod tests {
use hex::test_hex_unwrap as hex;
@ -284,3 +256,28 @@ mod tests {
assert_eq!(decode_check(&encode(&[1, 2, 3])), Err(TooShortError { length: 3 }.into()));
}
}
#[cfg(bench)]
mod benches {
use test::{black_box, Bencher};
#[bench]
pub fn bench_encode_check_50(bh: &mut Bencher) {
let data: alloc::vec::Vec<_> = (0u8..50).collect();
bh.iter(|| {
let r = super::encode_check(&data);
black_box(&r);
});
}
#[bench]
pub fn bench_encode_check_xpub(bh: &mut Bencher) {
let data: alloc::vec::Vec<_> = (0u8..78).collect(); // lenght of xpub
bh.iter(|| {
let r = super::encode_check(&data);
black_box(&r);
});
}
}