繁体   English   中英

内联函数在不同的转换单元中具有不同的编译器标志未定义的行为?

[英]inline function in different translation units with different compiler flags undefined behaviour?

在visual studio中,您可以为各个cpp文件设置不同的编译器选项。 例如:在“代码生成”下,我们可以在调试模式下启用基本运行时检查。 或者我们可以改变浮点模型(精确/严格/快速)。 这些只是一些例子。 有很多不同的旗帜。

只要定义相同,就可以在程序中多次定义内联函数。 我们将此函数放入标题中并将其包含在多个翻译单元中。 现在,如果不同的cpp文件中的不同编译器选项导致该函数的编译代码略有不同,会发生什么? 然后他们确实不同,我们有不确定的行为? 你可以使函数静态(或将它放入一个未命名的命名空间)但是更进一步,直接在类中定义的每个成员函数都是隐式内联的。 这意味着如果这些cpp文件共享相同的编译器标志,我们可能只包含不同cpp文件中的类。 我无法想象这是真的,因为这基本上是容易出错。

我们在未定义行为的土地上真的那么快吗? 还是编译器会处理这种情况?

就标准而言,命令行标志的每个组合都将编译器转换为不同的实现。 虽然实现能够使用其他实现生成的目标文件很有用,但标准并未要求他们这样做。

即使没有内联,也可以考虑在一个编译单元中使用以下功能:

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(同样跳过赋值)。 但是,如果一个编译单元使用了signed char而另一个使用了unsigned,并且如果实现期望在结果寄存器中返回值进行符号扩展或零扩展,则结果可能是编译器可能确定x不能大于127,即使它保持255,或者即使它保持-1也不能小于0。 因此,生成的代码可能会访问arr[255]arr[-1] ,结果可能是灾难性的。

虽然在许多情况下使用不同的编译器标志组合代码应该是安全的,但是标准没有努力区分那些混合安全的地方和不安全的混合。

如果这个问题确实存在,我最近为GCC测试编写了一些代码。

SPOILER:确实如此。

设定:

我正在使用AVX512指令编译我们的一些代码。 由于大多数cpus不支持AVX512,我们需要在没有AVX512的情况下编译大部分代码。 问题是:是否内联函数,在用AVX512编译的cpp文件中使用,可以用非法指令“毒害”整个库。

想象一下,非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];
}

我们定义内联(在链接器意义上)函数。 使用硬编码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。 它不知道它会臃肿:)

现在我们可以编译这些文件并将它们作为共享库链接起来(可能常规lib也可以)

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的顺序,它就会开始工作。 似乎链接器忽略了相同功能的后续实现。

只要定义相同,就可以在程序中多次定义内联函数

不。(“相同”在这里甚至不是一个明确定义的概念。)

在形式上,定义必须在某种非常强烈的意义上是等同的,这甚至不是有意义的,也没有人关心的:

// 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
}

这样的代码完全合理,但正式违反了一个定义规则:

在D的每个定义中,根据6.4 [basic.lookup]查找的相应名称,应指在D定义内定义的实体,或者在重载解析后引用同一实体(16.3 [over.match])并且在匹配部分模板特化(17.9.3 [temp.over])之后,除了如果对象在D的所有定义中具有相同的文字类型且名称可以引用具有内部链接或没有链接的const对象,并且object用常量表达式(8.20 [expr.const])初始化, 并且使用对象的值(但不是地址) ,并且对象在D的所有定义中具有相同的值;

因为异常不允许使用具有内部链接的const对象( const int是隐式静态的),目的是直接绑定const引用(然后仅使用引用作为其值)。 正确的版本是:

inline int impose_limit(int i) {
  return std::min(i, +limit_max); // OK
}

这里limit_max的值用在一元运算符+中, 然后const引用绑定到用该值初始化的临时值 谁真的这样做?

但即使委员会也不认为正式的ODR很重要,正如我们在核心问题1511中所看到的:

1511. const volatile变量和one-definition规则

条款:6.2 [basic.def.odr]状态:CD3发布者:Richard Smith日期:2012-06-18

[在2013年4月的会议上移至DR。]

对于以下示例,此措辞可能不够明确:

  const volatile int n = 0; inline int get() { return n; } 

我们看到委员会认为这公然违反​​了ODR的意图和目的 ,这是一个在每个TU中读取不同易失性对象的代码,即对不同对象有明显副作用的代码,所以不同的可见副作用, 是可以的,因为我们关心哪个是哪个

重要的是内联函数的效果是模糊等价的:执行volatile int读取,这是一个非常弱的等价,但足以自然使用 ODR,这是实例无差异:使用内联函数的特定实例没关系,也无法发挥作用

特别是,易失性读取读取的值根据定义不为编译器所知,因此编译器分析的此函数的后置条件和不变量是相同的。

当在不同的TU中使用不同的函数定义时,你需要确保从调用者的角度来看它们是完全相同的:通过用一个替换另一个来永远不可能让调用者感到惊讶。 这意味着即使代码不同, 可观察行为也必须严格相同

如果使用不同的编译器选项,则它们不得更改函数可能结果的范围(可能由编译器查看)。

因为“标准”(实际上不是编程语言的规范)允许浮点对象具有其官方声明类型不允许的实际表示,所以完全不受约束地使用任何非易失性限定浮点类型除非你激活“ double意味着double ”模式(这是唯一的理智模式),否则任何多次定义的ODR都会出现问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM