具有不同编译器的不同翻译单元中的内联函数标记未定义的行为?
inline function in different translation units with different compiler flags undefined behaviour?
在 visual studio 中,您可以为各个 cpp 文件设置不同的编译器选项。例如:在 "code generation" 下,我们可以在调试模式下启用基本的运行时检查。或者我们可以更改浮点模型 (precise/strict/fast)。这些只是例子。有很多不同的标志。
一个内联函数可以在程序中定义多次,只要定义相同即可。我们将此函数放入 header 并将其包含在多个翻译单元中。现在,如果不同 cpp 文件中的不同编译器选项导致函数的编译代码略有不同,会发生什么情况?那么他们确实不同,我们有未定义的行为?您可以将函数设为静态(或将其放入未命名的命名空间),但更进一步,直接在 class 中定义的每个成员函数都是隐式内联的。这意味着如果这些 cpp 文件共享相同的编译器标志,我们只能在不同的 cpp 文件中包含 classes。我无法想象这是真的,因为这基本上很容易出错。
我们在未定义行为领域真的那么快吗?还是编译器会处理这种情况?
就标准而言,命令行标志的每种组合都会将编译器转换为不同的实现。虽然实现能够使用其他实现生成的目标文件很有用,但标准没有强加要求它们这样做。
即使没有内联,也可以考虑在一个编译单元中包含以下函数:
char foo(void) { return 255; }
以及另一个:
char foo(void);
int arr[128];
void bar(void)
{
int x=foo();
if (x >= 0 && x < 128)
arr[x]=1;
}
如果 char
在两个编译单元中都是有符号类型,则第二个单元中 x
的值将小于零(从而跳过数组赋值)。如果它在两个单元中都是无符号类型,它将大于 127(同样跳过赋值)。但是,如果一个编译单元使用有符号 char
而另一个使用无符号,并且如果实现期望 return 值在结果寄存器中进行符号扩展或零扩展,则结果可能是编译器可能会确定 x
不能大于 127,即使它包含 255,或者它不能小于 0,即使它包含 -1。因此,生成的代码可能会访问 arr[255]
或 arr[-1]
,并可能导致灾难性的结果。
虽然在许多情况下使用不同的编译器标志组合代码应该是安全的,但标准没有努力区分这种混合是安全的还是不安全的。
an inline function can be defined multiple times in the program, as
long as the definitions are identical
没有。 ("Identical" 在这里甚至不是一个定义明确的概念。)
形式上,定义必须在某种非常强烈的意义上是等价的,这甚至没有意义作为一个要求,也没有人关心:
// in some header (included in multiple TU):
const int limit_max = 200; // implicitly static
inline bool check_limit(int i) {
return i<=limit_max; // OK
}
inline int impose_limit(int i) {
return std::min(i, limit_max); // ODR violation
}
这样的代码是完全合理的,但形式上违反了一个定义规则:
in each definition of D, corresponding names, looked up according to
6.4 [basic.lookup], shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload
resolution (16.3 [over.match]) and after matching of partial template
specialization (17.9.3 [temp.over]), except that a name can refer to a
const object with internal or no linkage if the object has the same
literal type in all definitions of D, and the object is initialized
with a constant expression (8.20 [expr.const]), and the value (but not
the address) of the object is used, and the object has the same value
in all definitions of D;
因为异常不允许使用具有内部链接的 const 对象(const int
是隐式静态的)以直接绑定 const 引用(然后仅将引用用于其值) .正确的版本是:
inline int impose_limit(int i) {
return std::min(i, +limit_max); // OK
}
这里 limit_max
的值用在一元运算符 + 中,然后一个 const 引用绑定到一个用该值 初始化的临时值。谁真的这样做了?
但即使是委员会也不认为正式的 ODR 很重要,正如我们在 Core Issue 1511 中看到的那样:
1511. const volatile variables and the one-definition rule
Section: 6.2 [basic.def.odr] Status: CD3 Submitter: Richard
Smith Date: 2012-06-18
[Moved to DR at the April, 2013 meeting.]
This wording is possibly not sufficiently clear for an example like:
const volatile int n = 0;
inline int get() { return n; }
我们看到,委员会认为,这种公然违反 ODR 的意图和目的的写法,是一种在每个 TU 中读取不同易失性对象的代码,即对 different 对象具有可见副作用的代码,因此 different 可见副作用 是可以的,因为我们这样做了不关心哪个是哪个.
重要的是内联函数的效果是模糊等价的:做一个 volatile int read,这是一个非常弱的等价,但足以自然使用 ODR 实例无差异:使用内联函数的哪个特定实例并不重要,也不会产生影响。
特别是 volatile 读取读取的值根据定义编译器是未知的,因此编译器分析的 post 条件和此函数的不变量是相同的。
当在不同的 TU 中使用不同的函数定义时,您需要确保从调用者的角度来看它们是严格等价的:永远不可能用一个替换另一个来让调用者感到惊讶。这意味着即使代码不同,可观察到的行为也必须完全相同。
如果您使用不同的编译器选项,它们不得更改函数可能结果的范围(编译器认为可能)。
因为 "standard"(这不是真正的编程语言规范)允许浮点对象具有其官方声明类型所不允许的真实表示,以完全不受约束的方式,使用任何除非您激活“double
意味着 double
”模式(这是唯一的理智模式),否则任何受 ODR 多重定义的非易失限定浮点类型似乎都有问题。
我最近写了一些GCC代码测试是否真的存在这个问题。
剧透:确实如此。
设置:
我正在使用 AVX512 指令编译我们的一些代码。由于大多数 cpu 不支持 AVX512,我们需要在没有 AVX512 的情况下编译大部分代码。问题是:用AVX512编译的cpp文件中使用的inline function是否可以"poison"整个库带有非法指令。
想象这样一种情况,非 AVX512 cpp 文件中的函数调用我们的函数,但它命中了来自 AVX512 编译单元的程序集。这将使我们在非 AVX512 机器上 illegal instruction
。
让我们试一试:
func.h
inline void __attribute__ ((noinline)) double_it(float* f) {
for (int i = 0; i < 16; i++)
f[i] = f[i] + f[i];
}
我们定义了一个内联函数(在 link 意义上)。使用硬编码 16 将使 GCC 优化器使用 AVX512 指令。我们必须让它 ((noinline)) 来防止编译器内联它(即将它的代码粘贴到调用者)。这是假装此函数太长不值得内联的廉价方法。
avx512.cpp
#include "func.h"
#include <iostream>
void run_avx512() {
volatile float f = 1;
float arr [16] = {f};
double_it(arr);
for (int i = 0; i < 16; i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
}
这是AVX512使用我们的double_it
功能。它将一些数组加倍并打印结果。我们将使用 AVX512 对其进行编译。
non512.cpp
#include "func.h"
#include <iostream>
void run_non_avx() {
volatile float f = 1;
float arr [16] = {f};
double_it(arr);
for (int i = 0; i < 16; i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
}
和之前一样的逻辑。这个不会用AVX512编译。
lib_user.cpp
void run_non_avx();
int main() {
run_non_avx();
}
一些用户代码。调用未使用 AVX512 编译的 `run_non_avx。它不知道它会膨胀 :)
现在我们可以编译这些文件并link将它们作为共享库(可能常规库也可以)
g++ -c avx512.cpp -o avx512.o -O3 -mavx512f -g3 -fPIC
g++ -c non512.cpp -o non512.o -O3 -g3 -fPIC
g++ -shared avx512.o non512.o -o libbad.so
g++ lib_user.cpp -L . -lbad -o lib_user.x
./lib_user.x
运行 我机器上的这个(没有 AVX512)给了我
$ ./lib_user.x
Illegal instruction (core dumped)
附带说明一下,如果我更改 avx512.o non512.o
的顺序,它就会开始工作。似乎 linker 忽略了相同功能的后续实现。
在 visual studio 中,您可以为各个 cpp 文件设置不同的编译器选项。例如:在 "code generation" 下,我们可以在调试模式下启用基本的运行时检查。或者我们可以更改浮点模型 (precise/strict/fast)。这些只是例子。有很多不同的标志。
一个内联函数可以在程序中定义多次,只要定义相同即可。我们将此函数放入 header 并将其包含在多个翻译单元中。现在,如果不同 cpp 文件中的不同编译器选项导致函数的编译代码略有不同,会发生什么情况?那么他们确实不同,我们有未定义的行为?您可以将函数设为静态(或将其放入未命名的命名空间),但更进一步,直接在 class 中定义的每个成员函数都是隐式内联的。这意味着如果这些 cpp 文件共享相同的编译器标志,我们只能在不同的 cpp 文件中包含 classes。我无法想象这是真的,因为这基本上很容易出错。
我们在未定义行为领域真的那么快吗?还是编译器会处理这种情况?
就标准而言,命令行标志的每种组合都会将编译器转换为不同的实现。虽然实现能够使用其他实现生成的目标文件很有用,但标准没有强加要求它们这样做。
即使没有内联,也可以考虑在一个编译单元中包含以下函数:
char foo(void) { return 255; }
以及另一个:
char foo(void);
int arr[128];
void bar(void)
{
int x=foo();
if (x >= 0 && x < 128)
arr[x]=1;
}
如果 char
在两个编译单元中都是有符号类型,则第二个单元中 x
的值将小于零(从而跳过数组赋值)。如果它在两个单元中都是无符号类型,它将大于 127(同样跳过赋值)。但是,如果一个编译单元使用有符号 char
而另一个使用无符号,并且如果实现期望 return 值在结果寄存器中进行符号扩展或零扩展,则结果可能是编译器可能会确定 x
不能大于 127,即使它包含 255,或者它不能小于 0,即使它包含 -1。因此,生成的代码可能会访问 arr[255]
或 arr[-1]
,并可能导致灾难性的结果。
虽然在许多情况下使用不同的编译器标志组合代码应该是安全的,但标准没有努力区分这种混合是安全的还是不安全的。
an inline function can be defined multiple times in the program, as long as the definitions are identical
没有。 ("Identical" 在这里甚至不是一个定义明确的概念。)
形式上,定义必须在某种非常强烈的意义上是等价的,这甚至没有意义作为一个要求,也没有人关心:
// in some header (included in multiple TU):
const int limit_max = 200; // implicitly static
inline bool check_limit(int i) {
return i<=limit_max; // OK
}
inline int impose_limit(int i) {
return std::min(i, limit_max); // ODR violation
}
这样的代码是完全合理的,但形式上违反了一个定义规则:
in each definition of D, corresponding names, looked up according to 6.4 [basic.lookup], shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution (16.3 [over.match]) and after matching of partial template specialization (17.9.3 [temp.over]), except that a name can refer to a const object with internal or no linkage if the object has the same literal type in all definitions of D, and the object is initialized with a constant expression (8.20 [expr.const]), and the value (but not the address) of the object is used, and the object has the same value in all definitions of D;
因为异常不允许使用具有内部链接的 const 对象(const int
是隐式静态的)以直接绑定 const 引用(然后仅将引用用于其值) .正确的版本是:
inline int impose_limit(int i) {
return std::min(i, +limit_max); // OK
}
这里 limit_max
的值用在一元运算符 + 中,然后一个 const 引用绑定到一个用该值 初始化的临时值。谁真的这样做了?
但即使是委员会也不认为正式的 ODR 很重要,正如我们在 Core Issue 1511 中看到的那样:
1511. const volatile variables and the one-definition rule
Section: 6.2 [basic.def.odr] Status: CD3 Submitter: Richard Smith Date: 2012-06-18
[Moved to DR at the April, 2013 meeting.]
This wording is possibly not sufficiently clear for an example like:
const volatile int n = 0; inline int get() { return n; }
我们看到,委员会认为,这种公然违反 ODR 的意图和目的的写法,是一种在每个 TU 中读取不同易失性对象的代码,即对 different 对象具有可见副作用的代码,因此 different 可见副作用 是可以的,因为我们这样做了不关心哪个是哪个.
重要的是内联函数的效果是模糊等价的:做一个 volatile int read,这是一个非常弱的等价,但足以自然使用 ODR 实例无差异:使用内联函数的哪个特定实例并不重要,也不会产生影响。
特别是 volatile 读取读取的值根据定义编译器是未知的,因此编译器分析的 post 条件和此函数的不变量是相同的。
当在不同的 TU 中使用不同的函数定义时,您需要确保从调用者的角度来看它们是严格等价的:永远不可能用一个替换另一个来让调用者感到惊讶。这意味着即使代码不同,可观察到的行为也必须完全相同。
如果您使用不同的编译器选项,它们不得更改函数可能结果的范围(编译器认为可能)。
因为 "standard"(这不是真正的编程语言规范)允许浮点对象具有其官方声明类型所不允许的真实表示,以完全不受约束的方式,使用任何除非您激活“double
意味着 double
”模式(这是唯一的理智模式),否则任何受 ODR 多重定义的非易失限定浮点类型似乎都有问题。
我最近写了一些GCC代码测试是否真的存在这个问题。
剧透:确实如此。
设置:
我正在使用 AVX512 指令编译我们的一些代码。由于大多数 cpu 不支持 AVX512,我们需要在没有 AVX512 的情况下编译大部分代码。问题是:用AVX512编译的cpp文件中使用的inline function是否可以"poison"整个库带有非法指令。
想象这样一种情况,非 AVX512 cpp 文件中的函数调用我们的函数,但它命中了来自 AVX512 编译单元的程序集。这将使我们在非 AVX512 机器上 illegal instruction
。
让我们试一试:
func.h
inline void __attribute__ ((noinline)) double_it(float* f) {
for (int i = 0; i < 16; i++)
f[i] = f[i] + f[i];
}
我们定义了一个内联函数(在 link 意义上)。使用硬编码 16 将使 GCC 优化器使用 AVX512 指令。我们必须让它 ((noinline)) 来防止编译器内联它(即将它的代码粘贴到调用者)。这是假装此函数太长不值得内联的廉价方法。
avx512.cpp
#include "func.h"
#include <iostream>
void run_avx512() {
volatile float f = 1;
float arr [16] = {f};
double_it(arr);
for (int i = 0; i < 16; i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
}
这是AVX512使用我们的double_it
功能。它将一些数组加倍并打印结果。我们将使用 AVX512 对其进行编译。
non512.cpp
#include "func.h"
#include <iostream>
void run_non_avx() {
volatile float f = 1;
float arr [16] = {f};
double_it(arr);
for (int i = 0; i < 16; i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
}
和之前一样的逻辑。这个不会用AVX512编译。
lib_user.cpp
void run_non_avx();
int main() {
run_non_avx();
}
一些用户代码。调用未使用 AVX512 编译的 `run_non_avx。它不知道它会膨胀 :)
现在我们可以编译这些文件并link将它们作为共享库(可能常规库也可以)
g++ -c avx512.cpp -o avx512.o -O3 -mavx512f -g3 -fPIC
g++ -c non512.cpp -o non512.o -O3 -g3 -fPIC
g++ -shared avx512.o non512.o -o libbad.so
g++ lib_user.cpp -L . -lbad -o lib_user.x
./lib_user.x
运行 我机器上的这个(没有 AVX512)给了我
$ ./lib_user.x
Illegal instruction (core dumped)
附带说明一下,如果我更改 avx512.o non512.o
的顺序,它就会开始工作。似乎 linker 忽略了相同功能的后续实现。