多指针解引用与引用之间的性能差异

时间:2011-04-28 16:39:30

标签: c++

这是几个月前我问过的面试问题:

以下哪项功能执行得更快,Foo1Foo2

void Foo(SomeObjectArray** array, unsigned int size)
{
    for (int i = 0; i < size; i++)
    {
        if (((*array) + i) != NULL)
        {
            ((*array) + i)->Operation1();
            ((*array) + i)->Operation2();
            ((*array) + i)->Operation3();
            ((*array) + i)->Operation4();
            ((*array) + i)->Operation5();
            ((*array) + i)->Operation6();
    }
}

void Foo(SomeObjectArray** array, unsigned int size)
{
    for (int i = 0; i < size; i++)
    {
        if (*((*array) + i) != NULL)
        {
            Object& obj = *((*array) + i);
            obj.Operation1();
            obj.Operation2();
            obj.Operation3();
            obj.Operation4();
            obj.Operation5();
            obj.Operation6();
        }
    }
}

请注意,这是来自内存所以我不完全记住代码,但总体思路是一样的。一个函数是使用指针而另一个函数使用引用(它可能有一个指向数组的指针,如上面的代码,但我不记得确切)。我说I am not sure, and will have to profile the code to find out, but if I had to guess Foo2 'may' be faster。他们没有留下深刻的印象......

当我遇到与此类似的代码(或编写代码)时,我在这里和那里唠叨了几次,并且想知道在这种情况下我应该做些什么。

我知道......

  1. 这是微优化
  2. 编译器很可能会优化它
  3.   

    编辑:我稍微更改了代码,现在它正在检查NULL指针。

8 个答案:

答案 0 :(得分:6)

我认为这是一个非常有趣的问题,我看到很多关于编译器可能会做什么的猜测,但我想仔细看看并确定。所以我拿了e.James的程序并通过GCC运行以获得程序集。我应该说我不知道​​集会,所以如果我错了,有人会纠正我,但我认为我们可以合理地推断出发生了什么。 :)

使用-O0进行编译(无优化)

对于Foo1,我们看到在每次函数调用之前计算数组偏移量:

movl    8(%ebp), %eax
movl    (%eax), %edx
movl    -4(%ebp), %eax
leal    (%edx,%eax), %eax
movl    %eax, (%esp)
call    __ZN10SomeObject10Operation1Ev

这是所有六个方法调用,只是使用不同的方法名称。 Foo2有一些设置代码来获取引用

movl    8(%ebp), %eax
movl    (%eax), %edx
movl    -4(%ebp), %eax
leal    (%edx,%eax), %eax
movl    %eax, -8(%ebp)

然后其中六个,看起来只是一个堆栈指针推送和一个函数调用:

movl    -8(%ebp), %eax
movl    %eax, (%esp)
call    __ZN10SomeObject10Operation1Ev

几乎没有优化我们期望的东西。输出是

Foo1: 18472
Foo2: 17684

使用-O1进行编译(最小化优化)

Foo1效率更高,但每次都会累加数组偏移量:

movl    %esi, %eax
addl    (%ebx), %eax
movl    %eax, (%esp)
call    __ZN10SomeObject10Operation1Ev

Foo2看起来会保存ebxaddl (%edi), %ebx)的值,然后拨打这些电话:

movl    %ebx, (%esp)
call    __ZN10SomeObject10Operation1Ev

这里的时间是

Foo1: 4979
Foo2: 4977

使用-O2进行编译(适度优化)

使用-O2进行编译时,GCC只是摆脱了整个事情,每次调用Foo1Foo2只会导致向dummy添加594(99个增量* 6个电话= 594个增量):

imull   $594, %eax, %eax
addl    %eax, _dummy

没有调用对象的方法,尽管这些方法仍然存在于代码中。正如我们所料,这里的时间是

Foo1: 1
Foo2: 0

我认为这告诉我们Foo2没有优化就会快一点,但实际上这是一个没有实际意义的点,因为一旦它开始优化,编译器就会在堆栈和寄存器之间移动几个长点。

答案 1 :(得分:4)

严格来说,没有优化我会说Foo2更快,因为Foo1每次都必须计算间接ptr,但那不会发生在任何地方。

我会说编译器会优化它并保持不变 看起来编译器有足够的空间,iarray在每次迭代时不会对整个块进行更改,因此它可以优化它将指针放入寄存器,完全相同会做参考。

答案 2 :(得分:3)

考虑到每行上的公共子表达式,编译器可能会将它们优化为相同。但不保证。

使用今天的编译器和处理器,通过这样的扶手椅推理,你无法得出任何合理的结论。要知道的唯一方法是尝试并计时。如果某人在面试答案中没有说清楚,那将是我自动失败

答案 3 :(得分:2)

两者都没有,任何合理的编译器都会愉快地使它们等效。你不是在模板元编程的深度讨论NRVO,它是一个基本的简单优化,使用Common Subexpression Elimination,这是非常常见且相对基本的,并且发布的代码具有微不足道的复杂性,使得绝大多数可能是编译器会做这样的优化。

答案 4 :(得分:2)

如果有人怀疑编译器会优化到相同的结果,这里有一个快速&amp;肮脏的测试程序:

#include <iostream>
#include <time.h>

using namespace std;
size_t dummy;

class SomeObject
{
    public:
        void Operation1();
        void Operation2();
        void Operation3();
        void Operation4();
        void Operation5();
        void Operation6();
};

void SomeObject::Operation1() { for (int i = 1; i < 100; i++) { dummy++; } }
void SomeObject::Operation2() { for (int i = 1; i < 100; i++) { dummy++; } }
void SomeObject::Operation3() { for (int i = 1; i < 100; i++) { dummy++; } }
void SomeObject::Operation4() { for (int i = 1; i < 100; i++) { dummy++; } }
void SomeObject::Operation5() { for (int i = 1; i < 100; i++) { dummy++; } }
void SomeObject::Operation6() { for (int i = 1; i < 100; i++) { dummy++; } }

void Foo1(SomeObject** array, unsigned int size)
{
    for (int i = 0; i < size; i++)
    {
        ((*array) + i)->Operation1();
        ((*array) + i)->Operation2();
        ((*array) + i)->Operation3();
        ((*array) + i)->Operation4();
        ((*array) + i)->Operation5();
        ((*array) + i)->Operation6();
    }
}

void Foo2(SomeObject** array, unsigned int size)
{
    for (int i = 0; i < size; i++)
    {
        SomeObject& obj = *((*array) + i);
        obj.Operation1();
        obj.Operation2();
        obj.Operation3();
        obj.Operation4();
        obj.Operation5();
        obj.Operation6();
    }
}


int main(int argc, char * argv[])
{
    clock_t timer;

    SomeObject * array[100];
    for (int i = 0; i < 100; i++)
    {
        array[i] = new SomeObject();
    }

    timer = clock();
    for (int i = 0; i < 100000; i++) { Foo1(array, 100); }
    cout << "Foo1: " << clock() - timer << endl;

    timer = clock();
    for (int i = 0; i < 100000; i++) { Foo2(array, 100); }
    cout << "Foo2: " << clock() - timer << endl;

    for (int i = 0; i < 100; i++)
    {
        delete array[i];
    }

    return 0;
}

结果总是在几毫秒之内:

  

Foo1:15437
  Foo2:15484

答案 5 :(得分:1)

Imho,哪个版本更快的问题无关紧要。在对象上一个接一个地调用6种不同的方法是OO设计气味。该对象可能应该提供一种方法来完成所有这些。

答案 6 :(得分:0)

Foo2有额外的对象创建,但除此之外,编译应该使它们大致相同

答案 7 :(得分:0)

我喜欢这个问题,并且不能帮助自己而是回答,虽然我知道我可能错了完全错了。不过,我猜Foo1会更快。

我的愚蠢理由? 好吧,我看到Foo2创建了一个对象引用,获取了“数组”的地址,然后调用了它的方法。

但在Foo1中,它直接使用地址,取消引用它,转到目标内存并直接调用该函数。没有像Foo2那样在Foo1中创建不必要的对象引用。并且我们不知道数组在继承方面的深度是多少,以及将调用多少基类构造函数来获取对象引用,这是额外的时间。所以我猜Foo1稍快一点。 Plz纠正我,因为我有信心我错了。干杯!