为啥将 xor 与文字而不是反转一起使用(按位不)

Posted

技术标签:

【中文标题】为啥将 xor 与文字而不是反转一起使用(按位不)【英文标题】:Why use xor with a literal instead of inversion (bitwise not)为什么将 xor 与文字而不是反转一起使用(按位不) 【发布时间】:2014-04-15 16:08:04 【问题描述】:

偶遇this CRC32 code,很好奇作者为什么会选择使用

crc = crc ^ ~0U;

而不是

crc = ~crc;

据我所知,它们是等价的。

我什至在 Visual Studio 2010 中反汇编了这两个版本。

未优化构建:

    crc = crc ^ ~0U;
009D13F4  mov         eax,dword ptr [crc]  
009D13F7  xor         eax,0FFFFFFFFh  
009D13FA  mov         dword ptr [crc],eax 

    crc = ~crc;
011C13F4  mov         eax,dword ptr [crc]  
011C13F7  not         eax  
011C13F9  mov         dword ptr [crc],eax  

我也无法通过考虑每条指令花费的周期数来证明代码的合理性,因为它们都应该花费 1 个周期才能完成。事实上,xor 可能会因为必须从某个地方加载文字而受到惩罚,尽管我不确定这一点。

所以我认为这可能只是描述算法的首选方式,而不是优化......这是否正确?

编辑 1:

由于我刚刚意识到crc 变量的类型可能很重要,因此我在此处包含了整个代码(少了查找表,太大了),因此您不必点击链接。

uint32_t crc32(uint32_t crc, const void *buf, size_t size)

    const uint8_t *p;

    p = buf;
    crc = crc ^ ~0U;

    while (size--)
    
        crc = crc32_tab[(crc ^ *p++) & 0xFF] ^ (crc >> 8);
    

    return crc ^ ~0U;

编辑 2:

由于有人提出优化构建会很有趣,所以我制作了一个并将其包含在下面。

优化构建:

请注意,整个函数(包括在下面的最后一次编辑中)是内联的。

// crc = crc ^ ~0U;
    zeroCrc = 0;
    zeroCrc = crc32(zeroCrc, zeroBufferSmall, sizeof(zeroBufferSmall));
00971148  mov         ecx,14h  
0097114D  lea         edx,[ebp-40h]  
00971150  or          eax,0FFFFFFFFh  
00971153  movzx       esi,byte ptr [edx]  
00971156  xor         esi,eax  
00971158  and         esi,0FFh  
0097115E  shr         eax,8  
00971161  xor         eax,dword ptr ___defaultmatherr+4 (973018h)[esi*4]  
00971168  add         edx,ebx  
0097116A  sub         ecx,ebx  
0097116C  jne         main+153h (971153h)  
0097116E  not         eax  
00971170  mov         ebx,eax  

// crc = ~crc;
    zeroCrc = 0;
    zeroCrc = crc32(zeroCrc, zeroBufferSmall, sizeof(zeroBufferSmall));
01251148  mov         ecx,14h  
0125114D  lea         edx,[ebp-40h]  
01251150  or          eax,0FFFFFFFFh  
01251153  movzx       esi,byte ptr [edx]  
01251156  xor         esi,eax  
01251158  and         esi,0FFh  
0125115E  shr         eax,8  
01251161  xor         eax,dword ptr ___defaultmatherr+4 (1253018h)[esi*4]  
01251168  add         edx,ebx  
0125116A  sub         ecx,ebx  
0125116C  jne         main+153h (1251153h)  
0125116E  not         eax  
01251170  mov         ebx,eax  

【问题讨论】:

您介意解释一下吗,不用@nonensickle 搜索编译器? C 是一种可移植语言。把它编译成一个特定的指令集并不是争论它的有用方法。 这可能与某些体系结构没有精确的按位不存在有关吗? (例如 MIPS)也许作者想以 xor 的形式给出它,所以他们不必依赖,但是编译器决定不模拟。 xor 更通用,因此他们可能更喜欢它,以使代码对移植更友好。 由于您的反汇编代码是为 x86 编写的,值得指出的是 XOR 将设置/清除零标志,而 NOT 不会(如果您想执行按位操作,有时很有用不影响依赖于先前操作的标志的跳转条件)。现在,考虑到您不是直接编写程序集,您确实无法以有意义的方式访问此标志,所以我怀疑这是偏爱一个而不是另一个的原因。 编译时是否启用了优化?我认为它不应该在优化的构建中将 eax 写回 [crc]。 【参考方案1】:

尚未有人提及的东西;如果这段代码是在一台 16 位 unsigned int 的机器上编译的,那么这两个代码 sn-ps 是不同的

crc 被指定为 32 位无符号整数类型。 ~crc 将反转所有位,但如果 unsigned int 是 16 位,则 crc = crc ^ ~0U 将仅反转低 16 位。

我对CRC算法了解得不够多,无法知道这是故意的还是错误的,也许hivert可以澄清一下;尽管查看了 OP 发布的示例代码,但它确实对后面的循环产生了影响。

注意。很抱歉将其发布为“答案”,因为它不是答案,但它太大而无法放在评论中:)

【讨论】:

