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

C++测量unordered_set查找等极快操作的耗时差异疑问

发帖渠道选择建议
  • 这个问题不适合发布在代码评审板块。代码评审板块的适配场景是代码可正常运行、寻求代码风格、可维护性、设计逻辑层面的优化建议,你当前的核心诉求是解释性能测试结果的底层原理,属于C++标准库实现、计算机体系结构相关的技术问答范畴,发在通用技术问答板块更合适。
unordered_set查找耗时测试相关内容

测试引入头文件

#include <iostream>
#include <vector>
#include <chrono>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <unordered_set>
#include <map>

完整测试代码

std::vector<int> eurastothenes_sieve(size_t count){
    std::vector<bool> sieve(count * std::log2(count), true);
    sieve[0] = false;
    sieve[1] = false;
    auto factor = 2;
    while(factor * factor < sieve.size()) {
        for (auto not_prime_idx = factor * 2; not_prime_idx < sieve.size(); not_prime_idx+= factor) {
            sieve[not_prime_idx] = false;
        }
        while(!sieve[++factor]);
    }
    std::vector<int> primes;
    for(int idx = 0; idx < sieve.size(); idx++){
        if(sieve[idx]){
            primes.push_back(idx);
        }
    }
    return primes;
}

std::vector<int> numbers_to_find(int max, size_t count) {
    std::vector<int> v(count);
    auto p_rand = [max](){
        return std::rand() % max;
    };
    std::generate(v.begin(), v.end(), p_rand);
    return v;
}

int main() {
    auto primes = eurastothenes_sieve(1000);
    std::unordered_set<decltype(primes)::value_type> primeset(primes.begin(), primes.end());

    std::map<int, double> timings;

    for(int i = 100; i <= 1000; i+=100 ){
        auto find_me = numbers_to_find(primes[i], 10000);
        auto start = std::chrono::steady_clock::now();
        auto found = 0;
        int count = 10000;
        while(count--)
        for(auto val: find_me)
        {
            if(primeset.find(val) != primeset.end()){
                found++;
            }
        }

        auto end = std::chrono::steady_clock::now();

        std::cout << "In " << (end - start).count() << " ns";
        std::cout << " we found " << found << "\n";
        timings.insert({i, (end - start).count() / (10000.0 * 10000)});
    }
    for(auto n_t : timings){
        std::cout << "N = " << n_t.first << ": " << n_t.second << "ns\n";
    }
}

unordered_set查找测试结果

N = 100: 15.5874ns
N = 200: 15.4113ns
N = 300: 15.3488ns
N = 400: 15.4806ns
N = 500: 15.4009ns
N = 600: 15.5415ns
N = 700: 15.3537ns
N = 800: 15.4371ns
N = 900: 15.2567ns
N = 1000: 15.3602ns

校准用累加测试代码

for(auto val: find_me)
    {
            found+=val;         
    }

累加测试结果

N = 100: 0.0657845ns
N = 200: 0.0657359ns
N = 300: 0.0649369ns
N = 400: 0.0651398ns
N = 500: 0.0649169ns
N = 600: 0.0650553ns
N = 700: 0.0654992ns
N = 800: 0.0645845ns
N = 900: 0.065315ns
N = 1000: 0.0649747ns

待澄清的疑问

  • 已知unordered_set为O(1)时间复杂度的访问结构,该特性符合测试结果,测试搭建逻辑无明显问题
  • 为什么取模运算+内存查找(unordered_set的核心查找逻辑)的耗时,是寄存器+缓存值操作(简单整数累加)的200倍?
  • 简单累加测试结果是否真实,即代码真的能做到每0.06纳秒完成一次随机整数加法吗?
  • 核心疑问:为什么取模和内存查找操作的耗时这么高?测试已尽量做缓存友好设计,测试所用CPU的L1缓存为32K,编译选项为CMake Release模式、g++ -O2优化等级。
问题解答

0.06ns/次的累加结果完全不反映单步加法的真实耗时。目前消费级CPU主频普遍在35GHz,单时钟周期约0.20.3ns,物理上不可能做到0.06ns完成一次整数加法。这个数值是编译器优化叠加CPU硬件特性带来的:-O2优化等级下,编译器会对纯累加的循环做向量化展开、循环不变量外提,甚至直接推导批量求和的计算逻辑,根本不会逐次执行单步加法;再加上CPU本身的多发射流水线、乱序执行能力,可以同时并行处理多条无依赖的加法指令,你测到的是批量计算摊薄后的平均耗时,不是单操作的真实执行时间。

你测到的unordered_set单次查找15ns左右的耗时完全在合理范围内,不存在异常偏高的问题,核心原因有三个:

  • 你认为的“缓存友好”实际不成立:gcc等主流标准库的unordered_set是链式哈希表实现,每个元素都是独立分配的堆节点,哪怕总数据量很小,节点的内存地址也不是连续的。查找时算完哈希位置后,需要通过指针跳转随机访问节点内存,哪怕所有数据都落在L1缓存里,单次L1访存就有1ns左右的延迟,还没算其他操作的开销。
  • unordered_set的查找流程远不止“取模+读内存”两步:首先要对key做哈希计算(哪怕int类型的默认哈希是恒等映射,取模计算桶索引本身就要几个时钟周期),之后读取对应桶的头指针,再跳转读取节点存储的key值做比对,如果遇到哈希冲突还要顺着桶内的链表继续遍历下一个节点。整个流程的每一步都强依赖上一步的执行结果,必须等上一步的值算完、读回来才能走下一步,完全是串行阻塞的,没法像无依赖的累加操作那样被CPU并行调度。
  • 两者的执行场景没有可比性:纯累加操作的所有操作数都可以放在寄存器里,没有任何内存访问开销,CPU可以同时塞满流水线并行执行几十条累加指令;而哈希查找全程夹杂随机访存、分支判断、依赖链阻塞,能跑到15ns/次已经是小数据量全命中L1缓存的优秀结果,要是数据量涨到超过L1/L2缓存,单次查找耗时会直接涨到几十甚至上百纳秒。

内容的提问来源于stack exchange,提问作者Captain Giraffe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 07:15:32