程序运算性能测量

定量分析是一切优化过程中最重要的过程之一。在性能优化和分析中,我们如何来描述一段代码的执行效率呢?这里,我将这一过程分为2步。

第一步:我们要找到一个客观的、相对稳定的单位,来描述一个程序的运行时间;

第二步:构造合理的、更接近起初情况的测试用例来进行测量。

这样,最终得到的结果,就是对于一段代码执行效率的客观描述。

一、单位确定

首先,我们要确定下来描述单位。通常来说,时间差被广泛地用做描述计算能力的单位。顾名思议,就是在代码前记下一个时间T1,在执行后记下一个时间T2。然后通过计算? = T2 - T1即可得到执行的时长。这种方法方便、快捷,而且也较为准确。它被广泛地用于对计算能力的约束上。如,暴力破解某某密码要几小时等等。相信大家都熟悉下面一段话:

“1976年9月,美国数学家阿佩尔和哈肯利用IBM360计算机工作了1200多个小时,终于证明了四色问题是正确的,轰动了世界。从此四色问题变成了四色定理。”

细心的读者一定已经发现,单纯的“1200多个小时”是不能够描述四色问题的验证过程时间的,它往往要跟上一个前提,在IBM360机器,要求有同型号、子型号、同频率、同批次的CPU,才可以得到相同的结果。那么,是否有更加客观的单位可以描述代码的执行能力呢?这个单位一定符合5个条件。

1)精度高——它不可能比时间精度低,通过gettimeofday得到的时间可以精确至“”;

2)时间相关——我们可以从它经过计算转换为相对时间:秒;

3)低耦合——尽量不要与CPU的主频等主要参数有过多的关系,换一台机器后它的值变化不会很大;

4)相对稳定——不会导致前后2次测量的变化过大。

CPU时钟周期,我们想到了这个基础单位。它的单位是Tick,对于主流的0.5GHz-2.0GHz的机器上,它分别可以精确至“”。我们可以将它与CPU主频进行相乘来得到相对时间。对于支持Pipeline、L1/L2 Cache,指令集相同的CPU,它的差别并不大。由于CPU周期是由硬件晶振控制的,在硬件不出故障情况下,它相对稳定。它完全符合精度高、时间相关、低耦合以及相对稳定4个特点。本文后续的讨论将全部基于CPU时间周期来进行讨论。

二、测量方法

我们已经确定了用纳秒级的计量单位“CPU时钟周期”。如何才能获得系统当前的CPU时钟周期呢?INTEL® CPU提供了一个64位的寄存器TSC来存储自开机来的周期值。并可通过指令“rdtsc”将其写入edx:eax。所以,我们可以在代码中插入这样的语句。

#define rdtscll_64(val) do {\
unsigned int __a,__d; __asm__ __volatile__("rdtsc" : "=a" (__a), "=d" (__d)); (val) = (((unsigned long long)__d)<<32) | (__a); } while(0);

通过调用rdtscll即可以得到64位的CPU时钟周期数。

三、测量误差

在了解测量方法之后,同样我们要了解到这个测量方法在哪些情况下会产生误差。

从TSC本身来说,最常见的误差便是多核的影响。CPU并不会对多个核的tsc进行同步,所以,我们首先要保证测量过程是在同一个核上进程的。如果被测代码段执行过程中存在进程切换,则测量值一定不会很准。所以如果我们在测量的过程中要保证被测量函数不要进行进程切换。也就是说如果被测函数有异步IO调用,我们则需要将进程绑定在一个核上。

由于cache的存在,导致我们在对访问密集型的代码进行测量的时候,前后2次测量的结果可能相差5-10倍,所以当我们希望获得单次非cache内内存获得开销的时候,我们需要需要取构造一个复杂的case。

时下非常流行的虚拟机技术也会影响测量的结果,因为有些虚拟机自己模拟了这个TSC这样我们拿到的值就是物理机硬件的tsc。因为在对虚拟机进行统计的时候需要关注它是如何获得TSC的。

当然,TSC本身也是有开销的,虽然它很小。大概在20-40个周期。

四、基本操作测量

1. 连续内存写操作

非常简单,只要memset一个大内存就OK了

2. 随机内存写操作

我们需要构造一个大的block_list,每2个相信节点间的距离都要大于 cache大小,且并不相等,前、后关系也不尽相同。同时,我们通过 memset一个和cache相同大小的buff来清空cache。便可以测量出随机内存写操作的开销。

3. 一些基本操作的开销


操作


平均周期数


操作


平均周期数


连续内存访问(4byte)


10


浮点数值运算(+、-、*)


10-12


随机内存访问(4byte)


208


浮点数值运算(/)


14


整数数值运算


7


系统随机


46

   
三角函数(sin/cos)


135

五、范例:函数耗时分析

测试使用的CPU信息如下:

范例分别用CTscStat和CTimeStat类来测量funcA,funcB和funcC,用usleep来模拟真实函数的耗时。

代码说明:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/time.h>
#include <string>
#include <vector>
#include <stdint.h>