不是正确的答案,但仍然是一个非常好的观点。 +1 我确实喜欢你的观点,但 crc 是一个 uint32_t,如果 unsigned int 的大小不是 32 位并且没有替代的 unsigned 类型,则没有定义它替换它。 unsigned long 可能是uint32_t 一个答案,事实上它是正确的答案。 ^ ~0U 是一个可移植性错误。对于uint32_t,应该使用~ 并且是可移植的。例如,如果使用了unsigned long,根据 C 标准,它必须至少为 32 位,那么如果它是在具有 64 位长的平台。那么唯一正确的方法就是^ 0xffffffff。这是最便携的方法。 @MarkAdler 是的,你是对的。似乎我最初误读了答案。现在我知道unsigned int 的大小是指0U,很明显它不是可移植的代码。有趣的是,我在 Apple 的网站上找到了它……我仍然相信当前标记为正确的答案也起作用,但我将不得不更改正确的答案选择。【参考方案2】:

简短的回答是:因为它允许对所有 CRC 有一个统一的算法

原因如下:CRC的变种很多。每一个都依赖于用于欧几里得除法的 Z/Z2 多项式。通常是使用In this paper by Aram Perez 描述的算法实现的。现在,根据您使用的多项式,算法末尾有一个最终的 XOR,它取决于旨在消除某些极端情况的多项式。碰巧,对于 CRC32,这与全局 not 相同,但并非所有 CRC 都如此。 作为This web page 的证据,您可以阅读(强调我的):

考虑一个以一定数量的零位开头的消息。余数永远不会包含除零以外的任何内容,直到消息中的第一个移入其中。 这是一种危险的情况,因为以一个或多个零开头的数据包可能是完全合法的,CRC 不会注意到丢弃或添加的零。(在某些应用程序中,即使是全零数据包也可能是合法的!)消除这个弱点的简单方法是从非零余数开始。称为初始余数的参数告诉您对特定 CRC 标准使用什么值。而 crcSlow() 和 crcFast() 函数只需要做一点小改动:

crc 余数 = INITIAL_REMAINDER;

最终的 XOR 值存在的原因类似。要实现此功能,只需更改 crcSlow() 和 crcFast() 返回的值,如下所示:

返回(余数^FINAL_XOR_VALUE);

如果最终的 XOR 值由所有的 1 组成(就像在 CRC-32 标准中所做的那样),这个额外的步骤将与补充最后的余数具有相同的效果。但是,以这种方式实现它方式允许在您的特定应用程序中使用任何可能的值。

【讨论】:

技术上return !digital_update_crc32(0xffffffff, buf, len); 应该是return ~digital_update_crc32(0xffffffff, buf, len); 但我知道你的意思... @nonsensickle :对不起,我完全搞砸了你的评论。 这就解释了为什么算法是这样表达的,因此也可能是为什么代码是这样写的(正如我们中的几个人所建议的那样)。但是这个函数只实现了一个特定的CRC,而不是通用的形式。 这根本没有回答问题! (但它被接受了。)它回答了一个完全不同的问题,即 为什么 CRC 实现通常对 CRC 进行前后处理(通常使用 CRC 位的反转)。然而,这里的问题是关于 如何 在这个特定代码中编写反转。正确答案是,如果代码是可移植的,^ ~0U 是一个错误。 我认为 OP 的问题更多是关于为什么在 32 位系统上一种形式会优于另一种形式(没有意识到它们不等效),而 ^ ~0U 版本可能不那么直观比位补版本。如果您将其修复为~0UL,那么我的反对意见就会消失,OP 的问题仍然存在。【参考方案3】:

只是为了添加我自己的猜测,x ^ 0x0001 保留最后一位并翻转其他位;关闭最后一位使用x & 0xFFFEx & ~0x0001;无条件地打开最后一位使用x | 0x0001。即,如果您正在做很多位玩弄,您的手指可能会知道这些成语,然后不假思索地把它们滚出来。

【讨论】:

我认为这在其中发挥了作用,但我怀疑@hivert 说它只是更通用算法的专门化是正确的。【参考方案4】:

我认为有些人写的原因是一样的

const int zero = 0;

其他人写

const int zero = 0x00000000;

不同的人有不同的想法。甚至是基本操作。

【讨论】:

我理解你的观点我认为这个例子可能有点做作。我还没有看到有人写后者,尽管我还有时间找到它的使用实例。 :) 人们经常写'\0',而不是完全相同的0 @nonsensickle:你会在硬件工程师及其后代编写的代码中找到很多后一个示例。【参考方案5】:

我怀疑有什么深层次的原因。也许这就是作者的想法(“我将与所有的异或”),或者也许它在算法定义中是如何表达的。

【讨论】:

如果编译器在过去的 20 年里取得了长足的进步,我不能确定没有测试就没有深层原因。 @Puciek 我可以想象一个编译器为 xor 生成的代码比 not 更糟糕,但反之亦然会很奇怪。另外,我想要一些证据证明原作者进行了微优化。 好吧,我没有投反对票,因为我没有任何证据,就像你一样。我只是指出它可能是优化甚至是变通方法。

以上是关于为啥将 xor 与文字而不是反转一起使用(按位不)的主要内容,如果未能解决你的问题,请参考以下文章

为啥将@Transactional 与@Service 一起使用而不是与@Controller 一起使用

我可以将 asm 函数与 c - 字符串变量而不是字符串文字作为参数一起使用吗? [复制]

从C语言的角度重构数据结构系列(十三)-位运算

按位运算的意外结果为啥 (1 | 1 & 2) 给出 1 而不是 2?

为啥我的引导 btn 反转按钮显示为灰色而不是黑色渐变?

为啥 self 与 init 一起使用而不是 color