【问题标题】:Function pointer runs faster than inline function. Why?函数指针比内联函数运行得更快。为什么?
【发布时间】:2013-05-17 14:53:05
【问题描述】:

我在我的电脑(Intel 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'时函数指针比内联函数快?

【问题讨论】:

  • 你没有优化。分析未优化的代码是没有意义的。
  • 加上-O3速度还是一样的。
  • 函数指针总是在 0.22 秒,内联函数在 0.26。
  • 我看到的恰恰相反。
  • 奇怪的是,当我分析内联函数时,总是会发生打嗝。我已经运行了大约 50 次基准测试,交替使用内联和指针,内联几乎总是较慢。

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


【解决方案1】:

简短的回答:不是。

  • 你用 -O0 编译,它没有优化(很多)。没有优化,你就没有“快”的说法,因为未优化的代码没有那么快。
  • 您使用toBigEndian 的地址,这样可以防止内联。 inline 关键字无论如何都是对编译器的提示,它可能会或可能不会遵循。您已尽力使其遵循该提示。

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

  • 优化您的代码
  • 使用两个函数,做同样的事情,一个被内联,另一个获得地址

【讨论】:

  • 显然我确实多次运行代码,因此“左右”。不过,您的其他观点似乎很好。此外,-O3 似乎根本不会影响性能。
  • 我想我必须接受你的回答。这太荒谬了,与程序本身无关。此外,我什至不必优化该函数,因为它在我需要它的程序中并不经常调用。
  • 我删除了关于多次运行循环的要点,因为我在那里有一些错误的假设。
【解决方案2】:

衡量性能的一个常见错误(除了忘记优化)是使用错误的工具进行衡量。如果您要测量整个 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;
}

【讨论】:

  • 感谢您提供了测量周期而不是秒数的好主意。
【解决方案3】:

哦,s**t(我需要在这里审查脏话吗?),我发现了。它在某种程度上与循环内的时间有关。当我将它移到外面时,

#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 秒。很抱歉打扰各位了。

【讨论】:

    【解决方案4】:

    首先,使用 -O0,您不会运行优化器,这意味着编译器会忽略您的内联请求,因为它是免费的。两个不同调用的成本应该几乎相同。试试 -O2。

    其次,如果您只运行 0.22 秒,则启动程序所涉及的奇怪可变成本完全支配了运行测试功能的成本。该函数调用只是一些指令。如果您的 CPU 以 2 GHz 运行,它应该在 20 纳秒左右执行该函数调用,因此您可以看到无论您测量的是什么,它都不是运行该函数的成本。

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

    【讨论】:

    • 好的,我让它循环了 500,000,000 次。函数指针11.3秒,其他函数13.2秒。
    【解决方案5】:

    对于许多/最自尊的现代编译器,您发布的代码仍将内联函数调用,即使通过指针调用它也是如此。 (假设编译器做出了合理的努力来优化代码)。这种情况太容易看穿了。换句话说,生成的代码很容易在两种情况下最终几乎相同,这意味着您的测试对于测量您要测量的内容并没有真正有用。

    如果你真的想确保调用是通过指针物理执行的,你必须努力“混淆”编译器,使其在编译时无法计算出指针值。例如,使指针值依赖于运行时,如

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

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

    【讨论】:

      猜你喜欢
      • 2021-02-18
      • 1970-01-01
      • 2011-04-30
      • 2013-03-14
      • 1970-01-01
      • 2012-06-29
      相关资源
      最近更新 更多