利用Oprofile对多核多线程进行性能分析

利用Oprofile对多核多线程进行性能分析

杨小华

工欲善其事,必先利其器

---墨子

性能分析工具简介

在对应用程序不断调优的过程中,除了制定完备的测试基准(Benchmark)外,还需要一把直中要害的利器——性能分析工具。

根据工具的复杂度和所提供的功能,可以将性能工具分为两个层次:

  1. 基本的计时工具

在普通生活中,秒表是最简单的计时工具。根据该思想,可以将计时函数放在代码的任意位置并多次调用,这样就可以测量出整个应用或者某一部分的运行时间。这种分析方法不够精细,误差大。

  1. 软件分析工具

目前,主要有两种不同类型的软件分析工具:采样和插桩。

Ø 采样型分析工具

主要通过周期性中断,来纪录相关的性能信息,如处理器指令指针、线程id、处理器id和事件计数器等。这种方法开销小,精确度高。在Linux系统中,比较常见的有Oprofile和Intel VTune性能分析器等。

Ø 插桩型分析工具

即可以使用直接的二进制插桩,也可以通过编译器在应用中插入分析代码。这种方式与自己在应用中增加计时函数类似,同时带来的开销大,但提供了更多的功能,如调用树,调用次数和函数开销等。在Linux系统中,比较常见的有gprof和Intel VTune性能分析器等。

本文将利用采样型工具Oprofile,对多核多线程程序进行性能分析,起一个抛砖引玉的作用。

衡量性能收益的方法

随着科学技术的不断发展,计算机系统结构朝着多核的方向发展,从而将并发编程推到了聚光灯下,但如何去衡量并行程序设计所带来的性能收益呢?

不得不想起1967年Gene Amdahl所做出的杰出贡献,他提出的Amdahl定律能够计算出并行程序相对于最优串行算法在性能提升上的理论最大值。

Amdahl定律

1

加速比=————————

S+(1-S)/n+H(n)

其中, S 表示执行程序中串行部分的比例, n 表示处理器核的数量, H(n)表示系统开销。

由于Amdahl定律本身做出了几个假设,但这些假设在现实世界中又不一定是正确的,因此使计算机界心灰意冷了很多年,认为根据Amdahl定律,开发更大的并行性所带来的性能收益可能是微不足道的,一直到Gustafson定律的出现,才改变了现状。

在Sandia实验室工作的基础上,E.Barsis提出了Gustafson定律:

扩展加速比=N+(1-N)*S

其中, S 表示执行程序中串行部分的比例,N 表示处理器核的数量。

幸运的是,Shi于1996年证明Gustafson定律和Amdahl定律是等效的。

Oprofile工作原理简介

根据CPU系统结构的不同, Oprofile支持两种采样方式:基于事件(Event Based)的采样和基于时间(Time Based)的采样。

如果CPU内部存在性能计数寄存器,则Oprofile基于事件采样,记录特定事件(如分支预测事件)发生的次数,当达到设定的定值时就采样一次。反之,则基于时间采样,主要是借助于操作系统的时钟中断机制,每当时钟中断发生时就采样一次。不难看出,基于时间的采样方式,要求被测程序不能屏蔽中断,其精度也低于事件采样。

对于x86体系结构,不同型号的CPU,采样方式也不同,具体细节如下表所示:

处理器

CPU_TYPE

采样方式

Athlon

i386/athlon

Event Based

Pentium Pro

i386/ppro

Event Based

Pentium II

i386/pii

Event Based

Pentium III

i386/piii

Event Based

Pentium M (P6 core)

i386/p6_mobile

Event Based

Pentium 4 (non-HT)

i386/p4

Event Based

Pentium 4 (HT)

i386/p4-ht

Event Based

Dual Core

timer

Time Based

Core 2 Duo

timer

Time Based

表一 x86各处理器采样方式

Oprofile主要分为两部分,其中一部分是内核模块(oprofile.ko),另外一部分是用户空间的守护进程(oprofiled)。前者主要负责访问性能计数寄存器或者注册基于时间采样的函数,并将采样结果置于内核的缓冲区中。后者在后台运行,负责从内核空间收集数据,并写入采样文件中,其交互流程如图1所示:

图1 oprofile交互流程图

安装Oprofile

oprofile.ko内核模块已经被集成到linux 2.6内核中,所以只需要安装前端工具,可以从oprofile官方网站下载源码来进行安装,当前最新版本为0.9.4。

该源码包是通过automake和autoconf生成的makefile,所以只需要以root用户进入oprofile目录,运行./configure、make及make install来完成所有的安装。 在目前大部分发行版中, 安装时可能缺少popt库,请另行下载安装。

