范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

C中的HashTable性能优化

  作者:leomryang,腾讯 PCG 后台开发工程师
  HashTable是开发中常用的数据结构。本文从C++ STL中的HashTable讲起,分析其存在的性能问题,对比业界改进的实现方式。通过基准测试对比其实际性能表现,总结更优的实现版本。 STL HashTable的问题
  STL中的HashTable实现为std::unordered_map,采用链接法(chaining)解决hash碰撞,hash值相同的多个pair组织为链表,如图1所示。
  图1 std::unordered_map内存布局
  查找key值时,需要遍历对应的链表,寻找值相同的key。链表中节点内存分布不连续,相邻节点处于同一cache line的概率很小,因此会产生较多的cpu cache miss,降低查询效率。改进版HashTable
  google absl::flat_hash_map采用开放寻址法(open addressing)解决hash碰撞,并使用二次探测(quadratic probing)方式。absl::flat_hash_map改进了内存布局,将所有pair放在一段连续内存中,将hash值相同的多个pair放在相邻位置,如图2所示。
  图2 absl::flat_hash_map内存布局
  查找key时,以二次探测方式遍历hash值相等的pair,寻找值相等的key。hash值相同的pair存储在相邻内存位置处,内存局部性好,对cpu cache友好,可提高查询效率。
  在内存用量方面,absl::flat_hash_map空间复杂度为 *O((sizeof(std::pair) + 1) * bucket_count())*。最大负载因子被设计为0.875,超过该值后,表大小会增加一倍。因此absl::flat_hash_map的size()通常介于 0.4375 * bucket_count() 和 0.875 * bucket_count() 之间。
  需要注意,absl::flat_hash_map在rehash时,会对pair进行move,因此pair的指针会失效,类似下述用法会访问非法内存地址。absl::flat_hash_map map; // ...  init map Foo& foo1 = map["f1"]; Foo* foo2 = &(map["f2"]); // ... rehash map foo1.DoSomething(); // 非法访问,foo1引用的对象已被move foo2->DoSomething(); // 非法访问,foo2指向的对象已被move
  为了解决上述pair指针失效问题,google absl::node_hash_map将pair存储在单独分配的节点中,在连续内存中存放指向这些节点的指针,其他设计与flat_hash_map相同,如图3所示。
  图3 absl::node_hash_map内存布局
  absl::node_hash_map在rehash时,pair本身无需移动,只需将指针拷贝至新的地址。可保证pair的指针稳定性。源码探究
  下面对absl两种HashTable的核心逻辑源码进行探索(省略不相关部分的代码):分配内存// flat_hash_map和node_hash_map均以raw_hash_set为父类实现,区别在于policy不同 template ,           class Eq = absl::container_internal::hash_default_eq,           class Allocator = std::allocator>> class flat_hash_map : public absl::container_internal::raw_hash_map<                           absl::container_internal::FlatHashMapPolicy,                           Hash, Eq, Allocator> {}; template ,           class Eq = absl::container_internal::hash_default_eq,           class Alloc = std::allocator>> class node_hash_map     : public absl::container_internal::raw_hash_map<           absl::container_internal::NodeHashMapPolicy, Hash, Eq,           Alloc> {};  template  class raw_hash_map : public raw_hash_set{};  // raw_hash_set分配连续内存的大小取决于 capacity_ 和 slot_type template  class raw_hash_set {    using slot_type = typename Policy::slot_type;    void initialize_slots() {     // 分配连续内存的大小为 capacity_ * slot_size 并对齐 slot_align     char* mem = static_cast(Allocate(         &alloc_ref(),         AllocSize(capacity_, sizeof(slot_type), alignof(slot_type))));     ctrl_ = reinterpret_cast(mem);     slots_ = reinterpret_cast(         mem + SlotOffset(capacity_, alignof(slot_type)));     ResetCtrl(capacity_, ctrl_, slots_, sizeof(slot_type));     reset_growth_left();     infoz().RecordStorageChanged(size_, capacity_);   }      inline size_t AllocSize(size_t capacity, size_t slot_size, size_t slot_align) {     return SlotOffset(capacity, slot_align) + capacity * slot_size;   } };  // flat_hash_map的slot包含pair template  struct FlatHashMapPolicy {   using slot_type = typename map_slot_type; }; template  union map_slot_type {   using value_type = std::pair;   using mutable_value_type =       std::pair, absl::remove_const_t>;    value_type value;   mutable_value_type mutable_value;   absl::remove_const_t key; }; // node_hash_map的slot为指向pair的指针 template  class NodeHashMapPolicy     : public absl::container_internal::node_slot_policy<           std::pair&, NodeHashMapPolicy>{}; template  struct node_slot_policy {   using slot_type = typename std::remove_cv<       typename std::remove_reference::type>::type*;  // slot为指向pair的指针 };
  2. 添加元素template  class raw_hash_set {    // 查找   // Attempts to find `key` in the table; if it isn"t found, returns a slot that   // the value can be inserted into, with the control byte already set to   // `key`"s H2.   template    std::pair find_or_prepare_insert(const K& key) {     prefetch_heap_block();     auto hash = hash_ref()(key);  // 在相同hash值的桶中查找     auto seq = probe(ctrl_, hash, capacity_);     while (true) {       Group g{ctrl_ + seq.offset()};       for (uint32_t i : g.Match(H2(hash))) {  // 如果key相等,则找到了目标元素         if (ABSL_PREDICT_TRUE(PolicyTraits::apply(                 EqualElement{key, eq_ref()},                 PolicyTraits::element(slots_ + seq.offset(i)))))           return {seq.offset(i), false};       }       if (ABSL_PREDICT_TRUE(g.MaskEmpty())) break;       seq.next();       assert(seq.index() <= capacity_ && "full table!");     }     // 如果没有相等的key,则插入元素     return {prepare_insert(hash), true};   }    // 找到下一个可用的slot   // Given the hash of a value not currently in the table, finds the next   // viable slot index to insert it at.   size_t prepare_insert(size_t hash) ABSL_ATTRIBUTE_NOINLINE {     auto target = find_first_non_full(ctrl_, hash, capacity_);     if (ABSL_PREDICT_FALSE(growth_left() == 0 &&                            !IsDeleted(ctrl_[target.offset]))) {       rehash_and_grow_if_necessary();       target = find_first_non_full(ctrl_, hash, capacity_);     }     ++size_;     growth_left() -= IsEmpty(ctrl_[target.offset]);     SetCtrl(target.offset, H2(hash), capacity_, ctrl_, slots_,             sizeof(slot_type));     infoz().RecordInsert(hash, target.probe_length);     return target.offset;   }      // 在找到的slot处构造元素   template    iterator lazy_emplace(const key_arg& key, F&& f) {     auto res = find_or_prepare_insert(key);     if (res.second) {       slot_type* slot = slots_ + res.first;       std::forward(f)((&alloc_ref(), &slot));       assert(!slot);     }     return iterator_at(res.first);   }  };
  3. rehashtemplate  class raw_hash_set {      // Called whenever the table *might* need to conditionally grow.   void rehash_and_grow_if_necessary() {     if (capacity_ == 0) {       resize(1);     } else if (capacity_ > Group::kWidth &&                size() * uint64_t{32} <= capacity_ * uint64_t{25}) {       drop_deletes_without_resize();     } else {       // Otherwise grow the container.       resize(capacity_ * 2 + 1);     }   }    void resize(size_t new_capacity) {     assert(IsValidCapacity(new_capacity));     auto* old_ctrl = ctrl_;     auto* old_slots = slots_;     const size_t old_capacity = capacity_;     capacity_ = new_capacity;     initialize_slots();      size_t total_probe_length = 0;     for (size_t i = 0; i != old_capacity; ++i) {       if (IsFull(old_ctrl[i])) {         size_t hash = PolicyTraits::apply(HashElement{hash_ref()},                                           PolicyTraits::element(old_slots + i));         auto target = find_first_non_full(ctrl_, hash, capacity_);         size_t new_i = target.offset;         total_probe_length += target.probe_length;         SetCtrl(new_i, H2(hash), capacity_, ctrl_, slots_, sizeof(slot_type));  // 调用 policy transfer 在新地址处构造元素         PolicyTraits::transfer(&alloc_ref(), slots_ + new_i, old_slots + i);       }     }     if (old_capacity) {       SanitizerUnpoisonMemoryRegion(old_slots,                                     sizeof(slot_type) * old_capacity);       Deallocate(           &alloc_ref(), old_ctrl,           AllocSize(old_capacity, sizeof(slot_type), alignof(slot_type)));     }     infoz().RecordRehash(total_probe_length);   } };  // flat_hash_map在rehash时,将元素移动至新地址处 template  struct map_slot_policy {   template    static void transfer(Allocator* alloc, slot_type* new_slot,                        slot_type* old_slot) {     emplace(new_slot);      // 将元素 move 至新地址处     if (kMutableKeys::value) {       absl::allocator_traits::construct(           *alloc, &new_slot->mutable_value, std::move(old_slot->mutable_value));     } else {       absl::allocator_traits::construct(*alloc, &new_slot->value,                                                    std::move(old_slot->value));     }     destroy(alloc, old_slot);   } }; // node_hash_map在rehash时,将指向元素的指针拷贝至新地址处 template  struct node_slot_policy {  template    static void transfer(Alloc*, slot_type* new_slot, slot_type* old_slot) {     // 将"指向元素的地址" copy 至新地址处     *new_slot = *old_slot;   } }; 基准测试
  基准测试场景如下:k、v均为std::stringk长度约20字节v长度约40字节写操作:向空map中,emplace N个k值不同的pair读操作:从包含N个pair的map中,find N次随机k值(提前生成好的随机序列)
  各容器读操作的cache miss情况如下:# std::unordered_map perf stat -e cpu-clock,cycles,instructions,L1-dcache-load-misses,L1-dcache-loads  -p 2864179    ^C  Performance counter stats for process id "2864179":           18,447.43 msec cpu-clock                 #    1.000 CPUs utilized               56,278,197,029      cycles                    #    3.051 GHz                         25,374,763,394      instructions              #    0.45  insn per cycle                 265,164,336      L1-dcache-load-misses     #    3.17% of all L1-dcache hits        8,377,925,973      L1-dcache-loads           #  454.151 M/sec        18.453787989 seconds time elapsed  # absl::node_hash_map perf stat -e cpu-clock,cycles,instructions,L1-dcache-load-misses,L1-dcache-loads  -p 2873181    ^C  Performance counter stats for process id "2873181":           42,683.27 msec cpu-clock                 #    1.000 CPUs utilized              130,088,665,294      cycles                    #    3.048 GHz                        134,531,389,793      instructions              #    1.03  insn per cycle                 334,111,936      L1-dcache-load-misses     #    0.74% of all L1-dcache hits       44,998,374,652      L1-dcache-loads           # 1054.239 M/sec        42.685607230 seconds time elapsed  # absl::flat_hash_map perf stat -e cpu-clock,cycles,instructions,L1-dcache-load-misses,L1-dcache-loads  -p 2867048   ^C  Performance counter stats for process id "2867048":           27,379.72 msec cpu-clock                 #    1.000 CPUs utilized               83,562,709,268      cycles                    #    3.052 GHz                         82,589,606,600      instructions              #    0.99  insn per cycle                 188,364,766      L1-dcache-load-misses     #    0.68% of all L1-dcache hits       27,605,138,227      L1-dcache-loads           # 1008.233 M/sec        27.388859157 seconds time elapsed
  可以看到std::unordered_map L1-dcache miss率为3.17%,absl::node_hash_map为0.74%,absl::flat_hash_map为0.68%,验证了上述关于不同内存模型下cpu cache性能表现的推论。
  基准测试结果如下(为了显示更清晰,将元素个数小于1024的和大于1024的分开展示):
  图4 写操作,元素个数小于等于1024
  图5 写操作,元素个数大于等于1024
  图6 读操作,元素个数小于等于1024
  图7 读操作,元素个数大于等于1024
  从测试结果可以看出,写操作耗时:std::unordered_map < absl::flat_hash_map < absl::node_hash_map,absl::flat_hash_map比std::unordered_map耗时平均增加9%;读操作耗时:absl::flat_hash_map < absl::node_hash_map < std::unordered_map,absl::flat_hash_map比std::unordered_map耗时平均降低20%。总结
  三种HashTable对比如下:
  HashTable类型
  内存模型
  性能表现
  推荐使用场景
  std::unordered_map
  以链表存储hash值相同的元素
  写操作:rehash友好,性能最好;读操作:内存不连续,cpu cache命中率较低,性能最差
  写多读少
  absl::node_hash_map
  在连续内存中存储hash值相同的元素的指针
  写操作:性能最差;读操作:性能略差于absl::flat_hash_map;
  读多写少,且需要保证pair的指针稳定性
  absl::flat_hash_map
  在连续内存中存储hash值相同的元素
  写操作:性能略差于std::unordered_map;读操作:内存连续,cpu cache命中率较高,性能最好
  读多写少
  在读多写少的场景使用HashTable,可以用absl::flat_hash_map替代std::unordered_map,获得更好的查询性能。但需注意absl::flat_hash_map在rehash时会将pair move到新的内存地址,导致访问原始地址非法。
  absl::flat_hash_map的接口类似于std::unordered_map,详细介绍可参阅absl官方文档:https://abseil.io/docs/cpp/guides/container#hash-tables扩展阅读absl::flat_hash_map源码:abseil-cpp/flat_hash_map.h at master · abseil/abseil-cpp · GitHubstd::unordered_map源码:gcc/unordered_map.h at master · gcc-mirror/gcc · GitHubfolly 14-way hash table:folly/F14.md at main · facebook/folly · GitHubrobin_hood hash table:GitHub - martinus/robin-hood-hashing: faster and more memory efficient hashtable based on robin hood hashing for C++11/14/17/20

在非洲屋脊实现通信梦这里终于有了通信信号,我可以跟家人联系了。近日,在海拔5895米的非洲乞力马扎罗山最高峰上,当地导游帕斯卡尔给家人报了平安,心情非常激动。帕斯卡尔陪同来自世界多地的游客攀登过60多有免费论文查重软件吗?免费查重的网站还是有不少的,但是大部分都是只能免费查询三千字之类的,作用不是很大,前两年我毕业的时候找到了一个还不错的网站,叫爱学术(不贴网址了,你直接搜索),这个网站是可以免费查多少钱建一个干混砂浆搅拌站?影响搅拌站价格因素主要包括生产规模施工场地配套设施投资额度等。建议咨询这方面的专业厂家,南方路机自创建以来,就一直从事这个设备的研发工作,他们厂的型号配置是目前国内比较齐全的。根据枸杞黄芪菊花红枣山楂该怎么搭配泡水喝?这些还可加胖大海,人参片,菊花等怎样搭配得看体质,究竟需要补气,补血?究竟阴虚,阳虚,还是阴阳两虚?根据身体健康情况也可搭配红茶,清茶,苦艼铁观音,碧螺春,普洱茶等,根据身体需求喝为什么现在谈药物利巴韦林色变呢?很高兴回答你的问题,欢迎关注利巴韦林作为广谱抗病毒药物,尤其是对儿童流感使用广泛,但是其不良反应较多而且严重,导致使用利巴韦林风险很大。利巴韦林,又叫病毒唑,是一种人工合成的鸟苷类国家电网的退休职工一个月能拿多少退休金?国家电网的退休职工一个月能拿多少养老金?说起国家电网,其知名度在老百姓口中几乎是家喻户晓的。由于我们四川重庆一带都是属于国家电网的用电覆盖范围,而且电网的服务网点已经覆盖到了每一个修鞋改衣配钥匙该去哪?这张小修小补地图火了日前,广东东莞新上线的一幅便民地图火了,上面登载的既不是城市的景点打卡地,也不是介绍各处餐饮美食而是让急需各种小修小补便民服务的市民,轻松找对地方。相信很多人都感觉到,随着城市快速首趟环塔里木盆地旅游专列开行途经库车喀什和田若羌和库尔勒等地的多个黄金旅游景区景点3月27日,在乌鲁木齐站站台,乘坐Y936次环塔之旅非凡印记旅游专列的乘客准备乘车出行。(全媒体记者陈岩摄)新疆网讯(全媒体记者王艳红)3月27日22时25分,满载523名游客的Y船到毛里塔尼亚,船员勇闯沙漠,当地人穷而快乐,真实经历不一样走了近100个国家,去了发达国家,发展中国家,再来穷国家,这里的一切都让我难以想象。要说非洲很穷的国家有很多,可要穷得像样的国家要数毛里塔尼亚。航海这么多年,去毛里塔尼亚的次数已记一场与梨花的约会,冠县诚邀您来记者陶春燕见习记者魏宪朝一年春光惹人醉,万顷梨花作雪飞。伴随着和煦春风,冠县第十八届梨园文化观光周如约而至。为扎实开展乡村好时节LETS购主题年活动,打造游武训故里赏梨园花海瞻红色钓鱼人一般考虑买什么小汽车?我的朋友为了钓鱼,特意买了台二手的五菱荣光。这五菱可是一台神车啊。我们这里有个深山区的小河,里面有一个水坝,里面有很多的溪石斑,还有鲤鱼和草鱼。在朋友的忽悠下,我这个朋友第一次开他
为什么人们常说十个创业九个死?我们常说的十个创业九个死,主要是想表达的是,创业的艰难,以及创业成功率较低。但,很多人仍然都会有一份创业的梦想。那么,为什么创业失败的会那么多。我们想从以下六个角度分析,造成创业失邮政储蓄卡余额显示为负九十多万是什么情况?我在邮政储蓄银行上班有六年多的时间,总共见到过有不少于10次,客户储蓄银行卡可用余额为负数的情况。距离现在最早的一次,就在上个月。我给大家说几个比较典型的案例,以及他们银行卡为负数40岁,一半已经过去了,原来做设计类工作,脑力和体力有些跟不上了,可以往哪行转?人过40天过午,再不努力要落伍,不知道你是从事哪方面设计工作,如果是在建筑类设计院从事设计工作转做监理是个不错的选择,监理行业对设计师比较青睐,我所接触到的总监很多都是设计院出来的作为子公司的一个财务人员,发现自己领导违规报账后,多次提醒后被领导威胁,应该怎么办?作为子公司的财务人员,一般都是被双层领导,一是受总公司财务的领导。二是受分公司经营班子领导。在这种双层领导下的财务人员,是非常难做的。总公司的领导要求你严格遵守财务制度,管好用好每孕妇便秘吃什么最有效?孕妇便秘吃什么最有效?孕妇便秘吃纤维素含量高的新鲜蔬菜和水果及谷薯类,有助于预防或缓解便秘,但是否最有效,因人而异。此外,起床后空腹喝一杯温开水或者蜂蜜水也有助于排便。孕妇在妊娠期为啥宝宝喝母乳频繁夜醒,喝奶粉却能一觉睡天亮?我不跟你说奶粉配置的什么营养全面啊,母乳易消化的原因。你就打个比方,奶粉是固体的,泡成液体。如果等量母乳你去蒸发,实际干成固体物质绝对没有那么多。母乳是真材实料的营养物质,奶粉人为有没有那么一句伤害你的话,让你记一辈子?有,生活中不仅要阳光,要自信,更要有一个平和的心态。心与花的距离,在于欣赏,在于懂得心与世界的距离,在于容纳,在于敬畏心与心的距离,在于理解,在于真诚。水,本清澈,无念人,本善良,小孩让人贩子抢了,被拦下后,但人贩子说是孩子的亲人,该怎么办?不要再问这种问题实在没意义了你活在原始社会吗?报警不会吗?求助不会吗?别说人贩子一个女的在大街上喊流氓都得围一群人!人贩子更跑不了了!在教你一招合法的如果人贩子强行带走小孩,你可以FPX对阵G2,Mlxg直播间直言FPX就是垃圾,G2必胜而且是大满贯,为什么呢?今年堪称毒奶大反转,大家都认为RNG心态平稳可以向前,结果提前结束,都以为SKT会夺冠,会击败G2,结果失败,都以为IG会完虐FPX,结果失败,都以为FPX会被SKT完虐,结果没遇为什么李霄鹏刚接任男足主教练就有人喊下课,他做了什么?李霄鹏刚上课就被喊下课,并不是他做了什么,他还什么也没做,喊他下课的人是因为对于土帅担任国足主帅不看好。你说喊李霄鹏下课的这个人8成就是指那个陕西日报的记者,他一直在喊李铁下课,并一只耳朵听力正常,另一只耳朵几乎全聋,需要配什么样的助听器?您好!一只耳朵听力正常,另一只耳朵几乎全聋,需不需要选配助听器要视个人听损情况而定。单侧性听损,对声源无法定位,人多及嘈杂环境下单耳聆听会比较吃力。目前有一些助听器厂家,针对单侧性