当前位置:  开发笔记 > 编程语言 > 正文

什么是严格别名规则?

如何解决《什么是严格别名规则?》经验,为你挑选了10个好方法。

当询问C中常见的未定义行为时,灵魂比我提到的严格别名规则更加开明.
他们在说什么?



1> Doug T...:

遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区(如指向uint32_ts或uint16_ts 的指针).当您通过指针转换将结构重叠到此类缓冲区或缓冲区到此类结构上时,您很容易违反严格的别名规则.

所以在这种设置中,如果我想发送消息,我必须有两个不兼容的指针指向同一块内存.我可能会天真地编写这样的代码:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

严格的混叠规则使该设置非法的:解引用该别名的对象是不是一个的指针兼容型或其他类型的被C 2011 6.5第7段允许的一个1是未定义的行为.不幸的是,您仍然可以通过这种方式编写代码,可能会收到一些警告,让它编译正常,只有在运行代码时才会出现奇怪的意外行为.

(海湾合作委员会在提供别名警告的能力方面似乎有些不一致,有时会给我们一个友好的警告,有时却没有.)

要了解为什么这种行为是未定义的,我们必须考虑严格别名规则购买编译器的原因.基本上,使用此规则,它不必考虑插入指令来刷新buff循环的每次运行的内容.相反,在优化时,通过一些关于别名的恼人的非强制性假设,它可以在循环运行之前省略那些指令,加载buff[0]buff[1进入CPU寄存器,并加速循环体.在引入严格别名之前,编译器必须处于偏执状态,buff任何人都可以随时随地改变内容.因此,为了获得额外的性能优势,并假设大多数人没有打字指针,引入了严格的别名规则.

请记住,如果您认为该示例是人为的,如果您将缓冲区传递给另一个为您执行发送的函数,则可能会发生这种情况.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

并重写了我们之前的循环,以利用这个方便的功能

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

编译器可能或可能不能够足够聪明地尝试内联SendMessage,它可能会也可能不会决定加载或不再加载buff.如果SendMessage是另一个单独编译的API的一部分,它可能有加载buff内容的指令.然后,也许你是在C++中,这是一些模板化的头只有实现,编译器认为它可以内联.或者它可能只是您在.c文件中编写的内容,以方便您使用.无论如何,未定义的行为仍可能随之而来.即使我们知道幕后发生的一些事情,它仍然违反了规则,因此没有明确定义的行为得到保证.所以只需通过包装一个函数来获取我们的单词分隔缓冲区并不一定有帮助.

那么我该如何解决这个问题呢?

使用工会.大多数编译器都支持这一点而不抱怨严格的别名.这在C99中是允许的,并且在C11中明确允许.

union {
    Msg msg;
    unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};

您可以在编译器中禁用严格别名(在gcc中f [no-] strict-aliasing))

您可以使用char*别名而不是系统的单词.规则允许char*(包括signed charunsigned char)的例外.它总是假设char*其他类型别名.然而,这不会以另一种方式起作用:没有假设你的结构别名为chars的缓冲区.

初学者要小心

当两种类型相互叠加时,这只是一个潜在的雷区.您还应该了解字节顺序,字对齐以及如何通过正确打包结构来处理对齐问题.

脚注

1 C 2011 6.5 7允许左值访问的类型有:

与对象的有效类型兼容的类型,

与对象的有效类型兼容的类型的限定版本,

与对象的有效类型对应的有符号或无符号类型的类型,

与有效类型的对象的限定版本对应的有符号或无符号类型的类型,

聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者

一个字符类型.