Oprofile工具链提供了6大工具,供用户控制oprofile和分析样本。其中opcontrol是一个bash脚本程序,主要用来控制oprofile的启动、暂停及设置,其他工具主要是对采样数据进行分析。

样例程序

程序功能:求从1一直到 APPLE_MAX_VALUE (100000000) 相加累计的和,并赋值给apple的a和b ;求orange数据结构中的 a[i]+b[i]的和,循环 ORANGE_MAX_VALUE次。

#define ORANGE_MAX_VALUE      1000000 #define APPLE_MAX_VALUE       100000000 #define MSECOND               1000000   struct apple {     unsigned long long a;     unsigned long long b; };   struct orange {     int a[ORANGE_MAX_VALUE];     int b[ORANGE_MAX_VALUE];     };   int main (int argc, const char * argv[]) {     // insert code here...     struct apple test;     struct orange test1;         for(sum=0;sum<APPLE_MAX_VALUE;sum++)     {        test.a += sum;        test.b += sum;     }         for(index=0;index<ORANGE_MAX_VALUE;index++)     {        sum += test1.a[index]+test1.b[index];     }     return 0; }

观察上述样例程序, S (串行部分的比例)所占比例非常小,基本为0, 根据Amdahl定律,在双核系统(n=2)上,则加速比为:加速比=1/(0+1/2+1%)=1.96,也就是说效率可以提高96%。

样例程序运行时间为:1.049046s,那么经过优化后,能不能达到理论值呢?请随着本文开始。

追踪热点

既然要充分利用双核的特性,则不得不对样例程序进行改造,进行并行程序设计。

可以将应用程序看成是众多相互依赖的任务的集合。将应用程序划分成多个独立的任务,并确定这些任务之间的相互依赖关系,这个过程称为分解(Decomosition)。分解问题的方式主要有三种:任务分解、数据分解和数据流分解。

运用任务分解的方法 ,不难发现计算apple的值和计算orange的值,属于完全不相关的两个操作,因此可以并行。

改造后的两线程程序:

void* add(void* x) {          for(sum=0;sum<APPLE_MAX_VALUE;sum++)     {        ((struct apple *)x)->a += sum;        ((struct apple *)x)->b += sum;      }            return NULL; }     int main (int argc, const char * argv[]) {        // insert code here...     struct apple test;     struct orange test1={{0},{0}};     pthread_t ThreadA;            pthread_create(&ThreadA,NULL,add,&test);            for(index=0;index<ORANGE_MAX_VALUE;index++)     {        sum += test1.a[index]+test1.b[index];     }              pthread_join(ThreadA,NULL);       return 0; }

现在通过oprofile来对多线程程序进行性能分析,收集热点信息。oprofile的功能非常强大,可以对每个线程进行单独采样,也可以对每个CPU单独采样,这些都是通过opcontrol的--separate选项来完成的。separate选项值含义如下:

separate选项值

含义

none

默认值

lib

对每个应用程序的所有lib进行采样

kernel

对每个应用程序的内核及内核模块采样

thread

对每个线程或任务采样

cpu

对每个CPU进行采样

all

以上所有选项的功能

表2 separate选项值含义

操作步骤如下:

# opcontrol --init # opcontrol --separate=thread --no-vmlinux # opcontrol --start Using 2.6+ OProfile kernel interface. Using log file /var/lib/oprofile/samples/oprofiled.log Daemon started. Profiler running. # ./twothreadprocess add thread:b7dc7b90,Used Time:1.225483 main thread:b7dc8ad0,Used Time:1.230151 a = 4999999950000000,b = 4999999950000000,sum=0 # opcontrol --shutdown Stopping profiling. Killing daemon.

识别出并行程序中的重载

运行opreporte,查看采样结果:

# opreport -l ./twothreadprocess CPU: CPU with timer interrupt, speed 0 MHz (estimated) Profiling through timer interrupt Processes with a thread ID of 5237 Processes with a thread ID of 5238 samples  %        samples  %          symbol name 30        100.000       0         0                 main 0              0           343   100.000         add

运行时间为:1.230151s,与理论值相差甚远,反而运行时间越来越长,原因何在呢?

通过分析结果,不难看出add线程负载非常重,而main负载较轻,负载不均衡,因此重点分析对象为add线程。根据多线程数据分解的原理,将计算apple值的过程一分为二,main线程也参与部分计算。由于都是循环,为了使负载均衡,则add线程里面应该循环(ORANGE_MAX_VALUE+APPLE_MAX_VALUE)/2次。修改后的程序如下:

#define ORANGE_MAX_VALUE      1000000 #define APPLE_MAX_VALUE       100000000 #define MSECOND               1000000 #define MIDLLE_VALUE          49500000     struct apple {     unsigned long long a;     unsigned long long b; };     struct orange {     int a[ORANGE_MAX_VALUE];     int b[ORANGE_MAX_VALUE];       };     unsigned long long value[2][2];     void* add(void* x) {       temp1 = ((struct apple *)x)->a;     temp2 = ((struct apple *)x)->b;            for(sum=MIDLLE_VALUE;sum<APPLE_MAX_VALUE;sum++)     {        temp1 += sum;        temp2 += sum;     }            value[0][0]=temp1;     value[1][0]=temp2;       return NULL; }     int main (int argc, const char * argv[]) {     // insert code here...     struct apple test;     struct orange test1={{0},{0}};     pthread_t ThreadA;            test.a= 0;     test.b= 0;         temp1 = test.a;     temp2 = test.b;          pthread_create(&ThreadA,NULL,add,&test);       for(sum=0;sum<MIDLLE_VALUE;sum++)     {        temp1 += sum;        temp2 += sum;     }         value[0][1]=temp1;     value[1][1]=temp2;            sum=0;            for(index=0;index<ORANGE_MAX_VALUE;index++)     {        sum=test1.a[index]+test1.b[index];     }                 pthread_join(ThreadA,NULL);         test.a = value[0][0]+value[0][1];     test.b = value[1][0]+value[1][1];            return 0; }

重新运行oprofile,再次收集采样数据,改造后的程序是否达到了负载均衡呢?

# opreport -l ./twothreadprofile CPU: CPU with timer interrupt, speed 0 MHz (estimated) Profiling through timer interrupt Processes with a thread ID of 5242 Processes with a thread ID of 5243 samples  %        samples  %          symbol name 135    100.000       0         0                 main 0              0           156   100.000         add

运行时间为:0.629821s,时间有了显著的提高,效率也提升了66%,已经逼近理论值,但还是有一点差距。

继续分析,不难看出,负载还是没有达到均衡,main线程还是有点轻。因此继续增大MIDLLE_VALUE的值到53500000,再次运行oprofile,看看效果如何:

# opreport -l ./twothreadprofilebig CPU: CPU with timer interrupt, speed 0 MHz (estimated) Profiling through timer interrupt Processes with a thread ID of 5248 Processes with a thread ID of 5249 samples  %        samples  %          symbol name 145    100.000       0         0                 main 0              0           146   100.000         add

运行时间为:0.540942s,比样例程序时间大约减少了一半,效率提升了92%,已经非常接近于理论值。相信通过不断的微调,将会越来越接近理论值。同时在计算理论值时,所假设的系统开销比较低,仅仅为1%。

由于Linux 内核进程调度器天生具有CPU软亲和力(affinity) 的特性,这就意味着进程通常不会在处理器之间频繁的迁移。在实际应用中,如果不存在数据竞争的影响,应用的不同部分分布到不同的CPU上运行,可能会带来更高的收益。

将样例程序的多线程版本绑定到不同的CPU上运行,效率会有所提升吗?

修改后的程序如下:

struct apple {     unsigned long long a;     unsigned long long b; };     struct orange {     int a[ORANGE_MAX_VALUE];     int b[ORANGE_MAX_VALUE];       };     int cpu_nums = 0; unsigned long long value[2][2];     inline int set_cpu(int i) {     CPU_ZERO(&mask);            if(2 <= cpu_nums)     {        CPU_SET(i,&mask);                   if(-1 == sched_setaffinity(gettid(),sizeof(&mask),&mask))        {            return -1;        }     }     return 0; }         void* add(void* x) {          if(-1 == set_cpu(1))     {        return NULL;     }         temp1=((struct apple *)x)->a;     temp2=((struct apple *)x)->b;            for(sum=MIDDLE_VALUE;sum<APPLE_MAX_VALUE;sum++)     {        temp1 += sum;        temp2 += sum;     }            value[0][0]=temp1;     value[1][0]=temp2;            return NULL; }   int main (int argc, const char * argv[]) {     // insert code here...     struct apple test;     struct orange test1={{0},{0}};     pthread_t ThreadA;            test.a= 0;     test.b= 0;            cpu_nums = sysconf(_SC_NPROCESSORS_CONF);            if(-1 == set_cpu(0))     {        return -1;     }         temp1=test.a;     temp2=test.b;            pthread_create(&ThreadA,NULL,add,&test);         for(sum=0;sum<MIDDLE_VALUE;sum++)     {        temp1 += sum;        temp2 += sum;     }         value[0][1]=temp1;     value[1][1]=temp2;         sum=0;            for(index=0;index<ORANGE_MAX_VALUE;index++)     {        sum+=test1.a[index]+test1.b[index];     }                 pthread_join(ThreadA,NULL);         test.a=value[0][0]+value[0][1];     test.b=value[1][0]+value[1][1];            return 0; }

修改opcontrol的--separate参数为cpu,然后开始采样:

# opcontrol --separate=cpu --no-vmlinux # opcontrol --reset # opcontrol --start Using 2.6+ OProfile kernel interface. Using log file /var/lib/oprofile/samples/oprofiled.log Daemon started. Profiler running. # ./affinitytwoprofile …… # opcontrol --shutdown Stopping profiling.

采样结果如下:

# opreport -l ./affinitytwoprofile CPU: CPU with timer interrupt, speed 0 MHz (estimated) Profiling through timer interrupt Samples on CPU 0 Samples on CPU 1 Samples on CPU all samples       %          samples     %          samples       %           symbol name 147           100.000       0             0              147      50.9559          main 0                  0             143      100.000        143      49.0441         add

程序运行时间为:0.575131s,与没有绑定之前的效果接近,但不代表CPU绑定没有用处,只是本样例程序运行时间较短,工作量不大,不适合使用CPU绑定而已。

oprofile分析多核程序与分析多线程程序类似,通过采样数目来识别重载,然后开始一步一步的优化。

总结

根据以上分析及实验,对所有改进方案的测试时间做一个综合对比,如下图所示:

图2 各优化时间对比图

利用Oprofile,一步一步的不断调优,最终使优化后的结果接近于理论值,让我们见证了优化工具所具有的魅力。但Oprofile不仅仅只有这些功能,关于更多的其他功能,请参看官方网站介绍或者本文参考资料所列出的资料2和3。

参考资料

[1] Oprofile官方网站

[2] PrPrasanna S. Panchamukhi,《用 OProfile 彻底了解性能》, IBM Developerworks

[3] John Engel,《 使用 OProfile for Linux on POWER 识别性能瓶颈》, IBM Developerworks

[4] 杨小华,《 利用多核多线程进行程序优化》, IBM Developerworks

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Y大宽

RNA-seq(6): reads计数,合并矩阵并进行注释

小结 计数分为三个水平: gene-level, transcript-level, exon-usage-level 标准化方法: FPKM RPKM ...

69050
来自专栏宏伦工作室

深度有趣 | 01-02 前言和准备工作

用 Python 做一些有意思的案例和应用,内容和领域不限,可以包括数据分析、自然语言理解、计算机视觉,等等等等

11420
来自专栏IT技术精选文摘

搜索引擎架构概述

架构 对软件系统来讲,从一个层面对系统的各个组件进行抽象.描述它们各自的功能、提供的接口以及它们之间的关系. 需求 架构为应付需求而产生,对搜索引擎来讲,它主要...

313100
来自专栏数据和云

性能优化:B*Tree 索引中的分裂事物控制

黄玮(Fuyuncat) 资深Oracle DBA,个人网www.HelloDBA.com,致力于数据库底层技术的研究,其作品获得广大同行的高度评价。 编辑手记...

40180
来自专栏数据小魔方

parklines迷你图系列1——Scales

按照之前的计划,今天开始按照sparklines插件的图表分类标准开始跟大家分享详细的做法。 按照该插件在excel菜单中的顺序,先来看测量尺度(Scales)...

30660
来自专栏机器之心

业界 | 谷歌发布TensorFlow 1.3.0版本,新加多个分类器、回归器

31640
来自专栏生信技能树

(10)仿写fastqc-生信菜鸟团博客2周年精选文章集

用仿写软件的方法来学习编程 我首先仿写了fastqc软件,学会了很多基础知识: 仿写fastqc软件的一些功能-R代码 仿写fastqc软件的部分功能-perl...

357100
来自专栏PPV课数据科学社区

《Kaggle项目实战》 泰坦尼克:从R开始数据挖掘(一)

摘要: 你是否为研究数据挖掘预测问题而感到兴奋?那么如何开始呢,本案例选自Kaggle上的数据竞赛的一个数据竞赛项目《泰坦尼克:灾难中的机器学习》,案例涉及一...

35960
来自专栏专知

【下载】PyTorch实现的神经网络翻译框架——机器翻译工具包 nmtpytorch

【导读】机器翻译是自然语言处理的重要组成部分,其目的是使用计算机自动将文本翻译成其他语言的形式。近年来,端到端的神经机器翻译发展迅速,已经成为机器翻译系统的新主...

40990
来自专栏熊二哥

快速入门系列--TSQL-01基础概念

    作为一名程序员,对于SQL的使用算是基础中的基础,虽然也写了很多年的SQL,但常常还是记不清一些常见的命令,故而通过一篇博文巩固相关的记忆,并把T-SQ...

21380

扫码关注云+社区

领取腾讯云代金券