C ++:结构访问速度比基本变量慢?

时间:2011-02-27 14:14:09

标签: c++ struct

我找到了一些像这样“优化”的代码:

void somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
}

并且评论说它会使变量访问速度更快,但我一直认为结构元素访问速度和它毕竟不是结构一样快。

有人可以清除我的头脑吗?

9 个答案:

答案 0 :(得分:14)

经验法则:它并不慢,除非探查者说它是。让编译器担心微观优化(他们对他们非常聪明;毕竟,他们已经做了多年)并专注于更大的图景。

答案 1 :(得分:12)

通常的优化规则(Michael A. Jackson)适用: 1.不要这样做。 2.(仅限专家:)不要这样做。

话虽如此,让我们假设它是最内层的循环,占用了性能关键型应用程序的80%的时间。即便如此,我怀疑你会看到任何不同。让我们使用这段代码:

struct Xyz {
    float x, y, z;
};

float f(Xyz param){
    return param.x + param.y + param.z;
}

float g(Xyz param){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    return x + y + z;
}

Running it through LLVM显示:只有在没有优化的情况下,两者按预期行事(g将结构成员复制到本地,然后对这些进行求和; f对从{{取得的值进行求和1}}直接)。使用标准优化级别,两者都会产生相同的代码(提取值一次,然后将它们相加)。

对于短代码,这种“优化”实际上是有害的,因为它不必要地复制浮动。对于在几个地方使用成员的较长代码,如果你主动告诉你的编译器是愚蠢的,那么它可能会有点帮助。使用65(而不是2)添加成员/本地的快速测试证实了这一点:没有优化,param重复加载结构成员,而f重用已经提取的本地成员。优化版本再次相同,并且仅提取成员一次。 (令人惊讶的是,即使启用LTO,也没有强度降低将附加功能转换为乘法,但这只表明所使用的LLVM版本无论如何都没有过于激烈地优化 - 因此它应该在其他编译器中也能正常工作。)

所以,最重要的是:除非你知道你的代码必须由编译器编译,这个编译器非常愚蠢和/或古老,以至于它不会优化任何东西,你现在有了证明编译器将使两种方式等效,从而可以消除以性能为名的可读性和酿造性犯罪。 (如有必要,请为您的特定编译器重复实验。)

答案 2 :(得分:4)

我不是编译大师,所以请耐心等待。我猜测代码的原作者假设通过将结构中的值复制到局部变量中,编译器已将这些变量“放置”到某些平台(例如x86)上可用的浮点寄存器中。如果没有足够的寄存器,它们将被放入堆栈中。

话虽这么说,除非这个代码处于密集计算/循环的中间,否则我会努力清晰而不是速度。很少有人会注意到时间上的一些指令差异。

答案 3 :(得分:2)

您必须查看特定实现的已编译代码才能确定,但​​原则上没有理由为什么您的首选代码(使用struct成员)必须比您显示的代码慢(复制到变量然后使用变量。)

someFunc按值获取结构,因此它有自己的该结构的本地副本。编译器完全可以自由地对结构成员应用完全相同的优化,因为它适用于float变量。它们都是自动变量,在这两种情况下,“as-if”规则允许它们存储在寄存器中而不是存储器中,只要该函数产生正确的可观察行为。

这是当然除非您获取指向结构的指针并使用它,在这种情况下,值需要以指针指向的正确顺序在某处写入内存。这开始限制优化,并且通过以下事实引入了其他限制:如果传递指向自动变量的指针,编译器就不能再认为变量名是对该内存的引用因此可以修改其内容的唯一方法。对同一对象进行多次引用称为“别名”,并且有时会阻止在对象以某种方式被称为别名时可以进行的优化。

然后再次,如果这是一个问题,并且函数中的其余代码以某种方式确实使用了指向结构的指针,那么当然你可能会狡猾地将值从正确性的POV复制到变量中。因此,声称的优化并不像在这种情况下看起来那么简单。

现在,可能有特定的编译器(或特定的优化级别)无法应用于结构化所允许应用的所有优化,但是对浮点变量应用等效优化。如果是这样,则评论是正确的,这就是为什么你必须检查以确定。例如,可以比较发出的代码:

float somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
    for (int i = 0; i < 10; ++i) {
        x += (y +i) * z;
    }
    return x;
}

用这个:

float somefunc(SomeStruct param){
    for (int i = 0; i < 10; ++i) {
        param.x += (param.y +i) * param.z;
    }
    return param.x;
}

可能还存在优化级别,其中额外的变量会使代码变得更糟。我不确定我是否非常信任代码评论,这些评论说“据说这可以让它更快地访问”,听起来像作者并不清楚它为什么重要。 “显然它可以更快地访问 - 我不知道为什么,但是确认这一点的测试并证明它在我们程序的上下文中产生明显的差异,在以下位置的源代码控制中”更像是它; - )

答案 4 :(得分:2)

在未经优化的代码中:

  • 函数参数(不通过引用传递)在堆栈上
  • 局部变量也在堆栈上

对汇编语言中的局部变量和函数参数的未优化访问看起来或多或少如下:

mov %eax, %ebp+ compile-time-constant

其中%ebp是一个帧指针(一个函数的'this'指针)。

如果访问参数或局部变量,则没有区别。

从结构中访问元素的事实与程序集/机器的观点完全没有区别。结构是用C语言构造的结构,使程序员的生活更轻松。

所以,最后,我的答案是:不,这样做绝对没有任何好处。

答案 5 :(得分:1)

当使用指针时,有充分和有效的理由进行这种优化,因为消耗所有输入首先使编译器免于可能的别名问题,从而阻止它产生最佳代码(现在有限制)但也是如此。

对于非指针类型,理论上存在开销,因为每个成员都是通过struct的this指针访问的。理论上,这可能在内循环中是显而易见的,否则在理论上它将是一个小的开销。
然而,在实践中,现代编译器几乎总是(除非有复杂的继承层次结构)产生完全相同的二进制代码

我问过自己与两年前的问题完全相同,并使用gcc 4.4进行了非常广泛的测试用例。我的发现是,除非你真的试图在编译器的两腿之间插图,否则生成的代码绝对没有区别。

答案 6 :(得分:0)

编译器可以制作更快的代码来复制float-to-float。 但是当使用x时,它将被转换为内部FPU表示。

答案 7 :(得分:0)

当你指定要操作的“简单”变量(不是结构/类)时,系统只需要去那个地方并获取它想要的数据。

但是当您在结构或类中引用变量(如A.B)时,系统需要计算BA区域内的位置(因为可能还有其他在它之前声明的变量),并且该计算比上面描述的更简单的访问需要更多。

答案 8 :(得分:0)

Piotr给出了真正的答案。这个只是为了好玩。

我测试了它。这段代码:

float somefunc(SomeStruct param, float &sum){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    float xyz = x * y * z;
    sum = x + y + z;
    return xyz;
}

这段代码:

float somefunc(SomeStruct param, float &sum){
    float xyz = param.x * param.y * param.z;
    sum = param.x + param.y + param.z;
    return xyz;
}

使用g++ -O2编译时生成相同的汇编代码。但是,它们会在关闭优化的情况下生成不同的代码。区别在于:

<   movl    -32(%rbp), %eax
<   movl    %eax, -4(%rbp)
<   movl    -28(%rbp), %eax
<   movl    %eax, -8(%rbp)
<   movl    -24(%rbp), %eax
<   movl    %eax, -12(%rbp)
<   movss   -4(%rbp), %xmm0
<   mulss   -8(%rbp), %xmm0
<   mulss   -12(%rbp), %xmm0
<   movss   %xmm0, -16(%rbp)
<   movss   -4(%rbp), %xmm0
<   addss   -8(%rbp), %xmm0
<   addss   -12(%rbp), %xmm0
---
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   mulss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   mulss   %xmm1, %xmm0
>   movss   %xmm0, -4(%rbp)
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   addss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   addss   %xmm1, %xmm0

标记为<的行对应于具有“优化”变量的版本。在我看来,“优化”版本甚至比没有额外变量的版本更慢。但是,这是可以预期的,因为x,y和z在堆栈上分配,就像param一样。分配更多堆栈变量以复制现有堆栈变量有什么意义?

如果执行“优​​化”的人更了解语言,他可能会将这些变量声明为register,但即便如此,“优化”版本也会稍微变慢和变长,至少在G ++ / X86-64。