C++ 如何度量函数的调用开销?

C++ 如何度量函数的调用开销?,c++,optimization,c++11,C++,Optimization,C++11,我想测量和比较不同函数调用的开销。在处理扩展类同时最小化代码修改的两种可选方法的意义上有所不同: 使用抽象基类并在虚拟成员函数中提供实现 使用策略主机类并使用静态和成员函数定义不同的策略 将这两个选项与根本不调用函数进行比较。我还了解在设计支持动态多态性的类时通常使用的习惯用法——我使用的示例只是开销的基准 以下是我尝试用于此目的的代码: #include <iostream> #include <vector> #include <chrono> #in

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

  • 使用抽象基类并在虚拟成员函数中提供实现
  • 使用策略主机类并使用静态和成员函数定义不同的策略
将这两个选项与根本不调用函数进行比较。我还了解在设计支持动态多态性的类时通常使用的习惯用法——我使用的示例只是开销的基准

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

#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;
}
对于未优化的代码,图表如下所示:

Q1通过
unique\ptr
调用虚拟函数是否会因为指针指向托管对象时涉及重定向而成为最昂贵的调用

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

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

这导致了下图:

问题2:在这种情况下,虚拟成员函数的成本是否最高,因为当通过基类指针或引用访问时(vtable dispatch打开),虚拟成员函数的成本最高

问题3:正是这个问题让我发布了这一切:在优化的图表中,静态和成员策略最终如何可能比这个简单示例中推出的代码更快

编辑:使
结果
不稳定
并在启用优化的情况下编译,会使策略的运行时间大得多,但它们与原始乘法代码类似:

编辑修改代码,以便在不使用
volatile的情况下将结果添加到而不是分配(由dyk在注释中提出):

result += ...

使用与原始代码相同的图表进行的结果。

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

  • noFunction
    staticPolicy
    memberPolicy
    降低为
    mulsd
    addsd
    addsd
    ,即标量SSE指令。Clang也不会矢量化(使用普通选项),但Intel的icc会矢量化(它会根据对齐和迭代次数生成矢量和非矢量版本以及跳跃)
  • virtualMemberFunction
    virtualMemberFunctionRaw
    导致动态函数调用(无反虚拟化和内联)
通过粘贴代码,您可以自己查看

要回答您的Q1“调试构建中的指针vs
unique\u ptr
”:在
-O0
中,调用不会自动内联,特别是
unique\u ptr::operator->
是显式调用的,没有内联,因此每个迭代调用2个函数,而不是常规指针调用1个函数。对于优化的构建,这种差异消失了


回答问题2“是否可以内联虚拟调用”:在本例中,gcc和clang不内联调用,因为它们可能没有进行足够的静态分析。但是你可以帮助他们。例如,Clang3.3(但不是3.2,也不是gcc)将方法声明为
const
,而
\u属性((纯))
完成了这项工作。在gcc(4.8,4.9之前)中,我尝试将该方法标记为
final
,并使用
-fwhole程序进行编译,但这并没有删除调用。因此,在这种特定情况下,可以进行去虚拟化,但不可靠。一般来说,jitted编译器(C#,Java)更擅长反虚拟化,因为它们可以从运行时信息中做出更好的假设。

您是否尝试过以不同的顺序运行测试?@dyp:当我将测试与展开乘法放在除开始之外的任何其他位置时,测量的时间与策略类代码有或多或少的重叠。你知道为什么会发生这种情况吗?我不完全确定,但我猜会发生:)可能与缓存有关。此外,你可以尝试
-march=native
-fwhole程序
,并将
结果
输出到某处;这将允许进行更积极的优化。在使用
result
之后,您是否对其进行了任何操作?死代码优化相对容易。副作用包括打印语句或返回值的死代码,则更少。“Jitted编译器(C#,Java)更擅长优化此特定情况。”请尝试
-fwhole program
也传递,并使用
final
,然后再得出此结论。@Ali:在这个特定情况下,我指的是虚拟调用,不仅当一个方法只有一个实现时,这是一种非常特殊的情况。一般来说,jitter可以访问动态+静态信息,因此在理论上它们可以比静态编译器做出更准确的假设。实际上,这取决于编译器实现;)@阿里:我用
最终版
整个程序
进行了测试,并编辑了我的答案(没有内联,但用
纯版
编写了3.3内联),谢谢你的反馈。
result += ...