@Matthieu:Signedness对别名规则没有区别,所以使用`unsigned char*`是可以的.
Bollocks,这个答案是*完全倒退*.它显示为非法的例子实际上是合法的,它显示为合法的例子实际上是非法的.
从工会成员读取不同于最后一个写入的行为成员是不是未定义的行为?
我似乎在战斗之后来了......可能``unsigned char*`远远用于`char*`?我倾向于使用`unsigned char`而不是`char`作为`byte`的基础类型,因为我的字节没有签名,我不希望签名行为的怪异(特别是wrt溢出)
你的`uint32_t*buff = malloc(sizeof(Msg));`和后续的联合`unsigned int asBuffer [sizeof(Msg)];`缓冲区声明将有不同的大小,两者都不正确.`malloc`调用依赖于引擎盖下的4字节对齐(不要这样做),并且联合将比它需要的大4倍...我明白它是为了清晰但它让我烦恼-the少...
@nubok:[Ben Voigt](http://stackoverflow.com/users/103167/ben-voigt)查看[答案](http://stackoverflow.com/a/7005988/15168).C标准的相关段落明确地说'字符类型',并没有提到'`void`'或'`void` pointer'.
@Grzegorz:编译时使用`-fno-strict-aliasing`,但是你没有使用C(或C++).无论如何,评论中的两段代码都是错误的.第二个甚至不会编译,因为你在初始化中有不匹配的间接.
这个例子不清楚.它显示`unsigned int`别名为`uint32_t`.然而,它是实现定义的`unsigned int`是否与`uint32_t`兼容.如果`uint32_t`是`unsigned int`的typedef,那么代码实际上没有严格的别名违规,否则就是.我建议编辑它,以便这两种类型明显不兼容.
根据我对http://stackoverflow.com/questions/6359629/union-hack-for-endian-testing-and-byte-swapping/6359836#6359836的评论:_C99表示与最后一个字节不对应的字节存储成员采用未指定的值(可能暗示从与最后存储的成员共享其所有字节的成员读取是合法的),但随后列出从最后存储为UB的成员读取的成员.C1X改变了这一点,从与最后存储的成员不对应的字节定义为UB读数._
为了解决@ nonsensickle的观察,union声明应该有这个成员声明:`unsigned int asBuffer [sizeof(Msg)/ sizeof(unsigned int)]`.
随着时间的推移,这个例子已经漂移了,现在需要清理它.声明SendWord采用指向uint32_t的指针,但随后在uint32_t上调用它.并且推荐"//从系统获取32位缓冲区"在sizeof(Msg)的分配之前,在大多数流行的实现上将是64位.我不知道哪些错误是故意的,指出违反严格的别名,哪些是意外.意图是什么意思?

2> Niall..:

我发现的最佳解释是Mike Acton,了解严格别名.它主要关注PS3开发,但这基本上只是GCC.

来自文章:

"严格别名是由C(或C++)编译器做出的一个假设,即取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名)."

所以基本上如果你有一个int*指向包含一个内存的内存int然后你指向一个float*内存并将其用作float你打破规则.如果您的代码不遵守这一点,那么编译器的优化器很可能会破坏您的代码.

规则的例外是a char*,允许指向任何类型.


@davmac:C89的作者从未打算过强迫程序员跳过篮球.我发现,为了优化的唯一目的而存在的规则应该以这样的方式解释,即要求程序员编写冗余复制数据的代码,以期优化器将删除冗余代码.
那么,使用2种不同类型的变量合法使用相同内存的规范方法是什么?或者每个人都只是复制?
Mike Acton的页面存在缺陷.至少,"通过工会(2)铸造"的部分是完全错误的; 他声称合法的代码不是.
@curiousguy:错.首先,工会背后的原始概念是,在任何时刻,在给定的联合对象中只有一个**成员对象"活动",而其他对象根本就不存在.因此,您似乎并不相信"同一地址的不同对象".其次,每个人都在谈论的别名违规是关于**将一个对象作为一个不同的对象访问**,而不仅仅是*拥有*具有相同地址的两个对象.只要没有类型 - 惩罚**访问**,就没有问题.这是最初的想法.后来,允许通过工会打字.

3> Ben Voigt..:

这是严格的别名规则,可以在C++ 03标准的3.10节中找到(其他答案提供了很好的解释,但没有提供规则本身):

如果程序试图通过不同于以下类型之一的左值访问对象的存储值,则行为未定义:

对象的动态类型,

一个cv限定版本的动态类型的对象,

与对象的动态类型对应的有符号或无符号类型的类型,

一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,

一种聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),

一个类型,它是对象动态类型的(可能是cv限定的)基类类型,

a charunsigned char类型.

C++ 11C++ 14措辞(强调变化):

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

对象的动态类型,

一个cv限定版本的动态类型的对象,

与对象的动态类型类似的类型(如4.4中所定义),

与对象的动态类型对应的有符号或无符号类型的类型,

一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,

聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),

一个类型,它是对象动态类型的(可能是cv限定的)基类类型,

a charunsigned char类型.

两个变化很小:glvalue而不是lvalue,以及聚合/联合案例的澄清.

第三个变化提供了更强有力的保证(放宽强混叠规则):类似类型的新概念现在可以安全别名.


另外,Ç措词(C99; ISO/IEC 9899:1999 6.5/7;完全相同的措词在ISO/IEC 9899中使用:2011§6.57):

对象的存储值只能由具有以下类型之一(73)或88)的左值表达式访问:

与对象的有效类型兼容的类型,

与对象的有效类型兼容的类型的限定版本,

与对象的有效类型对应的有符号或无符号类型的类型,

与有效类型的对象的限定版本对应的有符号或无符号类型的类型,

聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者

一个字符类型.

73)或88)此列表的目的是指定对象可能或可能不具有别名的情况.


Ben,因为人们经常被引导到这里,为了完整起见,我也允许自己添加对C标准的引用.
@supercat:结构规则的措辞方式,实际访问始终是对原始类型的访问。然后通过引用原始类型进行访问是合法的,因为类型匹配,而通过引用包含结构类型的访问则是合法的,因为这是特别允许的。

4> Shafik Yaghm..:

注意

这摘自我的"什么是严格的别名规则以及我们为什么关心?" 写上去.

什么是严格的别名?

在C和C++中,别名与我们允许通过哪些表达式类型访问存储的值有关.在C和C++中,标准指定允许哪些表达式类型为哪些类型设置别名.允许编译器和优化器假设我们严格遵循别名规则,因此术语严格别名规则.如果我们尝试使用不允许的类型访问值,则将其归类为未定义行为(UB).一旦我们有未定义的行为,所有的赌注都会被取消,我们的计划结果将不再可靠.

不幸的是,由于严格的别名违规,我们经常会得到我们期望的结果,而新的优化版本的编译器的未来版本可能会破坏我们认为有效的代码.这是不可取的,理解严格的别名规则以及如何避免违反它们是一个值得的目标.

为了更多地了解我们关心的原因,我们将讨论在违反严格别名规则时出现的问题,打字,因为类型惩罚中使用的常用技术经常违反严格的别名规则以及如何正确输入双关语.

初步的例子

让我们看一些例子,然后我们可以准确地讨论标准的含义,检查一些进一步的例子,然后看看如何避免严格的别名并捕捉我们错过的违规行为.这是一个不应该令人惊讶的例子(实例):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

我们有一个INT*指向被占用的内存INT这是一个有效的别名.优化器必须假设通过ip分配可以更新x占用的值.

下一个示例显示了导致未定义行为的别名(实例):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

在函数foo中我们采用int*float*,在这个例子中我们调用foo并将两个参数设置为指向同一个内存位置,在本例中包含一个int.注意,reinterpret_cast告诉编译器将表达式视为具有由其template参数指定的类型.在这种情况下,我们告诉它将表达式&x视为类型为float*.我们可以天真地期待第二的结果COUT0,但与优化使用支持-02 GCC和铛产生如下结果:

0
1

这可能不是预期的,但完全有效,因为我们调用了未定义的行为.甲浮子不能有效别名一个INT对象.因此,优化器可以假定常数1解除引用的时候存储将返回值因为通过商店˚F不能有效影响的INT对象.在Compiler Explorer中插入代码显示这正是发生的事情(实例):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

使用基于类型的别名分析(TBAA)的优化器假定将返回1并直接将常量值移动到带有返回值的寄存器eax中.TBAA使用有关允许别名的类型的语言规则来优化加载和存储.在这种情况下,TBAA知道float不能别名和int并且优化i的负载.

现在,到规则手册

该标准究竟是什么意思我们被允许而且不允许这样做?标准语言并不简单,因此对于每个项目,我将尝试提供演示其含义的代码示例.

C11标准说什么?

C11标准说,在节以下6.5表达式第7段:

对象的存储值只能由具有以下类型之一的左值表达式访问:88) - 与对象的有效类型兼容的类型,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- 与对象的有效类型兼容的类型的限定版本,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- 对应于对象的有效类型的有符号或无符号类型,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang有一个扩展,并且允许将unsigned int*赋值给int*,即使它们不是兼容类型.

- 对应于对象有效类型的限定版本的有符号或无符号类型,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- 角色类型.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

C++ 17草案标准所说的内容

[basic.lval]第11段中的C++ 17草案标准说:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为是未定义的:63 (11.1) - 对象的动态类型,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - 对象的动态类型的cv限定版本,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - 与对象的动态类型类似(如7.5中所定义)的类型,

(11.4) - 对应于对象动态类型的有符号或无符号类型,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - 对应于对象动态类型的cv限定版本的有符号或无符号类型,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - 一种聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - 一种类型,是对象的动态类型的(可能是cv限定的)基类类型,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - char,unsigned char或std :: byte类型.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

值得注意的是,上面列表中没有包含已签名的字符,这与C表示字符类型的显着区别.

什么是Punning类型

我们已经达到了这一点,我们可能想知道,为什么我们要为别名?答案通常是输入pun,通常使用的方法违反了严格的别名规则.

有时我们想要绕过类型系统并将对象解释为不同的类型.这称为类型双关,将一段内存重新解释为另一种类型.对于希望访问对象的基础表示以进行查看,传输或操作的任务,类型惩罚非常有用.我们发现使用类型惩罚的典型区域是编译器,序列化,网络代码等......

传统上,这是通过获取对象的地址,将其转换为我们想要将其重新解释为的类型的指针,然后访问该值,或者换句话说通过别名来实现的.例如:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

正如我们之前看到的,这不是一个有效的别名,所以我们正在调用未定义的行为.但是传统的编译器并没有利用严格的别名规则,这种类型的代码通常只是起作用,不幸的是开发人员习惯于这样做.类型惩罚的常见替代方法是通过联合,它在C中有效但在C++中是未定义的行为(参见实例):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

这在C++中无效,有些人认为联合的目的仅仅是为了实现变体类型,并且认为使用联合进行类型惩罚是一种滥用.

我们如何正确打字?

C和C++中类型双关语的标准方法是memcpy.这可能看起来有点沉重,但优化器应该认识到使用memcpy进行类型惩罚并优化它并生成寄存器来注册移动.例如,如果我们知道int64_tdouble的大小相同:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

我们可以使用memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

在足够的优化级别,任何体面的现代编译器都会生成与前面提到的reinterpret_cast方法相同的代码或用于类型惩罚的联合方法.检查生成的代码,我们看到它只使用寄存器mov(实时编译器资源管理器示例).

C++ 20和bit_cast

在C++ 20中,我们可以获得bit_cast(可以从提案链接中获得实现),它提供了一种简单而安全的类型 - 双关语以及在constexpr上下文中可用的方法.

以下是如何使用bit_cast将一个unsigned int类型设为float的示例(请参见实时):

std::cout << bit_cast(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

ToFrom类型不具有相同大小的情况下,它要求我们使用中间struct15.我们将使用包含sizeof(unsigned int)字符数组(假设4字节无符号int)的结构作为From类型,使用unsigned int作为To类型:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast(f);

   result += foo( result );
 }

 return result ;
}

不幸的是我们需要这种中间类型,但这是bit_cast的当前约束.

捕获严格的别名违规行为

我们没有很多很好的工具来捕获C++中的严格别名,我们的工具将会遇到一些严格的别名违规情况以及一些未对齐的加载和存储的情况.

gcc使用标志-fstrict-aliasing-Wstrict-aliasing可以捕获一些情况,尽管不是没有误报/否定.例如,以下情况将在gcc中生成警告(请参见实时):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast(&a)));
printf("%i\n", j = *(reinterpret_cast(&f)));

虽然它不会捕捉这个额外的情况(见它直播):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast(p)));

尽管clang允许使用这些标志,但它实际上并没有实现警告.

我们可以使用的另一个工具是ASan,它可以捕获未对齐的载荷和存储.虽然这些不是直接严格的别名冲突,但它们是严格别名冲突的常见结果.例如,使用-fsanitize = address使用clang构建时,以下情况将生成运行时错误

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

我将推荐的最后一个工具是C++特定的,并不是严格意义上的工具,而是编码实践,不允许使用C风格的转换.gcc和clang都将使用-Wold-style-cast为C风格的强制转换生成诊断.这将强制任何未定义类型的双关语使用reinterpret_cast,通常reinterpret_cast应该是更密切的代码审查的标志.搜索代码库以进行reinterpret_cast以执行审计也更容易.

对于C,我们已经涵盖了所有工具,我们还有一个tis-interpreter,一个静态分析器,可以详尽地分析C语言的大部分子程序.给出前面示例的C版本,其中使用-fstrict-aliasing错过了一个案例(请参见实时)

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis -interpeter能够捕获所有三个,下面的例子调用tis-kernal作为tis-interpreter(输出为简洁而编辑):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

最后还有TySan,目前正在开发中.此清理程序在影子内存段中添加类型检查信息,并检查访问以查看它们是否违反了别名规则.该工具可能应该能够捕获所有别名冲突,但可能会产生大量的运行时开销.


如果可以的话,+ 10,编写和解释得很好,也来自双方,编译器编写者和程序员...唯一的批评:上面有反例,看看标准禁止的内容会很好,不明显的种类 :-)

5> phorgan1..:

严格别名不仅仅指向指针,它也会影响引用,我为boost开发人员wiki写了一篇关于它的文章,并且它很受欢迎,我把它变成了我咨询网站上的一个页面.它完全解释了它是什么,为什么它如此混淆了人们以及如何处理它.严格别名白皮书.特别是它解释了为什么联合会是C++的危险行为,以及为什么使用memcpy是C和C++中唯一可移植的修复.希望这是有帮助的.


好纸.我的观点:(1)这种混淆 - '问题'是对糟糕编程的过度反应 - 试图保护坏程序员免受他/她的坏习惯.如果程序员有良好的习惯,那么这种混叠只是一种麻烦,可以安全地关闭检查.(2)编译器端优化只应在众所周知的情况下进行,并且如果有疑问则严格遵循源代码; 强迫程序员编写代码以满足编译器的特性,简单地说,错误.更糟糕的是使其成为标准的一部分.
@curiousguy:清除了一些混淆点后,很明显带有别名规则的C语言使程序无法实现与类型无关的内存池.某些类型的程序可以通过malloc/free获得,但其他程序需要更好地针对手头的任务量身定制的内存管理逻辑.我想知道为什么C89基本原理使用了这样一个关于别名规则的原因的例子,因为他们的例子使得看起来规则不会给执行任何合理的任务带来任何重大困难.
@curiousguy,大多数编译器套件在-O3上都包含-fstrict-aliasing作为默认值,这个隐藏的契约强制给那些从未听说过TBAA的用户,并编写代码就像系统程序员一样.我并不是说对系统程序员来说是不诚实的,但是这种优化应该保留在-O3的默认选项之外,对于那些知道TBAA的人来说应该是一个选择优化.查看编译器"bug"并不是很有趣,因为用户代码违反了TBAA,特别是在用户代码中跟踪源级别违规.
@slashmais(1)"_是对糟糕编程的过度反应_"废话.这是对坏习惯的拒绝.*你做吧?你支付的价格:不保证你!*(2)众所周知的案件?哪个?严格的别名规则应该是"众所周知的"!
"_Strict别名不仅仅指向指针,它也会影响引用_"实际上,它指的是**左值**."_使用memcpy是唯一的便携式修复_"听到了!
本文的"另一个破碎的版本,引用两次"部分没有任何意义.即使有一个序列点,它也不会给出正确的结果.也许您打算使用班次操作员而不是班次分配?但是代码定义明确,做得对.
@slashmais:我怀疑现代C的大部分可怕性源于这样一种信念,即在语言中添加新的指令和功能以告诉编译器它应该做出哪些推断是不好的,但更好地使用某些语言功能调用推断,这些推理虽然符合编译器必须执行的基线要求,但会破坏依赖于许多实现历史上很少或没有成本提供的常见且有用的扩展的代码.
@curiousguy:虽然我并不赞成导致各种版本标准的所有讨论,但我怀疑一个主要问题是政治上需要避免宣布任何成员的实施"非规范性".拥有一种可以在符号级机器上实现的语言是有用的,这种语言允许它运行用C语言编写的大部分代码.由于左移的意义,因此它的用处远没那么有用.负数或将超大的`无符号值转换为`int`在这些机器上没有明确定义,标准应该强加......
......即使在二合一机器上也没有要求这种操作的行为.此外,在许多情况下,在2009年前编译器上工作的代码总数达到99%以上的代码可以比任何必须满足相同要求的代码更快,更易读,这些代码在输入任何实际或理论上符合标准的编译器时.未能为广泛支持的行为定义规范标准不利于源代码可读性和机器代码效率 - 具有讽刺意味的是,超现代主义的假设理由是"优化".
@kchoi:至少,编译器应该默认识别某些表明可能存在别名的模式; 例如,如果将`T*`强制转换为'U*`,请确保在强制转换之前对`T*`的任何操作也先于对所得到的`U*`的任何使用,以及对`T*的任何操作跟随演员表的`也遵循在他们之前发生的结果"U*"的任何用法.在某些情况下会阻止本来可能有用的优化,但是如果代码必须使用`char*`并且编译器必须...

6> Ingo Blackma..:

作为Doug T.已经写过的补充,这里有一个简单的测试用例,可能用gcc触发它:

check.c

#include 

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 for x64上运行:

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

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


为了让事情变得更有趣,请使用指向不兼容但具有相同大小和表示的类型的指针(在某些系统中,例如`long long*`和`int64_t`*).有人可能会认为一个理智的编译器应该认识到`long long*`和`int64_t*`如果它们存储相同就可以访问相同的存储,但是这种处理不再流行.

7> Chris Jester..:

通过指针强制转换来键入punning(与使用union相反)是打破严格别名的一个主要示例.



8> supercat..:

根据C89的基本原理,标准的作者不希望要求编译器给出如下代码:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

应该要求重新加载x赋值和返回语句之间的值,以便允许p可能指向的可能性x,并且赋值*p可能因此改变值x.编译器应该有权假设在上述情况下不会出现锯齿的概念是没有争议的.

不幸的是,C89的作者以一种方式编写了他们的规则,如果按字面意思读取,即使是以下函数也会调用Undefined Behavior:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

因为它使用左值类型int来访问类型的对象struct S,并且int不属于可以用于访问的类型struct S.因为将结构和联合的非字符型成员的所有使用都视为未定义行为是荒谬的,几乎每个人都认识到至少有一些情况下可以使用一种类型的左值来访问另一种类型的对象.不幸的是,C标准委员会未能确定这些情况.

