Skip to content

feat: bucketized futexes - #2469

Open
zyuiop wants to merge 5 commits into
hermit-os:mainfrom
zyuiop:feat/bucketized-futexes
Open

feat: bucketized futexes#2469
zyuiop wants to merge 5 commits into
hermit-os:mainfrom
zyuiop:feat/bucketized-futexes

Conversation

@zyuiop

@zyuiop zyuiop commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

While tracking #2468, I initially suspected the futex lock to be an issue, so I applied the "Todo" and made a bucket list instead of the single lock.

I have based this off on #2468 so that we get performance results that make sense.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark Results

Details
Benchmark Current: d3446a9 Previous: 2e23902 Performance Ratio
startup_benchmark Build Time 82.65 s 80.34 s 1.03
startup_benchmark File Size 0.77 MB 0.80 MB 0.96
Startup Time - 1 core 0.69 s (±0.02 s) 0.75 s (±0.02 s) 0.93
Startup Time - 2 cores 0.70 s (±0.03 s) 0.74 s (±0.02 s) 0.95
Startup Time - 4 cores 0.72 s (±0.02 s) 0.74 s (±0.02 s) 0.97
multithreaded_benchmark Build Time 81.23 s 82.11 s 0.99
multithreaded_benchmark File Size 0.88 MB 0.86 MB 1.03
Multithreaded Pi Efficiency - 2 Threads 91.83 % (±8.80 %) 85.89 % (±6.61 %) 1.07
Multithreaded Pi Efficiency - 4 Threads 45.18 % (±2.89 %) 43.43 % (±2.56 %) 1.04
Multithreaded Pi Efficiency - 8 Threads 26.48 % (±1.70 %) 25.76 % (±1.53 %) 1.03
micro_benchmarks Build Time 80.25 s 80.40 s 1.00
micro_benchmarks File Size 0.89 MB 0.86 MB 1.03
Scheduling time - 1 thread 64.23 ticks (±1.98 ticks) 62.65 ticks (±4.06 ticks) 1.03
Scheduling time - 2 threads 37.50 ticks (±5.89 ticks) 34.08 ticks (±4.10 ticks) 1.10
Micro - Time for syscall (getpid) 3.21 ticks (±0.52 ticks) 3.45 ticks (±0.58 ticks) 0.93
Memcpy speed - (built_in) block size 4096 79646.67 MByte/s (±55666.05 MByte/s) 82448.38 MByte/s (±56997.13 MByte/s) 0.97
Memcpy speed - (built_in) block size 1048576 30604.34 MByte/s (±24669.01 MByte/s) 30585.98 MByte/s (±24707.84 MByte/s) 1.00
Memcpy speed - (built_in) block size 16777216 28763.57 MByte/s (±23568.32 MByte/s) 26340.06 MByte/s (±21720.96 MByte/s) 1.09
Memset speed - (built_in) block size 4096 79943.39 MByte/s (±55878.72 MByte/s) 82292.76 MByte/s (±56891.50 MByte/s) 0.97
Memset speed - (built_in) block size 1048576 31324.31 MByte/s (±25103.08 MByte/s) 31323.85 MByte/s (±25145.86 MByte/s) 1.00
Memset speed - (built_in) block size 16777216 29517.89 MByte/s (±24009.86 MByte/s) 27104.68 MByte/s (±22209.94 MByte/s) 1.09
Memcpy speed - (rust) block size 4096 75809.82 MByte/s (±52772.14 MByte/s) 74097.96 MByte/s (±51811.44 MByte/s) 1.02
Memcpy speed - (rust) block size 1048576 30528.91 MByte/s (±24576.30 MByte/s) 30361.60 MByte/s (±24602.37 MByte/s) 1.01
Memcpy speed - (rust) block size 16777216 29004.83 MByte/s (±23757.32 MByte/s) 27625.34 MByte/s (±22806.88 MByte/s) 1.05
Memset speed - (rust) block size 4096 76319.23 MByte/s (±53139.69 MByte/s) 74373.47 MByte/s (±51976.48 MByte/s) 1.03
Memset speed - (rust) block size 1048576 31264.77 MByte/s (±25007.03 MByte/s) 31110.89 MByte/s (±25033.24 MByte/s) 1.00
Memset speed - (rust) block size 16777216 29745.53 MByte/s (±24187.01 MByte/s) 28386.93 MByte/s (±23265.03 MByte/s) 1.05
alloc_benchmarks Build Time 78.67 s 74.76 s 1.05
alloc_benchmarks File Size 0.85 MB 0.87 MB 0.97
Allocations - Allocation success 91.38 % 91.31 % 1.00
Allocations - Deallocation success 100.00 % 100.00 % 1
Allocations - Pre-fail Allocations 61.60 % 61.44 % 1.00
Allocations - Average Allocation time 7836.82 Ticks (±195.83 Ticks) 5860.58 Ticks (±98.43 Ticks) 1.34
Allocations - Average Allocation time (no fail) 8542.16 Ticks (±307.81 Ticks) 6554.81 Ticks (±92.86 Ticks) 1.30
Allocations - Average Deallocation time 2492.95 Ticks (±798.79 Ticks) 1805.01 Ticks (±250.35 Ticks) 1.38
mutex_benchmark Build Time 79.32 s 79.82 s 0.99
mutex_benchmark File Size 0.89 MB 0.86 MB 1.03
Mutex Stress Test Average Time per Iteration - 1 Threads 12.32 ns (±0.47 ns) 12.10 ns (±0.41 ns) 1.02
Mutex Stress Test Average Time per Iteration - 2 Threads 99.62 ns (±5.91 ns) 40.26 ns (±1.68 ns) 2.47

