Skip to content

Reduce the cost of initializing a fresh OnceCell #398

Closed

Description

@tisonkun

A fresh OnceCell<usize> with an immediately ready initializer took 278 ms for six million initializations. The gap narrowed and the fastest peer changed when the initializer was already running and four other callers were waiting.

Apple M4 Max, macOS arm64, rustc 1.101.0-nightly. Each cell gives rank, elapsed time relative to Asyncband and the whole-batch median over 10 interleaved runs. Lower is faster. Supplemental measurements

Scenario Fixed rounds Asyncband Tokio async-once-cell async-lock
First initialization 6,000,000 #3, 1.00× (278 ms) #2, 0.26× (71 ms) #1, 0.22× (61 ms) #4, 2.41× (672 ms)
Initialize with four waiting callers 1,000,000 #3, 1.00× (326 ms) #1, 0.57× (187 ms) #2, 0.88× (286 ms) #4, 1.44× (472 ms)

get_or_try_init checks the value, acquires a semaphore permit, checks again and publishes the result. An allocation counter measured one 64-byte allocation per cold initialization on this macOS host and zero for Tokio and async-once-cell. The first lock of a fresh std::sync::Mutex produced the same allocation. In a separate controlled run, changing only the internal lock implementation to parking_lot reduced Asyncband from 284 to 115 ms, with async-once-cell at 61 ms. The cost of already initialized reads is a separate measurement.

First contribution

Reproduce the cold and four-waiter rows and add an initialized-read control. Count allocations separately from timing. A focused patch can then target work done before any second caller arrives. Preserve publication of one value, retry after a cancelled or failed initializer, and progress for waiting callers.

To run the supplemental peer_checks command below, save and run peer-checks.rb from the linked comment in a disposable checkout of 6102089. The benchmark and library sources match 36ba6f2. Generator and instructions

cargo x bench --bench primitives -- once::get_or_init::initialize_cell --repeat 10 --raw
cargo x bench --bench peer_checks -- once-cell/initialize_cell --repeat 10 --raw

Performance tracking issue #396

Activity

  1. rokadepiyush49-rgb commented on Nov 6, 2026

    @rokadepiyush49-rgb

    Could you assign this to me? I'd investigate once::get_or_init::initialize_cell on an Apple M4 (10 cores, macOS 27.0.1), then repeat it on Linux.

    As a first check I ran a small standalone probe, not the benchmark. The 64-byte allocation looks macOS-specific: std uses the pthread Mutex there, which boxes on first lock, and the futex version on Linux. OnceCell reaches that lock because Semaphore::release sees available == 0 after the only permit is taken.

    Over 6M rounds (rustc 1.94.1, median of 10), a fresh Mutex with one lock took 175 ms with one allocation. A fresh RwLock with one write took 15 ms with none. That roughly 160 ms gap is close to your parking_lot result, though that was on a different machine.

    cargo x bench also builds the whole workspace, which enables parking_lot for Tokio through tokio/full. If so, Tokio's rows may be using a different lock from Asyncband's, which could explain its zero allocations.

    Next I'd reproduce the cold and four-waiter rows with the initialized-read control. I haven't run those yet. I used Claude to help me read the code and write the probe.

  2. tisonkun commented on Nov 6, 2026

    @tisonkun
    MemberAuthor

    Thanks for your interest! We don't assign issues to individual contributors. Anyone is welcome to submit a PR directly for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions