性能调优--永远超乎想象 (原文最终修订于 2006-08-28 晚上11:48:38)

2023-10-09 06:18

本文主要是介绍性能调优--永远超乎想象 (原文最终修订于 2006-08-28 晚上11:48:38),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

多年以前,我在开发一个C++的应用程序。我的同伴Jim Newkirk(当时的)过来告诉说,我们的一个公用函数运行得非常的缓慢。这个函数是用来转换二进制的树结构数据为普通文本,并存储到文件中的。(这是在XML出现之前,但概念类似于XML)

我审视了这个函数一会儿,发现了一个线性查找算法,于是毫无疑问的将这个线性查找算法替换为二分查找法(译注:binary search),然后就把这个函数交回给了Jim。Jim几小时后就回来了,问我是否有做过任何的改进,因为这个函数还是迟缓如初。

看来是我没找到关键,于是就一遍又一遍的研究了这个函数,然后发现并改进了一些其它很明显的算法问题,可是性能依然没有丝毫改进。函数依然缓慢,看到我对这个函数的无计可施,Jim也越来越沮丧。

最后,Jim终于找到了一个能够去分析这个函数性能的办法,就发现这个问题出自一个底层的叫strstream的C++库(译注1)。这个库函数随着文本数据的不断增加,它不停的一次又一次的申请内存块。这个函数单纯根据即将读入的文本数据大小来预先申请内存块,速度也迅速的随之以数量级的趋势降低。


很久以前,曾有一次我要写一个计算任意多边形面积的算法。我想出了一个不断的把这个任意多边形细分为三角形的主意。每次细分一个三角形,多边形就会减少一个顶点,而它的面积就可以由此累加起来。由于不得不处理很多不规则的形状,好久我才把这个功能写好。一、两天后,我就完成了这个厉害的算法,它能计算任意的多边形的面积。

几天后,我的一个同事过来找我,说:“我新画了多边形的一条边,可它花了45分钟才计算出面积。所以,我要是重新绘制这条线断或是调整它,面积都显示不出。”45分钟啊,很长的时间了,所以我就问她这个多边形有多少个顶点,她告诉我有超过1,000个的。

看了看算法,我认识到这个算法的复杂度是O(N^3)的(译注2),所以对于小多边形来说很快,但对于大型的来说速度就慢得无法忍受了。我一遍又一遍的思考着这个问题,但却找不到一个更好的算法。(现今我们只要用google搜索就好了,可那是现在而这是那时...)于是我们就把这个自动显示面积的功能去掉,然后告诉客户这太耗时了。

两周之后,纯属偶然机会, 我正翻阅一本关于prolog编程语言的书(一个可爱又另类的编程语言,我建议你也学习学习它),然后就发现了一个计算多边形面积的算法。它优雅,简单,而且是线性阶(译注2)的,我是从来都没写出过这么漂亮的算法。我用了几分钟时间就实现了它,哇!即使拖动多边形一个顶点绕着屏幕乱转,面积竟然也可以及时更新。


 昨天晚上,我坐在一辆豪华轿车里,从O'Hare驶向我在芝加哥北部郊区的家。I-294公路正在施工,而我们凑好赶上交通阻塞。于是我拿出我的Macbook Pro,然后开始即兴的编写Ruby程序。为了好玩,我开始编写埃拉托色尼质数过滤算法(译注:Sieve of Eratosthenes)。我想让程序一跑起来就能看到Ruby有多快,所以就在程序中增加了benchmark模块来度量速度。它相当快!能在两秒钟内算出所有在百万以内的素数!对于一个解释型语言来说还不错。

我想知道这个算法的复杂度O(x)是什么样的。坐在车里不好算出来,于是我决定通过一些设定点采样的方法来测出它。我从100,000开始到5,000,000,每间隔100,000运行一次这个算法采样,然后把这些采样点绘在了一个图上。竟然是线性阶!

这个算法怎能是线性阶呢?它有一个嵌套的循环!难道不应该是复杂度类似于O(N^2),或者至少也应该是O(N log N)啊?这里就是代码,你自己来看看:

 require 'benchmark'
