diff --git a/examples/bench_contention.rs b/examples/bench_contention.rs new file mode 100644 index 000000000..6c2c57921 --- /dev/null +++ b/examples/bench_contention.rs @@ -0,0 +1,76 @@ +use std::sync::mpsc::channel; +use std::thread; +use std::time::Instant; +use std::alloc::Layout; + +#[global_allocator] +static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc; + +const BLOCK_SIZE: usize = 64; +const ITERATIONS: usize = 1_000_000; + +struct Ptr(*mut u8); +unsafe impl Send for Ptr {} + +fn main() { + let thread_count = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4); + println!("Running contention benchmark with {} threads, {} iterations per thread", thread_count, ITERATIONS); + + // Use std::sync::Barrier + let barrier = std::sync::Arc::new(std::sync::Barrier::new(thread_count + 1)); + + let mut senders = Vec::new(); + let mut receivers = Vec::new(); + + // Create a ring topology channels + for _ in 0..thread_count { + let (tx, rx) = channel::(); + senders.push(tx); + receivers.push(Some(rx)); + } + + let mut handles = Vec::new(); + + // Start timing from here, but actual work starts after barrier + let _start = Instant::now(); + + for i in 0..thread_count { + let barrier = barrier.clone(); + // Thread i sends to (i + 1) % N + let tx = senders[(i + 1) % thread_count].clone(); + // Thread i receives from i + let rx = receivers[i].take().unwrap(); + + handles.push(thread::spawn(move || { + // Pre-allocate some items to fill the pipe + let layout = Layout::from_size_align(BLOCK_SIZE, 8).unwrap(); + + barrier.wait(); // Synchronize start + + for _ in 0..ITERATIONS { + // 1. Allocate a new block + let ptr = unsafe { std::alloc::alloc(layout) }; + + // 2. Send to next neighbor (who will free it) + tx.send(Ptr(ptr)).unwrap(); + + // 3. Receive from prev neighbor (who allocated it) + let received = rx.recv().unwrap(); + + // 4. Free the received block + unsafe { std::alloc::dealloc(received.0, layout) }; + } + })); + } + + barrier.wait(); // Start timing + let loop_start = Instant::now(); + + for h in handles { + h.join().unwrap(); + } + + let duration = loop_start.elapsed(); + println!("Benchmark completed in {:.2?}", duration); + println!("Throughput: {:.2} Mops/sec", (thread_count * ITERATIONS) as f64 / duration.as_secs_f64() / 1_000_000.0); +} diff --git a/snmalloc-rs/Cargo.toml b/snmalloc-rs/Cargo.toml index 30e1de4e3..d914c725a 100644 --- a/snmalloc-rs/Cargo.toml +++ b/snmalloc-rs/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = ["snmalloc-sys"] +members = ["snmalloc-sys", "xtask"] [dependencies] snmalloc-sys = { version = "0.3.8", path = "snmalloc-sys", default-features = false } @@ -34,3 +34,9 @@ notls = ["snmalloc-sys/notls"] stats = ["snmalloc-sys/stats"] usewait-on-address = ["snmalloc-sys/usewait-on-address"] libc-api = ["snmalloc-sys/libc-api"] +tracing = ["snmalloc-sys/tracing"] +fuzzing = ["snmalloc-sys/fuzzing"] +vendored-stl = ["snmalloc-sys/vendored-stl"] +check-loads = ["snmalloc-sys/check-loads"] +pageid = ["snmalloc-sys/pageid"] +gwp-asan = ["snmalloc-sys/gwp-asan"] diff --git a/snmalloc-rs/README.md b/snmalloc-rs/README.md index 8f06ce5d0..61f041e3c 100644 --- a/snmalloc-rs/README.md +++ b/snmalloc-rs/README.md @@ -18,20 +18,14 @@ a global allocator for rust. snmalloc is a research allocator. Its key design fe Some old benchmark results are available in the [`snmalloc` paper](https://github.com/microsoft/snmalloc/blob/master/snmalloc.pdf). Some recent benchmark results are listed at -[bench_suite](https://github.com/SchrodingerZhu/bench_suite). There are three features defined in this crate: +[bench_suite](https://github.com/SchrodingerZhu/bench_suite). There are following features defined in this crate: -- `debug`: Enable the `Debug` mode in `snmalloc`. -- ~~`1mib`: Use the `1mib` chunk configuration. From `0.2.17`, this is set as a default feature~~ (removed since 0.3.0) -- ~~`16mib`: Use the `16mib` chunk configuration.~~ (removed since 0.3.0) -- ~~`cache-friendly`: Make the allocator more cache friendly (setting `CACHE_FRIENDLY_OFFSET` to `64` in building the - library).~~ (removed since 0.3.0) +- `debug`: Enable the `Debug` mode in `snmalloc`. This is also automatically enabled if Cargo's `DEBUG` environment variable is set to `true`. - `native-cpu`: Optimize `snmalloc` for the native CPU of the host machine. (this is not a default behavior since `0.2.14`) - `qemu`: Workaround `madvise` problem of QEMU environment -- ~~`stats`: Enable statistics~~ (removed since 0.3.0) - `local_dynamic_tls`: Workaround cannot allocate memory in static tls block - `build_cc`: Use of cc crate instead of cmake (cmake still default) as builder (more platform agnostic) -- ~~`usecxx20`: Enable C++20 standard if available~~ (removed since 0.3.0) - `usecxx17`: Use C++17 standard - `check`: Enable extra checks to improve security, see upstream [security docs](https://github.com/microsoft/snmalloc/tree/main/docs/security). Note that the `memcpy` protection is not enabled in Rust. @@ -40,6 +34,25 @@ are listed at - `notls`: Enables to be loaded dynamically, thus disable tls. - `stats`: Enables allocation statistics. - `libc-api`: Enables libc API backed by snmalloc. +- `usewait-on-address`: Enable `WaitOnAddress` support on Windows (enabled by default). +- `tracing`: Enable structured tracing/logging. +- `fuzzing`: Enable fuzzing support. +- `vendored-stl`: Use self-vendored STL. +- `check-loads`: Enable check loads feature. +- `pageid`: Enable page ID feature. +- `gwp-asan`: Enable GWP-ASan integration. Requires `SNMALLOC_GWP_ASAN_INCLUDE_PATH` and `SNMALLOC_GWP_ASAN_LIBRARY_PATH`. + +## Build Configuration + +The build script ensures architectural alignment between the Rust profile and the underlying `snmalloc` allocator: + +### Environment Variables +The following environment variables are automatically detected and propagated: +- `DEBUG`: Synchronizes the `snmalloc` build type with the Cargo profile. If `true`, `snmalloc` is built in `Debug` mode. +- `OPT_LEVEL`: Propagated to the C++ compiler to ensure optimization parity between Rust and C++ components. + +### Windows CRT Consistency +On Windows, the build script enforces static CRT linking (`/MT` or `/MTd`) across both `cc` and `cmake` builders. This prevents linker errors and ensures consistency when `snmalloc` is used as a global allocator. **To get the crates compiled, you need to choose either `1mib` or `16mib` to determine the chunk configuration** diff --git a/snmalloc-rs/snmalloc-sys/Cargo.toml b/snmalloc-rs/snmalloc-sys/Cargo.toml index 88e17154e..6b57c2767 100644 --- a/snmalloc-rs/snmalloc-sys/Cargo.toml +++ b/snmalloc-rs/snmalloc-sys/Cargo.toml @@ -17,7 +17,7 @@ cc = { version = "1.0", optional = true } cmake = { version = "0.1", optional = true } [features] -default = ["build_cmake"] +default = ["build_cmake", "usewait-on-address"] build_cc = ["cc"] build_cmake = ["cmake"] qemu = [] @@ -33,3 +33,9 @@ notls = [] stats = [] usewait-on-address = [] libc-api = [] +tracing = [] +fuzzing = [] +vendored-stl = [] +check-loads = [] +pageid = [] +gwp-asan = [] diff --git a/snmalloc-rs/snmalloc-sys/build.rs b/snmalloc-rs/snmalloc-sys/build.rs index 2a8ffebc1..0c147e2ce 100644 --- a/snmalloc-rs/snmalloc-sys/build.rs +++ b/snmalloc-rs/snmalloc-sys/build.rs @@ -61,11 +61,20 @@ struct BuildFeatures { android_lld: bool, local_dynamic_tls: bool, libc_api: bool, + tracing: bool, + fuzzing: bool, + vendored_stl: bool, + check_loads: bool, + pageid: bool, + gwp_asan: bool, } impl BuildConfig { fn new() -> Self { - let debug = cfg!(feature = "debug"); + let feature_debug = cfg!(feature = "debug"); + let cargo_debug = env::var("DEBUG").map(|v| v == "true").unwrap_or(false); + let debug = feature_debug || cargo_debug; + #[cfg(feature = "build_cc")] let builder = cc::Build::new(); @@ -74,7 +83,9 @@ impl BuildConfig { let mut config = Self { debug, - optim_level: (if debug { "-O0" } else { "-O3" }).to_string(), + optim_level: env::var("OPT_LEVEL") + .map(|v| format!("-O{}", v)) + .unwrap_or_else(|_| (if debug { "-O0" } else { "-O3" }).to_string()), target_os: env::var("CARGO_CFG_TARGET_OS").expect("target_os not defined!"), target_env: env::var("CARGO_CFG_TARGET_ENV").expect("target_env not defined!"), target_family: env::var("CARGO_CFG_TARGET_FAMILY").expect("target family not set"), @@ -204,6 +215,7 @@ trait BuilderDefine { fn build_lib(&mut self, target_lib: &str) -> std::path::PathBuf; fn configure_output_dir(&mut self, out_dir: &str) -> &mut Self; fn configure_cpp(&mut self, debug: bool) -> &mut Self; + fn compiler_define(&mut self, key: &str, value: &str) -> &mut Self; } #[cfg(feature = "build_cc")] @@ -232,6 +244,10 @@ impl BuilderDefine for cc::Build { .debug(debug) .static_crt(true) } + + fn compiler_define(&mut self, key: &str, value: &str) -> &mut Self { + self.define(key, Some(value)) + } } #[cfg(not(feature = "build_cc"))] @@ -253,12 +269,17 @@ impl BuilderDefine for cmake::Config { } fn configure_cpp(&mut self, debug: bool) -> &mut Self { - self.profile(if debug { "Debug" } else { "Release" }); self.define("SNMALLOC_RUST_SUPPORT", "ON") .very_verbose(true) .define("CMAKE_SH", "CMAKE_SH-NOTFOUND") .always_configure(true) .static_crt(true) + .profile(if debug { "Debug" } else { "Release" }) + } + + fn compiler_define(&mut self, key: &str, value: &str) -> &mut Self { + self.cxxflag(format!("-D{}={}", key, value)) + .cflag(format!("-D{}={}", key, value)) } } @@ -280,6 +301,12 @@ impl BuildFeatures { android_lld: cfg!(feature = "android-lld"), local_dynamic_tls: cfg!(feature = "local_dynamic_tls"), libc_api: cfg!(feature = "libc-api"), + tracing: cfg!(feature = "tracing"), + fuzzing: cfg!(feature = "fuzzing"), + vendored_stl: cfg!(feature = "vendored-stl"), + check_loads: cfg!(feature = "check-loads"), + pageid: cfg!(feature = "pageid"), + gwp_asan: cfg!(feature = "gwp-asan"), } } } @@ -302,36 +329,69 @@ fn configure_platform(config: &mut BuildConfig) { config.builder.flag_if_supported("-march=native"); } + // GCC LTO support - ensure fat LTO objects are created so they can be used by linkers that don't support LTO plugin + if config.features.lto && matches!(config.compiler, Compiler::Gcc) && !config.is_msvc() { + #[cfg(feature = "build_cc")] + config.builder.flag_if_supported("-ffat-lto-objects"); + } + // Platform-specific configurations - match () { - _ if config.is_windows() => { + if config.is_windows() { + if config.features.win8compat { + // Windows 8.1 (0x0603) for compatibility mode + config.builder.compiler_define("WINVER", "0x0603"); + config.builder.compiler_define("_WIN32_WINNT", "0x0603"); + } else { + // Windows 10 (0x0A00) default to enable VirtualAlloc2 and WaitOnAddress + // snmalloc requires NTDDI_WIN10_RS5 logic for these features in pal_windows.h + config.builder.compiler_define("WINVER", "0x0A00"); + config.builder.compiler_define("_WIN32_WINNT", "0x0A00"); + } + + if config.is_msvc() { + let msvc_flags = vec![ + "/nologo", "/W4", "/WX", "/wd4127", "/wd4324", "/wd4201", + "/Ob2", "/EHsc", "/Gd", "/TP", "/Gm-", "/GS", + "/fp:precise", "/Zc:wchar_t", "/Zc:forScope", "/Zc:inline" + ]; + for flag in msvc_flags { + config.builder.flag_if_supported(flag); + } + + if !config.debug { + #[cfg(feature = "build_cc")] + config.builder.define("NDEBUG", None); + } + + if config.features.lto { + config.builder + .flag_if_supported("/GL") + .define("CMAKE_INTERPROCEDURAL_OPTIMIZATION", "TRUE") + .define("SNMALLOC_IPO", "ON"); + println!("cargo:rustc-link-arg=/LTCG"); + } + + config.builder + .define("CMAKE_CXX_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG /EHsc") + .define("CMAKE_C_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG /EHsc"); + } else { let common_flags = vec!["-mcx16", "-fno-exceptions", "-fno-rtti", "-pthread"]; for flag in common_flags { config.builder.flag_if_supported(flag); } - if config.is_msvc() { - let msvc_flags = vec![ - "/nologo", "/W4", "/WX", "/wd4127", "/wd4324", "/wd4201", - "/Ob2", "/DNDEBUG", "/EHsc", "/Gd", "/TP", "/Gm-", "/GS", - "/fp:precise", "/Zc:wchar_t", "/Zc:forScope", "/Zc:inline" - ]; - for flag in msvc_flags { - config.builder.flag_if_supported(flag); - } - - if config.features.lto { - config.builder - .flag_if_supported("/GL") - .define("CMAKE_INTERPROCEDURAL_OPTIMIZATION", "TRUE") - .define("SNMALLOC_IPO", "ON"); - println!("cargo:rustc-link-arg=/LTCG"); - } - - config.builder - .define("CMAKE_CXX_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG /EHsc") - .define("CMAKE_C_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG /EHsc"); + // Ensure consistent Windows version targeting + if config.features.win8compat { + // Windows 8.1 (0x0603) for compatibility mode + config.builder.compiler_define("WINVER", "0x0603"); + config.builder.compiler_define("_WIN32_WINNT", "0x0603"); + } else { + // Windows 10 (0x0A00) default to enable VirtualAlloc2 and WaitOnAddress + // snmalloc requires NTDDI_WIN10_RS5 logic for these features in pal_windows.h + config.builder.compiler_define("WINVER", "0x0A00"); + config.builder.compiler_define("_WIN32_WINNT", "0x0A00"); } - else if let Some(msystem) = &config.msystem { + + if let Some(msystem) = &config.msystem { match msystem.as_str() { "CLANG64" | "CLANGARM64" => { let defines = vec![ @@ -361,27 +421,98 @@ fn configure_platform(config: &mut BuildConfig) { } } } - _ if config.is_unix() => { - let unix_flags = vec!["-fPIC", "-pthread", "-fno-exceptions", "-fno-rtti", "-mcx16", "-Wno-unused-parameter"]; - for flag in unix_flags { - config.builder.flag_if_supported(flag); - } + } else if config.is_unix() { + let unix_flags = vec!["-fPIC", "-pthread", "-fno-exceptions", "-fno-rtti", "-mcx16", "-Wno-unused-parameter"]; + for flag in unix_flags { + config.builder.flag_if_supported(flag); + } - if config.target_os != "haiku" { - let tls_model = if config.features.local_dynamic_tls { "-ftls-model=local-dynamic" } else { "-ftls-model=initial-exec" }; - config.builder.flag_if_supported(tls_model); - } + if config.target_os == "freebsd" { + config.builder.flag_if_supported("-w"); + } + + if config.target_os != "haiku" { + let tls_model = if config.features.local_dynamic_tls { "-ftls-model=local-dynamic" } else { "-ftls-model=initial-exec" }; + config.builder.flag_if_supported(tls_model); + } + + #[cfg(feature = "build_cc")] + if config.target_os == "linux" || config.target_os == "android" { + config.builder.define("SNMALLOC_HAS_LINUX_FUTEX_H", None); + config.builder.define("SNMALLOC_HAS_LINUX_RANDOM_H", None); + config.builder.define("SNMALLOC_PLATFORM_HAS_GETENTROPY", None); } - _ => {} } // Feature configurations config.builder .define("SNMALLOC_QEMU_WORKAROUND", if config.features.qemu { "ON" } else { "OFF" }) .define("SNMALLOC_ENABLE_DYNAMIC_LOADING", if config.features.notls { "ON" } else { "OFF" }) - .define("SNMALLOC_USE_WAIT_ON_ADDRESS", if config.features.wait_on_address { "1" } else { "0" }) .define("USE_SNMALLOC_STATS", if config.features.stats { "ON" } else { "OFF" }) - .define("SNMALLOC_RUST_LIBC_API", if config.features.libc_api { "ON" } else { "OFF" }); + .define("SNMALLOC_RUST_LIBC_API", if config.features.libc_api { "ON" } else { "OFF" }) + .define("SNMALLOC_USE_CXX17", if cfg!(feature = "usecxx17") { "ON" } else { "OFF" }); + + if config.features.tracing { + config.builder.define("SNMALLOC_TRACING", "ON"); + } + if config.features.fuzzing { + config.builder.define("SNMALLOC_ENABLE_FUZZING", "ON"); + } + if config.features.vendored_stl { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_USE_SELF_VENDORED_STL", "1"); + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_USE_SELF_VENDORED_STL", "ON"); + } + + if config.features.check_loads { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_CHECK_LOADS", "true"); + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_CHECK_LOADS", "ON"); + } else { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_CHECK_LOADS", "false"); + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_CHECK_LOADS", "OFF"); + } + + if config.features.pageid { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_PAGEID", "true"); + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_PAGEID", "ON"); + } else { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_PAGEID", "false"); + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_PAGEID", "OFF"); + } + + if config.features.gwp_asan { + config.builder.define("SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION", "ON"); + if let Ok(path) = env::var("SNMALLOC_GWP_ASAN_INCLUDE_PATH") { + config.builder.define("SNMALLOC_GWP_ASAN_INCLUDE_PATH", path.as_str()); + } + if let Ok(path) = env::var("SNMALLOC_GWP_ASAN_LIBRARY_PATH") { + config.builder.define("SNMALLOC_GWP_ASAN_LIBRARY_PATH", path.as_str()); + } + } + + // Handle wait_on_address configuration for different build systems + if config.features.wait_on_address { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_USE_WAIT_ON_ADDRESS", "1"); + + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_ENABLE_WAIT_ON_ADDRESS", "ON"); + } else { + #[cfg(feature = "build_cc")] + config.builder.define("SNMALLOC_USE_WAIT_ON_ADDRESS", "0"); + + #[cfg(not(feature = "build_cc"))] + config.builder.define("SNMALLOC_ENABLE_WAIT_ON_ADDRESS", "OFF"); + } // Android configuration if config.target.contains("android") { @@ -460,6 +591,9 @@ fn configure_linking(config: &BuildConfig) { println!("cargo:rustc-link-lib=dl"); println!("cargo:rustc-link-lib=m"); + // Force rust-lld + println!("cargo:rustc-link-arg=-fuse-ld=lld"); + if cfg!(feature = "usecxx17") && !config.is_clang_msys() { println!("cargo:rustc-link-lib=gcc"); } @@ -503,6 +637,16 @@ fn main() { println!("cargo:rustc-link-search={}/build/Debug", config.out_dir); println!("cargo:rustc-link-search={}/build/Release", config.out_dir); let mut _dst = config.builder.build_lib(&config.target_lib); - println!("cargo:rustc-link-lib={}", config.target_lib); + + if config.is_linux() { + // Use whole-archive to ensure all symbols (including FFI exports) are included + // This is critical for LTO and ensuring sn_rust_* symbols are available + println!("cargo:rustc-link-arg=-Wl,--whole-archive"); + println!("cargo:rustc-link-lib=static={}", config.target_lib); + println!("cargo:rustc-link-arg=-Wl,--no-whole-archive"); + } else { + println!("cargo:rustc-link-lib={}", config.target_lib); + } + configure_linking(&config); } diff --git a/snmalloc-rs/xtask/Cargo.toml b/snmalloc-rs/xtask/Cargo.toml new file mode 100644 index 000000000..801976a41 --- /dev/null +++ b/snmalloc-rs/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +xshell = "0.2" +clap = { version = "4.0", features = ["derive"] } +regex = "1" +anyhow = "1.0" diff --git a/snmalloc-rs/xtask/src/main.rs b/snmalloc-rs/xtask/src/main.rs new file mode 100644 index 000000000..8ec3fe837 --- /dev/null +++ b/snmalloc-rs/xtask/src/main.rs @@ -0,0 +1,70 @@ +use xshell::{cmd, Shell}; +use clap::{Parser, Subcommand}; +use regex::Regex; + +#[derive(Parser)] +#[command(name = "xtask")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + CompareBuilds, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + match cli.command { + Commands::CompareBuilds => compare_builds()?, + } + Ok(()) +} + +fn compare_builds() -> anyhow::Result<()> { + let sh = Shell::new()?; + + println!("=== Benchmarking build_cc (WaitOnAddress enabled) ==="); + // Clean to ensure rebuild + cmd!(sh, "cargo clean -p snmalloc-sys").run()?; + // Build and run with build_cc + // Note: We use release mode for benchmarks + let output_cc = cmd!(sh, "cargo run --release --example bench_contention --no-default-features --features build_cc,usewait-on-address").read()?; + println!("{}", output_cc); + let cc_throughput = parse_throughput(&output_cc); + + println!("\n=== Benchmarking build_cmake (Default, WaitOnAddress enabled) ==="); + cmd!(sh, "cargo clean -p snmalloc-sys").run()?; + // Build and run with build_cmake (explicitly set to avoid confusion) + // Note: snmalloc-sys/build_cmake is implied by snmalloc-rs features if mapped, but snmalloc-rs default is build_cmake + let output_cmake = cmd!(sh, "cargo run --release --example bench_contention --no-default-features --features snmalloc-sys/build_cmake,usewait-on-address").read()?; + println!("{}", output_cmake); + let cmake_throughput = parse_throughput(&output_cmake); + + println!("\n=== Results Comparison ==="); + println!("build_cc Throughput: {:.2} Mops/sec", cc_throughput); + println!("build_cmake Throughput: {:.2} Mops/sec", cmake_throughput); + + let diff = (cc_throughput - cmake_throughput) / cmake_throughput * 100.0; + println!("Difference: {:.2}%", diff); + + if diff.abs() < 10.0 { + println!("Performance is roughly equivalent (within 10%). Fix verified."); + } else if diff < -10.0 { + println!("WARNING: build_cc is significantly slower than build_cmake."); + } else { + println!("build_cc is faster than build_cmake."); + } + + Ok(()) +} + +fn parse_throughput(output: &str) -> f64 { + let re = Regex::new(r"Throughput: ([\d\.]+) Mops/sec").unwrap(); + if let Some(caps) = re.captures(output) { + caps[1].parse().unwrap_or(0.0) + } else { + 0.0 + } +}