大家好,我是 yes。
昨天表弟说有个学妹问他 Redis 为什么要用 CRC16(key) mod 16384
来计算 key 所处槽的位置,我想这 CRC 一般都是用来校验的,通过多项式转换成二进制再通过模2除法得到余数,这里用来做个 Hash 函数好像用的也没啥毛病(对于CRC不太了解的同学可以先去查查)。
于是我就去查,看看 Redis 的作者 antirez 有没有相关的介绍,这一查就查到了一位叫 mattsta 的老哥的 14 年写的文章,这老哥的文章可太有意思了,他认为 Redis 现在实现的 CRC 算法太简陋了,他有能提升 4 倍性能的增强版 CRC 算法 - CRCSpeed,因此他写了这篇文章进行了一波分析。
看完之后我马上去看了我本地 5.0 版本的源码,发现 CRC 算法并没有采纳他的增强版,还是老的实现。我又去看了最新 6.0 的版本,发现 CRC-64 改成了 CRCSpeed 的实现,在2020年4月28号提交的,提交者是 antirez,这就让我越发的好奇了,这2014到2020跨度有点大啊,到底发生了啥?
然后我又去看 mattsta 的 Github ,他还是 Redis 的 Contributor,于是我追踪了整个事情发展的历史脉络,事情越来越有意思了。我已经迫不及待地想和大家一起再来看一遍这哥们的文章,过一遍这件事。
不过我先到感谢一下我的表弟,不然我都没机会看到这篇文章,来表弟给大家打个招呼!
“大家好!我是表弟”。 好了表弟你可以走了。
表弟骂骂咧咧的退出群聊:“工具人就是工具人”。
接下来我就要用我蹩脚的英语来翻译了,如有纰漏敬请指正。
Fancy CRCing You Here
这老哥文章标题就有那味儿! Fancy CRCing You Here
,我还特意去找了我专八同学来翻译一下。
然后这老哥文章的第一句话就很为人着想。
Redis 的 CRC-64 的实现有很多人拷贝到他们的项目中使用,CRC-16 也有少量拷贝。这算法是能用啊,但是可以有更好的实现。
我看了下 Github 上有 2353 个项目用到了 Redis 的 CRC-64 实现,325 个项目用到了 Redis 的 CRC-16 实现。
简单翻一下就是:
- Redis 决定忽略吞吐量和延迟方面的提高,因为他们更喜欢像1999年那样编码。
- 代价就是他们过时的传统设计慢了40倍。大家干得漂亮啊。(不知道这老哥是笔误还是故意把4倍在开头写成40倍的,我就揣测下:),那个超链接跳过去就是标注着 antirez 写的 CRC-64 的实现,粗暴直接,我很喜欢)。
- 哎,如果现在有人写了一个代替 redis 和 memcached 的实现就好了啊。
然后老哥就开始放招。
What’s Wrong
他先说了下 CRC 本质上是无法并行的,因为下一次迭代的值取决于前一次迭代。然后又指出了 Redis 中的哪几个地方使用到 CRC。
CRC-64 用在了三个地方:
- 在跨实例迁移键时添加校验和,(并验证上述校验和)
- 为 RDB 输出添加一个校验和,用于复制和持久化(是可选项,可通过配置禁用,因为性能低)
- 用于内存测试
CRC-16 用在了一个地方:
- 作为集群中分配集群槽的键的散列函数
mattsta 接着放招:
简单翻译下:这 Redis 的实现极其简单(这个 extremely
单词我很是喜欢)。就是一个简单的查表法,然后循环一个字节一个字节比过去,时间复杂度是O(N)。
马上这位老哥就抛出如何做才好。
What’s Better
简单翻译下:我在网上乱翻了一番,想看看其他人是如何实现 CRC-64 的。但是大部分是拷贝 Redis 的(哎,恨其不争)。
然后 mattsta 发现了 stackoverflow 有个叫 Mark 的哥们自己写了个高速版 CRC-64 实现,他将 Mark 的实现和 Redis 的实现进行了对比,发现 Mark 的版本比 Redis 版本快了400% ,分别是1.6 GB/s 和400 MB/s。
原谅我的孤陋寡闻,我一直以为 pretty 和 girl 比较配,原来还能用来和 awful搭,我学到了,嗯。pretty f...。
然后 mattsta 说我们不能直接用 Mark 的实现,但是我们可以看看是Mark 的实现。
What’s Improved
mattsta 先展示了下 Redis 的实现,就是每个字节循环操作,然后查表。
于是这位老哥找到了灵感,他要自己实现一个和 Redis 匹配的 CRC 算法。