这将可以说是博客上更无聊的帖子之一,但是我认为一些敏锐的眼睛可能会提取一些有用的东西,即使不是技术。
注意 - 如果您想仔细查看划痕项目,您可以在此处找到它。
几个月后,我开始阅读理查德汉明,&#34的后期读一本有趣的书;做科学与工程的艺术:学习学习"
它是一本好书,但我们只会在汉明代码(和数字过滤器上的四个)中获取一章;然而,早期的章节肯定是一个读。真的而且真的很难理解大多数情况,但我确实喜欢学习的叙述,如何学习,以及如何发现汉明代码的故事。
由于我对数字过滤器的兴趣是,并且有些不存在的我决定汉明的代码将是一个很好的地方,了解几个有趣的主题和工具的一些原因:
通过这些特征,我希望探索分析,基准测试,装配,并发性,SIMD和整体位的几个区域,并在一个地方逐渐完成。
我将备用汉明代码的详细解释,因为已经在那里有很好的工作,以及根据硬件和软件接近问题的不同方式。也就是说,对于这两种情况来说,我分别强烈推荐两个视频3Blue1Blown和Ben Eater。
在这项特定的任务中,我希望能够做一件事并快速做。编码,添加噪声和解码二进制文件(是的,技术上它'三件事)。以下是一些尝试编码&amp的初始版本;解码Lorem IPSUM文本文件。
嗯,至少是它' s大致相等的痛苦。也许我得到了字节切片都错了?
似乎我做了,而不是修复它,我让它变得更糟。一些对齐问题,算法问题和endianness问题后来我们设法获得了一些基本文本编码&解码工作将与任何二进制文件(理论上)转化。
时间在按块的基础上应用一些合成噪声,使用66.67%的机会翻转单一。在现实世界场景中,二进制块是交错的,以减少单个块的机会具有超过1个汉明无法处理的错误错误。
这样的原因是,当在传输中有一点损坏时,它往往发生在短脉冲中是几个相邻的比特完全翻转。交织块对这种情况构成弹性。
我试着在一些图像中努力,以某种方式制作了诅咒的猫看起来不那么可怕。它' s一个不是bug的功能。
事实证明,我的文件阅读器只能处理到达,究竟,7168字节?我决定现在不用担心这个并继续前进;我' D非常乐意反馈替代方式来写这个。
在我收到问题的关键并查看其他类似的实施方面,它花了一些后面的一个人的干燥运行,我们缩小了一些工作的东西。我想要一些简单的东西,可以优化,所以我们降落了以下初步实施。
我们将介绍每个组件,(a)编码器,(b)解码器和(c)奇偶校验,独立检查,以避免上下文切换过多并简化改进。
初始编码器是一个稍微的位置,但可以说是该实现的最简单部分。我们需要计算多少个奇偶校验位,Len_Power,我们需要(log2 {64位} = 6),并且从该导出数据宽度,len(2 ^ 6 = 64);您将注意到这一点不需要这种整个舞蹈。
PUB FN编码(块:& mut U64) - > U64 {让Len_Power =(2 ..)。找到(|& R | 2u32.pow(r) - r - 1> = 32).unwrap();让Len = 2usize.pow(len_power); // ......}
这些是编码器需要知道的唯一参数,然后在我们开始编码我们的消息之前需要了解。从1001开始作为我们的示例输入消息,我们首先通过每个位,看看该位的索引是否是2的功率,因此是一个奇偶校验位。如果它是2的力量,则将输入消息左转(即跳过当前位),否则我们将该位插入到编码的消息中。
PUB FN编码(块:& mut U64) - > U64 {// ...让mut code = 0u64;因为我在0..len {//检查'i`如果(i!= 0)&& (I&(i - 1))!= 0 {code | =(0b1< i)& *块作为U64; }否则{*块<< = 1; }} // ......}
最后,我们通过每个奇偶校验位进行计算剩余的四个奇偶校验位,并将它们插入最终编码的消息。
PUB FN编码(块:& mut U64) - > U64 {// ...为我在0..len_power {//,如果奇偶校验检查是奇数,则将该位设置为1以否则继续前进。如果!奇偶校验(&代码,i){code | = 0b1<< (2usize.pow(i));编码}
如果我们要以图形方式思考,它可能看起来像这样,(1)我们得到了我们的原始输入,(2)我们将原始输入映射到编码输出作为其数据位的一部分和(3)我们计算的奇偶校验位并将其映射到编码输出作为奇偶校验位。
让'测试实施;结果与预期的输出和通过简单的基准(标准)匹配,不会产生有希望的结果。我们的基准中位数执行时间为430.44ns,为我们的编码器具有大量奇数异常值。我们现在有一些我们可以改进的东西。
有一些明确的胜利我们可以在这里合作。对于初学者来说,自从我们知道我们'重新始终使用U64(或任何固定宽度块大小),每次想要编码块时都不需要计算Len_Power和Len。让' s还删除了不需要的内部编码变量的副本。
PUB FN编码(块:& mut U64) - > U64 {让Len_Power = 6;让Len = 64;让mut code = 0u64; // ...删除“编码”,并仅在“代码”上(即可在“I //”奇偶校验是奇数的情况下,将该位设置为1,以否则将其设置为“。如果!奇偶校验(&代码,i){code | = 0b1<< (2usize.pow(i) - 1); } } 代码}
重新运行我们的基准,我们现在在361.27ns的中位时间运行,性能增加〜17.5%。我们仍然看到了一些异常值,但平均执行时间现在捆绑更好。
在这个阶段,我们的编码器中还剩两个分支条件,我将在很大程度上离开,以避免早期优化。让' s型材(通过perf)和plot(通过函数)我们的编码器,并在整个执行时间迈出峰值。
此SVG被手动编辑以节省空间并保持交互式。它并不好玩,该样品中的样品频率低,但它很好地转化为更大的样品速率(〜99Hz)。
这里的主要罪魁祸首是奇偶校验函数以总编码器执行时间的大约〜75%。它明确了我们的奇偶函数isn' t真的做得太大,所以下一个自然步骤是优化它会对整个实施产生积极影响。返回介绍,这是我的意思是由共享组件的意思。
让'逐渐落下了解实现,了解我们如何优化我们的实现。奇偶校验检查器' S作业是确定某个位序列是否是奇数(1),甚至(0)。回想一下我们的编码消息由数据位和奇偶校验位组成。
如果我们实际上看看奇偶校验位的索引,它们始终是2.所以我们的一组奇偶校验位P可以从P = {P1,P2,P3,P4}重写为P = {0001 ,0010,0100,1000}在那里,直观而优雅的模式出现 -
P1 = 2 ^ 0 = 1因此检查1位,然后跳过1位。 P2 = 2 ^ 1 = 2检查2位,然后跳过两位。 P3检查每4位然后跳过4位,最后p4每8位检查每8位,然后跳过8位。
注意 - 立即忽略第0位。这将是一个全局奇偶校验位,一部分扩展的汉明代码,检查奇偶校验所有位。我们' ll回到这个。
这种可视化有助于显示奇偶校验比特如何扩展和交错,以最大化其所有二进制块的覆盖范围。
我们有许多情况下我们不需要检查每一位。例如:
奇偶校验位P4从第8元开始,因此不需要检查前8位(我们可能会跳转);
不要逐步延迟每位迭代,而是跳过与给定奇偶校验位无关的位。如果P2检查0010和0011并跳过0100和0101,则不需要迭代后两个。
看看我们的奇偶校验检查,它可能看起来更涉及我们的编码器。如果我们将其与上面的点进行比较,我们'重新开始从第0个索引开始循环,而是从与给定奇偶校验相关的第一个比特。我们不那么做的一件事是这个实现越过每位,然后确定我们是否应该忽略它,这是昂贵的。
fn奇偶校验(代码:& u64,i:u32) - > BOOL {假设0B1< i) - 1;让(mut奇偶校验,mut忽略,mut计数器)=(true,false,0);对于Bi的J.64 {If!Ignore&& (代码& 0b1< j)!= 0b0 {parity =!parity; }计数器+ = 1;如果计数器> = 0b1<我{忽略=!忽略;计数器= 0;如果偶数}}}}奇偶校验//
看看潜在的汇编(通过锈旗上的货物ASM,我们已经看到了,我们看到了很多事情,主要是因为所有的柜台和忽略了我们上面提到的检查。
我最初的印象是,没有很多分支继续,但我们的循环只是比他们需要的长。让'我们看看我们是否可以编写实际跳过的跳过功能,它不需要计算(而不是计算,然后决定它应该跳过)。在一些背后的干燥运行后,我将其煮沸到以下,通过测试。
PUB FN奇偶校验(代码:& U64,I:U32) - > bool {让mut parity = true;让传播= 2u32.pow(i);让mut j =传播; j< 64 - 扩展+ 1 {对于0.spread {if(代码& 0b1< j + k)!= 0b0 {parity =!parity; }} J + = 2 *传播;奇偶校验}
我们消除忽略和计数器变量,而是根据我们正在检查的奇偶校验位跳过比特。每当我们登陆我们应该检查的比特块时,我们都会运行内部循环以扫描整个块,由某些传播(即,我们应该计算的连续比特数量)并随后跳转到下一个索引。
发射的组装仅略微减少,但更简单,更快地导致中值执行时间为3.76ns而不是72.23ns。
请记住,我们当前的奇偶校验检查我们没有覆盖的第0位? ' s一个特殊的奇偶校验位,用于检查整个块的奇偶校验(包括所有奇偶校验和数据位)。这被称为延伸的汉明码;在能够纠正单个比特错误之上,它还允许我们检测(但不正确)两位错误。让'尝试使用难度的实现来实现这一目标,该实现超过每位并计算全局奇偶校验。
PUB FN SLOW_PARTY(代码:U64) - > bool {让mut parity = true;对于我在0..63 {如果代码& 0b1<<<我!= 0 {parity =!奇偶校验; }}奇偶校验}
通过我们的基准运行我们可以看到这需要7.7907ns的中位数执行时间 - 不是很好。
慢速检查时间:[7.7806 ns 7.7907 ns 7.8013 ns]在100次测量中找到12个异常值(12.00%)8(8.00%)高温4(4.00%)高严重
在亨利S.沃伦JR的情况下,有一种更直观地解决这本书,黑客'令人愉快的乐趣(第2卷,第96卷)。如果你以前没有举行这本书,我真的无法推荐它。它'是一个金矿。这里'相关部分的摘录。
Let' s实现这一点,执行滚动XOR并采取右边的位,扩展到64位块而不是作者' s原始32位块。
PUB FN Fast_Parity(代码:U64) - > U64 {让Mut Y:U64 =代码^(代码> 1); y ^ = y>> 2; y ^ = y>> 4; y ^ = y>> 8; y ^ = y>> 16; y ^ = y>> 32; 0B1& y}
差异非常鲜明。通过我们的基准测试我们可以看到快速奇偶校验检查以937ps(其中1ps = 0.001ns)中值执行时间与7.7907ns的中值执行时间,转化为大约8300%的改进。
Fast_Parity检查时间:[937.54 PS 937.86 PS 938.26 PS]在100次测量中找到13个异常值(13.00%)5(5.00%)高温8(8.00%)High SevereSlow_Parity检查时间:[7.7806 NS 7.7907 NS 7.8013 NS]找到了12个异常值在100次测量(12.00%)中,8(8.00%)高温4(4.00%)高严重
如果我们比较生成的汇编代码,我们可以偷看如何通过编译器优化实现这一结果。
我们现在可以在返回非常底部的编码消息之前轻松使用Fast_Parity检查。
再次运行我们的基准测试,再次为编码器和解码器产生一些有希望的改进。
汉明编码时间:[153.33 ns 153.38 ns 153.43 ns]变化:[-57.763%-57.720%-57.680%](p = 0.05)性能提高了。[118.57 ns 118.60 ns 118.65 ns]变化:[-72.866%-72.823%-72.782%](P = 0.05)性能提高。
如果我们将其与以前的实现对比,我们可以看到更快的执行时间以及更少的异常值更快的执行时间以及更多的稳定性(由密度决定)。
在此阶段,这应该涵盖算法视角的大部分改进。可能有一些空间可以进一步优化整体奇偶校验者(不是快速全球的空间),但我们可以稍后获得。
我们实现的其余部分是解码器。我不会像实施非常简单一样详细融洽 -
如果有任何奇数阶段,则重新计算每个奇偶校验位,然后我们至少有一个错误;
如果和#34;检查"值大于0,我们有一个错误。翻转对应于&#34的比特;检查"值(例如,如果错误检查为10110,请在第11索引中翻转该位);
PUB FN解码(代码:& mut U64) - > U64 {让Len_Power = 6;让Len = 64;让mut检查= 0b0;对于我在0..len_power {if!奇偶校验(& code,i){check | = 0b1<一世; }} //如果检查> 0b0 {*代码^ = 0b1<<查看; } //删除所有奇偶校验位让mut offset = 0;让mut解码= 0b0;因为我在0..len {//检查'i`如果(i!= 0)&& (I&(I - 1))!= 0 {解码| =((0b1< i)& *代码)>>抵消; } else {offset + = 1;解码}
还有别的别人别的东西在这里添加,但我' ll仍然探索一些加速这一点的方法。请记住,现在没有全球奇偶校验。
这篇文章需要一段时间才能写作,但它旨在展示如何解决问题,并稍后优化它。实现不是功能完整的,有几个边界检查丢失,我们没有检查和测试解码器中的全局奇偶校验位 - 它'意图是逐步的学习练习。
我正在考虑扩展该解决方案,以通过SIMD和/或并行工作负载从工作负载分配立场优化它。如果我们使用较窄或更广泛的数据块(即256位)会发生什么?我们可以通过使用真实世界的场景来计算第二个我们可以使用此编码器来处理多少字节?什么'是预期的行业标准,我们可以接近(也许甚至利用架构具体说明)。
如果你对此感兴趣或有任何反馈' d喜欢通过,随意伸出援手,我喜欢听你的意见。