程序优化的5个方向.docx

上传人:b****5 文档编号:28227931 上传时间:2023-07-09 格式:DOCX 页数:17 大小:24.04KB
下载 相关 举报
程序优化的5个方向.docx_第1页
第1页 / 共17页
程序优化的5个方向.docx_第2页
第2页 / 共17页
程序优化的5个方向.docx_第3页
第3页 / 共17页
程序优化的5个方向.docx_第4页
第4页 / 共17页
程序优化的5个方向.docx_第5页
第5页 / 共17页
点击查看更多>>
下载资源
资源描述

程序优化的5个方向.docx

《程序优化的5个方向.docx》由会员分享,可在线阅读,更多相关《程序优化的5个方向.docx(17页珍藏版)》请在冰豆网上搜索。

程序优化的5个方向.docx

程序优化的5个方向

程序优化的5个方向

80/20法则:

程序执行中,80%的时间消耗在20%的代码上。

优化前,我们首先得找到这20%的关键路径;

各种语言都有专门的工具来找到这20%的关键路径,比如C++经常用到的

gprof;

参考《C++的性能优化实践》

在关键路径上对耗时的计算进行优化;

主要的优化方向为:

减少重复计算、预先计算、延后计算、降低计算代价、不计算;

减少重复计算

典型的例子如缓存,将之前相同的计算(查数据库,读写文件)存下来,等待下一次继续使用;适用场景:

计算结果有有效期,过段时间后需要再次计算;

预先计算

对于关键路径中比较耗时的计算,预先计算出来,节省每次计算的成本;

*预先计算出对照表关键路径中需要用到的映射关系对照表,将对照表预先计算,在关键路径中直接取用;

•将计算提前到初始化期间比如,内存分配耗时,将其提前到初始化的时间分配,建立内存池;

•将计算提前到编译期间

比如:

使用常量表达式,在编译期间将最终值计算出来,节省这部分的运行

时开销;

相关技术:

模版元编程;

适用场景:

计算出来的值一直有效,无需再次计算;

延迟计算

将计算耗时延迟到后期,这样,对于异常情况或其它分支情况,在中途就转换,不用再计算;

•有较多分支条件将最耗时的计算延后,这样,可能很多场景在中途就转到其它分支上,不用计算;

•判断条件中的技巧:

a||ba&&b如果判断条件比较耗时,将更耗时的放在后面计算;这样,对于a||b,当a成立时,b就不用再计算了;延迟计算的好处在于可能可以不用计算;

适用场景:

分支条件场景;

降低计算代价

这是通常能想到的最直接的优化手段,如何能够直接降低计算的代价;

-内存申请从堆上改为栈上

动态内存分配昂贵,将内存分配从堆上改为栈上;

*降低灵活性,使用自定制版本的函数代替库函数;

•使用更低级的指令或语言改写;

在C++中嵌入汇编语言;

使用SSE2等指令集;

•使用更优的算法或数据结构;

操作STL容器时,STL中的算法一般比自己手写的算法要高效,尽量使用

STL的算法来替换我们的手写算法;

参考:

《STL区间成员函数及区间算法总结》

《高效的使用STL》

适用场景:

这类优化一般是以降低代码可读性为代价的(STL的除外),用于优化的最后阶段;

不计算

优化的终极方案,不计算;

•业务发现用不到的业务逻辑,废弃的业务逻辑,仍然存在关键路径中的还在执行的;痛快的删除它;

•却掉临时对象开销

在我们的代码中,可能会有些临时对象是不知不觉的,而消除临时对象,将节省这部分开销;

参考:

《消除临时对象》

以上是单线程关键路径的优化,接下来,我们聊聊扩展到多核,在多线程上的优化;

当优化扩展到多核时

内容目录:

*任务分解的粒度

*数据竞争

*弓I入锁

*惊群现象

*数据复制

*数据分片

当优化扩展到多核时

"软件开发没有银弹,我们能做的就是选择和平衡;"

上一篇文章我们聊了在单线程下程序优化的5个方向(ref:

《程序优化的5个方向》);

当单核优化到极值后,就到了多任务的情况;

想起来很清晰,单个任务分解成多个任务,让多个cpu同时来工作,并行执行,效率自然

就上去了;

但,未必就这么简单;

