函数指针比内联函数运行得更快。为什么?

时间:2013-05-22 13:56:37

标签: c++ performance function-pointers inline-functions

我在计算机上运行了我的基准测试(英特尔i3-3220 @ 3.3GHz,Fedora 18),并得到了非常意外的结果。函数指针实际上比内联函数快一点。

代码:

#include <iostream>
#include <chrono>
inline short toBigEndian(short i)
{
    return (i<<8)|(i>>8);
}
short (*toBigEndianPtr)(short i)=toBigEndian;
int main()
{  
    std::chrono::duration<double> t;
    int total=0;
    for(int i=0;i<10000000;i++)
    {
        auto begin=std::chrono::high_resolution_clock::now();
        short a=toBigEndian((short)i);//toBigEndianPtr((short)i);
        total+=a;
        auto end=std::chrono::high_resolution_clock::now();
        t+=std::chrono::duration_cast<std::chrono::duration<double>>(end-begin);
    }
    std::cout<<t.count()<<", "<<total<<std::endl;
    return 0;
}

编译
g++ test.cpp -std=c++0x -O0

'toBigEndian'循环总是在0.26-0.27秒左右完成,而'toBigEndianPtr'则需要0.21-0.22秒。

更奇怪的是,当我删除'total'时,函数指针在0.35-0.37秒时变慢,而内联函数在0.27-0.28秒左右。

我的问题是:

当'total'存在时,为什么函数指针比内联函数更快?

5 个答案:

答案 0 :(得分:7)

简短回答:事实并非如此。

  • 使用-O0进行编译,但不进行优化(多次)。如果没有优化,你就不会说“快”了,因为未经优化的代码没有那么快。
  • 你取toBigEndian的地址,阻止内联。 inline关键字无论如何都是编译器的提示,它可能会也可能不会跟随。您尽力使其遵循该提示。

所以,为了给你的测量任何意义,

  • 优化您的代码
  • 使用两个函数,做同样的事情,一个内联,另一个采用

答案 1 :(得分:3)

衡量绩效的一个常见错误(除了忘记优化)是使用错误的工具来衡量。如果您正在测量整个10000000或500000000次迭代的性能,那么使用std :: chrono会很好。相反,你要求它测量toBigEndian的调用/内联。一个函数,它是所有6条指令。所以我切换到rdtsc(读取时间戳计数器,即时钟周期)。

允许编译器真正优化循环中的所有内容,而不是在每次微小迭代中记录时间而使其混乱,我们有不同的代码序列。现在,在使用g++ -O3 fp_test.cpp -o fp_test -std=c++11进行编译之后,我观察到了预期的效果。内联版本每次迭代平均大约2.15个周期,而函数指针每次迭代大约需要7.0个周期。

即使不使用rdtsc,差异仍然可以观察到。内联代码的挂钟时间为360毫秒,函数指针的挂钟时间为1.17秒。因此,可以在此代码中使用std :: chrono代替rdtsc。

修改后的代码如下:

#include <iostream>
static inline uint64_t rdtsc(void)
{
  uint32_t hi, lo;
  asm volatile ("rdtsc" : "=a"(lo), "=d"(hi));
  return ( (uint64_t)lo)|( ((uint64_t)hi)<<32 );
}
inline short toBigEndian(short i)
{
    return (i<<8)|(i>>8);
}
short (*toBigEndianPtr)(short i)=toBigEndian;
#define LOOP_COUNT 500000000
int main()
{
    uint64_t t = 0, begin=0, end=0;
    int total=0;
    begin=rdtsc();
    for(int i=0;i<LOOP_COUNT;i++)
    {
        short a=0;
        a=toBigEndianPtr((short)i);
        //a=toBigEndian((short)i);
        total+=a;   
    }
    end=rdtsc();
    t+=(end-begin);
    std::cout<<((double)t/LOOP_COUNT)<<", "<<total<<std::endl;
    return 0;
}

答案 2 :(得分:2)

哦,不是(我需要在这里审查咒骂?),我发现了。它在某种程度上与循环内部的时间有关。当我把它移到外面时,

#include <iostream>
#include <chrono>
inline short toBigEndian(short i)
{
    return (i<<8)|(i>>8);
}

short (*toBigEndianPtr)(short i)=toBigEndian;
int main()
{  
    int total=0;
    auto begin=std::chrono::high_resolution_clock::now();
    for(int i=0;i<100000000;i++)
    {
        short a=toBigEndianPtr((short)i);
        total+=a;
    }
    auto end=std::chrono::high_resolution_clock::now();
    std::cout<<std::chrono::duration_cast<std::chrono::duration<double>>(end-begin).count()<<", "<<total<<std::endl;
    return 0;
}

结果正如他们应该的那样。内联0.08秒,指针0.20秒。很抱歉打扰你们。

答案 3 :(得分:0)

首先,使用-O0,您没有运行优化器,这意味着编译器忽略了您的内联请求,因为它可以自由执行。两个不同呼叫的成本应该几乎相同。试试-O2。

其次,如果你只运行0.22秒,那么启动程序所涉及的奇怪可变成本将主导运行测试功能的成本。该函数调用只是一些指令。如果你的CPU运行速度为2 GHz,它应该以20纳秒的速度执行该函数调用,这样你就可以看到无论你测量的是什么,都不是运行该函数的成本。

尝试循环调用测试函数,比如1,000,000次。使循环次数增大10倍,直至> 10秒运行测试。然后将结果除以循环次数,以得出操作成本的近似值。

答案 4 :(得分:-1)

对于许多/大多数自尊的现代编译器,即使通过指针调用它,您发布的代码仍将内联函数调用。 (假设编译器合理地努力优化代码)。这种情况太容易被人看透了。换句话说,生成的代码在两种情况下都可以轻松地结束,这意味着您的测试对于测量您要测量的内容并不是非常有用。

如果你真的想确保通过指针物理执行调用,你必须努力将编译器“混淆”到在编译时无法找出指针值的程度。例如,使指针值与运行时相关,如

toBigEndianPtr = rand() % 1000 != 0 ? toBigEndian : NULL;

或沿着这些方向的东西。您还可以将函数指针声明为volatile,这通常会导致每次都进行真正的直通指针调用,并强制编译器在每次迭代时从内存中重新读取指针值。