Repository: fitzgen/bumpalo
Pull request #268 (2025-04-11 22:43:08)

Add support for enforcing minimum alignment in `Bump`
Enforcing a minimum alignment can speed up allocation of objects with alignment less than or equal to the minimum alignment. This comes at the cost of introducing otherwise-unnecessary padding between allocations of objects with alignment less than the minimum.


Patch:
diff --git a/.gitignore b/.gitignore
index a9d37c5..8bb7fd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 target
 Cargo.lock
+benches/results.json
+*perf.data*
diff --git a/Cargo.toml b/Cargo.toml
index e16758c..8c939fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,12 @@ path = "benches/benches.rs"
 harness = false
 required-features = ["collections"]
 
+[[bench]]
+name = "allocator_api"
+path = "benches/allocator_api.rs"
+harness = false
+required-features = ["bench_allocator_api"]
+
 [[test]]
 name = "try_alloc"
 path = "tests/try_alloc.rs"
@@ -45,6 +51,7 @@ criterion = "0.3.6"
 rand = "0.8.5"
 serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.115"
+blink-alloc = { version = "=0.3.1" }
 
 [features]
 default = []
@@ -54,5 +61,8 @@ allocator_api = []
 std = []
 serde = ["dep:serde"]
 
+# Feature for bumpalo's internal development only. Do not use!
+bench_allocator_api = ["allocator_api", "blink-alloc/nightly"]
+
 # [profile.bench]
 # debug = true