任务分解的粒度

首先,我们需要确定,我们的单个任务是否可以分解;比如解析很多个文件,这样的任务划

分成多个很简单;但如果是一个耗时的串行逻辑计算,后期的计算依赖前期的结果,这样就

不好拆分;这种形式可能需要在更高层次上来拆分;

数据竞争

编程就是计算和数据;计算并行了,但数据还是访问同一份,访问共同的资源会产生资源竞争;

如果不进行控制,可能导致同一份数据重复计算(多个读的场景)或是脏数据的产生(有回写的场景);

引入锁

为了让数据访问有序进行,需要引入锁来防止脏数据;

控制锁的粒度,是个需要精心考虑的话题;

比如对于大量读少量写的场景,相比一视同仁的加锁,使用读写锁能显著提升效率;

我们日常能接触到的产品中,数据库是个用锁高手,在更新数据的时候,是锁住行,还是列、或是表,不同的粒度性能相差明显;

惊群现象

考虑这样的场景:

多个线程都在等在一个锁,如果可以拿到锁,线程就开始工作(线程池)当锁被释放时,如果唤醒多个线程可能会产生惊群现象;

解决方案:

使用单线程方案/处理accpet连接处理等待锁的操作,让任何时刻只有一个线程在等待

锁;

更多细节参考:

《客户-服务器程序设计方法》中预先创建线程池,每个线程各自accept一节

数据复制

让每个线程使用自己的数据,让数据不共有,这样能去掉资源竞争,去掉锁;

将数据复制为多份,减少竞争,各自访问各自的数据;

但这又引入了一个新的问题:

如果各个线程回写了数据,如何保证这么数据的一致性?

毕竟它们代表的其实是一份数据;

涉及到数据的一致性,多份数据之间的同步又是个难题;

数据分片

那好,换个思路,不使用数据复制;我们使用数据分片;分片这个思想更容易想到,既然“计

算”被划分为多个小任务了,那么数据也可以同样处理;

将数据分片,每份数据存的内容不相同,它们之间没有共同点;

这样,数据访问没有数据竞争,同时由于数据不同,也不涉及到数据一致性同步的问题;

但,分片远远没有想的那么美好;

分片导致了每个线程看到数据不再是全集,而是片段;这就注定了这个线程只能处理这部分

的特定数据;这样,线程之间的计算失去了可替换性;某种工作只能在特定的线程上处理;

而如果有个任务需要访问所有的数据,这样就变得更加复杂;

原来,分片之后,我们将难题向上推了,推到线程层面,需要考虑到业务逻辑层面的处理;

这样,可能更加复杂;

ok,想要速度更快,使用多核来处理,需要面对更多的问题;

将单机扩展多机集群,涉及到架构层面来看,其实我们的面对的问题是类似的;

参考:

《大型网站技术架构》读书笔记[2]-架构的模式

软件开发没有银弹,我们能做的就是选择和平衡;

消除临时对象

内容目录:

・按值返回

*按值传递参数

4类型不匹配的隐式转换

4连续的对象之间的+操作符

•成员对象的初始化

C++的性能优化实践

内容目录:

•1Gprof

•2.gprof使用步骤

•1.初始化大对象耗时

・2.Map使用不当

优化准则:

1.二八法则:

在任何一组东西中,最重要的只占其中一小部分,约20%,

其余80%的尽管是多数,却是次要的;在优化实践中,我们将精力集

中在优化那20%最耗时的代码上,整体性能将有显著的提升;这个很

好理解。

函数A虽然代码量大,但在一次正常执行流程中,只调用了一次。

而另一个函数B代码量比A小很多,但被调用了1000次。

显然,我们更应关注B的优化。

2.编完代码,再优化;编码的时候总是考虑最佳性能未必总是好的;在强调最佳性能的编码方式的同时,可能就损失了代码的可读性和开发效率;

工具:

1Gprof

工欲善其事,必先利其器。

对于Linux平台下C++的优化,我们使用

gprof工具。

gprof是GNUprofile工具,可以运行于linux、AIX、Sun等操作系统进行C、C++、Pascal、Fortran程序的性能分析,用于程序的性能优化以及程序瓶颈问题的查找和解决。

通过分析应用程序

