严格的别名规则背后的原理是什么?

时间:2018-12-28 12:31:26

标签: c language-lawyer c11 strict-aliasing

我目前想知道严格的别名规则背后的原理。我知道在C语言中不允许使用某些别名,并且意图是允许进行优化,但是令我惊讶的是,在定义标准时,这是跟踪类型强制转换的首选解决方案。

因此,显然以下示例违反了严格的别名规则:

uint64_t swap(uint64_t val)
{
    uint64_t copy = val;
    uint32_t *ptr = (uint32_t*)© // strict aliasing violation
    uint32_t tmp = ptr[0];
    ptr[0] = ptr[1];
    ptr[1] = tmp;
    return copy;
}

我可能是错的,但据我所知,编译器应该能够完美地跟踪类型强制转换,并避免对显式强制转换的类型进行优化(就像它避免对同类型指针进行此类优化一样) )在任何带有受影响值的调用上。

那么,我是否错过了严格的别名规则的哪些问题,导致编译器无法轻松解决以自动检测可能的优化)?

4 个答案:

答案 0 :(得分:12)

由于在此示例中,所有代码对于编译器都是可见的,因此,假设编译器可以确定请求的内容并生成所需的汇编代码。但是,证明一种理论上不需要严格的混叠规则的情况并不能证明没有其他情况需要该条件。

考虑代码是否包含:

foo(&val, ptr)

其中foo的声明为void foo(uint64_t *a, uint32_t *b);。然后,在foo内部(可能在另一个翻译单元中),编译器将无法知道ab指向同一对象(的一部分)。

那么有两种选择:一种是语言可以允许别名,在这种情况下,编译器在翻译foo时不能依靠*a*b的事实进行优化。是不同的。例如,每当将某些内容写入*b时,编译器都必须生成汇编代码以重新加载*a,因为它可能已更改。不允许进行诸如在寄存器中保留*a的副本之类的优化。

第二个选择,两个是禁止混叠(具体来说,如果程序执行此操作,则不定义行为)。在这种情况下,编译器可以根据*a*b不同的事实进行优化。

C委员会选择选项二是因为它提供了更好的性能,同时又不过度限制程序员。

答案 1 :(得分:5)

它允许编译器优化变量重载,而无需限制指针。

示例:

int f(long *L, short *S)
{
    *L=42;
    *S=43;
    return *L;
}

int g(long *restrict L, short *restrict S)
{
    *L=42;
    *S=43;
    return *L;
}

在x86_64上用gcc -O3 -fno-strict-aliasing编译:

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movq    (%rdi), %rax ; <<*L reloaded here cuz *S =43 might have changed it
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax     ; <<42 constant-propagated from *L=42 because *S=43 cannot have changed it  (because of `restrict`)
        ret

在x86_64上用gcc -O3(暗含-fstrict-alising)进行编译:

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax   ; <<same as w/ restrict
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax
        ret

https://gcc.godbolt.org/z/rQDNGt

当您使用大型阵列时,这可能会有所帮助,否则可能会导致大量不必要的重新加载。

答案 2 :(得分:1)

指定编程语言来支持标准化委员会成员认为合理,常识性的做法。人们认为,使用非常不同的类型的不同指针来别名相同的对象是不合理的,并且某些事情,编译器不必费劲地做到这一点

这样的代码:

float f(int *pi, float *pf) {
  *pi = 1;
  return *pf;
}

pipf都使用相同的地址时,其中*pf用于重新解释最近写入的*pi的位时,被视为 因此,尊敬的委员会成员(以及在他们之前C语言的设计者)并不认为要求编译器来避免常识性的程序转换(在稍微复杂一些的示例中)是适当的:

float f(int *pi, double *pf) {
  (*pi)++;
  (*pf) *= 2.;
  (*pi)++;
}

在这种情况下,允许两个指针都指向同一对象的极端情况将简化任何将增量融合在一起的情况;假设不会发生这种混叠,则允许将代码编译为:

float f(int *pi, double *pf) {
  (*pf) *= 2.;
  (*pi) += 2;
}

答案 3 :(得分:0)

N1570 p6.5p7的脚注清楚地说明了该规则的目的:说出什么时候可能出现混淆。关于为什么编写规则以禁止像您的这样不涉及别名的构造(因为使用uint32_t*的所有访问都在明显是新派生的上下文中执行来自uint64_t的说法,最有可能是因为该标准的作者认识到,只要做出真诚的努力来产生适合于低级编程的高质量实现的人,都将支持像您这样的构造(作为“流行扩展”)是否

unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (x*y) & 65535u; }

根据基本原理,即使结果在INT_MAX+1uUINT_MAX 之间,但通用实现将以与无符号算术等效的方式处理短无符号值的运算,除了当某些条件适用时。当结果强制为unsigned时,不需要特殊规则使编译器将涉及短无符号类型的表达式视为无符号,因为根据标准的作者,通用实现这样做即使没有这样的规则

该标准从未打算完全说明声称适用于任何特定目的的质量实施的所有期望。确实,它甚至不需要任何实现都适合于任何有用的目的(基本原理甚至承认“合格的”实现的质量很差,除了单个人为和无用的程序之外,就无法有意义地处理任何事情)。