我有一个问题,了解使用GCC的工会可以做什么和不可以做什么.我阅读了有关它的问题(特别是这里和这里),但他们关注C++标准,我觉得C++标准和实践(常用的编译器)之间存在不匹配.
特别是,我最近在阅读有关编译标志-fstrict-aliasing的GCC在线文档中发现了令人困惑的信息.它说:
-fstrict走样
允许编译器采用适用于正在编译的语言的最严格的别名规则.对于C(和C++),这将根据表达式的类型激活优化.特别地,假设一种类型的对象永远不会与不同类型的对象驻留在相同的地址,除非类型几乎相同.例如,a
unsigned intcan可以是aint,但不是avoid*或adouble.字符类型可以别名为任何其他类型.特别注意这样的代码:Run Code Online (Sandbox Code Playgroud)union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }从不同的工会成员阅读的做法比最近写的那个(称为"打字式")很常见.即使使用-fstrict-aliasing,只要通过union类型访问内存,就允许类型为punning.因此,上面的代码按预期工作.
这是我认为我从这个例子和我的疑虑中理解的:
1)别名仅适用于相似类型或char
1)的后果:别名 - 正如文字暗示的那样 - 是你有一个值和两个成员来访问它(即相同的字节);
怀疑:当它们具有相同的字节大小时,两种类型是相似的吗?如果没有,什么是类似的类型?
1)对于非相似类型(无论这意味着什么)的后果,别名不起作用;
2)类型双关语是指我们读的不同于我们写的成员; 它是常见的,只要通过union类型访问内存,它就可以正常工作;
怀疑:在类型相似的特定情况下别名是什么类型?
我感到困惑,因为它表示unsigned int和double不相似,所以别名不起作用; 然后在示例中它是int和double之间的别名,它清楚地表明它按预期工作,但称之为类型 - 惩罚:不是因为类型是或不相似,而是因为它是从一个不写的成员读取.但是从一个没有写的成员那里读取的是我所理解的混淆(正如这个词所暗示的那样).我迷路了.
问题: 有人可以澄清别名和类型惩罚之间的区别,这两种技术的用途是如何在GCC中发挥作用的?编译器标志有什么作用?
此代码是否违反严格别名?
struct {int x;} a;
*(int*)&a = 3
Run Code Online (Sandbox Code Playgroud)
更抽象的是,只要原始读/写操作类型正确,在不同类型之间进行转换是否合法?
严格别名会阻止我们使用不兼容的类型访问相同的内存位置.
int* i = malloc( sizeof( int ) ) ; //assuming sizeof( int ) >= sizeof( float )
*i = 123 ;
float* f = ( float* )i ;
*f = 3.14f ;
Run Code Online (Sandbox Code Playgroud)
根据C标准,这将是非法的,因为编译器"知道" 左值int不能访问float.
如果我使用该指针指向正确的内存,如下所示:
int* i = malloc( sizeof( int ) + sizeof( float ) + MAX_PAD ) ;
*i = 456 ;
Run Code Online (Sandbox Code Playgroud)
首先,我为内存分配内存int,float最后一部分是允许float存储在对齐地址的内存.float需要在4的倍数上对齐,MAX_PAD通常是16个字节中的8个,具体取决于系统.在任何情况下,MAX_PAD足够大,所以float可以正确对齐.
然后,我写的int进入i,到目前为止,一切顺利.
float* …Run Code Online (Sandbox Code Playgroud) 根据我对严格别名规则的理解,这个快速反平方根的代码将导致C++中未定义的行为:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // type punning
i = 0x5f3759df - ( i >> 1 );
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
return y;
}
Run Code Online (Sandbox Code Playgroud)
这段代码确实会导致UB吗?如果是,如何以符合标准的方式重新实现?如果没有,为什么不呢?
假设:在调用此函数之前,我们已经以某种方式检查了浮点数是IEEE 754 32位格式, …
就在几周前,我了解到C++标准有一个严格的别名规则.基本上,我曾经问过一个关于移位的问题 - 而不是一次一个地移动每个字节,以最大化性能我想加载我的处理器的本机寄存器(分别为32或64位)并执行4/8的移位所有字节都在一条指令中.
这是我想避免的代码:
unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };
for (int i = 0; i < 3; ++i)
{
buffer[i] <<= 4;
buffer[i] |= (buffer[i + 1] >> 4);
}
buffer[3] <<= 4;
Run Code Online (Sandbox Code Playgroud)
相反,我想使用类似的东西:
unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };
unsigned int *p = (unsigned int*)buffer; // unsigned int is 32 bit on my platform
*p <<= 4;
Run Code Online (Sandbox Code Playgroud)
有人在评论中提到我提出的解决方案违反了C++别名规则(因为p是类型int*,缓冲区是类型的char*,我正在取消引用p来执行移位.(请忽略对齐和字节顺序的可能问题 - 我处理那个片段之外的那些人)我很惊讶地了解他严格别名规则,因为我经常对缓冲区中的数据进行操作,将其从一种类型转换为另一种类型并且从未出现任何问题.进一步调查显示我使用的编译器(MSVC) )没有强制执行严格的别名规则,因为我只是在业余时间开发gcc/g ++作为业余爱好,我可能还没有遇到过这个问题.
那么我问了一个关于严格别名规则和C++的Placement new运算符的问题:
IsoCpp.org提供有关放置新的常见问题解答,它们提供以下代码示例:
#include …Run Code Online (Sandbox Code Playgroud) 问题:
下面的代码是否违反了严格的别名规则?也就是说,是否允许智能编译器打印00000(或其他一些令人讨厌的效果),因为然后通过int*?访问首先作为其他类型访问的缓冲区?
如果没有,那么只会移动ptr2大括号之前的定义和初始化(因此ptr2将在定义ptr1范围时定义)打破它吗?
如果没有,将删除大括号(因此ptr1,ptr2并在同一范围内)打破它?
如果是,代码如何修复?
额外的问题:如果代码没问题,并且2.或3.也不要破坏它,如何更改它以便打破严格的别名规则(例如,转换支撑循环使用int16_t)?
int i;
void *buf = calloc(5, sizeof(int)); // buf initialized to 0
{
char *ptr1 = buf;
for(i = 0; i < 5*sizeof(int); ++i)
ptr1[i] = i;
}
int *ptr2 = buf;
for(i = 0; i < 5; ++i)
printf("%d", ptr2[i]);
Run Code Online (Sandbox Code Playgroud)
寻找确认,如此简短(ish),关于这个特定代码的专家答案,理想情况下用最小的标准报价,就是我所追求的.我不是经过长时间的严格别名规则解释,只是与此代码相关的部分.如果答案明确列举上面的编号问题,那将是很好的.
还假设没有整数陷阱值的通用CPU,也可以说int是32位和2位补码.
由于C++ 11 std::complex<T>[n]保证T[n*2]具有可定义的值,因此具有良好定义的值.这正是人们对任何主流架构的期望.对于我自己的类型,这种保证是否可以通过标准C++实现,struct vec3 { float x, y, z; }或者只有在编译器的特殊支持下才能实现?
以下面的代码片段为例:
*pInt = 0xFFFF;
*pFloat = 5.0;
Run Code Online (Sandbox Code Playgroud)
由于他们是int和float指针,编译器将假定他们不这样做别名,例如可以交换.
现在让我们假设我们用这个来加强它:
*pInt = 0xFFFF;
*pChar = 'X';
*pFloat = 5.0;
Run Code Online (Sandbox Code Playgroud)
因为char*允许别名,它可能指向*pInt,所以赋值*pInt不能超出赋值*pChar,因为它可以合法地指向*pInt并将其第一个字节设置为'X'.类似地pChar可以指向*pFloat,*pFloat在char赋值之前不能移动赋值,因为代码可能打算通过重新分配来取消先前字节设置的效果*pFloat.
这是否意味着我可以通过编写和读取char*来为重新排列和其他严格的别名相关优化创建障碍?
这是关于内存重用的另一个问题的后续内容.由于最初的问题是关于具体实施,答案与具体实施有关.
所以我想知道,在一致的实现中,为一个不同类型的数组重用基本类型数组的内存是否合法:
我结束了以下示例代码:
#include <iostream>
constexpr int Size = 10;
void *allocate_buffer() {
void * buffer = operator new(Size * sizeof(int), std::align_val_t{alignof(int)});
int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok
for (int i=0; i<Size; i++) in[i] = i; // Defined behaviour because int is a fundamental type:
// lifetime starts when is receives a value
return buffer;
}
int main() {
void *buffer = allocate_buffer(); // Ok, defined behaviour
int *in = …Run Code Online (Sandbox Code Playgroud) 例如,此代码是有效的,还是通过违反别名规则来调用未定义的行为?
int x;
struct s { int i; } y;
x = 1;
y = *(struct s *)&x;
printf("%d\n", y.i);
Run Code Online (Sandbox Code Playgroud)
我的兴趣在于使用基于此的技术来开发用于执行别名读取的可移植方法.
更新:这是预期的用例,有点不同,但当且仅当上述内容有效时才有效:
static inline uint32_t read32(const unsigned char *p)
{
struct a { char r[4]; };
union b { struct a r; uint32_t x; } tmp;
tmp.r = *(struct a *)p;
return tmp.x;
}
Run Code Online (Sandbox Code Playgroud)
GCC根据需要将其编译为单个32位负载,并且它似乎避免了如果p实际指向除其他类型之外可能发生的混叠问题char.换句话说,它似乎是GNU C __attribute__((__may_alias__))属性的可移植替代品.但我不确定它是否真的定义明确......