运行时产生的“flatprofile”,可以得到每个函数的调用次数,消耗的CPU时间(只统计CPU时间,对IO瓶颈无能为力),也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函数调用花费了多少时间。

2.gprof使用步骤

1)用gcc、g++、xlC编译程序时,使用-pg参数,如:

g++-pg-otest.exe

test.cpp编译器会自动在目标代码中插入用于性能测试的代码片断,这

些代码在程序运行时采集并记录函数的调用关系和调用次数,并记录函

数自身执行时间和被调用函数的执行时间。

2)执行编译后的可执行程序,如:

./test.exe。

该步骤运行程序的时间会稍慢于正常编译的可执行程序的运行时间。

程序运行结束后,会在程序所在路径下生成一个缺省文件名为gmon.out的文件,这个文件就是

记录程序运行的性能、调用关系、调用次数等信息的数据文件。

3)使用gprof命令来分析记录程序运行信息的gmon.out文件,如:

gproftest.exegmon.out则可以在显示器上看到函数调用相关的统计、

分析信息。

上述信息也可以采用gproftest.exegmon.out>gprofresult.txt重定向到文本文件以便于后续分析。

以上只是gpro的使用步骤简介,关于gprof使用实例详见附录1;

实践

我们的程序遇到了性能瓶颈,在采用架构改造,改用内存数据库之前,我们考虑从代码级入手,先尝试代码级的优化;通过使用gprof分析,

我们发现以下2个最为突出的问题:

1.初始化大对象耗时

分析报告:

3076.5%VOBJ1:

:

VOBJ1@240038VOBJ1

在整个执行流程中被调用307次,其对象初始化耗时占到6.5%。

这个对象很大,包含的属性多,属于基础数据结构;

在程序进入构造函数函数体之前,类的父类对象和所有子成员变量对象已经被生成和构造。

如果在构造函数体内位其执行赋值操作,显示属于浪费。

如果在构造函数时已经知道如何为类的子成员变量初始化,那么

应该将这些初始化信息通过构造函数的初始化列表赋予子成员变量,而

不是在构造函数函数体中进行这些初始化。

因为进入构造函数函数体之

前,这些子成员变量已经初始化过一次了。

在C++程序中,创建/销毁对象是影响性能的一个非常突出的操作。

首先,如果是从全局堆中生成对象,则需要首先进行动态内存分配操作。

众所周知,动态分配/回收在C/C++程序中一直都是非常耗时的。

因为

牵涉到寻找匹配大小的内存块,找到后可能还需要截断处理,然后还需

要修改维护全局堆内存使用情况信息的链表等。

解决方法:

我们将大部分的初始化操作都移到初始化列表中,性能消耗

降到1.8%。

2.Map使用不当

分析报告:

896.8%Recordset:

:

GetField

Recordset的getField被调用了89次,性能消耗占到6.8%;

Recordset是我们在在数据库层面的包装,对应取出数据的记录集;(用过ADO的朋友很熟悉);由于我们使用的是底层c++数据库接口,通

过对数据库原始api进行一层包装,从而屏蔽开发人员对底层api的直接操作。

这样的包装,带来的好处就是不用直接与底层数据库交互,在代码编写方面方便不少,代码可读性也很好;带来的问题就是性能的损失;

分析:

(2点原因)

1)在GetField函数中,使用了map[“a”]来查询数据,如果找不到

“a”,则map会自动插入key”a”,并设value为0;而m.find(“a”)

不会自动插入上述pair,执行效率更高;原有逻辑:

1stringRecordset:

:

GetField(conststring&strName)

2{

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17}

intnlndex;

if(hasIndex==false)

{

nlndex=m_nPos;

}_

else

{

nlndex=m_vSort[m_nPos].m_iorder;

}~~~

if(m_fields[strName]==0)