diff --git a/benches/README.md b/benches/README.md
new file mode 100644
index 0000000..a510008
--- /dev/null
+++ b/benches/README.md
@@ -0,0 +1,160 @@
+# Benchmarks
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Reproducing](#reproducing)
+- [Benchmark Results](#benchmark-results)
+    - [allocator-api](#allocator-api)
+    - [warm-up](#warm-up)
+    - [reset](#reset)
+    - [vec](#vec)
+
+## Overview
+
+This directory contains two suites of benchmarks:
+
+1. `allocator_api.rs`: `std::alloc::Allocator`-based benchmarks that aim to
+   measure the performance of bump allocators within the generic `Allocator`
+   API.
+
+2. `benches.rs`: Miscellaneous Bumpalo-specific benchmarks.
+
+The tables of benchmark results listed below are the results for the suite of
+`std::alloc::Allocator`-based benchmarks. They are originally adapted from
+[`blink-alloc`] (another fine bump allocator crate) which was already measuring
+the relative performance between `blink-alloc` and `bumpalo`. I wasn't able to
+reproduce many of their results showing that `blink-alloc` was faster than
+`bumpalo`, however, which was part of the motivation to bring a subset of the
+benchmarks into this repo and document reproduction steps.
+
+Furthermore, the tables below include a `std::alloc::System` column, but their
+results come with a few caveats. First, in order to implement a `reset` method
+for the system allocator and deallocate everything that was allocated within a
+certain region of code, I had to add additional bookkeeping to dynamically track
+every live allocation. That bookkeeping generally won't be present in real
+programs, which will instead use things like `Drop` implementations, so it makes
+the system allocator's results look worse than they otherwise would
+be. Additionally, these benchmarks are really designed to show off the strengths
+of bump allocators and measure the operations that are important for bump
+allocators. The system allocator is expected to perform worse, but that's
+because it is designed for general purpose scenarios, where as bump allocators
+are designed for very specific scenarios. These columns should mostly serve as
+just a general reference point to get an idea of the magnitude of allocation
+speed up you can expect in the very specific scenarios where using a bump
+allocator makes sense.
+
+Finally, all these benchmarks are synthetic. They are micro benchmarks. You
+shouldn't expect that anything here will directly translate into speed ups for
+your application. Application performance is what really matters, and things
+observed in the micro often disappear in the macro. If your application isn't
+bottlenecked on allocation, or can't abide by the constraints that a bump
+allocator imposes, there's nothing that a bump allocator can do to improve its
+performance.
+
+[`blink-alloc`]: https://github.com/zakarumych/blink-alloc/blob/845b2db273371260eef2e9858386f6c6aa180e98/BENCHMARKS.md
+
+## Reproducing
+
+The `std::alloc::Allocator`-based benchmarks require using nightly Rust, since
+the `Allocator` trait is still unstable. You must additionally enable Bumpalo's
+`allocator_api` cargo feature:
+
+```
+$ cargo +nightly bench --bench allocator_api --features allocator_api
+```
+
+The miscellaneous benchmarks require Bumpalo's `collections` cargo feature:
+
+```
+$ cargo bench --bench benches --features collections
+```
+
+To update the tables below, use `cargo-criterion` and [`criterion-table`]:
+
+```
+$ cd bumpalo/benches/
+$ cargo +nightly bench --features bench_allocator_api \
+    --bench allocator_api \
+    --message-format=json \
+    > results.json
+$ criterion-table < results.json > README.md
+```
+
+[`cargo-criterion`]: https://github.com/bheisler/cargo-criterion
+[`criterion-table`]: https://github.com/nu11ptr/criterion-table
+
+## Benchmark Results
+
+### allocator-api
+
+Benchmarks that measure calls into `std::alloc::Allocator` methods directly.
+
+These operations are generally the ones that happen most often, and therefore
+their performance is generally most important. Following the same logic, raw
+allocation is generally the very most important.
+
+|                                                     | `bumpalo::Bump`          | `blink_alloc::BlinkAlloc`          | `std::alloc::System`               |
+|:----------------------------------------------------|:-------------------------|:-----------------------------------|:---------------------------------- |
+| **`allocate(u8) x 10007`**                          | `16.65 us` (✅ **1.00x**) | `20.13 us` (❌ *1.21x slower*)      | `475.36 us` (❌ *28.55x slower*)    |
+| **`allocate(u32) x 10007`**                         | `16.41 us` (✅ **1.00x**) | `19.58 us` (❌ *1.19x slower*)      | `525.99 us` (❌ *32.06x slower*)    |
+| **`allocate(u64) x 10007`**                         | `16.69 us` (✅ **1.00x**) | `16.51 us` (✅ **1.01x faster**)    | `564.42 us` (❌ *33.82x slower*)    |
+| **`allocate(u128) x 10007`**                        | `15.97 us` (✅ **1.00x**) | `16.41 us` (✅ **1.03x slower**)    | `618.64 us` (❌ *38.73x slower*)    |
+| **`allocate([u8; 0]) x 10007`**                     | `22.04 us` (✅ **1.00x**) | `17.40 us` (✅ **1.27x faster**)    | `197.37 us` (❌ *8.96x slower*)     |
+| **`allocate([u8; 1]) x 10007`**                     | `22.03 us` (✅ **1.00x**) | `17.24 us` (✅ **1.28x faster**)    | `484.81 us` (❌ *22.01x slower*)    |
+| **`allocate([u8; 7]) x 10007`**                     | `22.09 us` (✅ **1.00x**) | `17.41 us` (✅ **1.27x faster**)    | `567.44 us` (❌ *25.68x slower*)    |
+| **`allocate([u8; 8]) x 10007`**                     | `22.09 us` (✅ **1.00x**) | `17.41 us` (✅ **1.27x faster**)    | `561.20 us` (❌ *25.41x slower*)    |
+| **`allocate([u8; 31]) x 10007`**                    | `22.09 us` (✅ **1.00x**) | `17.34 us` (✅ **1.27x faster**)    | `675.39 us` (❌ *30.57x slower*)    |
+| **`allocate([u8; 32]) x 10007`**                    | `21.99 us` (✅ **1.00x**) | `17.57 us` (✅ **1.25x faster**)    | `690.94 us` (❌ *31.42x slower*)    |
+| **`grow same align (u32 -> [u32; 2]) x 10007`**     | `29.65 us` (✅ **1.00x**) | `31.03 us` (✅ **1.05x slower**)    | `1.15 ms` (❌ *38.75x slower*)      |
+| **`grow smaller align (u32 -> [u16; 4]) x 10007`**  | `30.12 us` (✅ **1.00x**) | `31.06 us` (✅ **1.03x slower**)    | `1.15 ms` (❌ *38.07x slower*)      |
+| **`grow larger align (u32 -> u64) x 10007`**        | `37.50 us` (✅ **1.00x**) | `39.16 us` (✅ **1.04x slower**)    | `1.15 ms` (❌ *30.79x slower*)      |
+| **`shrink same align ([u32; 2] -> u32) x 10007`**   | `19.66 us` (✅ **1.00x**) | `20.39 us` (✅ **1.04x slower**)    | `1.09 ms` (❌ *55.61x slower*)      |
+| **`shrink smaller align (u32 -> u16) x 10007`**     | `19.97 us` (✅ **1.00x**) | `19.93 us` (✅ **1.00x faster**)    | `1.08 ms` (❌ *54.32x slower*)      |
+| **`shrink larger align ([u16; 4] -> u32) x 10007`** | `19.60 us` (✅ **1.00x**) | `39.14 us` (❌ *2.00x slower*)      | `1.09 ms` (❌ *55.76x slower*)      |
+
+### warm-up
+
+Benchmarks that measure the first allocation in a fresh allocator.
+
+These aren't generally very important, since the first allocation in a fresh
+bump allocator only ever happens once by definition. This is mostly measuring
+how long it takes the underlying system allocator to allocate the initial chunk
+to bump allocate out of.
+
+|                            | `bumpalo::Bump`          | `blink_alloc::BlinkAlloc`          | `std::alloc::System`             |
+|:---------------------------|:-------------------------|:-----------------------------------|:-------------------------------- |
+| **`first u32 allocation`** | `24.16 ns` (✅ **1.00x**) | `21.65 ns` (✅ **1.12x faster**)    | `74.88 ns` (❌ *3.10x slower*)    |
+
+### reset
+
+Benchmarks that measure the overhead of resetting a bump allocator to an empty
+state, ready to be reused in a new program phase.
+
+This generally doesn't happen as often as allocation, and therefore is generally
+less important, but it is important to keep an eye on generally since
+deallocation-en-masse and reusing already-allocated chunks can be selling points
+for bump allocation over using a generic allocator in certain scenarios.
+
+|                                         | `bumpalo::Bump`           | `blink_alloc::BlinkAlloc`          | `std::alloc::System`                |
+|:----------------------------------------|:--------------------------|:-----------------------------------|:----------------------------------- |
+| **`reset after allocate(u32) x 10007`** | `163.62 ns` (✅ **1.00x**) | `192.34 ns` (❌ *1.18x slower*)     | `127.35 us` (❌ *778.30x slower*)    |
+
+### vec
+
+Benchmarks that measure the various `std::vec::Vec<T> operations when used in
+conjuction with a bump allocator.
+
+Bump allocators aren't often used directly, but instead through some sort of
+collection. These benchmarks are important in the sense that the standard
+`Vec<T>` type is probably the most-commonly used collection (although not
+necessarily the most commonly used with bump allocators in Rust, at least until
+the `Allocator` trait is stabilized).
+
+|                                | `bumpalo::Bump`          | `blink_alloc::BlinkAlloc`          | `std::alloc::System`              |
+|:-------------------------------|:-------------------------|:-----------------------------------|:--------------------------------- |
+| **`push(usize) x 10007`**      | `16.66 us` (✅ **1.00x**) | `15.21 us` (✅ **1.10x faster**)    | `42.36 us` (❌ *2.54x slower*)     |
+| **`reserve_exact(1) x 10007`** | `2.26 ms` (✅ **1.00x**)  | `60.24 us` (🚀 **37.44x faster**)   | `683.34 us` (🚀 **3.30x faster**)  |
+
+---
+Made with [criterion-table](https://github.com/nu11ptr/criterion-table)
diff --git a/benches/allocator_api.rs b/benches/allocator_api.rs
new file mode 100644
index 0000000..5c4d3f0
--- /dev/null
+++ b/benches/allocator_api.rs
@@ -0,0 +1,465 @@
+//! This benchmark is adapted from [`blink-alloc`] (MIT/Apache-2) but with a
+//! bunch of extraneous stuff trimmed away.
+//!
+//! [`blink-alloc`]: https://github.com/zakarumych/blink-alloc/blob/845b2db273371260eef2e9858386f6c6aa180e98/benches/bench.rs
+
+#![feature(allocator_api)]
+
+use criterion::*;
+use std::{
+    alloc::{AllocError, Allocator, Layout},
+    cell::RefCell,
+    collections::HashMap,
+    mem,
+    ptr::NonNull,
+    time::{Duration, Instant},
+};
+
+/// Trait for resetting a bump allocator to its initial state.
+trait BumpAllocator: Default
+where
+    for<'a> &'a Self: Allocator,
+{
+    fn with_capacity(cap: usize) -> Self;
+    fn reset(&mut self);
+}
+
+type Bumpalo = bumpalo::Bump<{ std::mem::align_of::<usize>() }>;
+impl BumpAllocator for Bumpalo {
+    fn with_capacity(cap: usize) -> Self {
+        let b = Bumpalo::with_min_align_and_capacity(cap);
+        b.set_allocation_limit(Some(cap));
+        b
+    }
+
+    #[inline(always)]
+    fn reset(&mut self) {
+        self.reset();
+    }
+}
+
+impl BumpAllocator for blink_alloc::BlinkAlloc {
+    fn with_capacity(cap: usize) -> Self {
+        blink_alloc::BlinkAlloc::with_chunk_size(cap)
+    }
+
+    #[inline(always)]
+    fn reset(&mut self) {
+        self.reset();
+    }
+}
+
+/// System allocator, as if it were a bump allocator. See caveats in
+/// `benches/README.md`; it isn't expected that this super accurately reflects
+/// the system allocator's performance.
+#[derive(Default)]
+struct SystemAlloc {
+    alloc: std::alloc::System,
+    live: RefCell<HashMap<NonNull<u8>, Layout>>,
+}
+
+impl BumpAllocator for SystemAlloc {
+    fn with_capacity(cap: usize) -> Self {
+        SystemAlloc {
+            alloc: std::alloc::System,
+            live: RefCell::new(HashMap::with_capacity(cap)),
+        }
+    }
+
+    fn reset(&mut self) {
+        let mut live = self.live.borrow_mut();
+        for (ptr, layout) in live.drain() {
+            unsafe {
+                self.alloc.deallocate(ptr, layout);
+            }
+        }
+    }
+}
+
+unsafe impl<'a> Allocator for &'a SystemAlloc {
+    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
+        let ptr = self.alloc.allocate(layout)?;
+
+        let mut live = self.live.borrow_mut();
+        live.insert(ptr.cast(), layout);
+
+        Ok(ptr)
+    }
+
+    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
+        self.alloc.deallocate(ptr, layout);
+        let mut live = self.live.borrow_mut();
+        live.remove(&ptr);
+    }
+
+    fn allocate_zeroed(&self, _layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
+        unimplemented!()
+    }
+
+    unsafe fn grow(
+        &self,
+        ptr: NonNull<u8>,
+        old_layout: Layout,
+        new_layout: Layout,
+    ) -> Result<NonNull<[u8]>, AllocError> {
+        {
+            let mut live = self.live.borrow_mut();
+            live.remove(&ptr);
+        }
+
+        let ptr = self.alloc.grow(ptr, old_layout, new_layout)?;
+
+        let mut live = self.live.borrow_mut();
+        live.insert(ptr.cast(), new_layout);
+
+        Ok(ptr)
+    }
+
+    unsafe fn grow_zeroed(
+        &self,
+        _ptr: NonNull<u8>,
+        _old_layout: Layout,
+        _new_layout: Layout,
+    ) -> Result<NonNull<[u8]>, AllocError> {
+        unimplemented!()
+    }
+
+    unsafe fn shrink(
+        &self,
+        ptr: NonNull<u8>,
+        old_layout: Layout,
+        new_layout: Layout,
+    ) -> Result<NonNull<[u8]>, AllocError> {
+        {
+            let mut live = self.live.borrow_mut();
+            live.remove(&ptr);
+        }
+
+        let ptr = self.alloc.shrink(ptr, old_layout, new_layout)?;
+
+        let mut live = self.live.borrow_mut();
+        live.insert(ptr.cast(), new_layout);
+
+        Ok(ptr)
+    }
+}
+
+// The number of allocations to perform in each iteration of the
+// benchmarks. This used to be 17453, but it wasn't clear to me why that number
+// was chosen, or how it related to the warm up allocation's size. Instead, I've
+// chosen 10_007 because it is a prime number and therefore should hopefully
+// help us avoid any kind of unwanted harmonics in our measurements. It is also
+// large enough that we can start to filter out the noise from our alloc
+// operations, but small enough that running the benchmarks takes a reasonable
+// amount of time. Finally, I factored out the warm-up logic to be directly tied
+// to this number, and ensure that we avoid measuring any resizes during our
+// allocations, as (a) they are already covered by the "warm-up" benchmark and
+// (b) resizing is rare and amortized across allocations (which happen
+// frequently, and whose performance is actually important).
+const NUM_ALLOCS: usize = 10_007;
+
+fn bench_allocator_api<A>(name: &str, c: &mut Criterion)
+where
+    for<'a> &'a A: Allocator,
+    A: BumpAllocator + Default + 'static,
+{
+    let mut group = c.benchmark_group(format!("allocator-api/{name}"));
+
+    group.bench_function(format!("allocate(u8) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<u8>() * NUM_ALLOCS);
+        b.iter(|| {
+            for _ in 0..NUM_ALLOCS {
+                let ptr = (&alloc).allocate(Layout::new::<u8>()).unwrap();
+                black_box(ptr);
+            }
+            alloc.reset();
+        })
+    });
+
+    group.bench_function(format!("allocate(u32) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<u32>() * NUM_ALLOCS);
+        b.iter(|| {
+            for _ in 0..NUM_ALLOCS {
+                let ptr = (&alloc).allocate(Layout::new::<u32>()).unwrap();
+                black_box(ptr);
+            }
+            alloc.reset();
+        })
+    });
+
+    group.bench_function(format!("allocate(u64) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<u64>() * NUM_ALLOCS);
+        b.iter(|| {
+            for _ in 0..NUM_ALLOCS {
+                let ptr = (&alloc).allocate(Layout::new::<u64>()).unwrap();
+                black_box(ptr);
+            }
+            alloc.reset();
+        })
+    });
+
+    group.bench_function(format!("allocate(u128) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<u128>() * NUM_ALLOCS);
+        b.iter(|| {
+            for _ in 0..NUM_ALLOCS {
+                let ptr = (&alloc).allocate(Layout::new::<u128>()).unwrap();
+                black_box(ptr);
+            }
+            alloc.reset();
+        })
+    });
+
+    // Choose some small, medium, and "large" lengths, as well as some prime
+    // numbers to see how the allocators deal with "unaligned" sizes.
+    for len in [0, 1, 7, 8, 31, 32] {
+        group.bench_function(format!("allocate([u8; {len}]) x {NUM_ALLOCS}"), |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<u8>() * len * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    // NB: black box the length but not the whole layout, since
+                    // that more accurately reflects things like `Vec` where the
+                    // element size (and therefore its alignment) is statically
+                    // known but the collection length is dynamic.
+                    let len = black_box(len);
+                    let layout = Layout::array::<u8>(len).unwrap();
+
+                    let ptr = (&alloc).allocate(layout).unwrap();
+                    black_box(ptr);
+                }
+                alloc.reset();
+            })
+        });
+    }
+
+    group.bench_function(
+        format!("grow same align (u32 -> [u32; 2]) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<[u32; 2]>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc).allocate(Layout::new::<u32>()).unwrap();
+                        let ptr = black_box(&alloc)
+                            .grow(ptr.cast(), Layout::new::<u32>(), Layout::new::<[u32; 2]>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.bench_function(
+        format!("grow smaller align (u32 -> [u16; 4]) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<[u16; 4]>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc).allocate(Layout::new::<u32>()).unwrap();
+                        let ptr = black_box(&alloc)
+                            .grow(ptr.cast(), Layout::new::<u32>(), Layout::new::<[u16; 4]>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.bench_function(
+        format!("grow larger align (u32 -> u64) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<u64>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc).allocate(Layout::new::<u32>()).unwrap();
+                        let ptr = black_box(&alloc)
+                            .grow(ptr.cast(), Layout::new::<u32>(), Layout::new::<u64>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.bench_function(
+        format!("shrink same align ([u32; 2] -> u32) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<u32>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc)
+                            .allocate(Layout::new::<[u32; 2]>())
+                            .unwrap();
+                        let ptr = black_box(&alloc)
+                            .shrink(ptr.cast(), Layout::new::<[u32; 2]>(), Layout::new::<u32>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.bench_function(
+        format!("shrink smaller align (u32 -> u16) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<u32>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc).allocate(Layout::new::<u32>()).unwrap();
+                        let ptr = black_box(&alloc)
+                            .shrink(ptr.cast(), Layout::new::<u32>(), Layout::new::<u16>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.bench_function(
+        format!("shrink larger align ([u16; 4] -> u32) x {NUM_ALLOCS}"),
+        |b| {
+            let mut alloc = A::with_capacity(mem::size_of::<[u16; 4]>() * NUM_ALLOCS);
+            b.iter(|| {
+                for _ in 0..NUM_ALLOCS {
+                    unsafe {
+                        let ptr = black_box(&alloc)
+                            .allocate(Layout::new::<[u16; 4]>())
+                            .unwrap();
+                        let ptr = black_box(&alloc)
+                            .shrink(ptr.cast(), Layout::new::<[u16; 4]>(), Layout::new::<u32>())
+                            .unwrap();
+                        black_box(ptr);
+                    }
+                }
+                alloc.reset();
+            })
+        },
+    );
+
+    group.finish();
+}
+
+fn bench_warm_up<A>(name: &str, c: &mut Criterion)
+where
+    for<'a> &'a A: Allocator,
+    A: BumpAllocator + Default,
+{
+    let mut group = c.benchmark_group(format!("warm-up/{name}"));
+
+    group.bench_function(format!("first u32 allocation"), |b| {
+        b.iter(|| {
+            let alloc = A::default();
+            let ptr = black_box(&alloc).allocate(Layout::new::<u32>()).unwrap();
+            black_box(ptr);
+        })
+    });
+
+    group.finish();
+}
+
+fn bench_reset<A>(name: &str, c: &mut Criterion)
+where
+    for<'a> &'a A: Allocator,
+    A: BumpAllocator + Default,
+{
+    let mut group = c.benchmark_group(format!("reset/{name}"));
+
+    group.bench_function(format!("reset after allocate(u32) x {NUM_ALLOCS}"), |b| {
+        b.iter_custom(move |iters| {
+            let mut duration = Duration::from_millis(0);
+
+            for _ in 0..iters {
+                // NB: do not use `with_capacity` here, we want to measure
+                // resetting with multiple internal bump chunks.
+                let mut alloc = A::default();
+
+                for _ in 0..NUM_ALLOCS {
+                    black_box((&alloc).allocate(Layout::new::<u32>()).unwrap());
+                }
+
+                let start = Instant::now();
+
+                alloc.reset();
+                black_box(&alloc);
+
+                duration += start.elapsed();
+            }
+
+            duration
+        });
+    });
+
+    group.finish();
+}
+
+fn bench_vec<A>(name: &str, c: &mut Criterion)
+where
+    for<'a> &'a A: Allocator,
+    A: BumpAllocator + Default,
+{
+    let mut group = c.benchmark_group(format!("vec/{name}"));
+
+    // Additional room because the vectors are going to potentially resize
+    // multiple times.
+    const RESIZE_FACTOR: usize = 10;
+
+    group.bench_function(format!("push(usize) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<usize>() * NUM_ALLOCS * RESIZE_FACTOR);
+        b.iter(|| {
+            let mut vec = Vec::new_in(&alloc);
+            for i in 0..NUM_ALLOCS {
+                vec.push(i);
+            }
+            drop(vec);
+            alloc.reset();
+        })
+    });
+
+    group.bench_function(format!("reserve_exact(1) x {NUM_ALLOCS}"), |b| {
+        let mut alloc = A::with_capacity(mem::size_of::<usize>() * NUM_ALLOCS * RESIZE_FACTOR);
+        b.iter(|| {
+            let mut vec = Vec::<u32, &A>::new_in(&alloc);
+            for i in 0..NUM_ALLOCS {
+                vec.reserve_exact(i);
+            }
+            drop(vec);
+            alloc.reset();
+        })
+    });
+
+    group.finish();
+}
+
+pub fn criterion_benchmark(c: &mut Criterion) {
+    bench_allocator_api::<Bumpalo>("bumpalo::Bump", c);
+    bench_allocator_api::<blink_alloc::BlinkAlloc>("blink_alloc::BlinkAlloc", c);
+    bench_allocator_api::<SystemAlloc>("std::alloc::System", c);
+
+    bench_warm_up::<Bumpalo>("bumpalo::Bump", c);
+    bench_warm_up::<blink_alloc::BlinkAlloc>("blink_alloc::BlinkAlloc", c);
+    bench_warm_up::<SystemAlloc>("std::alloc::System", c);
+
+    bench_reset::<Bumpalo>("bumpalo::Bump", c);
+    bench_reset::<blink_alloc::BlinkAlloc>("blink_alloc::BlinkAlloc", c);
+    bench_reset::<SystemAlloc>("std::alloc::System", c);
+
+    bench_vec::<Bumpalo>("bumpalo::Bump", c);
+    bench_vec::<blink_alloc::BlinkAlloc>("blink_alloc::BlinkAlloc", c);
+    bench_vec::<SystemAlloc>("std::alloc::System", c);
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
diff --git a/benches/tables.toml b/benches/tables.toml
new file mode 100644
index 0000000..a0f8c49
--- /dev/null
+++ b/benches/tables.toml
@@ -0,0 +1,113 @@
+[top_comments]
+
+Overview = """
+This directory contains two suites of benchmarks:
+
+1. `allocator_api.rs`: `std::alloc::Allocator`-based benchmarks that aim to
+   measure the performance of bump allocators within the generic `Allocator`
+   API.
+
+2. `benches.rs`: Miscellaneous Bumpalo-specific benchmarks.
+
+The tables of benchmark results listed below are the results for the suite of
+`std::alloc::Allocator`-based benchmarks. They are originally adapted from
+[`blink-alloc`] (another fine bump allocator crate) which was already measuring
+the relative performance between `blink-alloc` and `bumpalo`. I wasn't able to
+reproduce many of their results showing , which was part of the motivation to bring a
+subset of them into this repo and document reproduction.
+
+Furthermore, the tables below include a `std::alloc::System` column, but their
+results come with a few caveats. First, in order to implement a `reset` method
+for the system allocator and deallocate everything that was allocated within a
+certain region of code, I had to add additional bookkeeping to dynamically track
+every live allocation. That bookkeeping generally won't be present in real
+programs, which will instead use things like `Drop` implementations, so it makes
+the system allocator's results look worse than they otherwise would
+be. Additionally, these benchmarks are really designed to show off the strengths
+of bump allocators and measure the operations that are important for bump
+allocators. The system allocator is expected to perform worse, but that's
+because it is designed for general purpose scenarios, where as bump allocators
+are designed for very specific scenarios. These columns should mostly serve as
+just a general reference point to get an idea of the magnitude of allocation
+speed up you can expect in the very specific scenarios where using a bump
+allocator makes sense.
+
+Finally, all these benchmarks are synthetic. They are micro benchmarks. You
+shouldn't expect that anything here will directly translate into speed ups for
+your application. Application performance is what really matters, and things
+observed in the micro often disappear in the macro. If your application isn't
+bottlenecked on allocation, and can't live with the constraints a bump allocator
+imposes, there's nothing that a bump allocator can do to help you.
+
+[`blink-alloc`]: https://github.com/zakarumych/blink-alloc/blob/845b2db273371260eef2e9858386f6c6aa180e98/BENCHMARKS.md
+"""
+
+Reproducing = """
+The `std::alloc::Allocator`-based benchmarks require using nightly Rust, since
+the `Allocator` trait is still unstable. You must additionally enable Bumpalo's
+`allocator_api` cargo feature:
+
+```
+$ cargo +nightly bench --bench allocator_api --features allocator_api
+```
+
+The miscellaneous benchmarks require Bumpalo's `collections` cargo feature:
+
+```
+$ cargo bench --bench benches --features collections
+```
+
+To update the tables below, use `cargo-criterion` and [`criterion-table`]:
+
+```
+$ cd bumpalo/benches/
+$ cargo +nightly bench --features allocator_api \\
+    --bench allocator_api \\
+    --message-format=json \\
+    > results.json
+$ criterion-table < results.json > README.md
+```
+
+[`cargo-criterion`]: https://github.com/bheisler/cargo-criterion
+[`criterion-table`]: https://github.com/nu11ptr/criterion-table
+"""
+
+[table_comments]
+
+allocator-api = """
+Benchmarks that measure calls into `std::alloc::Allocator` methods directly.
+
+These operations are generally the ones that happen most often, and therefore
+their performance is generally most important. Following the same logic, raw
+allocation is generally the very most important.
+"""
+
+warm-up = """
+Benchmarks that measure the first allocation in a fresh allocator.
+
+These aren't generally very important, since the first allocation in a fresh
+bump allocator only ever happens once by definition. This is mostly measuring
+how long it takes the underlying system allocator to allocate the initial chunk
+to bump allocate out of.
+"""
+
+reset = """
+Benchmarks that measure the overhead of resetting a bump allocator to an empty
+state, ready to be reused in a new program phase.
+
+This generally doesn't happen as often as allocation, and therefore is generally
+less important, but it is important to keep an eye on generally since
+deallocation-en-masse and reusing already-allocated chunks can be selling points
+for bump allocation over using a generic allocator in certain scenarios.
+"""
+
+vec = """
+Benchmarks that measure the various `std::vec::Vec<T> operations when used in
+conjuction with a bump allocator.
+
+Bump allocators aren't often used directly, but instead through some sort of
+collection. These benchmarks are important in the sense that the standard
+`Vec<T>` type is probably the most-commonly used collection (although not
+necessarily the most commonly used with bump allocators in Rust, at least until
+the `Allocator` trait is stabilized).
+"""
diff --git a/src/lib.rs b/src/lib.rs
index 28e8351..61f140a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,6 +15,7 @@ pub mod collections;
 mod alloc;
 
 use core::cell::Cell;
+use core::cmp::Ordering;
 use core::fmt::Display;
 use core::iter;
 use core::marker::PhantomData;
@@ -287,9 +288,8 @@ impl<E: Display> Display for AllocOrInitError<E> {
 /// Because of backwards compatibility, allocations that fail
 /// due to allocation limits will not present differently than
 /// errors due to resource exhaustion.
-
 #[derive(Debug)]
-pub struct Bump {
+pub struct Bump<const MIN_ALIGN: usize = 1> {
     // The current chunk we are bump allocating within.
     current_chunk_footer: Cell<NonNull<ChunkFooter>>,
     allocation_limit: Cell<Option<usize>>,
@@ -377,13 +377,13 @@ impl ChunkFooter {
     }
 }
 
-impl Default for Bump {
-    fn default() -> Bump {
-        Bump::new()
+impl<const MIN_ALIGN: usize> Default for Bump<MIN_ALIGN> {
+    fn default() -> Self {
+        Self::with_min_align()
     }
 }
 
-impl Drop for Bump {
+impl<const MIN_ALIGN: usize> Drop for Bump<MIN_ALIGN> {
     fn drop(&mut self) {
         unsafe {
             dealloc_chunk_list(self.current_chunk_footer.get());
@@ -404,7 +404,7 @@ unsafe fn dealloc_chunk_list(mut footer: NonNull<ChunkFooter>) {
 // chunks until you start allocating from it. But by the time you allocate from
 // it, the returned references to allocations borrow the `Bump` and therefore
 // prevent sending the `Bump` across threads until the borrows end.
-unsafe impl Send for Bump {}
+unsafe impl<const MIN_ALIGN: usize> Send for Bump<MIN_ALIGN> {}
 
 #[inline]
 fn is_pointer_aligned_to<T>(pointer: *mut T, align: usize) -> bool {
@@ -416,10 +416,26 @@ fn is_pointer_aligned_to<T>(pointer: *mut T, align: usize) -> bool {
 }
 
 #[inline]
-pub(crate) fn round_up_to(n: usize, divisor: usize) -> Option<usize> {
+pub(crate) const fn round_up_to(n: usize, divisor: usize) -> Option<usize> {
     debug_assert!(divisor > 0);
     debug_assert!(divisor.is_power_of_two());
-    Some(n.checked_add(divisor - 1)? & !(divisor - 1))
+    match n.checked_add(divisor - 1) {
+        Some(x) => Some(x & !(divisor - 1)),
+        None => None,
+    }
+}
+
+/// Like `round_up_to` but turns overflow into undefined behavior rather than
+/// returning `None`.
+#[inline]
+pub(crate) unsafe fn round_up_to_unchecked(n: usize, divisor: usize) -> usize {
+    match round_up_to(n, divisor) {
+        Some(x) => x,
+        None => {
+            debug_assert!(false, "round_up_to_unchecked failed");
+            core::hint::unreachable_unchecked()
+        }
+    }
 }
 
 #[inline]
@@ -437,17 +453,33 @@ pub(crate) fn round_mut_ptr_down_to(ptr: *mut u8, divisor: usize) -> *mut u8 {
     ptr.wrapping_sub(ptr as usize & (divisor - 1))
 }
 
-// After this point, we try to hit page boundaries instead of powers of 2
-const PAGE_STRATEGY_CUTOFF: usize = 0x1000;
+#[inline]
+pub(crate) unsafe fn round_mut_ptr_up_to_unchecked(ptr: *mut u8, divisor: usize) -> *mut u8 {
+    debug_assert!(divisor > 0);
+    debug_assert!(divisor.is_power_of_two());
+    let aligned = round_up_to_unchecked(ptr as usize, divisor);
+    let delta = aligned - (ptr as usize);
+    ptr.add(delta)
+}
+
+// The typical page size these days.
+//
+// Note that we don't need to exactly match page size for correctness, and it is
+// okay if this is smaller than the real page size in practice. It isn't worth
+// the portability concerns and lack of const propagation that dynamically
+// looking up the actual page size implies.
+const TYPICAL_PAGE_SIZE: usize = 0x1000;
 
 // We only support alignments of up to 16 bytes for iter_allocated_chunks.
 const SUPPORTED_ITER_ALIGNMENT: usize = 16;
 const CHUNK_ALIGN: usize = SUPPORTED_ITER_ALIGNMENT;
 const FOOTER_SIZE: usize = mem::size_of::<ChunkFooter>();
 
-// Assert that ChunkFooter is at most the supported alignment. This will give a compile time error if it is not the case
-const _FOOTER_ALIGN_ASSERTION: bool = mem::align_of::<ChunkFooter>() <= CHUNK_ALIGN;
-const _: [(); _FOOTER_ALIGN_ASSERTION as usize] = [()];
+// Assert that `ChunkFooter` is at most the supported alignment. This will give a
+// compile time error if it is not the case
+const _FOOTER_ALIGN_ASSERTION: () = {
+    assert!(mem::align_of::<ChunkFooter>() <= CHUNK_ALIGN);
+};
 
 // Maximum typical overhead per allocation imposed by allocators.
 const MALLOC_OVERHEAD: usize = 16;
@@ -458,16 +490,19 @@ const MALLOC_OVERHEAD: usize = 16;
 // after adding a footer, malloc overhead and alignment, the chunk of memory
 // the allocator actually sets aside for us is X+OVERHEAD rounded up to the
 // nearest suitable size boundary.
-const OVERHEAD: usize = (MALLOC_OVERHEAD + FOOTER_SIZE + (CHUNK_ALIGN - 1)) & !(CHUNK_ALIGN - 1);
+const OVERHEAD: usize = match round_up_to(MALLOC_OVERHEAD + FOOTER_SIZE, CHUNK_ALIGN) {
+    Some(x) => x,
+    None => panic!(),
+};
 
-// Choose a relatively small default initial chunk size, since we double chunk
-// sizes as we grow bump arenas to amortize costs of hitting the global
-// allocator.
+// The target size of our first allocation, including our overhead. The
+// available bump capacity will be smaller.
 const FIRST_ALLOCATION_GOAL: usize = 1 << 9;
 
-// The actual size of the first allocation is going to be a bit smaller
-// than the goal. We need to make room for the footer, and we also need
-// take the alignment into account.
+// The actual size of the first allocation is going to be a bit smaller than the
+// goal. We need to make room for the footer, and we also need take the
+// alignment into account. We're trying to avoid this kind of situation:
+// https://blog.mozilla.org/nnethercote/2011/08/05/clownshoes-available-in-sizes-2101-and-up/
 const DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER: usize = FIRST_ALLOCATION_GOAL - OVERHEAD;
 
 /// The memory size and alignment details for a potential new chunk
@@ -485,12 +520,18 @@ fn layout_from_size_align(size: usize, align: usize) -> Result<Layout, AllocErr>
     Layout::from_size_align(size, align).map_err(|_| AllocErr)
 }
 
+#[cold]
 #[inline(never)]
 fn allocation_size_overflow<T>() -> T {
     panic!("requested allocation size overflowed")
 }
 
-impl Bump {
+// NB: We don't have constructors as methods on `impl<N> Bump<N>` that return
+// `Self` because then `rustc` can't infer the `N` if it isn't explicitly
+// provided, even though it has a default value. There doesn't seem to be a good
+// workaround, other than putting constructors on the `Bump<DEFAULT>`; even
+// `std` does this same thing with `HashMap`, for example.
+impl Bump<1> {
     /// Construct a new arena to bump allocate into.
     ///
     /// ## Example
@@ -499,7 +540,7 @@ impl Bump {
     /// let bump = bumpalo::Bump::new();
     /// # let _ = bump;
     /// ```
-    pub fn new() -> Bump {
+    pub fn new() -> Self {
         Self::with_capacity(0)
     }
 
@@ -511,11 +552,12 @@ impl Bump {
     /// let bump = bumpalo::Bump::try_new();
     /// # let _ = bump.unwrap();
     /// ```
-    pub fn try_new() -> Result<Bump, AllocErr> {
+    pub fn try_new() -> Result<Self, AllocErr> {
         Bump::try_with_capacity(0)
     }
 
-    /// Construct a new arena with the specified byte capacity to bump allocate into.
+    /// Construct a new arena with the specified byte capacity to bump allocate
+    /// into.
     ///
     /// ## Example
     ///
@@ -523,19 +565,156 @@ impl Bump {
     /// let bump = bumpalo::Bump::with_capacity(100);
     /// # let _ = bump;
     /// ```
-    pub fn with_capacity(capacity: usize) -> Bump {
-        Bump::try_with_capacity(capacity).unwrap_or_else(|_| oom())
+    ///
+    /// ## Panics
+    ///
+    /// Panics if allocating the initial capacity fails.
+    pub fn with_capacity(capacity: usize) -> Self {
+        Self::try_with_capacity(capacity).unwrap_or_else(|_| oom())
     }
 
-    /// Attempt to construct a new arena with the specified byte capacity to bump allocate into.
+    /// Attempt to construct a new arena with the specified byte capacity to
+    /// bump allocate into.
+    ///
+    /// Propagates errors when allocating the initial capacity.
     ///
     /// ## Example
     ///
     /// ```
-    /// let bump = bumpalo::Bump::try_with_capacity(100);
-    /// # let _ = bump.unwrap();
+    /// # fn _foo() -> Result<(), bumpalo::AllocErr> {
+    /// let bump = bumpalo::Bump::try_with_capacity(100)?;
+    /// # let _ = bump;
+    /// # Ok(())
+    /// # }
     /// ```
     pub fn try_with_capacity(capacity: usize) -> Result<Self, AllocErr> {
+        Self::try_with_min_align_and_capacity(capacity)
+    }
+}
+
+impl<const MIN_ALIGN: usize> Bump<MIN_ALIGN> {
+    /// Create a new `Bump` that enforces a minimum alignment.
+    ///
+    /// The minimum alignment must be a power of two and no larger than `16`.
+    ///
+    /// Enforcing a minimum alignment can speed up allocation of objects with
+    /// alignment less than or equal to the minimum alignment. This comes at the
+    /// cost of introducing otherwise-unnecessary padding between allocations of
+    /// objects with alignment less than the minimum.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// type BumpAlign8 = bumpalo::Bump<8>;
+    /// let bump = BumpAlign8::with_min_align();
+    /// for x in 0..u8::MAX {
+    ///     let x = bump.alloc(x);
+    ///     assert_eq!((x as *mut _ as usize) % 8, 0, "x is aligned to 8");
+    /// }
+    /// ```
+    ///
+    /// # Panics
+    ///
+    /// Panics on invalid minimum alignments.
+    //
+    // Because of `rustc`'s poor type inference for default type/const
+    // parameters (see the comment above the `impl Bump` block with no const
+    // `MIN_ALIGN` parameter) and because we don't want to force everyone to
+    // specify a minimum alignment with `Bump::new()` et al, we have a separate
+    // constructor for specifying the minimum alignment.
+    pub fn with_min_align() -> Self {
+        assert!(
+            MIN_ALIGN.is_power_of_two(),
+            "MIN_ALIGN must be a power of two; found {MIN_ALIGN}"
+        );
+        assert!(
+            MIN_ALIGN <= CHUNK_ALIGN,
+            "MIN_ALIGN may not be larger than {CHUNK_ALIGN}; found {MIN_ALIGN}"
+        );
+
+        Bump {
+            current_chunk_footer: Cell::new(EMPTY_CHUNK.get()),
+            allocation_limit: Cell::new(None),
+        }
+    }
+
+    /// Create a new `Bump` that enforces a minimum alignment and starts with
+    /// room for at least `capacity` bytes.
+    ///
+    /// The minimum alignment must be a power of two and no larger than `16`.
+    ///
+    /// Enforcing a minimum alignment can speed up allocation of objects with
+    /// alignment less than or equal to the minimum alignment. This comes at the
+    /// cost of introducing otherwise-unnecessary padding between allocations of
+    /// objects with alignment less than the minimum.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// type BumpAlign8 = bumpalo::Bump<8>;
+    /// let mut bump = BumpAlign8::with_min_align_and_capacity(8 * 100);
+    /// for x in 0..100_u64 {
+    ///     let x = bump.alloc(x);
+    ///     assert_eq!((x as *mut _ as usize) % 8, 0, "x is aligned to 8");
+    /// }
+    /// assert_eq!(
+    ///     bump.iter_allocated_chunks().count(), 1,
+    ///     "initial chunk had capacity for all allocations",
+    /// );
+    /// ```
+    ///
+    /// # Panics
+    ///
+    /// Panics on invalid minimum alignments.
+    ///
+    /// Panics if allocating the initial capacity fails.
+    pub fn with_min_align_and_capacity(capacity: usize) -> Self {
+        Self::try_with_min_align_and_capacity(capacity).unwrap_or_else(|_| oom())
+    }
+
+    /// Create a new `Bump` that enforces a minimum alignment and starts with
+    /// room for at least `capacity` bytes.
+    ///
+    /// The minimum alignment must be a power of two and no larger than `16`.
+    ///
+    /// Enforcing a minimum alignment can speed up allocation of objects with
+    /// alignment less than or equal to the minimum alignment. This comes at the
+    /// cost of introducing otherwise-unnecessary padding between allocations of
+    /// objects with alignment less than the minimum.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # fn _foo() -> Result<(), bumpalo::AllocErr> {
+    /// type BumpAlign8 = bumpalo::Bump<8>;
+    /// let mut bump = BumpAlign8::try_with_min_align_and_capacity(8 * 100)?;
+    /// for x in 0..100_u64 {
+    ///     let x = bump.alloc(x);
+    ///     assert_eq!((x as *mut _ as usize) % 8, 0, "x is aligned to 8");
+    /// }
+    /// assert_eq!(
+    ///     bump.iter_allocated_chunks().count(), 1,
+    ///     "initial chunk had capacity for all allocations",
+    /// );
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Panics
+    ///
+    /// Panics on invalid minimum alignments.
+    ///
+    /// Panics if allocating the initial capacity fails.
+    pub fn try_with_min_align_and_capacity(capacity: usize) -> Result<Self, AllocErr> {
+        assert!(
+            MIN_ALIGN.is_power_of_two(),
+            "MIN_ALIGN must be a power of two; found {MIN_ALIGN}"
+        );
+        assert!(
+            MIN_ALIGN <= CHUNK_ALIGN,
+            "MIN_ALIGN may not be larger than {CHUNK_ALIGN}; found {MIN_ALIGN}"
+        );
+
         if capacity == 0 {
             return Ok(Bump {
                 current_chunk_footer: Cell::new(EMPTY_CHUNK.get()),
@@ -543,11 +722,11 @@ impl Bump {
             });
         }
 
-        let layout = layout_from_size_align(capacity, 1)?;
+        let layout = layout_from_size_align(capacity, MIN_ALIGN)?;
 
         let chunk_footer = unsafe {
             Self::new_chunk(
-                Bump::new_chunk_memory_details(None, layout).ok_or(AllocErr)?,
+                Self::new_chunk_memory_details(None, layout).ok_or(AllocErr)?,
                 layout,
                 EMPTY_CHUNK.get(),
             )
@@ -560,6 +739,24 @@ impl Bump {
         })
     }
 
+    /// Get this bump arena's minimum alignment.
+    ///
+    /// All objects allocated in this arena get aligned to this value.
+    ///
+    /// ## Example
+    ///
+    /// ```
+    /// let bump2 = bumpalo::Bump::<2>::with_min_align();
+    /// assert_eq!(bump2.min_align(), 2);
+    ///
+    /// let bump4 = bumpalo::Bump::<4>::with_min_align();
+    /// assert_eq!(bump4.min_align(), 4);
+    /// ```
+    #[inline]
+    pub fn min_align(&self) -> usize {
+        MIN_ALIGN
+    }
+
     /// The allocation limit for this arena in bytes.
     ///
     /// ## Example
@@ -626,39 +823,39 @@ impl Bump {
             .unwrap_or(true)
     }
 
-    /// Determine the memory details including final size, alignment and
-    /// final size without footer for a new chunk that would be allocated
-    /// to fulfill an allocation request.
+    /// Determine the memory details including final size, alignment and final
+    /// size without footer for a new chunk that would be allocated to fulfill
+    /// an allocation request.
     fn new_chunk_memory_details(
         new_size_without_footer: Option<usize>,
         requested_layout: Layout,
     ) -> Option<NewChunkMemoryDetails> {
+        // We must have `CHUNK_ALIGN` or better alignment...
+        let align = CHUNK_ALIGN
+            // and we have to have at least our configured minimum alignment...
+            .max(MIN_ALIGN)
+            // and make sure we satisfy the requested allocation's alignment.
+            .max(requested_layout.align());
+
         let mut new_size_without_footer =
             new_size_without_footer.unwrap_or(DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
 
-        // We want to have CHUNK_ALIGN or better alignment
-        let mut align = CHUNK_ALIGN;
-
-        // If we already know we need to fulfill some request,
-        // make sure we allocate at least enough to satisfy it
-        align = align.max(requested_layout.align());
         let requested_size =
             round_up_to(requested_layout.size(), align).unwrap_or_else(allocation_size_overflow);
         new_size_without_footer = new_size_without_footer.max(requested_size);
 
-        // We want our allocations to play nice with the memory allocator,
-        // and waste as little memory as possible.
-        // For small allocations, this means that the entire allocation
-        // including the chunk footer and mallocs internal overhead is
-        // as close to a power of two as we can go without going over.
-        // For larger allocations, we only need to get close to a page
-        // boundary without going over.
-        if new_size_without_footer < PAGE_STRATEGY_CUTOFF {
+        // We want our allocations to play nice with the memory allocator, and
+        // waste as little memory as possible. For small allocations, this means
+        // that the entire allocation including the chunk footer and mallocs
+        // internal overhead is as close to a power of two as we can go without
+        // going over. For larger allocations, we only need to get close to a
+        // page boundary without going over.
+        if new_size_without_footer < TYPICAL_PAGE_SIZE {
             new_size_without_footer =
                 (new_size_without_footer + OVERHEAD).next_power_of_two() - OVERHEAD;
         } else {
             new_size_without_footer =
-                round_up_to(new_size_without_footer + OVERHEAD, 0x1000)? - OVERHEAD;
+                round_up_to(new_size_without_footer + OVERHEAD, TYPICAL_PAGE_SIZE)? - OVERHEAD;
         }
 
         debug_assert_eq!(align % CHUNK_ALIGN, 0);
@@ -703,9 +900,24 @@ impl Bump {
         debug_assert_eq!(footer_ptr as usize % CHUNK_ALIGN, 0);
         let footer_ptr = footer_ptr as *mut ChunkFooter;
 
-        // The bump pointer is initialized to the end of the range we will
-        // bump out of.
-        let ptr = Cell::new(NonNull::new_unchecked(footer_ptr as *mut u8));
+        // The bump pointer is initialized to the end of the range we will bump
+        // out of, rounded down to the minimum alignment. It is the
+        // `NewChunkMemoryDetails` constructor's responsibility to ensure that
+        // even after this rounding we have enough non-zero capacity in the
+        // chunk.
+        let ptr = round_mut_ptr_down_to(footer_ptr.cast::<u8>(), MIN_ALIGN);
+        debug_assert_eq!(ptr as usize % MIN_ALIGN, 0);
+        debug_assert!(
+            data.as_ptr() < ptr,
+            "bump pointer {ptr:#p} should still be greater than or equal to the \
+             start of the bump chunk {data:#p}"
+        );
+        debug_assert_eq!(
+            (ptr as usize) - (data.as_ptr() as usize),
+            new_size_without_footer
+        );
+
+        let ptr = Cell::new(NonNull::new_unchecked(ptr));
 
         // The `allocated_bytes` of a new chunk counts the total size
         // of the chunks, not how much of the chunks are used.
@@ -771,6 +983,10 @@ impl Bump {
             dealloc_chunk_list(prev_chunk);
 
             // Reset the bump finger to the end of the chunk.
+            debug_assert!(
+                is_pointer_aligned_to(cur_chunk.as_ptr(), MIN_ALIGN),
+                "bump pointer {cur_chunk:#p} should be aligned to the minimum alignment of {MIN_ALIGN:#x}"
+            );
             cur_chunk.as_ref().ptr.set(cur_chunk.cast());
 
             // Reset the allocated size of the chunk.
@@ -1675,27 +1891,92 @@ impl Bump {
         // modulo alignment. This keeps the fast path optimized for non-ZSTs,
         // which are much more common.
         unsafe {
-            let footer = self.current_chunk_footer.get();
-            let footer = footer.as_ref();
+            let footer_ptr = self.current_chunk_footer.get();
+            let footer = footer_ptr.as_ref();
+
             let ptr = footer.ptr.get().as_ptr();
             let start = footer.data.as_ptr();
-            debug_assert!(start <= ptr);
-            debug_assert!(ptr as *const u8 <= footer as *const _ as *const u8);
+            debug_assert!(
+                start <= ptr,
+                "start pointer {start:#p} should be less than or equal to bump pointer {ptr:#p}"
+            );
+            debug_assert!(
+                ptr <= footer_ptr.cast::<u8>().as_ptr(),
+                "bump pointer {ptr:#p} should be less than or equal to footer pointer {footer_ptr:#p}"
+            );
+            debug_assert!(
+                is_pointer_aligned_to(ptr, MIN_ALIGN),
+                "bump pointer {ptr:#p} should be aligned to the minimum alignment of {MIN_ALIGN:#x}"
+            );
 
-            if (ptr as usize) < layout.size() {
-                return None;
-            }
+            // This `match` should be boiled away by LLVM: `MIN_ALIGN` is a
+            // constant and the layout's alignment is also constant in practice
+            // after inlining.
+            let aligned_ptr = match layout.align().cmp(&MIN_ALIGN) {
+                Ordering::Less => {
+                    // We need to round the size up to a multiple of `MIN_ALIGN`
+                    // to preserve the minimum alignment. This might overflow
+                    // since we cannot rely on `Layout`'s guarantees.
+                    let aligned_size = round_up_to(layout.size(), MIN_ALIGN)?;
+
+                    let capacity = (ptr as usize) - (start as usize);
+                    if aligned_size > capacity {
+                        return None;
+                    }
 
-            let ptr = ptr.wrapping_sub(layout.size());
-            let aligned_ptr = round_mut_ptr_down_to(ptr, layout.align());
+                    ptr.wrapping_sub(aligned_size)
+                }
+                Ordering::Equal => {
+                    // `Layout` guarantees that rounding the size up to its
+                    // align cannot overflow (but does not guarantee that the
+                    // size is initially a multiple of the alignment, which is
+                    // why we need to do this rounding).
+                    let aligned_size = round_up_to_unchecked(layout.size(), layout.align());
+
+                    let capacity = (ptr as usize) - (start as usize);
+                    if aligned_size > capacity {
+                        return None;
+                    }
 
-            if aligned_ptr >= start {
-                let aligned_ptr = NonNull::new_unchecked(aligned_ptr);
-                footer.ptr.set(aligned_ptr);
-                Some(aligned_ptr)
-            } else {
-                None
-            }
+                    ptr.wrapping_sub(aligned_size)
+                }
+                Ordering::Greater => {
+                    // `Layout` guarantees that rounding the size up to its
+                    // align cannot overflow (but does not guarantee that the
+                    // size is initially a multiple of the alignment, which is
+                    // why we need to do this rounding).
+                    let aligned_size = round_up_to_unchecked(layout.size(), layout.align());
+
+                    let aligned_ptr = round_mut_ptr_down_to(ptr, layout.align());
+                    let capacity = (aligned_ptr as usize).wrapping_sub(start as usize);
+                    if aligned_ptr < start || aligned_size > capacity {
+                        return None;
+                    }
+
+                    aligned_ptr.wrapping_sub(aligned_size)
+                }
+            };
+
+            debug_assert!(
+                is_pointer_aligned_to(aligned_ptr, layout.align()),
+                "pointer {aligned_ptr:#p} should be aligned to layout alignment of {:#}",
+                layout.align()
+            );
+            debug_assert!(
+                is_pointer_aligned_to(aligned_ptr, MIN_ALIGN),
+                "pointer {aligned_ptr:#p} should be aligned to minimum alignment of {:#}",
+                MIN_ALIGN
+            );
+            debug_assert!(
+                start <= aligned_ptr && aligned_ptr <= ptr,
+                "pointer {aligned_ptr:#p} should be in range {start:#p}..{ptr:#p}"
+            );
+
+            debug_assert!(!aligned_ptr.is_null());
+            let aligned_ptr = NonNull::new_unchecked(aligned_ptr);
+
+            footer.ptr.set(aligned_ptr);
+            Some(aligned_ptr)
         }
     }
 
@@ -1724,7 +2005,6 @@ impl Bump {
     #[cold]
     fn alloc_layout_slow(&self, layout: Layout) -> Option<NonNull<u8>> {
         unsafe {
-            let size = layout.size();
             let allocation_limit_remaining = self.allocation_limit_remaining();
 
             // Get a new chunk from the global allocator.
@@ -1748,7 +2028,7 @@ impl Bump {
                 if base_size >= min_new_chunk_size || bypass_min_chunk_size_for_small_limits {
                     let size = base_size;
                     base_size /= 2;
-                    Bump::new_chunk_memory_details(Some(size), layout)
+                    Self::new_chunk_memory_details(Some(size), layout)
                 } else {
                     None
                 }
@@ -1756,11 +2036,11 @@ impl Bump {
 
             let new_footer = chunk_memory_details
                 .filter_map(|chunk_memory_details| {
-                    if Bump::chunk_fits_under_limit(
+                    if Self::chunk_fits_under_limit(
                         allocation_limit_remaining,
                         chunk_memory_details,
                     ) {
-                        Bump::new_chunk(chunk_memory_details, layout, current_footer)
+                        Self::new_chunk(chunk_memory_details, layout, current_footer)
                     } else {
                         None
                     }
@@ -1775,25 +2055,11 @@ impl Bump {
             // Set the new chunk as our new current chunk.
             self.current_chunk_footer.set(new_footer);
 
-            let new_footer = new_footer.as_ref();
-
-            // Move the bump ptr finger down to allocate room for `val`. We know
-            // this can't overflow because we successfully allocated a chunk of
-            // at least the requested size.
-            let mut ptr = new_footer.ptr.get().as_ptr().sub(size);
-            // Round the pointer down to the requested alignment.
-            ptr = round_mut_ptr_down_to(ptr, layout.align());
-            debug_assert!(
-                ptr as *const _ <= new_footer,
-                "{:p} <= {:p}",
-                ptr,
-                new_footer
-            );
-            let ptr = NonNull::new_unchecked(ptr);
-            new_footer.ptr.set(ptr);
-
-            // Return a pointer to the freshly allocated region in this chunk.
-            Some(ptr)
+            // And then we can rely on `tray_alloc_layout_fast` to allocate
+            // space within this chunk.
+            let ptr = self.try_alloc_layout_fast(layout);
+            debug_assert!(ptr.is_some());
+            ptr
         }
     }
 
@@ -1880,8 +2146,8 @@ impl Bump {
     ///     assert_eq!(chunk[2].assume_init(), b'a');
     /// }
     /// ```
-    pub fn iter_allocated_chunks(&mut self) -> ChunkIter<'_> {
-        // SAFE: Ensured by mutable borrow of `self`.
+    pub fn iter_allocated_chunks(&mut self) -> ChunkIter<'_, MIN_ALIGN> {
+        // Safety: Ensured by mutable borrow of `self`.
         let raw = unsafe { self.iter_allocated_chunks_raw() };
         ChunkIter {
             raw,
@@ -1906,7 +2172,7 @@ impl Bump {
     ///
     /// In addition, all of the caveats when reading the chunk data from
     /// [`iter_allocated_chunks()`](Bump::iter_allocated_chunks) still apply.
-    pub unsafe fn iter_allocated_chunks_raw(&self) -> ChunkRawIter<'_> {
+    pub unsafe fn iter_allocated_chunks_raw(&self) -> ChunkRawIter<'_, MIN_ALIGN> {
         ChunkRawIter {
             footer: self.current_chunk_footer.get(),
             bump: PhantomData,
@@ -1964,7 +2230,14 @@ impl Bump {
         // otherwise they are simply leaked -- at least until somebody calls reset().
         if self.is_last_allocation(ptr) {
             let ptr = self.current_chunk_footer.get().as_ref().ptr.get();
-            let ptr = NonNull::new_unchecked(ptr.as_ptr().add(layout.size()));
+            let ptr = ptr.as_ptr().add(layout.size());
+
+            let ptr = round_mut_ptr_up_to_unchecked(ptr, MIN_ALIGN);
+            debug_assert!(
+                is_pointer_aligned_to(ptr, MIN_ALIGN),
+                "bump pointer {ptr:#p} should be aligned to the minimum alignment of {MIN_ALIGN:#x}"
+            );
+            let ptr = NonNull::new_unchecked(ptr);
             self.current_chunk_footer.get().as_ref().ptr.set(ptr);
         }
     }
@@ -1985,16 +2258,20 @@ impl Bump {
         // 2. the pointer is not aligned to the new layout's demanded alignment,
         //    and we are unlucky.
         //
-        // In the case of (2), to successfully "shrink" the allocation, we would
-        // have to allocate a whole new region for the new layout, without being
-        // able to free the old region. That is unacceptable, so simply return
-        // an allocation failure error instead.
+        // In the case of (2), to successfully "shrink" the allocation, we have
+        // to allocate a whole new region for the new layout.
         if old_layout.align() < new_layout.align() {
-            if is_pointer_aligned_to(ptr.as_ptr(), new_layout.align()) {
-                return Ok(ptr);
+            return if is_pointer_aligned_to(ptr.as_ptr(), new_layout.align()) {
+                Ok(ptr)
             } else {
-                return Err(AllocErr);
-            }
+                let new_ptr = self.try_alloc_layout(new_layout)?;
+
+                // We know that these regions are nonoverlapping because
+                // `new_ptr` is a fresh allocation.
+                ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), new_layout.size());
+
+                Ok(new_ptr)
+            };
         }
 
         debug_assert!(is_pointer_aligned_to(ptr.as_ptr(), new_layout.align()));
@@ -2004,7 +2281,7 @@ impl Bump {
 
         // This is how much space we would *actually* reclaim while satisfying
         // the requested alignment.
-        let delta = round_down_to(old_size - new_size, new_layout.align());
+        let delta = round_down_to(old_size - new_size, new_layout.align().max(MIN_ALIGN));
 
         if self.is_last_allocation(ptr)
                 // Only reclaim the excess space (which requires a copy) if it
@@ -2043,6 +2320,10 @@ impl Bump {
             // NB: new_ptr is aligned, because ptr *has to* be aligned, and we
             // made sure delta is aligned.
             let new_ptr = NonNull::new_unchecked(footer.ptr.get().as_ptr().add(delta));
+            debug_assert!(
+                is_pointer_aligned_to(new_ptr.as_ptr(), MIN_ALIGN),
+                "bump pointer {new_ptr:#p} should be aligned to the minimum alignment of {MIN_ALIGN:#x}"
+            );
             footer.ptr.set(new_ptr);
 
             // NB: we know it is non-overlapping because of the size check
@@ -2065,7 +2346,10 @@ impl Bump {
         new_layout: Layout,
     ) -> Result<NonNull<u8>, AllocErr> {
         let old_size = old_layout.size();
+
         let new_size = new_layout.size();
+        let new_size = round_up_to(new_size, MIN_ALIGN).ok_or(AllocErr)?;
+
         let align_is_compatible = old_layout.align() >= new_layout.align();
 
         if align_is_compatible && self.is_last_allocation(ptr) {
@@ -2102,14 +2386,15 @@ impl Bump {
 /// [`Bump`]: struct.Bump.html
 /// [`iter_allocated_chunks`]: struct.Bump.html#method.iter_allocated_chunks
 #[derive(Debug)]
-pub struct ChunkIter<'a> {
-    raw: ChunkRawIter<'a>,
+pub struct ChunkIter<'a, const MIN_ALIGN: usize = 1> {
+    raw: ChunkRawIter<'a, MIN_ALIGN>,
     bump: PhantomData<&'a mut Bump>,
 }
 
-impl<'a> Iterator for ChunkIter<'a> {
+impl<'a, const MIN_ALIGN: usize> Iterator for ChunkIter<'a, MIN_ALIGN> {
     type Item = &'a [mem::MaybeUninit<u8>];
-    fn next(&mut self) -> Option<&'a [mem::MaybeUninit<u8>]> {
+
+    fn next(&mut self) -> Option<Self::Item> {
         unsafe {
             let (ptr, len) = self.raw.next()?;
             let slice = slice::from_raw_parts(ptr as *const mem::MaybeUninit<u8>, len);
@@ -2118,7 +2403,7 @@ impl<'a> Iterator for ChunkIter<'a> {
     }
 }
 
-impl<'a> iter::FusedIterator for ChunkIter<'a> {}
+impl<'a, const MIN_ALIGN: usize> iter::FusedIterator for ChunkIter<'a, MIN_ALIGN> {}
 
 /// An iterator over raw pointers to chunks of allocated memory that this
 /// arena has bump allocated into.
@@ -2132,12 +2417,12 @@ impl<'a> iter::FusedIterator for ChunkIter<'a> {}
 /// [`Bump`]: struct.Bump.html
 /// [`iter_allocated_chunks_raw`]: struct.Bump.html#method.iter_allocated_chunks_raw
 #[derive(Debug)]
-pub struct ChunkRawIter<'a> {
+pub struct ChunkRawIter<'a, const MIN_ALIGN: usize = 1> {
     footer: NonNull<ChunkFooter>,
-    bump: PhantomData<&'a Bump>,
+    bump: PhantomData<&'a Bump<MIN_ALIGN>>,
 }
 
-impl Iterator for ChunkRawIter<'_> {
+impl<const MIN_ALIGN: usize> Iterator for ChunkRawIter<'_, MIN_ALIGN> {
     type Item = (*mut u8, usize);
     fn next(&mut self) -> Option<(*mut u8, usize)> {
         unsafe {
@@ -2152,7 +2437,7 @@ impl Iterator for ChunkRawIter<'_> {
     }
 }
 
-impl iter::FusedIterator for ChunkRawIter<'_> {}
+impl<const MIN_ALIGN: usize> iter::FusedIterator for ChunkRawIter<'_, MIN_ALIGN> {}
 
 #[inline(never)]
 #[cold]
@@ -2160,7 +2445,7 @@ fn oom() -> ! {
     panic!("out of memory")
 }
 
-unsafe impl<'a> alloc::Alloc for &'a Bump {
+unsafe impl<'a, const MIN_ALIGN: usize> alloc::Alloc for &'a Bump<MIN_ALIGN> {
     #[inline(always)]
     unsafe fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, AllocErr> {
         self.try_alloc_layout(layout)
@@ -2168,7 +2453,7 @@ unsafe impl<'a> alloc::Alloc for &'a Bump {
 
     #[inline]
     unsafe fn dealloc(&mut self, ptr: NonNull<u8>, layout: Layout) {
-        Bump::dealloc(self, ptr, layout);
+        Bump::<MIN_ALIGN>::dealloc(self, ptr, layout);
     }
 
     #[inline]
@@ -2194,7 +2479,7 @@ unsafe impl<'a> alloc::Alloc for &'a Bump {
 }
 
 #[cfg(any(feature = "allocator_api", feature = "allocator-api2"))]
-unsafe impl<'a> Allocator for &'a Bump {
+unsafe impl<'a, const MIN_ALIGN: usize> Allocator for &'a Bump<MIN_ALIGN> {
     #[inline]
     fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
         self.try_alloc_layout(layout)
@@ -2206,7 +2491,7 @@ unsafe impl<'a> Allocator for &'a Bump {
 
     #[inline]
     unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
-        Bump::dealloc(self, ptr, layout)
+        Bump::<MIN_ALIGN>::dealloc(self, ptr, layout)
     }
 
     #[inline]
@@ -2216,7 +2501,7 @@ unsafe impl<'a> Allocator for &'a Bump {
         old_layout: Layout,
         new_layout: Layout,
     ) -> Result<NonNull<[u8]>, AllocError> {
-        Bump::shrink(self, ptr, old_layout, new_layout)
+        Bump::<MIN_ALIGN>::shrink(self, ptr, old_layout, new_layout)
             .map(|p| unsafe {
                 NonNull::new_unchecked(ptr::slice_from_raw_parts_mut(p.as_ptr(), new_layout.size()))
             })
@@ -2230,7 +2515,7 @@ unsafe impl<'a> Allocator for &'a Bump {
         old_layout: Layout,
         new_layout: Layout,
     ) -> Result<NonNull<[u8]>, AllocError> {
-        Bump::grow(self, ptr, old_layout, new_layout)
+        Bump::<MIN_ALIGN>::grow(self, ptr, old_layout, new_layout)
             .map(|p| unsafe {
                 NonNull::new_unchecked(ptr::slice_from_raw_parts_mut(p.as_ptr(), new_layout.size()))
             })
@@ -2269,8 +2554,8 @@ mod tests {
         use crate::alloc::Alloc;
 
         unsafe {
-            const CAPACITY: usize = 1024 - OVERHEAD;
-            let mut b = Bump::with_capacity(CAPACITY);
+            const CAPACITY: usize = DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER;
+            let mut b = Bump::<1>::with_min_align_and_capacity(CAPACITY);
 
             // `realloc` doesn't shrink allocations that aren't "worth it".
             let layout = Layout::from_size_align(100, 1).unwrap();
@@ -2298,8 +2583,8 @@ mod tests {
             let layout = Layout::from_size_align(1, 1).unwrap();
             let p = b.alloc_layout(layout);
             let q = (&b).realloc(p, layout, CAPACITY + 1).unwrap();
-            assert!(q.as_ptr() as usize != p.as_ptr() as usize - CAPACITY);
-            b = Bump::with_capacity(CAPACITY);
+            assert_ne!(q.as_ptr() as usize, p.as_ptr() as usize - CAPACITY);
+            b.reset();
 
             // `realloc` will allocate and copy when reallocating anything that
             // wasn't the last allocation.


Test patch:
diff --git a/tests/all/alloc_fill.rs b/tests/all/alloc_fill.rs
index 399e7b6..a18239e 100644
--- a/tests/all/alloc_fill.rs
+++ b/tests/all/alloc_fill.rs
@@ -7,9 +7,9 @@ use std::mem;
 #[test]
 fn alloc_slice_fill_zero() {
     let b = Bump::new();
-    let layout = Layout::new::<u8>();
+    let u8_layout = Layout::new::<u8>();
 
-    let ptr1 = b.alloc_layout(layout);
+    let ptr1 = b.alloc_layout(u8_layout);
 
     struct MyZeroSizedType;
 
@@ -26,8 +26,13 @@ fn alloc_slice_fill_zero() {
         ptr2 as *mut _ as usize
     );
 
-    let ptr3 = b.alloc_layout(layout);
-    assert_eq!(ptr2 as *mut _ as usize, ptr3.as_ptr() as usize + 1);
+    let ptr3 = b.alloc_layout(u8_layout);
+    dbg!(ptr2 as *mut _);
+    dbg!(ptr3);
+    assert_eq!(
+        ptr2 as *mut _ as usize,
+        (ptr3.as_ptr() as usize) + b.min_align().max(u8_layout.align()),
+    );
 }
 
 #[test]
@@ -48,14 +53,16 @@ fn alloc_slice_try_fill_with_fails() {
 #[test]
 fn alloc_slice_try_fill_iter_succeeds() {
     let b = Bump::new();
-    let res: Result<&mut [u16], ()> = b.alloc_slice_try_fill_iter(repeat(42).take(10).map(Ok));
+    let elems = repeat(42).take(10).collect::<Vec<_>>();
+    let res: Result<&mut [u16], ()> = b.alloc_slice_try_fill_iter(elems.into_iter().map(Ok));
     assert_eq!(res.map(|arr| arr[5]), Ok(42));
 }
 
 #[test]
 fn alloc_slice_try_fill_iter_fails() {
     let b = Bump::new();
-    let res: Result<&mut [u16], ()> = b.alloc_slice_try_fill_iter(repeat(()).take(10).map(Err));
+    let elems = repeat(()).take(10).collect::<Vec<_>>();
+    let res: Result<&mut [u16], ()> = b.alloc_slice_try_fill_iter(elems.into_iter().map(Err));
     assert_eq!(res, Err(()));
 }
 
diff --git a/tests/all/alloc_try_with.rs b/tests/all/alloc_try_with.rs
index a9a4273..8112659 100644
--- a/tests/all/alloc_try_with.rs
+++ b/tests/all/alloc_try_with.rs
@@ -134,7 +134,8 @@ fn alloc_slice_try_fill_with_large_length() {
 fn alloc_slice_try_fill_iter_large_length() {
     let b = Bump::new();
 
-    assert!(b
-        .alloc_slice_try_fill_iter(repeat(Err::<u8, _>(())).take(10_000_000))
-        .is_err());
+    let elems = repeat(Err::<u8, _>(()))
+        .take(10_000_000)
+        .collect::<Vec<_>>();
+    assert!(b.alloc_slice_try_fill_iter(elems).is_err());
 }
diff --git a/tests/all/quickchecks.rs b/tests/all/quickchecks.rs
index 05bff9f..b445f4f 100644
--- a/tests/all/quickchecks.rs
+++ b/tests/all/quickchecks.rs
@@ -188,7 +188,7 @@ quickcheck! {
     fn test_alignment_chunks(sizes: Vec<usize>) -> () {
         const SUPPORTED_ALIGNMENTS: &[usize] = &[1, 2, 4, 8, 16];
         for &alignment in SUPPORTED_ALIGNMENTS {
-            let mut b = Bump::with_capacity(513);
+            let mut b = Bump::<1>::with_min_align_and_capacity(513);
             let mut sizes = sizes.iter().map(|&size| (size % 10) * alignment).collect::<Vec<_>>();
 
             for &size in &sizes {
diff --git a/tests/all/tests.rs b/tests/all/tests.rs
index 1c7256a..2a41477 100644
--- a/tests/all/tests.rs
+++ b/tests/all/tests.rs
@@ -1,5 +1,6 @@
 use bumpalo::Bump;
 use std::alloc::Layout;
+use std::fmt::Debug;
 use std::mem;
 use std::usize;
 
@@ -136,23 +137,30 @@ fn small_size_and_large_align() {
 
 fn with_capacity_helper<I, T>(iter: I)
 where
-    T: Copy + Eq,
+    T: Copy + Debug + Eq,
     I: Clone + Iterator<Item = T> + DoubleEndedIterator,
 {
     for &initial_size in &[0, 1, 8, 11, 0x1000, 0x12345] {
-        let mut b = Bump::with_capacity(initial_size);
+        let mut b = Bump::<1>::with_min_align_and_capacity(initial_size);
 
         for v in iter.clone() {
             b.alloc(v);
         }
 
-        let pushed_values = b.iter_allocated_chunks().flat_map(|c| {
+        let mut pushed_values = b.iter_allocated_chunks().flat_map(|c| {
             let (before, mid, after) = unsafe { c.align_to::<T>() };
             assert!(before.is_empty());
             assert!(after.is_empty());
             mid.iter().copied()
         });
-        assert!(pushed_values.eq(iter.clone().rev()));
+
+        let mut iter = iter.clone().rev();
+        for (expected, actual) in iter.by_ref().zip(pushed_values.by_ref()) {
+            assert_eq!(expected, actual);
+        }
+
+        assert!(iter.next().is_none());
+        assert!(pushed_values.next().is_none());
     }
 }