#define rdtscll_64(val) do {\
unsigned int __a,__d; __asm__ __volatile__("rdtsc" : "=a" (__a), "=d" (__d)); (val) = (((unsigned long long)__d)<<32) | (__a); } while(0);

class CTscStat
{
public:
    CTscStat(const std::string& sCallerName)
    : m_sCallerName(sCallerName)
    {
        rdtscll_64(m_start);
        rdtscll_64(m_lastcall);
    }

    ~CTscStat()
    {
        uint64_t end;
        rdtscll_64(end);
        uint64_t totalConsumeTsc = end - m_start;
        printf("TscStat---TOTAL:func=[%s] tsc_cost=[%lu]\n", m_sCallerName.c_str(), totalConsumeTsc);
    }

    void AddCall(const std::string& sCall)
    {
        AddCall(sCall, GetTscSub());
    }

private:
    void AddCall(const std::string& sCallerName, uint64_t consumeTsc)
    {
        printf("TscStat---SubCall:func=[%s] sub_func=[%s] tsc_cost=[%lu]\n",
            m_sCallerName.c_str(), sCallerName.c_str(), consumeTsc);
        rdtscll_64(m_lastcall);
    }

    uint64_t GetTscSub()
    {
        uint64_t now;
        rdtscll_64(now);

        return now - m_lastcall;
    }

private:
    struct SCall
    {
        std::string sCallerName;
        uint64_t    consumeTsc;
    };

private:
    std::string         m_sCallerName;  // 统计接口
    std::vector<SCall>  m_vecCall;      // 请求列表

    uint64_t            m_start;        // 开始TSC
    uint64_t            m_lastcall;     // 上次TSC
};

#define TSC_START(sCn) CTscStat oTscS(sCn);
#define TSC_APICALL(API)         do {             oTscS.AddCall(API);         } while(0)

class CTimeStat
{
public:
    CTimeStat(const std::string& sCallerName)
    : m_sCallerName(sCallerName)
    {
        gettimeofday(&m_start, 0);
        gettimeofday(&m_lastcall, 0);
    }

    ~CTimeStat()
    {
        struct timeval end;
        struct timeval subresult;

        gettimeofday(&end, 0);
        timersub(&end, &m_start, &subresult);
        uint32_t dwTotalConsumeTime = subresult.tv_sec * 1000 + subresult.tv_usec / 1000;

        printf("TimeStat---TOTAL:func=[%s] time_cost=[%u]\n", m_sCallerName.c_str(), dwTotalConsumeTime);
    }

    void AddCall(const std::string& sCall)
    {
        AddCall(sCall, GetTimeSub());
    }

private:
    void AddCall(const std::string& sCallerName, uint32_t dwConsumeTime)
    {
        printf("TimeStat---SubCall:func=[%s] sub_func=[%s] time_cost=[%u]\n",
            m_sCallerName.c_str(), sCallerName.c_str(), dwConsumeTime);
        gettimeofday(&m_lastcall, 0);
    }

    uint32_t GetTimeSub()
    {
        struct timeval now;
        struct timeval subresult;
        gettimeofday(&now, 0);

        timersub(&now, &m_lastcall, &subresult);

        return subresult.tv_sec * 1000 + subresult.tv_usec / 1000;
    }

private:
    struct SCall
    {
        std::string sCallerName;
        uint32_t    dwConsumeTime;
    };

private:
    std::string         m_sCallerName;  // 统计接口
    std::vector<SCall>  m_vecCall;      // 请求列表

    struct timeval      m_start;        // 开始时间
    struct timeval      m_lastcall;     // 上次请求时间
};

#define TS_START(sCn) CTimeStat oTimeS(sCn);
#define TS_APICALL(API)         do {             oTimeS.AddCall(API);         } while(0)

void funcA()
{
    usleep(1000); // 1ms
}

void funcB()
{
    usleep(100000); // 100ms
}

void funcC()
{
    sleep(1); // 1s
}

void TestTsc()
{
    TSC_START("TestTsc");
    funcA();
    TSC_APICALL("funcA");
    funcB();
    TSC_APICALL("funcB");
    funcC();
    TSC_APICALL("funcC");
}

void TestTime()
{
    TS_START("TestTsc");
    funcA();
    TS_APICALL("funcA");
    funcB();
    TS_APICALL("funcB");
    funcC();
    TS_APICALL("funcC");
}

int main(int argc, char** argv)
{
    ////////// tsc ///////////
    TestTsc();

    ///////// time ///////////
    TestTime();

    return 0;
}

结果说明:

[[email protected] linux_programming]# ./module_perform

TscStat---SubCall:func=[TestTsc] sub_func=[funcA] tsc_cost=[6391184]

TscStat---SubCall:func=[TestTsc] sub_func=[funcB] tsc_cost=[362433912]

TscStat---SubCall:func=[TestTsc] sub_func=[funcC] tsc_cost=[3595441050]

TscStat---TOTAL:func=[TestTsc] tsc_cost=[3964662011]

