22

直到今天,我一直认为体面编译器会自动将结构传递值转换为传递引用,如果结构足够大,后者会更快。据我所知,这似乎是一个不费吹灰之力的优化。然而,为了满足我对这是否真的发生的好奇心,我在C++和D中创建了一个简单的测试用例,并查看了GCC和Digital Mars D的输出。两者都坚持按值传递32字节的结构,有问题的函数是加起来的成员和返回的值,没有修改传入的结构。C++版本如下。为什么不通过引用传递结构通用优化?

#include "iostream.h" 

struct S { 
    int i, j, k, l, m, n, o, p; 
}; 

int foo(S s) { 
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p; 
} 

int main() { 
    S s; 
    int bar = foo(s); 
    cout << bar; 
} 

我的问题是,为什么赫克不会像这样由编译器优化,以传递通过引用,而不是实际推动所有这些int小号到堆栈?注:使用的编译器开关:GCC -O2(-O3内联foo()。),DMD -O -inline -release。

编辑:显然,在通常情况下,传值与传递引用的语义不会相同,例如,如果涉及到复制构造函数或原始结构在被调用方中被修改。然而,在很多现实场景中,语义在可观察行为方面将是相同的。这些是我所问的情况。

回答

22

不要忘记,在C/C++中编译器需要能够仅基于函数声明来编译对函数的调用。

鉴于调用者可能仅使用该信息,编译器无法编译该函数以利用您正在讨论的优化。调用者不知道该函数将不会修改任何内容,因此它不能通过ref。由于一些呼叫者可能因缺乏详细信息而传递价值,所以函数必须按照价值传递进行编译,并且每个人都需要按价值传递。

请注意,即使您将该参数标记为'const',编译器仍然无法执行优化,因为该函数可能在说谎并丢弃了常量(这是允许的并且定义良好,只要传入的对象实际上不是const)。

我认为,对于静态函数(或匿名命名空间中的那些函数),编译器可能会进行您正在讨论的优化,因为该函数没有外部链接。只要函数的地址没有传递给其他例程或存储在指针中,就不应该从其他代码中调用。在这种情况下,编译器可以充分了解所有调用者,所以我认为它可以进行优化。

我不确定是否有这样做(实际上,如果有的话,我会感到惊讶,因为它可能无法经常应用)。

当然,作为程序员(使用C++时),只要有可能,就可以使用const&参数强制编译器执行此优化。我知道你在问为什么编译器不能自动执行,但我认为这是最好的。

+0

当进行链接时间优化,也就是链接时间代码生成或整个程序编译时,编译器不需要仅基于声明来编译该调用。它充分了解发生了什么。为了编译对大小和速度敏感的嵌入式应用程序,链接时间代码生成是唯一的方法。 – 2015-02-03 16:16:22

10

一个答案是,编译器需要检测被调用的方法不会以任何方式修改结构的内容。如果确实如此,那么通过引用传递的效果将不同于传递值。

+2

你可以通过写入时复制语义结构等。 – 2012-01-22 19:57:29

+0

是的,我想你可以让你的语言的ABI声明结构是通过引用传递的,但如果被调用者试图修改它们,它就会变成副本。听起来有点混乱 - 你需要使用所有棘手的最坏情况混叠分析来确定它们保证不被触摸。只是为了在语言中有明确的规则更容易 - 如果你不想传递一个结构来减慢你的速度,请给它一个引用或指针。 – Edmund 2012-01-24 05:00:55

11

问题是你要求编译器做出关于用户代码意图的决定。也许我希望我的超级大结构被值传递,以便我可以在复制构造函数中做些什么。相信我,在那里有人有他们有效需要在副本构造函数中调用这样的场景。通过引用切换将绕过复制构造函数。

这是一个编译器生成的决定将是一个坏主意。原因在于它无法推断代码的流向。你不能看电话,并知道它会做什么。你必须a)知道代码和b)猜测编译器优化。

4

确实如果某些语言的编译器可以访问被调用的函数,并且他们可以假定被调用的函数不会改变,那么编译器可以这样做。这有时被称为全局优化,看起来很可能某些C或C++编译器实际上会优化这种情况 - 更可能是通过将代码嵌入这样一个简单的函数中。

3

有很多理由传递价值,并让编译器优化你的意图可能会破坏你的代码。

例如,被调用函数以任何方式修改结构。如果你打算将结果传回给调用者,那么你可以传递一个指针/引用或者自己返回它。

您要求编译器执行的操作是更改代码的行为,这将被视为编译器错误。

如果你想进行优化并通过引用传递,那么通过一切手段修改某人现有的函数/方法定义来接受引用;这并不是那么难。你可能会惊讶于你没有意识到的破裂。

2

按值更改为引用将更改函数的签名。如果函数不是静态的,则会导致其他编译单元发生链接错误,这些编译单元不知道您所做的优化。
的确,实现这种优化的唯一方法是通过某种后链接全局优化阶段。这些非常难以完成,但一些编译器在某种程度上做了它们。

1

嗯,简单的答案是结构在内存中的位置是不同的,因此你传递的数据是不同的。我认为更复杂的答案是线程化。

您的编译器需要检测a)foo不会修改结构; b)foo不对结构元素的物理位置进行任何计算;和c)调用者或调用者产生的另一个线程在foo运行完成之前不会修改该结构。

在你的例子中,可以想象编译器可以做这些事情 - 但保存的内存是不重要的,可能不值得猜测。如果你用一个有200万个元素的结构运行同一个程序会发生什么?

2

我认为这绝对是您可以实现的一种优化(在一些假设下,请参见最后一段),但我不清楚它是否有利可图。而不是将参数推入堆栈(或根据调用约定将参数传递到寄存器),您可以推送一个指针,通过它读取值。这种额外的间接会花费周期。它也需要传入的参数在内存中(所以你可以指向它)而不是在寄存器中。如果传递的记录具有许多字段并且接收记录的功能只读取其中的一部分,那将是有益的。间接浪费的额外周期将不得不弥补通过推送不需要的字段而浪费的周期。

您可能会对LLVM中实际实施的反向优化argument promotion感到惊讶。这将内部函数的参数参数转换为一个值参数(或一个聚集为标量),只有少量字段只能从中读取。这对于通过引用几乎可以传递所有内容的语言特别有用。如果按照dead argument elimination进行操作,则不必传递未触及的字段。

它承担提的是,这种变化的函数被调用只能在被优化功能的工作方式优化是内部的模块被编译(您可以通过使用C声明一个函数static,并与C++模板得到这个)。优化器不仅要解决功能问题,还要解决所有的调用点。这使得这种优化的范围相当有限,除非您在链接时进行优化。另外,当涉及拷贝构造函数时(如其他海报所提到的),优化永远不会被调用,因为它可能会改变程序的语义,优秀的优化器永远不应该这样做。

2

传递引用只是传递地址/指针的语法糖。所以函数必须隐式地引用一个指针来读取参数的值。解引用指针可能会更昂贵(如果在循环中),那么结构拷贝用于按值复制。

更重要的是,像其他人一样,传递引用具有不同于传递值的语义。 const参考做不是表示参考值不会改变。其他函数调用可能会更改引用的值。

1

编译器将需要确保传递进来(如调用代码命名)的结构没有被修改

double x; // using non structs, oh-well 

void Foo(double d) 
{ 
     x += d; // ok 
     x += d; // Oops 
} 

void main() 
{ 
    x = 1; 
    Foo(x); 
}