def sievePerformance(n)
  r = Benchmark.realtime() do
    sieve = Array.new(n,true)
    sieve[0..1] = [false,false]
   
    2.upto(n) do |i|
      if sieve[i]
        (2*i).step(n,i) do |j|
          sieve[j] = false
        end
      end
    end
  end
  r
end

我的儿子Micah就坐在我旁边,他看了看然后说:“这个循环最多只应做到n平方根。”我惭愧的意识到这一定是导致线性阶的原因。这个循环本应该在它刚到n平方根的时候就结束了的,可却陷入了无用的线性迭代之中一直到n。

这个简单的改变应该不仅仅能在采样图表上展现出原本的曲线形状,算法的性能也应能提高不少。如下:

require 'benchmark'
def sievePerformance(n)
  r = Benchmark.realtime() do
    sieve = Array.new(n,true)
    sieve[0..1] = [false,false]
   
    2.upto(Integer(Math.sqrt(n)) do |i|
      if sieve[i]
        (2*i).step(n,i) do |j|
          sieve[j] = false
        end
      end
    end
  end
  r

我把这两幅采样图表拼接在了一起,如下:

真令人失望。首先,在图上的sqrt(n)没有展现出曲线来;其次,sqrt(n)的性能仅仅是原先的两倍!一个函数的外循环上限在指数级别上变为的一半(即原来的平方根),可是速度的提升却怎能只有2倍?

随着我对这个算法理解的加深,我认识到外层循环的迭代次数的增加,内层循环所耗用时间会因为两个因素而减少。首先,步长增大了;其次,在筛选过程中出现了更多的'false'值,因此判断语句会更少频率的被执行。这两个导致时间耗用降低的因素一定是导致算法保持线性阶的某种平衡因素。

我不是计算机科学家,而且对鉴别这个算法到底是线性与否的数学问题我也不是非常感兴趣。谁能猜出当外部循环的范围缩小到原来上限值的平方根而性能却只有2倍增长的原因?谁能猜到算法本身竟然是线性阶的?!


六年前,当大家刚开始沉迷于XP的时候,Kent Beck(译注3)要在一组学生(大概30个左右)前示意一个算法,我就为他写了这个埃拉托色尼质数过滤算法的Java例程。我惊讶的看到他从函数中把n的平方根删掉,并替换成了n。他说“我不知道这是不是真的能让算法加快,不管如何,把上限设为n使得可读性更好。”于是,他删掉了这个特别的注释,那是我在平方根周围注释来解释为何不把上限设为n的聪明之处。

那时我眼珠子乱转而且还在一旁偷偷傻笑。我确信,如果n很大的时候,上限是n平方根会让算法的效率在数量级上大于n的,我还深信n每扩大一百倍,它所耗费的时间只会随之增加大概十倍。六年后(昨晚),我终于知道了程序的结果,而且知道了增幅是2倍线性阶的,而对此Kent一直都是对的。

 

译注:

1,strstream,标准C/C++的字符串流类,派生自iostream。因性能问题,C++标准委员会做了修补,用stringstream替换之,因此也不建议再使用strstream。

2,复杂度(本文指时间复杂度),以算法中基本运算的重复执行次数作为算法时间复杂度的时间量度,并以符号O(x)来表示。通常,时间复杂度由小到大分为几个等级,a)常量阶 O(c),b)对数阶 O(log2n),c)线性阶 O(n),d)多项式阶 O(nm)等

