Skip to content

Commit

Permalink
Improve macro performance
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Jan 27, 2025
1 parent a04de51 commit 3e0fa45
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:

strategy:
matrix:
toolchain: ['1.60', stable, beta, nightly]
toolchain: ['1.66', stable, beta, nightly]

steps:
- uses: actions/checkout@v3
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ repository = "https://github.com/rossmacarthur/constcat"
license = "MIT OR Apache-2.0"
keywords = ["concat", "const"]
categories = ["no-std", "rust-patterns"]
include = ["src/lib.rs", "LICENSE-*", "README.md"]

[features]
# Private API: compiles the tests for the concat_bytes! macro
_bytes = []

[[bench]]
name = "large"
harness = false

[[bench]]
name = "many"
harness = false
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const COLORS: &[(u8, u8, u8)] = concat_slices!([(u8, u8, u8)]: PRIMARIES, SECOND

### MSRV

This crate supports Rust 1.60 and above.
This crate supports Rust 1.66 and above.

[`std::concat!`]: core::concat

Expand Down
14 changes: 14 additions & 0 deletions benches/large.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// hyperfine "cargo clean && cargo bench --bench large --no-run"

const LARGE: &str = include_str!("large.txt");
const LARGES: &str = constcat::concat!(LARGE, LARGE);

fn main() {
let expected = {
let mut s = String::new();
s.push_str(LARGE);
s.push_str(LARGE);
s
};
assert_eq!(LARGES, expected);
}
191 changes: 191 additions & 0 deletions benches/large.txt

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions benches/many.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// hyperfine "cargo clean && cargo bench --bench many --no-run"

const T: &str = "testing123\n";
const MANY: &str = constcat::concat!(
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
);

fn main() {
let expected = {
let mut s = String::new();
for _ in 0..512 {
s.push_str(T);
}
s
};
assert_eq!(MANY, expected);
}
69 changes: 46 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@
//!
//! ## MSRV
//!
//! This crate supports Rust 1.60 and above.
//! This crate supports Rust 1.66 and above.
#![no_std]

#[doc(hidden)]
pub use core;

use core::mem::MaybeUninit;

////////////////////////////////////////////////////////////////////////////////
// concat!
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -256,9 +258,9 @@ macro_rules! _concat_slices {
}};

([$T:ty]: $($s:expr),+) => {{
use $crate::core::mem;
use $crate::core::mem::MaybeUninit;
use $crate::core::primitive::{u8, usize};
use $crate::core::mem;
$(
const _: &[$T] = $s; // require constants
)*
Expand All @@ -267,24 +269,18 @@ macro_rules! _concat_slices {
const ZERO: TypeAsBytes<$T> = TypeAsBytes { bytes: [0; TSIZE] };
const LEN: usize = $( $s.len() + )* 0;
const ARR: [$T; LEN] = {
// Ideally we should use MaybeUninit::zeroed() but we want to
// support older versions of Rust.
let mut arr: [MaybeUninit<$T>; LEN] = [ unsafe { ZERO.inner }; LEN];
let mut base: usize = 0;
$({
let mut i = 0;
while i < $s.len() {
// Ideally this should use `MaybeUninit::write` once it is
// made const.
// Documentation: https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#method.write
// Tracking issue: https://github.com/rust-lang/rust/issues/63567
arr[base + i] = MaybeUninit::new($s[i]);
i += 1;
}
base += $s.len();
})*
if base != LEN { panic!("invalid length"); }
let arr = $crate::concat::<LEN, $T>(
// SAFETY:
// This is safe because we are initializing a `MaybeUninit` and
// all values are overwritten before being read.
//
// Ideally we should use MaybeUninit::zeroed() but we want to
// support older versions of Rust and that was only stabilized
// in Rust 1.75.
unsafe { ZERO.inner },

&[$($s),+]
);
// SAFETY:
// As per the documentation of `core::mem::MaybeUninit`:
// <https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#layout-1>
Expand All @@ -294,14 +290,41 @@ macro_rules! _concat_slices {
// This means as long as all of the MaybeUninits are initialized
// then it is safe to transmute a MaybeUninit<T> to T, and therefore
// also [MaybeUninit<T>; N] to [T; N]. We know that all of the
// elements are initialized because in the loop above the number of
// initialized elements are computed and then there is a guard that
// compares that to the total length of the array.
// elements are initialized because in the function call above the
// number of initialized elements are computed and then there is a
// guard that compares that to the total length of the array.
//
// See for more information:
// https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-an-array-element-by-element
unsafe { mem::transmute(arr) }
unsafe { $crate::core::mem::transmute(arr) }
};
&ARR
}};
}

#[doc(hidden)]
pub const fn concat<const LEN: usize, T: Copy>(
zero: MaybeUninit<T>,
slices: &[&[T]],
) -> [MaybeUninit<T>; LEN] {
let mut arr: [MaybeUninit<T>; LEN] = [zero; LEN];
let mut base = 0;
let mut i = 0;
while i < slices.len() {
let slice = slices[i];
let mut j = 0;
while j < slice.len() {
// Ideally this should use `MaybeUninit::write` but we want to
// support older versions of Rust and that was only stabilized in
// Rust 1.85.
arr[base + j] = MaybeUninit::new(slice[j]);
j += 1;
}
base += slice.len();
i += 1;
}
if base != LEN {
panic!("invalid length");
}
arr
}

0 comments on commit 3e0fa45

Please sign in to comment.