取消引用 char* 会抑制严格的别名优化吗?
Does dereferencing a char* inhibit strict aliasing optimizations?
以以下代码段为例:
*pInt = 0xFFFF;
*pFloat = 5.0;
因为它们是 int
和 float
指针,编译器会假设它们没有别名并且可以交换它们。
现在让我们假设我们用这个来调味:
*pInt = 0xFFFF;
*pChar = 'X';
*pFloat = 5.0;
由于 char*
允许别名任何东西,它可能指向 *pInt
,因此对 *pInt
的赋值不能超出 *pChar
的赋值,因为它可能合法地指向 *pInt
并将其第一个字节设置为 'X'。
类似地,pChar
可能指向 *pFloat
,对 *pFloat
的赋值不能在 char 赋值之前移动,因为代码可能打算通过重新赋值 [=20 来抵消先前字节设置的影响=].
这是否意味着我可以写入和读取 char*
来为重新排列和其他与别名相关的严格优化设置障碍?
我认为通常不能将其用作排序障碍。原因是编译器可以对您的代码进行某种版本控制
if (pInt == pChar || pFloat == pChar) {
// be careful
} else {
// no aliasing
}
显然,对于您介绍的简单情况,这没有任何优势,但如果您的指针在大部分代码中没有更改,则可能会有所帮助。
如果您只是通过使用虚拟 pChar
将其用作 "barrier" 的手段,那么 else
部分将始终获胜。但是编译器可以假设没有发生别名并且总是可以重新排序赋值。
C 标准提供重新排序保证的唯一不相关的数据是按顺序一致性操作的原子对象。
指针别名在编译器不知道指针变量是否别名另一个指针的情况下最有意义。就像您编译位于与调用者不同的翻译单元中的函数时的情况。
void func (char* pChar, float* pFloat)
{
*pChar = 'X';
*pFloat = 5.0;
}
这里的pFloat
确实不能排在pChar
之前,因为编译器无法推断出pChar
与[=不指向同一位置11=].
但是,当遇到这种情况时,编译器可以(并且可能会)添加一个 运行 时间检查以查看地址是否指向重叠内存。如果他们这样做,那么代码必须按照给定的顺序排序。如果没有,那么可能会重新组织和优化代码。
这意味着如果指针实际上在重叠内存中执行 alias/point,您只会获得类似内存屏障的行为。如果不是,那么所有关于指令排序的赌注都将落空。所以这可能不是您应该依赖的机制。
如果程序需要使用基于指针的类型双关,确保它能与 gcc 一起工作的唯一可靠方法,也可能与 clang 一起工作,是使用“-fno-strict-aliasing”。 "Modern" 编译器会积极地剔除不能更改对象所持有的位的代码,然后使用由此产生的此类代码的缺失来 "justify" 优化,否则这将是不合法的。例如,
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
int test(struct s1 *p1, struct s2 *p2)
{
if (p1->x)
{
p2->x = 12;
unsigned char *cp = (unsigned char*)p1;
unsigned char c0=cp[0] ^ 1,c1=cp[1] ^ 2;
cp[0]=c0 ^ 1; cp[1]=c1 ^ 2;
}
return p1->x;
}
clang 和 gcc 都会生成 returns 执行 "if" 语句时 p1 所具有的值的代码。我在标准中看不到任何东西可以证明这一点(如果 p1==p2,*p2 的全部内容将通过字符类型读取到类型 "char" 的离散对象中,这是定义的行为,以及那些内容离散对象将用于覆盖 *p1 的全部内容,这也是定义的行为)但是 gcc 和 clang 都会决定,因为写入 cp[0] 和 cp[1] 的值将匹配已经存在的值,它应该省略那些操作。
以以下代码段为例:
*pInt = 0xFFFF;
*pFloat = 5.0;
因为它们是 int
和 float
指针,编译器会假设它们没有别名并且可以交换它们。
现在让我们假设我们用这个来调味:
*pInt = 0xFFFF;
*pChar = 'X';
*pFloat = 5.0;
由于 char*
允许别名任何东西,它可能指向 *pInt
,因此对 *pInt
的赋值不能超出 *pChar
的赋值,因为它可能合法地指向 *pInt
并将其第一个字节设置为 'X'。
类似地,pChar
可能指向 *pFloat
,对 *pFloat
的赋值不能在 char 赋值之前移动,因为代码可能打算通过重新赋值 [=20 来抵消先前字节设置的影响=].
这是否意味着我可以写入和读取 char*
来为重新排列和其他与别名相关的严格优化设置障碍?
我认为通常不能将其用作排序障碍。原因是编译器可以对您的代码进行某种版本控制
if (pInt == pChar || pFloat == pChar) {
// be careful
} else {
// no aliasing
}
显然,对于您介绍的简单情况,这没有任何优势,但如果您的指针在大部分代码中没有更改,则可能会有所帮助。
如果您只是通过使用虚拟 pChar
将其用作 "barrier" 的手段,那么 else
部分将始终获胜。但是编译器可以假设没有发生别名并且总是可以重新排序赋值。
C 标准提供重新排序保证的唯一不相关的数据是按顺序一致性操作的原子对象。
指针别名在编译器不知道指针变量是否别名另一个指针的情况下最有意义。就像您编译位于与调用者不同的翻译单元中的函数时的情况。
void func (char* pChar, float* pFloat)
{
*pChar = 'X';
*pFloat = 5.0;
}
这里的pFloat
确实不能排在pChar
之前,因为编译器无法推断出pChar
与[=不指向同一位置11=].
但是,当遇到这种情况时,编译器可以(并且可能会)添加一个 运行 时间检查以查看地址是否指向重叠内存。如果他们这样做,那么代码必须按照给定的顺序排序。如果没有,那么可能会重新组织和优化代码。
这意味着如果指针实际上在重叠内存中执行 alias/point,您只会获得类似内存屏障的行为。如果不是,那么所有关于指令排序的赌注都将落空。所以这可能不是您应该依赖的机制。
如果程序需要使用基于指针的类型双关,确保它能与 gcc 一起工作的唯一可靠方法,也可能与 clang 一起工作,是使用“-fno-strict-aliasing”。 "Modern" 编译器会积极地剔除不能更改对象所持有的位的代码,然后使用由此产生的此类代码的缺失来 "justify" 优化,否则这将是不合法的。例如,
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
int test(struct s1 *p1, struct s2 *p2)
{
if (p1->x)
{
p2->x = 12;
unsigned char *cp = (unsigned char*)p1;
unsigned char c0=cp[0] ^ 1,c1=cp[1] ^ 2;
cp[0]=c0 ^ 1; cp[1]=c1 ^ 2;
}
return p1->x;
}
clang 和 gcc 都会生成 returns 执行 "if" 语句时 p1 所具有的值的代码。我在标准中看不到任何东西可以证明这一点(如果 p1==p2,*p2 的全部内容将通过字符类型读取到类型 "char" 的离散对象中,这是定义的行为,以及那些内容离散对象将用于覆盖 *p1 的全部内容,这也是定义的行为)但是 gcc 和 clang 都会决定,因为写入 cp[0] 和 cp[1] 的值将匹配已经存在的值,它应该省略那些操作。