This comment was automatically generated by workflow using github-action-benchmark.

@mkroening mkroening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks okay to me. Unless it is too much effort, could you make the bucket map implementation generic? Did you measure any speedup in any stress test with this?

CC: @joboet for the original implementation.

@mkroening mkroening self-assigned this Aug 22, 2026
@zyuiop

zyuiop commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

I can indeed make it generic, it's just that AFAIK there is no other usecase in the code for now for such a map. So this may be a case of "premature abstraction" :D

But if you insist, I'll gladly do it :)

Comment thread src/synch/futex.rs
}

fn hash_key(v: usize) -> usize {
let v = (v >> 3).to_be_bytes();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're trying to remove the zero bits resulting from AtomicU32's alignment, then you have to shift by 2, not 3. Since you're hashing anyway, I don't think this is necessary anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to remove the last 3 bits from the address, which for some reason I believed to always be 0

But you are correct that this is in any case not needed since we hash. It may be the remain of an attempt to not use a hash function at all for better performance.

If it produced a more or less uniformly distributed distribution, (addr >> 3) % N would likely be better as it is less intensive than computing a hash.

Let me know what you think :)

Comment thread src/synch/futex.rs Outdated
type Bucket = InterruptSpinMutex<TaskListBucket>;

#[repr(transparent)]
struct TaskListBucket(LinkedList<BucketElem>);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a linked-list here is a bit unfortunate – if there are a lot of tasks, the $O(n)$ lookup will start to matter. You could try using hashbrown::HashTable as an inner map to avoid having to recompute the hash while keeping hashmap-like lookup performance. Or alternatively, use a BTreeMap – that will also reduce memory usage as addresses become unused.

@zyuiop zyuiop Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is the same reasoning here... Wanting to avoid doing two hashes of the key. But I can switch to a BTreeMap.
And also it was likely easier to think in terms of iterators on a linked list than on a map

@zyuiop
zyuiop force-pushed the feat/bucketized-futexes branch from 2f2342e to d3446a9 Compare September 8, 2026 16:50
@zyuiop
zyuiop requested a review from joboet September 8, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants