Ben*_*oit 778 c strict-aliasing undefined-behavior type-punning
当询问C中常见的未定义行为时,灵魂比我提到的严格别名规则更加开明.
他们在说什么?
Dou*_* T. 554
遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区(如指向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]);
}
}
Run Code Online (Sandbox Code Playgroud)
严格的混叠规则使该设置非法的:解引用该别名的对象是不是一个的指针兼容型或其他类型的被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]);
}
}
Run Code Online (Sandbox Code Playgroud)
并重写了我们之前的循环,以利用这个方便的功能
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Run Code Online (Sandbox Code Playgroud)
编译器可能或可能不能够足够聪明地尝试内联SendMessage,它可能会也可能不会决定加载或不再加载buff.如果SendMessage是另一个单独编译的API的一部分,它可能有加载buff内容的指令.然后,也许你是在C++中,这是一些模板化的头只有实现,编译器认为它可以内联.或者它可能只是您在.c文件中编写的内容,以方便您使用.无论如何,未定义的行为仍可能随之而来.即使我们知道幕后发生的一些事情,它仍然违反了规则,因此没有明确定义的行为得到保证.所以只需通过包装一个函数来获取我们的单词分隔缓冲区并不一定有帮助.
那么我该如何解决这个问题呢?
使用工会.大多数编译器都支持这一点而不抱怨严格的别名.这在C99中是允许的,并且在C11中明确允许.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Run Code Online (Sandbox Code Playgroud)您可以在编译器中禁用严格别名(在gcc中f [no-] strict-aliasing))
您可以使用char*别名而不是系统的单词.规则允许char*(包括signed char和unsigned char)的例外.它总是假设char*其他类型别名.然而,这不会以另一种方式起作用:没有假设你的结构别名为chars的缓冲区.
初学者要小心
当两种类型相互叠加时,这只是一个潜在的雷区.您还应该了解字节顺序,字对齐以及如何通过正确打包结构来处理对齐问题.
1 C 2011 6.5 7允许左值访问的类型有:
Nia*_*all 228
我发现的最佳解释是Mike Acton,了解严格别名.它主要关注PS3开发,但这基本上只是GCC.
来自文章:
"严格别名是由C(或C++)编译器做出的一个假设,即取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名)."
所以基本上如果你有一个int*指向包含一个内存的内存int然后你指向一个float*内存并将其用作float你打破规则.如果您的代码不遵守这一点,那么编译器的优化器很可能会破坏您的代码.
规则的例外是a char*,允许指向任何类型.
Ben*_*igt 132
这是严格的别名规则,可以在C++ 03标准的3.10节中找到(其他答案提供了很好的解释,但没有提供规则本身):
如果程序试图通过不同于以下类型之一的左值访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 一个cv限定版本的动态类型的对象,
- 与对象的动态类型对应的有符号或无符号类型的类型,
- 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,
- 一种聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),
- 一个类型,它是对象动态类型的(可能是cv限定的)基类类型,
- a
char或unsigned char类型.
C++ 11和C++ 14措辞(强调变化):
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 一个cv限定版本的动态类型的对象,
- 与对象的动态类型类似的类型(如4.4中所定义),
- 与对象的动态类型对应的有符号或无符号类型的类型,
- 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,
- 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
- 一个类型,它是对象动态类型的(可能是cv限定的)基类类型,
- a
char或unsigned char类型.
两个变化很小:glvalue而不是lvalue,以及聚合/联合案例的澄清.
第三个变化提供了更强有力的保证(放宽强混叠规则):类似类型的新概念现在可以安全别名.
另外,Ç措词(C99; ISO/IEC 9899:1999 6.5/7;完全相同的措词在ISO/IEC 9899中使用:2011§6.57):
对象的存储值只能由具有以下类型之一(73)或88)的左值表达式访问:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 与对象的有效类型对应的有符号或无符号类型的类型,
- 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者
- 一个字符类型.
73)或88)此列表的目的是指定对象可能或可能不具有别名的情况.
Sha*_*our 65
这摘自我的"什么是严格的别名规则以及我们为什么关心?" 写上去.
在C和C++中,别名与我们允许通过哪些表达式类型访问存储的值有关.在C和C++中,标准指定允许哪些表达式类型为哪些类型设置别名.允许编译器和优化器假设我们严格遵循别名规则,因此术语严格别名规则.如果我们尝试使用不允许的类型访问值,则将其归类为未定义行为(UB).一旦我们有未定义的行为,所有的赌注都会被取消,我们的计划结果将不再可靠.
不幸的是,由于严格的别名违规,我们经常会得到我们期望的结果,而新的优化版本的编译器的未来版本可能会破坏我们认为有效的代码.这是不可取的,理解严格的别名规则以及如何避免违反它们是一个值得的目标.
为了更多地了解我们关心的原因,我们将讨论在违反严格别名规则时出现的问题,打字,因为类型惩罚中使用的常用技术经常违反严格的别名规则以及如何正确输入双关语.
让我们看一些例子,然后我们可以准确地讨论标准的含义,检查一些进一步的例子,然后看看如何避免严格的别名并捕捉我们错过的违规行为.这是一个不应该令人惊讶的例子(实例):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Run Code Online (Sandbox Code Playgroud)
我们有一个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<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
Run Code Online (Sandbox Code Playgroud)
在函数foo中我们采用int*和float*,在这个例子中我们调用foo并将两个参数设置为指向同一个内存位置,在本例中包含一个int.注意,reinterpret_cast告诉编译器将表达式视为具有由其template参数指定的类型.在这种情况下,我们告诉它将表达式&x视为类型为float*.我们可以天真地期待第二的结果COUT是0,但与优化使用支持-02 GCC和铛产生如下结果:
0
1
Run Code Online (Sandbox Code Playgroud)
这可能不是预期的,但完全有效,因为我们调用了未定义的行为.甲浮子不能有效别名一个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
Run Code Online (Sandbox Code Playgroud)
使用基于类型的别名分析(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
Run Code Online (Sandbox Code Playgroud)
- 与对象的有效类型兼容的类型的限定版本,
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
Run Code Online (Sandbox Code Playgroud)
- 对应于对象的有效类型的有符号或无符号类型,
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者
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 );
Run Code Online (Sandbox Code Playgroud)
- 角色类型.
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.
Run Code Online (Sandbox Code Playgroud)
[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
Run Code Online (Sandbox Code Playgroud)
(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
Run Code Online (Sandbox Code Playgroud)
(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;
}
Run Code Online (Sandbox Code Playgroud)
(11.5) - 对应于对象动态类型的cv限定版本的有符号或无符号类型,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
Run Code Online (Sandbox Code Playgroud)
(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 );
Run Code Online (Sandbox Code Playgroud)
(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;
}
Run Code Online (Sandbox Code Playgroud)
(11.8) - char,unsigned char或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
}
Run Code Online (Sandbox Code Playgroud)
值得注意的是,上面列表中没有包含已签名的字符,这与C表示字符类型的显着区别.
我们已经达到了这一点,我们可能想知道,为什么我们要为别名?答案通常是输入pun,通常使用的方法违反了严格的别名规则.
有时我们想要绕过类型系统并将对象解释为不同的类型.这称为类型双关,将一段内存重新解释为另一种类型.对于希望访问对象的基础表示以进行查看,传输或操作的任务,类型惩罚非常有用.我们发现使用类型惩罚的典型区域是编译器,序列化,网络代码等......
传统上,这是通过获取对象的地址,将其转换为我们想要将其重新解释为的类型的指针,然后访问该值,或者换句话说通过别名来实现的.例如:
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 ) ;
Run Code Online (Sandbox Code Playgroud)
正如我们之前看到的,这不是一个有效的别名,所以我们正在调用未定义的行为.但是传统的编译器并没有利用严格的别名规则,这种类型的代码通常只是起作用,不幸的是开发人员习惯于这样做.类型惩罚的常见替代方法是通过联合,它在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
Run Code Online (Sandbox Code Playgroud)
这在C++中无效,有些人认为联合的目的仅仅是为了实现变体类型,并且认为使用联合进行类型惩罚是一种滥用.
C和C++中类型双关语的标准方法是memcpy.这可能看起来有点沉重,但优化器应该认识到使用memcpy进行类型惩罚并优化它并生成寄存器来注册移动.例如,如果我们知道int64_t与double的大小相同:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
Run Code Online (Sandbox Code Playgroud)
我们可以使用memcpy:
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Run Code Online (Sandbox Code Playgroud)
在足够的优化级别,任何体面的现代编译器都会生成与前面提到的reinterpret_cast方法相同的代码或用于类型惩罚的联合方法.检查生成的代码,我们看到它只使用寄存器mov(实时编译器资源管理器示例).
在C++ 20中,我们可以获得bit_cast(可以从提案链接中获得实现),它提供了一种简单而安全的类型 - 双关语以及在constexpr上下文中可用的方法.
以下是如何使用bit_cast将一个unsigned int类型设为float的示例(请参见实时):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
Run Code Online (Sandbox Code Playgroud)
在To和From类型不具有相同大小的情况下,它要求我们使用中间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<unsigned int>(f);
result += foo( result );
}
return result ;
}
Run Code Online (Sandbox Code Playgroud)
不幸的是我们需要这种中间类型,但这是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<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
Run Code Online (Sandbox Code Playgroud)
虽然它不会捕捉这个额外的情况(见它直播):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Run Code Online (Sandbox Code Playgroud)
尽管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]
Run Code Online (Sandbox Code Playgroud)
我将推荐的最后一个工具是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));
Run Code Online (Sandbox Code Playgroud)
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.
Run Code Online (Sandbox Code Playgroud)
最后还有TySan,目前正在开发中.此清理程序在影子内存段中添加类型检查信息,并检查访问以查看它们是否违反了别名规则.该工具可能应该能够捕获所有别名冲突,但可能会产生大量的运行时开销.
pho*_*an1 44
严格别名不仅仅指向指针,它也会影响引用,我为boost开发人员wiki写了一篇关于它的文章,并且它很受欢迎,我把它变成了我咨询网站上的一个页面.它完全解释了它是什么,为什么它如此混淆了人们以及如何处理它.严格别名白皮书.特别是它解释了为什么联合会是C++的危险行为,以及为什么使用memcpy是C和C++中唯一可移植的修复.希望这是有帮助的.
Ing*_*man 34
作为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;
}
Run Code Online (Sandbox Code Playgroud)
编译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
Run Code Online (Sandbox Code Playgroud)
所以if条件完全从汇编代码中消失了.
sup*_*cat 16
根据C89的基本原理,标准的作者不希望要求编译器给出如下代码:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
Run Code Online (Sandbox Code Playgroud)
应该要求重新加载x赋值和返回语句之间的值,以便允许p可能指向的可能性x,并且赋值*p可能因此改变值x.编译器应该有权假设在上述情况下不会出现锯齿的概念是没有争议的.
不幸的是,C89的作者以一种方式编写了他们的规则,如果按字面意思读取,即使是以下函数也会调用Undefined Behavior:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
Run Code Online (Sandbox Code Playgroud)
因为它使用左值类型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);
}
Run Code Online (Sandbox Code Playgroud)
缺陷报告#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;
}
Run Code Online (Sandbox Code Playgroud)
内部没有冲突,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;
}
Run Code Online (Sandbox Code Playgroud)
这里,标记行之间存在别名冲突p和访问,s.x因为在执行的那一点存在另一个将用于访问同一存储的引用.
有缺陷报告028说原始示例调用了UB,因为两个指针的创建和使用之间存在重叠,这样可以使事情更加清晰,而无需添加"有效类型"或其他此类复杂性.
Mys*_*yst 10
在阅读了许多答案之后,我觉得有必要添加一些东西:
严格的别名(我稍后会描述)非常重要,因为:
内存访问可能很昂贵(性能明智),这就是为什么数据在写回物理内存之前在CPU寄存器中被操作的原因.
如果将两个不同CPU寄存器中的数据写入相同的存储空间,那么当我们用C编码时,我们无法预测哪些数据会"存活".
在汇编中,我们手动编写CPU寄存器的加载和卸载代码,我们将知道哪些数据保持不变.但是C(幸运的是)摘录了这个细节.
由于两个指针可以指向内存中的相同位置,因此可能会导致处理可能的冲突的复杂代码.
这个额外的代码很慢并且会损害性能,因为它执行额外的内存读/写操作,这些操作既慢又可能(不太可能).
在严格别名规则可以让我们避免多余的机器代码在它的情况下应该是安全的假设,两个指针没有指向同一个内存块(另见restrict关键字).
严格别名说明可以安全地假设指向不同类型的指针指向内存中的不同位置.
如果编译器注意到两个指针指向不同的类型(例如,a int *和a float *),它将假定内存地址不同,并且它不会防止内存地址冲突,从而导致更快的机器代码.
例如:
让我们假设以下功能:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Run Code Online (Sandbox Code Playgroud)
为了处理a == b(两个指针指向同一个内存)的情况,我们需要命令和测试我们将数据从内存加载到CPU寄存器的方式,因此代码最终可能会像这样:
负荷a和b从存储器.
添加a到b.
保存 b并重新加载 a.
(从CPU寄存器保存到存储器并从存储器加载到CPU寄存器).
添加b到a.
保存a(从CPU寄存器)到内存.
第3步非常慢,因为它需要访问物理内存.但是,需要保护其中的实例a并b指向相同的内存地址.
严格别名将允许我们通过告诉编译器这些存储器地址明显不同(在这种情况下,将允许甚至进一步优化,如果指针共享存储器地址,则无法执行)来防止这种情况.
这可以通过两种方式告诉编译器,使用不同的类型指向.即:
void merge_two_numbers(int *a, long *b) {...}
Run Code Online (Sandbox Code Playgroud)使用restrict关键字.即:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Run Code Online (Sandbox Code Playgroud)现在,通过满足严格别名规则,可以避免步骤3,并且代码将以明显更快的速度运行.
事实上,通过添加restrict关键字,整个功能可以优化为:
负荷a和b从存储器.
添加a到b.
将结果保存到a和b.
这种优化不可能有人做过,因为可能发生碰撞的(这里a和b将增至三倍,而不是增加了一倍).
| 归档时间: |
|
| 查看次数: |
194594 次 |
| 最近记录: |