在询问common undefined behavior in C时,灵魂比我提到的严格别名规则更加开明 他们在说什么?
答案 0 :(得分:529)
遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区上(如指向uint32_t
或{{1}的指针} S)。当你通过指针转换将结构重叠到这样的缓冲区或缓冲区到这样的结构上时,你很容易违反严格的别名规则。
所以在这种设置中,如果我想发送消息,我必须有两个不兼容的指针指向同一块内存。我可能会天真地编写这样的代码:
uint16_t
严格别名规则使此设置非法:取消引用一个指针,该指针将不是compatible type的对象或C 2011 6.5第7段 1 允许的其他类型之一别名化是未定义的行为。不幸的是,你仍然可以用这种方式编码,也许得到一些警告,让它编译好,只是在你运行代码时有奇怪的意外行为。
(海湾合作委员会在提供别名警告的能力方面似乎有些不一致,有时会给我们一个友好的警告,有时却没有。)
要了解未定义此行为的原因,我们必须考虑严格别名规则购买编译器的原因。基本上,使用此规则,它不必考虑插入指令以在每次循环运行时刷新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]);
}
}
的内容。相反,在优化时,对于一些令人烦恼的非执行的别名假设,它可以省略这些指令,在循环运行之前将buff
和buff[0]
加载到CPU寄存器中,并加速循环体。在引入严格别名之前,编译器必须处于偏执状态,buff[1
的内容可以随时随地改变。因此,为了获得额外的性能优势,并假设大多数人没有输入双关语指针,引入了严格的别名规则。
请记住,如果你认为这个例子是假的,如果你把缓冲区传递给另一个为你做发送的函数,你甚至可能会这样做。
buff
并重写了我们之前的循环以利用这个方便的功能
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
编译器可能或者可能不能够足够聪明地尝试内联SendMessage,它可能会也可能不会决定加载或不再加载buff。如果for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
是另一个单独编译的API的一部分,它可能有加载buff内容的指令。然后,也许你是在C ++中,这是一些模板化的头仅实现,编译器认为它可以内联。或者也许这只是您在.c文件中编写的内容,以方便您使用。无论如何,未定义的行为仍可能随之而来。即使我们知道幕后发生的一些事情,它仍然违反了规则,因此没有明确定义的行为得到保证。因此,只需通过包含我们的单词分隔缓冲区的函数来帮助就不一定了。
那么如何解决这个问题?
使用联合。大多数编译器都支持这一点而不抱怨严格的别名。这在C99中是允许的,并且在C11中明确允许。
SendMessage
您可以在编译器中禁用严格别名(gcc中为f[no-]strict-aliasing))
您可以使用union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
进行别名而不是系统的单词。规则允许char*
(包括char*
和signed char
)例外。始终假设unsigned char
别名为其他类型。然而,这不会起到另一种作用:没有假设你的结构为一个字符缓冲区别名。
初学者要小心
当两种类型相互叠加时,这只是一个潜在的雷区。您还应该了解endianness,word alignment以及如何通过packing structs正确处理对齐问题。
1 C 2011 6.5 7允许左值访问的类型包括:
答案 1 :(得分:226)
我发现的最佳解释是Mike Acton,Understanding Strict Aliasing。它主要关注PS3开发,但基本上只是GCC。
来自文章:
“严格别名是由C(或C ++)编译器做出的假设,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名。)”
所以基本上如果你有一个int*
指向包含int
的内存,然后你将float*
指向该内存并将其用作float
你打破规则。如果你的代码不尊重这一点,那么编译器的优化器很可能会破坏你的代码。
规则的例外是char*
,允许指向任何类型。
答案 2 :(得分:129)
这是严格的别名规则,可在 C ++ 03 标准的3.10节中找到(其他答案提供了很好的解释,但没有提供规则本身):
如果程序试图通过以下类型之一以外的左值访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型对应的有符号或无符号类型的类型
- 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),
- 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
char
或unsigned char
类型。
C ++ 11 和 C ++ 14 措辞(强调更改):
如果某个程序试图通过以下某种类型之外的 glvalue 访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型相似的类型(如4.4中所定义),
- 与对象的动态类型对应的有符号或无符号类型的类型
- 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,其元素或非静态数据成员中包含上述类型之一(包括递归地,元素或非静态数据成员子集合或包含的联合>,
- 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
char
或unsigned char
类型。
两个变化很小: glvalue 而不是左值,并澄清了聚合/联合案例。
第三个更改提供了更强有力的保证(放宽了强烈的别名规则):类似类型的新概念,现在可以安全地使用别名。
C 措辞(C99; ISO / IEC 9899:1999 6.5 / 7; ISO / IEC 9899:2011§6.5¶7中使用完全相同的措辞):
对象的存储值只能由左值访问 表达式具有以下类型之一 73)或88):
- 与对象的有效类型兼容的类型
- 与有效类型兼容的类型的合格版本 对象,
- 一种类型,是与之对应的有符号或无符号类型 有效的对象类型,
- 与a对应的有符号或无符号类型的类型 对象的有效类型的合格版本,
- 包含上述之一的聚合或联合类型 其成员之间的类型(包括,递归地,成员 subaggregate或contains union),或
- 字符类型。
73)或88)此列表的目的是指定对象可能存在或不存在别名的情况。
答案 3 :(得分:51)
这摘录自我的"What is the Strict Aliasing Rule and Why do we care?"文章。
在C和C ++中,别名与允许我们通过哪些表达式类型访问存储的值有关。在C和C ++中,标准均指定允许使用哪种表达式类型作为别名。允许编译器和优化器假定我们严格遵循别名规则,因此,术语 strict别名规则。如果我们尝试使用不允许的类型访问值,则将其分类为undefined behavior( UB )。一旦我们具有不确定的行为,所有的赌注都将消失,我们的程序结果将不再可靠。
不幸的是,在严格违反别名的情况下,我们通常会获得预期的结果,从而可能会出现将来的带有新优化的编译器版本破坏我们认为有效的代码的可能性。这是不希望的,并且了解严格的别名规则以及如何避免违反它们是一个值得的目标。
要了解有关我们为什么关心的更多信息,我们将讨论在违反严格的别名规则,键入punning时出现的问题,因为在类型punning中使用的常见技术经常会违反严格的别名规则以及如何正确键入pun。
让我们看一些示例,然后我们可以确切地讨论标准所说的内容,研究其他示例,然后看看如何避免严格的混叠并捕获我们错过的违规行为。这是一个不足为奇的示例(live example):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
我们有一个 int * 指向 int 占用的内存,这是有效的别名。优化程序必须假定通过 ip 进行的分配可以更新 x 占用的值。
下一个示例显示了导致未定义行为(live example)的别名:
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<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
在函数 foo 中,我们使用 int * 和 float * ,在本示例中,我们将调用 foo >,然后将两个参数都设置为指向同一内存位置,在此示例中,该内存位置包含 int 。注意,reinterpret_cast告诉编译器将表达式视为具有其模板参数指定的类型。在这种情况下,我们告诉它将表达式&x 视为具有 float * 类型。我们可能天真地希望第二个 cout 的结果为 0 ,但是使用 -O2 启用优化后,gcc和clang都会产生以下结果:
0
1
由于我们已经调用了未定义的行为,因此可能不期望这样做,但是完全有效。 float 不能有效地别名 int 对象。因此,优化程序可以假定在取消引用 i 时存储的常数1 将是返回值,因为通过 f 进行的存储不能有效地影响 int 对象。将代码插入Compiler Explorer可以显示这正是正在发生的情况(live example):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
使用Type-Based Alias Analysis (TBAA)的优化器假定将返回 1 并将其直接移到带有返回值的寄存器 eax 中。 TBAA使用有关允许使用哪种类型的别名的语言规则来优化负载和存储。在这种情况下,TBAA知道 float 不能别名和 int 并优化了 i 的负载。
该标准确切说明了我们允许和不允许我们做什么?标准语言不是很简单,因此我将为每一项尝试提供代码示例以说明其含义。
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 has an extension和also允许将 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.
[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)-字符,无符号字符或std :: byte类型。
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
值得注意的是,签名字符未包含在上面的列表中,这与 C 表示字符类型的显着区别。 / p>
到此为止,我们可能想知道,为什么要使用别名?答案通常是 pun 类型,通常所使用的方法违反严格的别名规则。
有时,我们想绕过类型系统,并将对象解释为其他类型。这称为 type punning ,用于将内存段重新解释为另一种类型。 punning 类型对于需要访问对象的基础表示形式以进行查看,运输或操作的任务很有用。我们发现使用的类型修剪的典型领域是编译器,序列化,网络代码等……
传统上,这是通过获取对象的地址,将其转换为我们要重新解释为该类型的指针,然后访问该值,或者换句话说通过别名来实现的。例如:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
如前所述,这不是有效的别名,因此我们正在调用未定义的行为。但是传统上,编译器没有利用严格的别名规则,这种类型的代码通常只能工作,不幸的是,开发人员已经习惯了这种方式。联合修剪的一种常见替代方法是通过联合,该联合在C中有效,但在C ++(see live example)中为未定义行为:
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 ++中用于 type punning 的标准方法是 memcpy 。这似乎有些繁重,但是优化程序应该认识到 memcpy 用于 type punning 的使用,并对其进行优化,并生成一个寄存器来记录移动。例如,如果我们知道 int64_t 与 double 的大小相同:
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 方法或 union 方法用于 punning 相同的代码。检查生成的代码,我们看到它仅使用了mov(live Compiler Explorer Example)。
在C ++ 20中,我们可能会获得 bit_cast (implementation available in link from proposal),它提供了一种简单而安全的方式来进行双打和在constexpr上下文中使用。
以下是如何使用 bit_cast 将 unsigned int 键入为 float 的示例,{see it live) :
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
如果 To 和 From 类型的大小不相同,则需要我们使用中间struct15。我们将使用包含 sizeof(unsigned int)字符数组(假定为4字节unsigned 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<unsigned int>(f);
result += foo( result );
}
return result ;
}
很不幸,我们需要这种中间类型,但这是 bit_cast 的当前约束。
我们没有很多很好的工具来捕获C ++中的严格别名,我们拥有的工具将捕获某些情况下的严格别名冲突以及某些情况下的装入和存储未对齐。
使用标志 -fstrict-aliasing 和 -Wstrict-aliasing 的gcc可以捕获某些情况,尽管并非没有假阳性/阴性。例如,以下情况将在gcc(see it live)中生成警告:
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<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
尽管不会捕获到这种额外的情况(see it live):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(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解释器,这是一个静态分析器,可以对很大一部分C语言的程序进行详尽的分析。给定较早示例的C版本,其中使用 -fstrict-aliasing 遗漏了一种情况(see it live)
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-解释器(为简洁起见,对输出进行了编辑):
./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个正在开发中。此清理程序在影子内存段中添加类型检查信息,并检查访问以查看它们是否违反别名规则。该工具可能应该能够捕获所有混叠违规行为,但可能会有较大的运行时开销。
答案 4 :(得分:42)
严格别名不仅仅指向指针,它也会影响引用,我为boost开发人员wiki写了一篇关于它的文章,它很受欢迎,我把它变成了我咨询网站上的一个页面。它完全解释了它是什么,为什么它如此混淆了人们以及如何处理它。 Strict Aliasing White Paper。特别是它解释了为什么工会是C ++的危险行为,以及为什么使用memcpy是C和C ++中唯一可移植的解决方案。希望这有用。
答案 5 :(得分:33)
作为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 for x64上运行:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
所以if条件完全从汇编代码中消失了。
答案 6 :(得分:16)
Type punning通过指针强制转换(与使用联合相反)是打破严格别名的一个主要示例。
答案 7 :(得分:14)
根据C89的基本原理,标准的作者不希望要求编译器给出如下代码:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
应该要求在赋值和返回语句之间重新加载x
的值,以便p
可能指向x
,并赋值给{{{因此,可能会改变*p
的值。编译器应该有权假设在上述之类的情况下不会出现别名这一概念是没有争议的。
不幸的是,C89的作者以一种方式编写了他们的规则,如果按字面意思读取,即使是以下函数也会调用未定义的行为:
x
因为它使用类型为void test(void)
{
struct S {int x;} s;
s.x = 1;
}
的左值来访问类型为int
的对象,而struct S
不属于可用于访问int
的类型。因为将结构和联合的非字符型成员的所有使用都视为未定义行为是荒谬的,几乎每个人都认识到至少有一些情况下可以使用一种类型的左值来访问另一种类型的对象。不幸的是,C标准委员会未能确定这些情况是什么。
很多问题都是缺陷报告#028的结果,该报告询问了如下程序的行为:
struct S
缺陷报告#28指出程序调用未定义的行为,因为编写类型为“double”的联合成员并读取类型为“int”的联合成员的操作会调用实现定义的行为。这种推理是荒谬的,但却形成了有效类型规则的基础,这种规则不必要地使语言复杂化,同时无需解决原始问题。
解决原始问题的最佳方法可能是对待 关于规则目的的脚注,好像它是规范性的,并且已经制定 该规则不可执行,除非实际涉及使用别名进行冲突访问的情况。给出类似的东西:
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);
}
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
中没有冲突,因为{ {1}}显然来自test
,并且在下次使用p
时,将通过struct S
对该存储的所有访问都已经发生。< / p>
如果代码稍有改动......
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;
}
的访问之间存在别名冲突,因为在执行该点时,将存在另一个用于访问同一存储的引用
有缺陷报告028说原始示例调用了UB,因为两个指针的创建和使用之间存在重叠,这样可以使事情更加清晰,而无需添加“有效类型”或其他此类复杂性。 / p>
答案 8 :(得分:10)
在阅读了许多答案后,我觉得有必要添加一些内容:
严格别名(稍后我会描述)很重要,因为:
内存访问可能很昂贵(性能明智),这就是为什么数据在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
。
将a
(从CPU寄存器)保存到内存中。
步骤3非常慢,因为它需要访问物理内存。但是,需要保护a
和b
指向同一内存地址的实例。
严格别名将允许我们通过告诉编译器这些内存地址明显不同(在这种情况下,如果指针共享内存地址,将允许进一步优化而无法执行)来阻止这种情况。 / p>
这可以通过两种方式告诉编译器,使用不同的类型指向。即:
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
将增加三倍而不是加倍),之前无法进行此优化。
答案 9 :(得分:6)
严格别名不允许不同的指针类型使用相同的数据。
This article应该可以帮助您详细了解这个问题。
答案 10 :(得分:-2)
从技术上讲,在C ++中,严格的别名规则可能永远都不适用。
请注意间接(* operator)的定义:
一元*运算符执行间接操作:它所指向的表达式 应用应为对象类型的指针或对象类型的指针 函数类型,然后结果是指向对象的左值,或者 表达式指向的函数 。
glvalue是一个表达式,其求值确定...的身份 一个对象,(...片段)
因此,在任何定义良好的程序跟踪中,glvalue均指向一个对象。 因此所谓的严格别名规则永远不会适用。这可能不是设计人员想要的。