7

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

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

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

4

3 回答 3

3

就标准而言,命令行标志的每种组合都会将编译器转换为不同的实现。虽然实现能够使用由其他实现产生的目标文件是有用的,但标准没有强加他们这样做的要求。

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

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而另一个使用无符号,并且如果实现预期在结果寄存器中返回值以符号扩展或零扩展,则结果可能是编译器可能确定x不能更大大于 127,即使它持有 255,或者它不能小于 0,即使它持有 -1。因此,生成的代码可能会访问arr[255]arr[-1],并带来潜在的灾难性结果。

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

于 2018-08-28T16:23:48.947 回答
2

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

剧透:确实如此。

设置:

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

想象这样一种情况,来自非 AVX512 cpp 文件的函数调用我们的函数,但它遇到了来自 AVX512 编译单元的程序集。这会给我们illegal instruction非 AVX512 机器。

试一试吧:

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。它不知道它会起泡:)

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

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

于 2019-08-12T21:35:14.517 回答
-1

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

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

形式上,定义必须在某种非常强烈的意义上是等价的,这甚至没有任何意义作为要求,也没有人关心:

// 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 对象,并且用常量表达式(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 变量和单定义规则

部分: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,即实例无差异:使用内联函数的哪个特定实例没关系,也不能有所作为

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

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

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

因为“标准”(实际上并不是编程语言的规范)允许浮点对象具有其官方声明类型所不允许的真实表示,以完全不受约束的方式,使用任何非 volatile 限定的浮点类型任何受 ODR 多重定义的东西似乎都有问题,除非您激活“double手段double”模式(这是唯一理智的模式)。

于 2018-11-30T02:32:54.383 回答