这是几个月前我问过的面试问题:
以下哪项功能执行得更快,Foo1
或Foo2
?
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
。他们没有留下深刻的印象......
当我遇到与此类似的代码(或编写代码)时,我在这里和那里唠叨了几次,并且想知道在这种情况下我应该做些什么。
我知道......
编辑:我稍微更改了代码,现在它正在检查NULL指针。
答案 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
看起来会保存ebx
(addl (%edi), %ebx
)的值,然后拨打这些电话:
movl %ebx, (%esp)
call __ZN10SomeObject10Operation1Ev
这里的时间是
Foo1: 4979
Foo2: 4977
使用-O2
进行编译(适度优化)
使用-O2
进行编译时,GCC只是摆脱了整个事情,每次调用Foo1
或Foo2
只会导致向dummy
添加594(99个增量* 6个电话= 594个增量):
imull $594, %eax, %eax
addl %eax, _dummy
没有调用对象的方法,尽管这些方法仍然存在于代码中。正如我们所料,这里的时间是
Foo1: 1
Foo2: 0
我认为这告诉我们Foo2
没有优化就会快一点,但实际上这是一个没有实际意义的点,因为一旦它开始优化,编译器就会在堆栈和寄存器之间移动几个长点。
答案 1 :(得分:4)
严格来说,没有优化我会说Foo2更快,因为Foo1每次都必须计算间接ptr,但那不会发生在任何地方。
我会说编译器会优化它并保持不变
看起来编译器有足够的空间,i
和array
在每次迭代时不会对整个块进行更改,因此它可以优化它将指针放入寄存器,完全相同会做参考。
答案 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)
答案 6 :(得分:0)
Foo2有额外的对象创建,但除此之外,编译应该使它们大致相同
答案 7 :(得分:0)
我喜欢这个问题,并且不能帮助自己而是回答,虽然我知道我可能错了完全错了。不过,我猜Foo1会更快。
我的愚蠢理由? 好吧,我看到Foo2创建了一个对象引用,获取了“数组”的地址,然后调用了它的方法。
但在Foo1中,它直接使用地址,取消引用它,转到目标内存并直接调用该函数。没有像Foo2那样在Foo1中创建不必要的对象引用。并且我们不知道数组在继承方面的深度是多少,以及将调用多少基类构造函数来获取对象引用,这是额外的时间。所以我猜Foo1稍快一点。 Plz纠正我,因为我有信心我错了。干杯!