3,Kent Beck,是软件开发方法学的泰斗、XP的创始人,长期致力于软件工程的理论研究和实践,并具有讲授XP的丰富经验。作为软件业内最富创造,哇和最有口碑的领导人之一,KentBeck极力推崇模式、极限编程和测试驱动开发。他现在加盟于ThreeRivers研究所,是多部畅销书如《Smalltalk Best PracticePatterns》、《解析极限编程——拥抱变化》和《规划极限编程》(和Martin Fowler合著)的作者,并且是超级畅销书《重构——改善既有代码的设计》(中国电力出版社出版中英文版)的特约撰稿人。

 (原文链接网址:http://www.butunclebob.com/ArticleS.UncleBob.PerformanceTuning; Robert C. Martin的英文blog网址: http://www.butunclebob.com/ArticleS.UncleBob 

作者简介:Robert C. MartinObject Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。他不仅是Jolt获奖图书《敏捷软件开发:原则、模式与实践》(中文版)(《敏捷软件开发》(英文影印版))的作者,还是畅销书Designing Object-Oriented C++ Applications Using the Booch Method的作者。MartinPattern Languages of Program Design 3More C++ Gems的主编,并与James Newkirk合著了XP in Practice。他是国际程序员大会上著名的发言人,并在C++ Report杂志担任过4年的编辑。

这篇关于性能调优--永远超乎想象 (原文最终修订于 2006-08-28 晚上11:48:38)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/170915

相关文章

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”

Java慢查询排查与性能调优完整实战指南

《Java慢查询排查与性能调优完整实战指南》Java调优是一个广泛的话题,它涵盖了代码优化、内存管理、并发处理等多个方面,:本文主要介绍Java慢查询排查与性能调优的相关资料,文中通过代码介绍的非... 目录1. 事故全景:从告警到定位1.1 事故时间线1.2 关键指标异常1.3 排查工具链2. 深度剖析:

深入解析Java NIO在高并发场景下的性能优化实践指南

《深入解析JavaNIO在高并发场景下的性能优化实践指南》随着互联网业务不断演进,对高并发、低延时网络服务的需求日益增长,本文将深入解析JavaNIO在高并发场景下的性能优化方法,希望对大家有所帮助... 目录简介一、技术背景与应用场景二、核心原理深入分析2.1 Selector多路复用2.2 Buffer

基于Python Playwright进行前端性能测试的脚本实现

《基于PythonPlaywright进行前端性能测试的脚本实现》在当今Web应用开发中,性能优化是提升用户体验的关键因素之一,本文将介绍如何使用Playwright构建一个自动化性能测试工具,希望... 目录引言工具概述整体架构核心实现解析1. 浏览器初始化2. 性能数据收集3. 资源分析4. 关键性能指

Zabbix在MySQL性能监控方面的运用及最佳实践记录

《Zabbix在MySQL性能监控方面的运用及最佳实践记录》Zabbix通过自定义脚本和内置模板监控MySQL核心指标(连接、查询、资源、复制),支持自动发现多实例及告警通知,结合可视化仪表盘,可有效... 目录一、核心监控指标及配置1. 关键监控指标示例2. 配置方法二、自动发现与多实例管理1. 实践步骤

MySQL深分页进行性能优化的常见方法

《MySQL深分页进行性能优化的常见方法》在Web应用中,分页查询是数据库操作中的常见需求,然而,在面对大型数据集时,深分页(deeppagination)却成为了性能优化的一个挑战,在本文中,我们将... 目录引言:深分页,真的只是“翻页慢”那么简单吗?一、背景介绍二、深分页的性能问题三、业务场景分析四、

MySQL 多列 IN 查询之语法、性能与实战技巧(最新整理)

《MySQL多列IN查询之语法、性能与实战技巧(最新整理)》本文详解MySQL多列IN查询,对比传统OR写法,强调其简洁高效,适合批量匹配复合键,通过联合索引、分批次优化提升性能,兼容多种数据库... 目录一、基础语法:多列 IN 的两种写法1. 直接值列表2. 子查询二、对比传统 OR 的写法三、性能分析

Linux系统性能检测命令详解

《Linux系统性能检测命令详解》本文介绍了Linux系统常用的监控命令(如top、vmstat、iostat、htop等)及其参数功能,涵盖进程状态、内存使用、磁盘I/O、系统负载等多维度资源监控,... 目录toppsuptimevmstatIOStatiotopslabtophtopdstatnmon

JVisualVM之Java性能监控与调优利器详解

《JVisualVM之Java性能监控与调优利器详解》本文将详细介绍JVisualVM的使用方法,并结合实际案例展示如何利用它进行性能调优,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全... 目录1. JVisualVM简介2. JVisualVM的安装与启动2.1 启动JVisualVM2