From 83320bbf5880bc208f9591276a14c9d175c0e736 Mon Sep 17 00:00:00 2001 From: michael parsons Date: Tue, 10 Nov 2026 19:47:04 -0800 Subject: [PATCH] perf(bin): add an opt-in mimalloc global allocator A new `mimalloc` feature on the bin crate makes mimalloc the global allocator in place of the system allocator. Default builds do not change. The clippy workflow builds the feature. Measured on the PostgreSQL build of main at 9c3fbfb on glibc, under conditional PutItem and PartiQL INSERT load with 32 requests in flight: with mimalloc, server CPU for each request fell by about 12% and throughput rose by about 2% to 4%. Peak resident memory was higher. ADR-0030 records the measurements and why the feature is off by default. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Fable 5.2 Signed-off-by: michael parsons --- .github/workflows/clippy.yml | 5 +- Cargo.lock | 21 ++++- Cargo.toml | 3 + crates/bin/Cargo.toml | 4 + crates/bin/src/main.rs | 7 ++ docs/adr/0030-optional-mimalloc-allocator.md | 95 ++++++++++++++++++++ docs/adr/README.md | 1 + docs/manuals/04-quickstart-setup-guide.md | 3 + 8 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0030-optional-mimalloc-allocator.md diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index fafd81908..76fabbabd 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -34,8 +34,9 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} # With the Cassandra and MongoDB crates' `test-hooks` features, so the # hook modules (`src/test_hooks.rs`) and their call sites, which only - # the integration test binaries build otherwise, are linted too. - - run: cargo clippy --all-targets --features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks -- -D warnings + # the integration test binaries build otherwise, are linted too. With the + # bin crate's `mimalloc` feature as well, which no other job builds. + - run: cargo clippy --all-targets --features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks,extenddb/mimalloc -- -D warnings # The backup job's kill-point pauses (`EXTENDDB_BACKUP_PAUSE`), which # `devtools/backup-kill-points.sh` runs by hand on a build with the # feature; no other job builds it. diff --git a/Cargo.lock b/Cargo.lock index 0209610a9..741252de9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1645,6 +1645,7 @@ dependencies = [ "extenddb-storage-mongodb", "extenddb-storage-postgres", "extenddb-storage-sqlite", + "mimalloc", "sqlx", "tokio", "uuid", @@ -2892,6 +2893,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.16" @@ -3059,6 +3069,15 @@ dependencies = [ "rapidhash", ] +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -5252,7 +5271,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c22b5a57b..c1bba77b6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,9 @@ extenddb-auth = { path = "crates/auth", version = "=0.1.14" } extenddb-server = { path = "crates/server", version = "=0.1.14" } extenddb-app = { path = "crates/app", version = "=0.1.14" } +# Memory allocator, opt-in through the `mimalloc` feature of the bin crate +mimalloc = { version = "0.1", default-features = false } + # Serialization serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 7a2705c20..31536922b 100755 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -38,6 +38,9 @@ mongodb-test-hooks = [ # fails (`main.rs` checks this at compile time). A production `sqlite` build # leaves it off. dev-mode = ["extenddb-app/dev-mode"] +# Use mimalloc as the global allocator in place of the system allocator. Off by +# default. See ADR-0030 for the measurements. +mimalloc = ["dep:mimalloc"] [dependencies] extenddb-app = { workspace = true } @@ -48,6 +51,7 @@ extenddb-storage-postgres = { workspace = true, optional = true } extenddb-storage-sqlite = { workspace = true, optional = true } anyhow = { workspace = true } extenddb-config = { workspace = true } +mimalloc = { workspace = true, optional = true } [dev-dependencies] sqlx = { workspace = true } diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs index 0f8e81bdd..bfd5a8cc3 100755 --- a/crates/bin/src/main.rs +++ b/crates/bin/src/main.rs @@ -17,6 +17,13 @@ //! installs one backend per process, so a build with more than one would be ambiguous //! and is rejected at compile time. +// The `mimalloc` feature replaces the system allocator with mimalloc. The +// request path allocates for each JSON value, attribute and string that it +// builds, on many tokio worker threads. See ADR-0030 for the measurements. +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + // Exactly one backend feature must be enabled. #[cfg(any( all(feature = "postgres", feature = "cassandra"), diff --git a/docs/adr/0030-optional-mimalloc-allocator.md b/docs/adr/0030-optional-mimalloc-allocator.md new file mode 100644 index 000000000..7f3912acf --- /dev/null +++ b/docs/adr/0030-optional-mimalloc-allocator.md @@ -0,0 +1,95 @@ +# ADR-0030: mimalloc is an opt-in global allocator for the server binary + +- Status: Proposed +- Date: 2026-11-10 +- Deciders: @parsnips + +## Context + +The `extenddb` binary uses the system allocator. On Linux that is glibc +malloc. + +The request path allocates often. A request body becomes a `serde_json::Value`, +then `AttributeValue`s. A PartiQL statement becomes a `PutItem` body, which is +parsed again. The item becomes JSONB for the storage backend. Each step +allocates strings, maps and vectors, and frees them on one of many tokio worker +threads. + +We measured the PostgreSQL build of `main` at 9c3fbfb under a load generator +that sends conditional inserts with unique keys to a table with a hash and a +range key and no indexes. It sends them as `PutItem` with +`attribute_not_exists` on the partition key, and as `ExecuteStatement` with an +`INSERT` statement. Each item has a 256-byte value. The host was a 24-core +x86_64 Linux machine. The server ran in a Debian 12 container (glibc 2.36), +with PostgreSQL 16 in a second container and connection pools of 20. + +We profiled the server with `perf` (frame pointers on) under the `PutItem` +load with 32 requests in flight. Code in `libc.so.6` took 16% of the server's +samples, which is 22% of the samples in user space. + +## Options Considered + +1. **Keep the system allocator.** No change. +2. **mimalloc as an opt-in feature.** A `mimalloc` feature on the bin crate + sets `mimalloc::MiMalloc` as the `#[global_allocator]`. Default builds do + not change. +3. **mimalloc by default.** The same change with the feature on in the default + feature set, so every release binary uses it. +4. **jemalloc (`tikv-jemallocator`).** Not measured here. Its build needs + `make` and runs a configure script, and it does not build for MSVC + targets. + +## Decision + +The bin crate has an opt-in `mimalloc` feature that makes mimalloc the global +allocator (Option 2). + +## Rationale + +- Measured on the workload above, with 32 requests in flight for 10 seconds + for each operation. Each measurement used a fresh database, and the two + builds were the same apart from the `mimalloc` feature. The runs alternated + between the builds. The host also ran other work, which disturbed one + window (marked \*). + + | Operation | Allocator | Throughput (ops/s), per measurement | Server CPU per request (µs) | + |---|---|---|---| + | `PutItem` | glibc malloc | 15,092, 15,099, 14,992 | 257, 257, 251 | + | `PutItem` | mimalloc | 15,679, 15,065, 15,415 | 226, 222, 227 | + | `INSERT` | glibc malloc | 14,613, 6,616\*, 14,058 | 297, 319\*, 287 | + | `INSERT` | mimalloc | 14,909, 14,595, 15,134 | 259, 263, 253 | + + Server CPU for each request fell by about 12% for both operations. + Throughput rose by about 2% to 4%, which is close to the variation between + measurements. With mimalloc, `libc.so.6` took 5% of the server's samples. +- Peak resident memory of the server after each measurement (about 600,000 + inserts) was 309 to 359 MB with glibc malloc and 398 to 428 MB with + mimalloc. +- The `mimalloc` crate (0.1.52, which builds mimalloc v3) compiles its C + sources with the `cc` crate. The workspace already builds C through `cc` for + `aws-lc-sys`. The crate's `secure` feature (guard pages, encoded free lists) + stays off. +- An opt-in feature does not change memory behaviour for existing + deployments. mimalloc holds and returns memory differently from glibc, and + resident memory was higher in the measurement above. Operators can measure + it on their own workload before the default changes. +- A `#[global_allocator]` in the bin crate applies to the whole process and + does not touch any library crate. A third-party thin bin can choose its own + allocator. + +## Consequences + +- A build that spends less CPU in the allocator is one flag away: + `cargo build --release --features mimalloc`. +- Release images and packages keep the system allocator until a later decision + turns the feature on by default. That decision needs measurements from more + workloads (read-heavy, Scan, the other backends) and of resident memory. +- The feature adds a C dependency to builds that turn it on. +- Reverting is a one-line change to the feature set. + +--- + +## License + +Copyright 2026 ExtendDB contributors. Licensed under the Apache License, Version 2.0. +See [LICENSE](../../LICENSE) for the full text. diff --git a/docs/adr/README.md b/docs/adr/README.md index 372bf38f3..43a68be68 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -55,3 +55,4 @@ decision, write a new ADR. | [0026](0026-credential-crypto-on-aws-lc.md) | Credential cryptography uses AWS-LC through aws-lc-rs | Accepted | | [0027](0027-cassandra-index-row-version-stamps.md) | Cassandra: index rows carry the base-row version as their write timestamp | Accepted | | [0029](0029-crates-io-packaging.md) | The workspace crates go to crates.io together, at one version with exact pins | Accepted | +| [0030](0030-optional-mimalloc-allocator.md) | mimalloc is an opt-in global allocator for the server binary | Proposed | diff --git a/docs/manuals/04-quickstart-setup-guide.md b/docs/manuals/04-quickstart-setup-guide.md index d3d197414..7a8920e7d 100755 --- a/docs/manuals/04-quickstart-setup-guide.md +++ b/docs/manuals/04-quickstart-setup-guide.md @@ -121,6 +121,9 @@ cargo build # Release build (slower compilation, optimized runtime) cargo build --release + +# Release build with mimalloc in place of the system allocator (ADR-0030) +cargo build --release --features mimalloc ``` The binary is at `target/release/extenddb` (or `target/debug/extenddb`).