很多问题都是缺陷报告#028的结果,它报告了一个程序的行为,如:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

缺陷报告#28指出程序调用未定义的行为,因为编写类型为"double"的union成员并读取类型为"int"的行为成员的操作会调用Implementation-Defined行为.这种推理是荒谬的,但却形成了有效类型规则的基础,这种规则不必要地使语言复杂化,同时无需解决原始问题.

解决原始问题的最佳方法可能是将关于规则目的的脚注视为规范,并使规则无法执行,除非实际涉及使用别名进行冲突访问的情况.给出如下内容:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

内部没有冲突,inc_int因为对所访问的存储的所有访问*p都是通过类型的左值完成的int,并且没有冲突,test因为p可见地从a中导出struct S,并且在下一次s使用时,将对该存储进行所有访问通过p将已经发生.

如果代码略有改变......

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

这里,标记行之间存在别名冲突p和访问,s.x因为在执行的那一点存在另一个将用于访问同一存储的引用.

有缺陷报告028说原始示例调用了UB,因为两个指针的创建和使用之间存在重叠,这样可以使事情更加清晰,而无需添加"有效类型"或其他此类复杂性.



9> Myst..:

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

严格的别名(我稍后会描述)非常重要,因为:

    内存访问可能很昂贵(性能明智),这就是为什么数据在写回物理内存之前在CPU寄存器中被操作的原因.

    如果将两个不同CPU寄存器中的数据写入相同的存储空间,那么当我们用C编码时,我们无法预测哪些数据会"存活".

    在汇编中,我们手动编写CPU寄存器的加载和卸载代码,我们将知道哪些数据保持不变.但是C(幸运的是)摘录了这个细节.

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

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

