当问到C语言中常见的未定义行为时,人们有时会提到严格的混叠规则。 他们在谈论什么?


当前回答

通过指针强制转换(而不是使用联合)的类型双关是打破严格混叠的一个主要例子。

其他回答

在阅读了许多答案后,我觉得有必要补充一些东西:

严格的混叠(我将在后面描述)很重要,因为:

内存访问可能是昂贵的(就性能而言),这就是为什么数据在写回物理内存之前要在CPU寄存器中进行操作。 如果两个不同CPU寄存器中的数据将被写入相同的内存空间,那么当我们使用C语言编码时,我们无法预测哪些数据将“存活”下来。 在汇编中,我们手动编写CPU寄存器的加载和卸载代码,我们将知道哪些数据保持完整。但是C(谢天谢地)抽象了这个细节。

由于两个指针可以指向内存中的相同位置,这可能导致处理可能冲突的复杂代码。

这些额外的代码速度很慢,并且会损害性能,因为它执行额外的内存读/写操作,这些操作既慢又(可能)不必要。

严格混叠规则允许我们在假定两个指针不指向同一个内存块是安全的情况下避免冗余机器码(另请参阅restrict关键字)。

严格别名表示,可以安全地假设指向不同类型的指针指向内存中的不同位置。

如果编译器注意到两个指针指向不同的类型(例如,int *和float *),它会假设内存地址是不同的,并且不会防止内存地址冲突,从而导致更快的机器代码。

例如:

让我们假设下面的函数:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

为了处理a == b(两个指针都指向同一个内存)的情况,我们需要排序并测试从内存加载数据到CPU寄存器的方式,因此代码可能会像这样结束:

从内存中加载a和b。 把a加到b。 保存b并重新加载a。 (从CPU寄存器保存到内存,从内存加载到CPU寄存器)。 把b和a相加。 (从CPU寄存器)保存到内存。

步骤3非常慢,因为它需要访问物理内存。但是,为了防止a和b指向相同内存地址的情况,这是必需的。

严格的混叠将允许我们通过告诉编译器这些内存地址明显不同来防止这种情况(在这种情况下,这将允许进一步的优化,如果指针共享内存地址则无法执行)。

这可以通过两种方式告诉编译器,通过使用不同的类型来指向。例如: Void merge_two_numbers(int *a, long *b){…} 使用restrict关键字。例如: Void merge_two_ints(int * restrict a, int * restrict b){…}

现在,通过满足严格别名规则,可以避免步骤3,代码将运行得更快。

实际上,通过添加restrict关键字,整个函数可以优化为:

从内存中加载a和b。 把a加到b。 将结果保存到a和b。

这种优化在以前是不可能完成的,因为可能会发生冲突(a和b将是三倍而不是两倍)。

严格的混叠不只是指指针,它也影响引用,我为boost开发者wiki写了一篇关于它的论文,它很受欢迎,我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人们如此困惑,以及如何应对它。严格的混叠白皮书。它特别解释了为什么联合对于c++来说是危险的行为,以及为什么使用memcpy是唯一可以在C和c++之间移植的修复程序。希望这对你有帮助。

作为Doug T.已经写过的附录,在这里 是一个简单的测试用例,可能会触发GCC:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

编译gcc -O2 -o check check.c。 通常(我尝试过的大多数gcc版本)这会输出“严格的混叠问题”,因为编译器假设“h”不能与“check”函数中的“k”地址相同。因此,编译器会优化if (*h == 5),并始终调用printf。

对于那些感兴趣的人,这里有x64汇编代码,由gcc 4.6.3生成,运行在ubuntu 12.04.2的x64上:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

所以if条件从汇编代码中完全消失了。

严格的混叠是不允许不同的指针类型指向相同的数据。

本文将帮助您全面详细地理解这个问题。

通过指针强制转换(而不是使用联合)的类型双关是打破严格混叠的一个主要例子。