{_

LOG_ERR("Recordset:

:

GetField:

"v

!

");

}

returnm_records[nlndex].GetValue(m_fields[strName]-1);

改造后的逻辑:

1

2

3

4

5

6

7

stringRecordset:

:

GetField(conststring&strName)

unordered_map:

:

iteratoriter=m_fields.find(strName);

if(iter==m_fields.end())

{_

LOG_ERR("[Recordset:

:

GetField]"<

调整后的Recordset:

:

GetField的执行时间约是之前的1/2;且易读性

更高;

2)在Recordset中,对于每个字段的存储,使用的是mapm_fields;

g++中的stl标准库中默认使用的红黑树作为map的底层数据结构;

通过附录中的文档2,我们发现其实有更快的结构,在效率上,unorder

map优于hashmap,hashmap优于红黑树;如果不要求map有序,unordered_map是更好的选择;

解决方法:

将map结构换成unordered_map,性能消耗降到1.4%;

总结

我们修改不到30行代码,整体性能提升10%左右,效果明显;打蛇打

七寸,性能优化的关键在于找准待优化的点,之后的事,也就水到渠成;

附录:

附1:

prof工具介绍及实践

附2:

maphash_mapunordered_map性能测试

Redis数据导入工具优化过程总结

内容目录:

•背景

・优化效果

・用到的工具

・优化过程

*满足生产要求

Redis数据导入工具优化过程总结

背景

使用C++开发了一个Redis数据导入工具

从oracle中将所有表数据导入到redis中;

不是单纯的数据导入,每条oracle中的原有记录,需要经过业务逻辑处理,

并添加索引(redis集合);

工具完成后,性能是个瓶颈;

优化效果

使用了2个样本数据测试:

样本数据a表8763条记录;

b表940279条记录;

优化前,a表耗时11.417s;

优化后,a表耗时1.883s;

用到的工具

gprof,pstrace,time

使用time工具查看每次执行的耗时,分别包含用户时间和系统时间;

使用pstrace打印实时运行,查询进程主要的系统调用,发现耗时点;

使用gprof统计程序的耗时汇总,集中精力优化最耗时的地方;

使用简介:

1.对g++的所有编辑和连接选项都必须要加上-pg(第一天由于没有在连接处加上-pg选项

导致无法出统计报告);

2.执行完程序后,本目录会产生gmon.out文件;

3.gprofredistoolgmou.out>report,生成可读文件report,打开report集中优化最耗

时的函数;

优化过程

优化前11.417s:

time./redistoolimaa.csv

real0m11.417s

user0m6.035s

sys0m4.782s(发现系统调用时间过长)

文件内存映射

系统调用时间过长,主要是文件读写,初步考虑是读取文件时,调用

api次数过于频繁;

读取样本采用的是文件

fgets一行行的读取,采用文件内存映射

mmap后,可直接使用指

针操作整个文件内存快;

日志开关提前

改进了文件读写后,发现优化效果比较有限(提高了2s左右)

;fgets是C的文件读取库

函数,相比系统read(),是带了缓冲区了,应该不会太慢(网上有人测试,文件内存映射

相比fgets()能快上一个数量级,感觉场景应该比较特殊);

之后通过pstrace工具发现log.dat打开次数过多;原来是调试日志的开关写到了后面,导

致调试日志都是会打开日志文件open("log.dat");

将日志开关提前;改进后,3.53s

time./redistoolimaa.csv

real0m3.530s

user0m2.890s

sys0m0.212s

vector空间预先分配

后续通过gprof分析,某个函数的vector内存分配次数多,并有不少复制次数:

改进以下这行代码:

vectorvSegment;

使用静态vector变量,并预先分配内存:

staticvectorvSegment;

vSegment.clear();

staticintnCount=0;

if(0==nCount)

{

vSegment.reserve(64);

}

++nCount;

优化后,提升至2.286s

real0m2.286s

user0m1.601s

sys0m0.222s

冋样,另外一个类中的成员vector也使用预先分配空间(在构造函数中):

m_vtPipecmd.reserve(256);

优化后,提升至2.166s;

real

0m2.166s

user

0m1.396s

sys

0m0.204s

函数改写&&内联

继续执行程序,发现SqToolStrSplitByCh()函数消耗过大,改写整个函数逻辑,并将改写

后的函数内联:

优化后,提升至1.937s

real0m1.937s

user0m1.301s

sys0m0.186s

去除调试符和优化监测符号

real

0m1.883s

user

0m1.239s

sys

0m0.191s

满足生产要求

以上最后几步看似毫秒级的提升,扩大到全表数据后,效果就很明显了;

优化后,生产上a表为152w,导入耗时大约326s(~6分钟);b表数据420w,导入耗时大约1103s(~18分钟)

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 教学研究 > 教学计划

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1