严格别名规则可以让我们避免多余的机器代码在它的情况下应该是安全的假设,两个指针没有指向同一个内存块(另见restrict关键字).

严格别名说明可以安全地假设指向不同类型的指针指向内存中的不同位置.

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

例如:

让我们假设以下功能:

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

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

    负荷ab从存储器.

    添加ab.

    保存 b重新加载 a.

    (从CPU寄存器保存到存储器并从存储器加载到CPU寄存器).

    添加ba.

    保存a(从CPU寄存器)到内存.

第3步非常慢,因为它需要访问物理内存.但是,需要保护其中的实例ab指向相同的内存地址.

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

    这可以通过两种方式告诉编译器,使用不同的类型指向.即:

    void merge_two_numbers(int *a, long *b) {...}
    

    使用restrict关键字.即:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

现在,通过满足严格别名规则,可以避免步骤3,并且代码将以明显更快的速度运行.

事实上,通过添加restrict关键字,整个功能可以优化为:

    负荷ab从存储器.

    添加ab.

    将结果保存到ab.

这种优化不可能有人做过,因为可能发生碰撞的(这里ab将增至三倍,而不是增加了一倍).



10> Jason Dagit..:

严格别名不允许不同的指针类型指向相同的数据.

本文应该帮助您详细了解该问题.


您也可以在引用之间以及引用和指针之间进行别名.请参阅我的教程http://dbp-consulting.com/tutorials/StrictAliasing.html
允许对同一数据使用不同的指针类型.严格混叠的地方是当通过一个指针类型写入相同的内存位置并通过另一个指针类型读取时.此外,允许一些不同的类型(例如`int`和包含`int`的结构).
推荐阅读
凹凸曼00威威_694
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有