You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义NoOpHasher单独执行插入/删除时性能更优,但组合执行删除+插入操作时性能骤降的问题求助

自定义NoOpHasher单独执行插入/删除时性能更优,但组合执行删除+插入操作时性能骤降的问题求助

大家好,我最近在做针对<u8, BoxedFnMut>类型的HashMap性能基准测试,其中BoxedFnMut定义为type BoxedFnMut = Box<dyn FnMut() + Send + 'static>。测试工具用的是divan 0.1.21,Rust版本是nightly 1.90.0。

我的核心思路是:因为Key是固定的u8类型,我想完全跳过哈希计算的开销,直接用u8的值作为哈希值,所以实现了一个极简的NoOpHasher,只针对u8/usize类型做写入处理,其他写入方法会直接panic(毕竟只给u8的Key用)。

我的NoOpHasher实现

type BoxedFnMut = Box<dyn FnMut() + Send + 'static>;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NoOpHasher(pub usize);

impl std::hash::Hasher for NoOpHasher {
    #[inline(always)]
    fn finish(&self) -> u64 {
        self.0 as u64
    }

    fn write(&mut self, _: &[u8]) {
        panic!(
            "NoOpHasher::write should not be called: this is a no-op hasher custom built for u8 only"
        )
    }

    #[inline(always)]
    fn write_u8(&mut self, i: u8) {
        unsafe {
            std::ptr::write_volatile(&mut self.0, i as usize)
        };
    }

    #[inline(always)]
    fn write_usize(&mut self, i: usize) {
        unsafe {
            std::ptr::write_volatile(&mut self.0, i)
        };
    }
}

impl std::hash::BuildHasher for NoOpHasher {
    type Hasher = NoOpHasher;

    fn build_hasher(&self) -> Self::Hasher {
        NoOpHasher(0)
    }
}

基准测试代码结构

我把测试分成了三个独立的bench组:

  1. insert组:批量插入预生成的<u8, BoxedFnMut>数据
  2. remove组:批量删除预插入的所有Key
  3. update组:对每个Key执行「先删除再插入」的组合操作

核心测试代码如下(已补全必要依赖逻辑):

use divan::Bencher;
use divan::black_box;
use std::collections::HashMap as StdHashMap;
use rand::Rng;

type BoxedFnMut = Box<dyn FnMut() + Send + 'static>;

fn main() {
    divan::main()
}

#[inline(always)]
fn generate_fn() -> BoxedFnMut {
    Box::new(|| {})
}

#[inline(always)]
fn prepare_data() -> Vec<(u8, BoxedFnMut)> {
    let mut rng = rand::thread_rng();
    let mut data: Vec<(u8, BoxedFnMut)> = (0u8..255).map(|i| (i, generate_fn())).collect();
    data.shuffle(&mut rng);
    data
}

#[divan::bench_group]
mod insert {
    use super::*;

    #[divan::bench]
    fn insert_std_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity(255);
        let data = prepare_data();
        bencher.bench_local(|| {
            for (key, value) in &data {
                black_box(map.insert(key, value));
            }
        });
    }

    #[divan::bench]
    fn insert_noophasher_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity_and_hasher(255, NoOpHasher::default());
        let data = prepare_data();
        bencher.bench_local(|| {
            for (key, value) in &data {
                black_box(map.insert(key, value));
            }
        });
    }
}

#[divan::bench_group]
mod remove {
    use super::*;

    #[divan::bench]
    fn remove_std_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity(255);
        for (key, value) in prepare_data() {
            black_box(map.insert(key, value));
        }
        let mut range = (0u8..255).collect::<Vec<_>>();
        range.shuffle(&mut rand::thread_rng());
        bencher.bench_local(|| {
            for key in &range {
                black_box(map.remove(&key));
            }
        });
    }

    #[divan::bench]
    fn remove_noophasher_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity_and_hasher(255, NoOpHasher::default());
        for (key, value) in prepare_data() {
            black_box(map.insert(key, value));
        }
        let mut range = (0u8..255).collect::<Vec<_>>();
        range.shuffle(&mut rand::thread_rng());
        bencher.bench_local(|| {
            for key in &range {
                black_box(map.remove(&key));
            }
        });
    }
}

#[divan::bench_group]
mod update {
    use super::*;

    #[divan::bench]
    fn update_std_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity(255);
        let data = prepare_data();
        for (key, value) in &data {
            black_box(map.insert(key, value));
        }
        let range = prepare_data();
        bencher.bench_local(|| {
            for (key, value) in &range {
                black_box({
                    map.remove(key);
                    map.insert(key, value)
                });
            }
        });
    }

    #[divan::bench]
    fn update_noophasher_hashmap(bencher: Bencher) {
        let mut map = StdHashMap::with_capacity_and_hasher(255, NoOpHasher::default());
        let data = prepare_data();
        for (key, value) in &data {
            black_box(map.insert(key, value));
        }
        let range = prepare_data();
        bencher.bench_local(|| {
            for (key, value) in &range {
                black_box({
                    map.remove(key);
                    map.insert(key, value)
                });
            }
        });
    }
}

奇怪的性能现象

  • 单独测试insert和remove时,用NoOpHasher的HashMap比标准库默认实现快非常多,完全符合预期(毕竟跳过了哈希计算的开销)
  • 但测试update(先删除再插入同一个Key)时,NoOpHasher版本的性能暴跌,完全达不到「remove耗时 + insert耗时」的预期,和标准库版本的性能差距也大幅缩小甚至反超

我已经尝试过的排查步骤

  • 确认HashMap预分配了刚好足够的容量(255,和Key总数一致),排除扩容带来的额外开销
  • 用perf stat分析性能,没发现两个版本在指令数、缓存命中这类指标上有明显差异
  • 生成cargo flamegraph,但结果里divan框架的调用占比过高,看不到HashMap内部操作的细节
  • 替换成hashbrown::HashMap,insert和remove的性能确实提升了,但update的问题依然存在
  • 测试hashbrown的默认哈希器,发现它的update性能比标准库快2倍,这说明问题大概率出在我的NoOpHasher实现上,尤其是和remove操作的交互逻辑
  • 尝试换成FxHashMap,结果类似;给所有相关函数加上#[inline(always)]也没有改善

现在我实在摸不着头脑,有没有大佬能帮我分析下:为什么单独的remove和insert都快,但组合起来就性能暴跌?我的NoOpHasher哪里写得有问题吗?或者HashMap内部在处理这种自定义Hasher时,remove和insert的组合会触发什么额外开销?


内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 13:05:28