TimeStat---SubCall:func=[TestTsc] sub_func=[funcA] time_cost=[2]

TimeStat---SubCall:func=[TestTsc] sub_func=[funcB] time_cost=[100]

TimeStat---SubCall:func=[TestTsc] sub_func=[funcC] time_cost=[1000]

TimeStat---TOTAL:func=[TestTsc] time_cost=[1104]

可以看到,当sleep 1ms的时候,测试结果与预期有较大差距,这个跟系统的精度有关。

当sleep 1s 和100ms的时候用Tsc测量的结果分别是主频和主频的1/10。

时间: 2024-12-09 20:02:41

程序运算性能测量的相关文章

Liam的计组学习历程(二):对比程序运算时间(2015.10.20)

对比执行时间实验 机器规格: CPU型号:Intel Core i7-366U: CPU主频:2.0 GHz(最大睿频:3.2GHz): 核心数: 双核心, 线程数:四线程: RAM :8GB: 操作系统位数:64位. 在VS中编写代码如下: #include "stdafx.h" #include "iostream" #include "windows.h" #include "time.h" using namespac

高斯坐标正反算程序运算步骤

第一步.选择原始坐标系为测点经纬度第二步.选择新坐标系为 高斯坐标第三歩.录入原始坐标数据第四歩.录入输出路径并OK核心代码:

第一章:C语言概述及如何上机运行C程序

第一章:C语言概述及如何上机运行C程序 让编程改变世界 Change the world by program C语言概述 C语言的发展过程 C语言是在 70 年代初问世的.一九七八年由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言.同时由B.W.Kernighan和D.M.Ritchit合著了著名的“THE C PROGRAMMING LANGUAGE”一书.通常简称为<K&R>,也有人称之为<K&R>标准.但是,在<K&R>中

mbed 程序的语言基础

mBed程序采用C++进行编写,并在此基础上添加了一些自定义的函数和常量,所以我们在这有必要简单地了解一下相应的语言基础. 常量 常量是在程序运算过程中不变的量,常量在程序中经常直接出现,如数字1768:字符‘m’:字符串“mbed”等,此时只要求它们符合相应类型数据的表示方法.相应于各种数据类型,有整型常量.浮点型常量.字符型常量及字符串常量.有时为了代码编写的方便,我们会用一个标识符来代表一个常量,通过宏定义预处理指令来实现,这就是常量定义,格式如下:#define 标识符常量 由用户命名的

Scalaz(53)- scalaz-stream: 程序运算器-application scenario

从上面多篇的讨论中我们了解到scalaz-stream代表一串连续无穷的数据或者程序.对这个数据流的处理过程就是一个状态机器(state machine)的状态转变过程.这种模式与我们通常遇到的程序流程很相似:通过程序状态的变化来推进程序进展.传统OOP式编程可能是通过一些全局变量来记录当前程序状态,而FP则是通过函数组合来实现状态转变的.这个FP模式讲起来有些模糊和抽象,但实际上通过我们前面长时间对FP编程的学习了解到FP编程讲究避免使用任何局部中间变量,更不用说全局变量了.FP程序的数据A是

4.9位运算

Q:用位运算操作实现两个整数的交换. #include <iostream> using namespace std; int main() { int a,b; cin>>a>>b; cout<<a<<" "<<b<<endl; a=a^b; b=a^b; a=a^b; cout<<a<<" "<<b<<endl; return 0;

python 简单编写的计算器程序

计算器思路 1.要求输入公式 2.检测公式是否包含字母 3.如果包含字母则需要重新输入,否则进行下一步运算 4.检测是否存在括号,如果存在括号,把括号里面的值进行优先级运算,然后赋值到原位,没有则按照优先级运算,取出最后运算结果 流程图 具体代码如下: #!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'weibinf' import re   #导入模块 def kuohao(self):   #判断是否存在括号     se

由[交换两个变量的值问题]理解程序的时空复杂度

由一个编程经典问题,交换两个变量的值入手,理解程序的时空复杂度(以下均使用PHP代码演示问题的解决方法)解决问题的方法一:    1.方法文字描述:使用一个中间变量temp,实现两个变量值的互换    2.实现的具体代码: <?php  //声明两个变量$a,$b  $a = $_GET['a'];   $b = $_GET['b'];  //打印没有互换前,$a和$b var_dump("未替换前:a={$a} and b={$b}"); //使用中间变量$temp实现$a和$

由web程序出现乱码开始挖掘(Bom头、字符集与乱码)

从第一次开始写web程序,自己还有身边同事开发出现乱码情况基本都没有消停过.估计以后还会一样继续. 这么些年,不断修修改改,也总结也归纳.程序从asp,asp.net,jsp,php,服务器从windows到linux,数据库也从sqlserver,mysql到oracle:它还是偶尔会出现. 好了,我总结下我与它较量的一些收获吧.乱码都与字符集有关系,一切都从它开始说. 什么是字符集,什么是字符编码,它做什么用? 字符(Charcter)是文字与符号的总称,包括文字.图形符号.数学符号等.而字