学到一个编码技巧:用重复写入代替if判断,减少程序分支
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!
近期阅读了rust标准库的hashbrown库(也就是一个hashmap的实现),并搞了一个中文注释的版本,有兴趣的同学请看:https://github.com/ahfuzhang/rust-hashbrown-v0.12.0 。
hashbrown的原理来自google开源的swiss table。我之前写了一篇swiss table的介绍:《Swisstable:C++中比std::unordered_map更快的hash表》。
hashbrown中,hash表的冲突管理是通过一个与buckets一样长的ctrl byte数组来实现的,每个桶的位置被占用后,就会把对应下标的ctrl byte写为key的高7bit。实现的代码如下:
/// Sets a control byte, and possibly also the replicated control byte at
/// the end of the array.
#[inline]
unsafe fn set_ctrl(&self, index: usize, ctrl: u8) {
// Replicate the first Group::WIDTH control bytes at the end of
// the array without using a branch:
// - If index >= Group::WIDTH then index == index2.
// - Otherwise index2 == self.bucket_mask + 1 + index.
//
// The very last replicated control byte is never actually read because
// we mask the initial index for unaligned loads, but we write it
// anyways because it makes the set_ctrl implementation simpler.
//
// If there are fewer buckets than Group::WIDTH then this code will
// replicate the buckets at the end of the trailing group. For example
// with 2 buckets and a group size of 4, the control bytes will look
// like this:
//
// Real | Replicated
// ---------------------------------------------
// | [A] | [B] | [EMPTY] | [EMPTY] | [A] | [B] |
// ---------------------------------------------
let index2 = ((index.wrapping_sub(Group::WIDTH)) & self.bucket_mask) + Group::WIDTH;
*self.ctrl(index) = ctrl;
*self.ctrl(index2) = ctrl; //index和index2几乎是一样,这里为什么要重复再写一次???
}
代码中:
- index是需要写入的ctrl byte数组的下标
- Group::WIDTH为16字节
- index.wrapping_sub(Group::WIDTH))是在无符号整数上做二进制减法。
- rust中存在会溢出的减法,一定要使用wrapping_sub(),否则会panic。我在这里做了个实验。
*self.ctrl(index) = ctrl;这行代码,对数组中指定下标的ctrl byte进行赋值- 疑惑的是这行:
*self.ctrl(index2) = ctrl;- 当index>=16时,index和index2的值完全相等,重复赋值,看似是没有意义的;
- 当index<16是,index2会出现在ctrl byte数组后的0~15字节中,是超过了ctrl byte数组范围的;
- 用python代码实验一下:
- 假定桶的长度为1024(hashbrown中,桶的长度一定是2的幂),则bucket_mask等于1023,也就是b01111_11111
- 假设index为2,则:
(2 - 16) & (2**10-1) + 16 = 1026 - 1026指向了ctrl byte数组尾部的第三个下标
看不懂的时候再认真读读注释:把ctrl byte数组的第一个Group复制到最后,从而避免使用分支!
现在,我们回到最初,从头开始解释这个写法:
- hashbrown的桶长度必须是2的幂,假定此处是1024个
- 分配KV数据的结构可以表示为:
struct hashbrown{
struct {
KEY_TYPE key;
VALUE_TYPE value;
} buckets[1024];
}
- hashbrown采用相邻地址法来解决hash冲突,因此需要分配一个与桶长度一致的ctrl byte数组:
- 每16个ctrl byte成为一个Group,使用SSE的指令能够一次搜索16字节,可以提升性能
- 在分配的时候,在ctrl byte数组的尾部,再多分配16字节。这16字节就是为了复制ctrl byte数组头部的16字节。
- ctrl byte数组,包含其后的16字节,都只为0x80,即 b1000_0000,最高位为1说明这个位置未使用。
- Ctrl byte数组的内容可以表示为:
struct hashbrown{
struct {
KEY_TYPE key;
VALUE_TYPE value;
} buckets[1024];
byte ctrls[1024+16];
}
为什么要把前16个ctrl byte复制在数组末位之后呢?这里涉及hashmap在插入时候搜索空桶的逻辑:
- 每次根据KEY计算出一个64位的hashcode
- hashcode 取模桶的长度得到了桶的下标
- 如果这个位置未被占用,则使用这个位置,并把ctrls数组中的对应下标写为hashcode的高7bit
- 如果这个位置被占用了,则需要从相邻的位置去寻找空位。
hashbrown(或者说swiss table)的精彩之处就在于相邻位置的查找:
- ctrls数组中连续的16字节(128bit)称为一个Group
- 搜索的时候,把128bit加载到SSE的寄存器
- 通过SSE指令可以一次性判断16字节的内容是否有空位
下面是搜索插入位置的代码:
/// Searches for an empty or deleted bucket which is suitable for inserting
/// a new element.
///
/// There must be at least 1 empty bucket in the table.
#[inline]
fn find_insert_slot(&self, hash: u64) -> usize {
let mut probe_seq = self.probe_seq(hash); //构造ProbeSeq对象,进行三角数跳跃(第一次跳跃1个,第二次(在上一次基础上)跳跃2个,第三次跳跃3个……)
loop {
unsafe { //当这一字节处于整个ctrl数组的边缘的时候,就必须在最后加一个Group,以此避免溢出
let group = Group::load(self.ctrl(probe_seq.pos)); //加载当前Group
if let Some(bit) = group.match_empty_or_deleted().lowest_set_bit() { //当前group找个空位
let result = (probe_seq.pos + bit) & self.bucket_mask; //这个就是找到的插入位置
// In tables smaller than the group width, trailing control
// bytes outside the range of the table are filled with
// EMPTY entries. These will unfortunately trigger a
// match, but once masked may point to a full bucket that
// is already occupied. We detect this situation here and
// perform a second scan starting at the beginning of the
// table. This second scan is guaranteed to find an empty
// slot (due to the load factor) before hitting the trailing
// control bytes (containing EMPTY).
if unlikely(is_full(*self.ctrl(result))) { //当桶的长度小于16时,触发这里的逻辑
debug_assert!(self.bucket_mask < Group::WIDTH);
debug_assert_ne!(probe_seq.pos, 0);
return Group::load_aligned(self.ctrl(0))
.match_empty_or_deleted()
.lowest_set_bit_nonzero();
}
return result;
}
}
probe_seq.move_next(self.bucket_mask); //找不到空位,就跳跃到下个三角数
}
} //最坏的情况,这个函数会遍历整个的ctrls数组,直到找到空位。不过上层函数保障了一定有空位
- 以上的代码解释了为什么要在ctrls数组的最后多加16个字节:假设桶长度为1024,假设当前开始load group的下标为
(1024-1)-16+1=1008,这个位置距离ctrls的末位不足一个Group。 - 一般性的思维就是:加个if判断,处于边界的时候特殊处理。而作者则是多分配了一个Group在尾部,使得按照Group加载的时候,一定不会溢出。
- 同时,每次写入ctrls数组时,前16个ctrl byte总是被复制到了最后16字节的溢出区;这样,从末位加载的Group起始包含了回绕到头部的ctrl byte的信息。作者通过复制,来解决了搜索时候的回绕,且不用if语句来做特殊判断。
- 总结一下:
- 为了解决hash冲突,使用了额外的ctrl byte数组来表示buckets的占用情况;
- 为了高效的搜索桶的占用情况,使用了以Group为单位的搜索,通过SSE指令一次搜索16个位置;
- 为了解决Group加载在边缘位置可能溢出的问题,使用了额外的16字节来作为溢出区,避免了用if去判断;
- 当搜索到末尾再回绕到头部搜索的时候,ctrls数组的前16字节在写入时就会复制到末位的溢出区;这样,回绕的时候,尾部的bit等于头部bit的内容;仍然也不需要if进行边界条件的判断。
- 重复写入一个byte是一条指令,用if语句判断边界条件也是一条指令。相比之下,大多数时候的重复写入代替了if语句,成本上与直接使用if一致,并没有浪费;并且,替代了if语句,使得不需要CPU做分支预测等工作,理论上能够提升指令cache的命中率,并提升性能。
学到一个编码技巧:用重复写入代替if判断,减少程序分支的更多相关文章
- 转:WOM 编码与一次写入型存储器的重复使用
转自:WOM 编码与一次写入型存储器的重复使用 (很有趣的算法设计)——来自 Matrix67: The Aha Moments 大神 计算机历史上,很多存储器的写入操作都是一次性的. Wikiped ...
- [css 揭秘]:CSS编码技巧
CSS编码技巧 我的github地址:https://github.com/FannieGirl/ifannie 喜欢的给我一个星吧 尽量减少代码重复 尽量减少改动时需要编辑的地方 当某些值相互依赖时 ...
- CSS编码技巧
前面的话 本文将从DRY.currentColor.inherit和合理使用简写这几方面来详细介绍CSS编码技巧 DRY DRY,即don`t repeat yourself,尽量减少代码重复 在软件 ...
- 代码优化:Java编码技巧之高效代码50例
出处: Java编码技巧之高效代码50例 1.常量&变量 1.1.直接赋值常量值,禁止声明新对象 直接赋值常量值,只是创建了一个对象引用,而这个对象引用指向常量值. 反例: Long i = ...
- 快速掌握iOS API的一个小技巧
快速掌握iOS API的一个小技巧 周银辉 iOS SDK和Developer Library中提供了各个类以及函数的帮助文档,这很棒,但要想了解整个库的大体结构(比如UIKit下有哪些类,他们的继承 ...
- jquery 实现重复点击一个元素时不重复执行效果
jquery 实现重复点击一个元素时不重复执行效果 这需要用到jquery的stop方法 实例 停止当前正在运行的动画: $("#stop").click(function(){ ...
- SQL Server获取下一个编码字符串的实现方案分割和进位
我在前一种解决方案SQL Server获取下一个编码字符实现和后一种解决方案SQL Server获取下一个编码字符实现继续重构与增强两篇博文中均提供了一种解决编码的方案,考虑良久对比以上两种方 ...
- set集合,是一个无序且不重复的元素集合
set集合,是一个无序且不重复的元素集合 class set(object): """ set() -> new empty set object ...
- Js判断一个单词是否有重复字母
今天上午刷到一道题,大体是写一个方法判断一个单词中是否有重复的字母(或者说一个字符串中是否有重复的字符).我的思路是一个字符一个字符地遍历,如果发现有重复的停止: function isIsogram ...
- 【flash】关于flash的制作透明gif的一个小技巧
关于flash的制作透明gif的一个小技巧 或者说是一个需要注意的地方 1.导出影片|gif,得到的肯定是不透明的.2.想要透明背景,必须通过发布.3.flash中想要发布gif动画的话,不能有文字, ...
随机推荐
- Apache Superset 1.2.0教程 (一)—— 安装(Windows版)
Apache Superset 是一款由 Airbnb 开源的"现代化的企业级 BI(商业智能) Web 应用程序",其通过创建和分享 dashboard,为数据分析提供了轻量级的 ...
- JAVA SQLServerException: 通过端口 1433 连接到主机 127.0.0.1 的 TCP/IP 连接失败
[2021-01-15 13:20:14.623] ERROR [Druid-ConnectionPool-Create-497208183] DruidDataSource.java:2471 - ...
- 详解 SSL(一):网址栏的小绿锁有什么意义?
随着互联网的飞速发展,用户信息泄漏.数据泄露等安全问题的事件频繁发生.这一切不一定是网站的问题,有时候可能是自己不经意间泄露了自己的信息.例如钓鱼网站就是日常生活中比较常见的,钓鱼网站和真实网站差别细 ...
- 【QT】tr()的作用
函数 tr() 全名是 QObject::tr() ,被它处理的 字符串可以 使用工具提取出来翻译成其他语言, 也就是做国际化使用. 只要记住,Qt 的最佳实践:如果你想让你的程序国际化的话,那么,所 ...
- Codeforces Round #481 (Div. 3) 经典几道思维题
A - AAA POJ - 3321 给你一颗树,支持两种操作 1.修改某一节点的权值 2.查询子树的权值(子树中节点的个数) 很显然可以用树状数组/线段树维护 B - BBB CodeForces ...
- 基于阿里云Serverless函数计算开发的疫情数据统计推送机器人
一.Serverless函数计算 什么是Serverless? 在<Serverless Architectures>中对 Serverless 是这样子定义的: Serverless w ...
- uni-app阿里图标引用
@font-face { font-family: "iconfont"; /* Project id 2566540 */ src: url('~@/static/fonts/i ...
- distributor和gateway联合实现出中继的负载均衡+故障转移
概述 freeswitch是一款简单好用的VOIP开源软交换平台. 在之前的文章,我们介绍过distributor模块实现多线路分发的配置方法,但是当线路发生故障时,distributor并不会自动跳 ...
- C#单向链表的实现
节点 public class ListNode { public ListNode(int NewValue) { Value = NewValue; } //前一个 public ListNode ...
- 接口自动化测试复习巩固第二天,管理员后端验证和接口抓包+requests实现
接口自动化测试第二天,需要用到的第三方库有os,openpyxl,json,pytest,requests 首选我们今天的目标是写出一个测试登录用例的脚本,这里我用的是分层设计,整个框架暂时被分为工具 ...
