C语言 为什么编译器执行别名,如果它降低运行时性能?

ct3nt3jp  于 2023-03-28  发布在  其他
关注(0)|答案(3)|浏览(146)

我一直在学习C和计算机科学的主题纯粹出于兴趣,它使我成为编译器的兴趣.我所读到的一切告诉我,别名的结果在较慢的汇编输出,需要重新加载值在每次迭代.
我已经能够在英特尔C / C++编译器的一些基准测试中使用-fno-alias标志获得轻微的增加。GCC和Clang / LLVM没有等效的标志。有-fargument-noalias-fargument-noalias-global,我猜这会禁用函数参数上的别名-如果我错了请纠正我-但我没有注意到它在运行时甚至编译时性能上有任何不同。
声明是否
别名通过阻止编译器执行某些优化来影响性能。
始终成立,编译器别名有什么好处?为什么除了英特尔编译器之外,现代编译器的一项功能不能通过标记轻松关闭?不使用别名的编译会导致代码不符合ABI标准吗?

ocebsuys

ocebsuys1#

编译器不执行别名。你的程序可能会执行别名。
编译器要么允许您执行别名,要么假设您没有执行别名,然后编写机器码,如果您执行别名,则机器码无法工作。
例如,你可以这样写代码:

void f(int *x, int *y, int *z) {
    *y += *x;
    *z += *x;
}

函数只是把*x加到*y*z上,对吧?所以汇编代码应该是这样的吧?(我用伪C写的汇编代码)

eax = argument x
ebx = argument y
ecx = *eax
*ebx += ecx
ebx = argument z
*ebx += ecx

***错误。***如果yx指向相同的位置,则此汇编代码的工作方式与C代码不同,因为它使用了 * 旧 * *x。要正确工作,它必须像这样:

eax = argument x
ebx = argument y
ecx = *eax
*ebx += ecx
ebx = argument z
ecx = *eax // extra instruction
*ebx += ecx

这是另一条指令,只有当x==y时才有用,但编译器必须将其放在那里以防万一。* 别名 * 意味着*x*y是同一变量的不同名称。如果编译器假设xy不能是同一指针(即没有别名),则它可以跳过该指令,因此指令更少,程序运行更快。
你可以通过使用restrict来告诉编译器它们没有别名,就像这样:

void f(int *restrict x, int *restrict y, int *restrict z) {
    *y += *x;
    *z += *x;
}

-fno-alias告诉Intel编译器别名不会在任何地方发生。这可能会在某些地方产生bug,因为参数有时会别名-尽管它可能在小程序中工作正常,因为它们都没有别名。restrict更仔细地针对您知道不会别名的特定参数。

a6b3iqyw

a6b3iqyw2#

别名是关于编译器可以允许对某些变量做出什么样的假设。C编译器基于翻译单元工作,这意味着它不太可能知道另一个翻译单元中发生了什么。所有具有“外部链接”的东西,无论是变量还是函数,都可以从不同的翻译单元访问。所以通常编译器不会知道这样的函数是如何被调用的。
例如:

int func1 (int* x, int* y)
{
  *y = 2;
  *x = 1;
  if(*y == 1)
  {
    return 1;
  }
  return 2;
}

如果编译器不知道这个函数是如何被调用的,那么在if语句中,它就不能假设*y2,因为x可能指向与y相同的位置--它们可能是别名,所以编译器必须将值加载到内存中并检查以确保这一点。
但在这种情况下,我们将指针类型更改为不允许别名的类型:

int func2 (double* x, int* y)
{
  *y = 2;
  *x = 1;
  if(*y == 1)
  {
    return 1;
  }
  return 2;
}

现在编译器可以自由地假设*x不接触*y,所以这个函数将总是返回2,并可以相应地进行优化。允许哪些类型别名的规则被非正式地称为“严格指针别名”。
使用gcc选项也可以实现类似的效果,但没有理由这样做......因为从C99开始,我们就有C标准支持,可以向编译器证明两个值不是别名,即restrict指针限定符:

int func3 (int* x, int* restrict y)
{
  *y = 2;
  *x = 1;
  if(*y == 1)
  {
    return 1;
  }
  return 2;
}

这告诉编译器所有对y所指向的值的访问都是通过指针y进行的。所以现在编译器可以自由地假设函数总是返回2。这是程序员和编译器之间的契约-程序员必须通过不传递指向同一变量的指针等方式来履行它。
“所有访问都通过这个指针”很重要,因为这也包括全局变量。

int x;

int func4 (int* y)
{
  *y = 2;
  x = 1;
  if(*y == 1)
  {
    return 1;
  }
  return 2;
}

这里编译器不能假设y没有被更新,因为它不能假设函数没有被调用为:

extern int x;
func4(&x);

类似地,如果我们将x声明为static int x;而不是全局变量,这将达到相同的目的,因为编译器可以假设x不会在当前翻译单元之外被访问。
你可以在https://godbolt.org/z/68Earfafn中看到这些例子生成的机器代码。正如你所知道的,编译器必须假设两个指针都可以别名的版本效率较低,因为它必须加载值并执行比较指令(这会增加一个分支)。

qoefvg9y

qoefvg9y3#

在阅读了对我的问题的评论后,我意识到我对编译器的别名是什么意思感到非常困惑。我错误地将其理解为编译器的一个单独功能,但实际上它是代码中的一种情况。别名是在系统编程语言(如C和C++)中使用指针或引用访问同一内存位置的一种方式。
因此,两个不同的名称被用来引用内存中的同一个变量或对象。当其中一个指针或引用修改内存位置,而另一个指针或引用不知道此更改时,这可能导致意外行为。
如果使用指针更改变量的值,则另一个指针访问的函数或调用中的变量值(s)也会改变。因为编译器必须确保通过一个指针所做的更改通过另一个指针可见,它限制了编译器可以执行的各种优化技术,并且通常导致需要在每次执行时获取引用变量的附加汇编指令。函数或循环的迭代,因为它易于在整个运行时被程序中的另一个函数或调用改变。

相关问题