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

 咖啡十伴侣 发布于 2023-01-18 22:12

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

使用抽象基类并在虚拟成员函数中提供实现

使用策略宿主类并使用静态和成员函数定义不同的策略

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

这是我试图用于此目的的代码:

#include 
#include 
#include 
#include 
#include 

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
class Generic
:
    public Function
{
    public:
        using Function::calculate;
};

using namespace std;

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

    unique_ptr sUptr(new Square());

    Interface* sPtr = new Square(); 

    Generic gStatic; 
    Generic 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.

通过原始指针在一次测试中访问虚拟成员函数,在另一次测试中通过a访问虚拟成员函数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'

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

非优化函数调用开销.

Q1对虚拟函数的调用unique_ptr最终是最昂贵的,因为它在解除引用托管对象的指针时涉及重定向?

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

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

结果如下图所示:

在此输入图像描述

Q2:在这种情况下,虚拟成员的功能是否成本最高,因为当通过基类指针或引用访问时(vtable调度打开),编译器不可能使它们内联?

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

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

在此输入图像描述

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

result += ...

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

1 个回答
  • -O3 -march=native -std=c++11查看代码的反汇编表明编译器通过检测对同一个未使用变量的不必要的重新实现来进行"太多"优化.正如评论中所建议的,我用的是+=代替=.我也初始化result = 0main返回result而不是0确保编译器计算其值.此修改后的代码给出:

    noFunction,staticPolicymemberPolicy降低为mulsd,addsd,addsd,即标量SSE指令.Clang也没有矢量化(使用vanilla选项),但Intel的icc确实(它根据对齐和迭代计数生成矢量和非矢量版本和跳转).

    virtualMemberFunctionvirtualMemberFunctionRaw导致动态函数调用(无去内核和内联)

    你可以在这里粘贴你的代码来亲眼看看.

    要回答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)在去虚拟化方面更胜一筹,因为它们可以从运行时信息中做出更好的假设.

    2023-01-18 22:33 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有