如何衡量函数的调用开销?

时间:2014-03-10 13:47:40

标签: c++ optimization c++11

我想测量并比较不同函数调用的开销。在最小化代码修改的同时处理扩展类的两种替代方法的意义不同:

  • 使用抽象基类并在虚拟成员函数中提供实现
  • 使用策略宿主类并使用静态和成员函数定义不同的策略

将这些选项与根本不调用任何功能进行比较。我也知道在设计支持动态多态的类时通常使用的NVI习惯用法 - 我使用的示例只是开销的基准。

以下是我尝试用于此目的的代码:

#include <iostream>
#include <vector>
#include <chrono>
#include <ctime>
#include <memory>

class Interface 
{
    public:
        virtual double calculate(double t) = 0; 
        virtual ~Interface() = default;

};

class Square
: 
    public Interface
{
    public:

       double calculate(double d)
       {
           return d*d;
       }

};

class SquareStaticFunction
{
    public:
        static double calculate(double d)
        {
            return d*d; 
        }
};

class SquareMemberFunction
{
    public:
        double calculate(double d)
        {
            return d*d; 
        }
};

template<typename Function>
class Generic
:
    public Function
{
    public:
        using Function::calculate;
};

using namespace std;

int main(int argc, const char *argv[])
{
    vector<double> test(1e06, 5); 

    unique_ptr<Interface> sUptr(new Square());

    Interface* sPtr = new Square(); 

    Generic<SquareStaticFunction> gStatic; 
    Generic<SquareMemberFunction> gMember; 

    double result;

    typedef std::chrono::high_resolution_clock Clock; 

    auto start = Clock::now();
    for (auto d : test)
    {
        result = d * d;  
    }
    auto end = Clock::now(); 

    auto noFunction = end - start; 

    start = Clock::now();  

    for (auto d : test)
    {
        result = sUptr->calculate(d);
    }
    end = Clock::now();  

    auto virtualMemberFunction = end - start; 

    start = Clock::now();  

    for (auto d : test)
    {
        result = sPtr->calculate(d);
    }
    end = Clock::now();  

    auto virtualMemberFunctionRaw = end - start; 

    start = Clock::now();
    for (auto d : test)
    {
        result = gStatic.calculate(d);  
    }
    end = Clock::now(); 

    auto staticPolicy = end - start; 

    start = Clock::now();
    for (auto d : test)
    {
        result = gMember.calculate(d);  
    }
    end = Clock::now(); 

    auto memberPolicy = end - start; 

    cout << noFunction.count() << " " << virtualMemberFunction.count() 
        << " " << virtualMemberFunctionRaw.count() 
        << " " << staticPolicy.count() 
        << " " << memberPolicy.count() << endl;

    delete sPtr; 
    sPtr = nullptr;

    return 0;
}

我使用gcc 4.8.2编译代码,并在Linux x86_64机器上使用以下CPU型号编译:Intel(R)Core(TM)i7-4700MQ CPU @ 2.40GHz。

通过原始指针在一次测试中访问虚拟成员函数,在另一次测试中通过unique_ptr访问虚拟成员函数。首先,我编译了代码而没有任何优化:

g++ -std=c++11 main.cpp -o main

并使用以下shell命令运行1000次测试:

for i in {1..1000}; do ./main >> results; done

我使用以下gnuplot脚本绘制的结果文件(注意对数y轴):

set terminal png size 1600,800
set logscale y 
set key out vert right top
set out 'results.png' 
plot 'results' using 0:1 title "no function" , \
     'results' using 0:2 title "virtual member function (unique ptr)", \
     'results' using 0:3 title "virtual member function (raw ptr)", \
     'results' using 0:4 title "static policy", \
     'results' using 0:5 title 'member function policy'

对于非优化代码,图表如下所示:

Non-optimized function call overhead.

Q1通过unique_ptr对虚函数的调用最终是最昂贵的,因为当dereferencing指向托管对象的指针时,它涉及重定向吗?

然后我打开优化并用以下代码编译代码:

g++ -std=c++11 -O3 main.cpp -o main

产生了以下图表:

enter image description here

Q2:在这种情况下,虚拟成员的功能是否最高,因为当通过基类指针或引用访问时(vtable调度已打开),它是not possible for the compiler to make them inline

问题3:这个问题让我发布了所有这些:在优化图中,静态和成员策略最终是否比这个简单示例的推出代码更快?

修改:启用result volatile并在启用优化的情况下进行编译,使策略的运行时间更长,但它们与原始乘法代码类似:

enter image description here

修改修改代码,以便在不使用 volatile的情况下添加结果而不是分配(由评论中的dyk提议)

result += ...

结果与原始代码的图表相同。

1 个答案:

答案 0 :(得分:5)

用您的代码查看-O3 -march=native -std=c++11的反汇编表明编译器通过检测对同一个未使用变量的不必要的重新影响来进行“太多”优化。正如评论中所建议的那样,我使用的是+=而不是=。我还初始化result = 0main返回result而不是0,以确保编译器计算其值。这个修改过的代码给出了:

  • noFunctionstaticPolicymemberPolicy被降低为mulsdaddsdaddsd,即标量SSE指令。 Clang也没有矢量化(使用vanilla选项),但Intel的icc确实(它根据对齐和迭代计数生成矢量和非矢量版本和跳转)。
  • virtualMemberFunctionvirtualMemberFunctionRaw会导致动态函数调用(无去内核和内联)

您可以通过粘贴代码here来亲眼看到。

在调试版本中回答你的Q1“指针vs unique_ptr”:在-O0调用中没有自动内联,特别是unique_ptr::operator->被明确调用而没有内联,所以这是2函数对于常规指针,每次迭代调用而不是1。对于优化构建,这种差异消失了

回答你的Q2“是否有可能内联虚拟呼叫”:在这个例子中,gcc和clang不会内联呼叫,因为它们可能没有做足够的静态分析。但你可以帮助他们。例如,使用clang 3.3(但不是3.2而不是gcc)将方法声明为const__attribute((pure))完成工作。在gcc(4.8,pre-4.9)中,我尝试将方法标记为final并使用-fwhole-program进行编译,但这并没有删除调用。所以,在这种特定情况下,可以去虚拟化,但不可靠。一般来说,jitted编译器(C#,Java)在去虚拟化方面更胜一筹,因为它们可以从运行时信息中做出更好的假设。