本文主要是介绍【软件构造】课件精译(二十一) 代码调优,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、代码调优的策略和过程
代码调优是修改正确的代码以实现更高效地运行。
调优通常是指小规模的改变
在代码层面调优之前,应首先考虑以下策略:
不要过分强调不需要的性能
高层的架构设计
更换数据类型或算法
是否由类库中隐藏的对OS的调用导致的问题
优化编译器
升级硬件
代码调优是最后的选择
程序优化的Pareto原则
20%的程序消耗了80%的执行时间
调优的目标就是找到20%的热点,并优化
完美是优良的敌人,努力做到完美,可能会妨碍软件的完成。先解决有无问题,再解决优劣问题。
代码调优并不是减少代码行、根据猜测进行优化、随时随地优化、优化排在第一位
(1)代码行数与效率
代码行数与程序的资源占用和运行速度之间没有必然的联系
(2)通过剖析和测量进行调优
性能分析有助于发现热点
性能分析为度量改进后的效果提供了基础
代码调优应基于精确地度量
每次调优都意味着需要重新性能分析
否则,在一种环境下的调优,在其他环境下有可能反而降低性能
(3)完成代码后再优化
忙于微观的优化,会导致忽略了全局的优化
完成开发之前,难以发现瓶颈
即使发现并纠正了瓶颈,也可能造成其他的瓶颈
早期关注优化,会忽略更重要的目标
(4)速度与正确性
显然正确是最重要的,只有很少的项目把性能摆在首位,对多数程序而言,早期优化会影响包括性能在内的整体质量
起始于书写良好的代码,只在最后进行调优
何时调优
使用高品质的设计
使程序正确
使其模块化,易于修改,以便以后轻松使用
完成并正确后,检查性能。 如果程序变细,请快速小巧
在需要之前不要进行优化
代码调优过程
使用精心设计的代码来开发软件,这些代码很容易理解和修改。
如果性能比较差:
保存可工作版本
评价系统,发现热点
确定问题根源(设计、数据类型、算法?),调优是否适合
二、低效率的常见原因
(1)I/O
不必要的I/O操作,在允许的情况下,尽量使用内存运算
(2)Paging分页
导致OS进行内存分页操作的程序的运算速度,比不分页的慢很多
内存越小,效果越明显
(3)系统调用
系统子程序调用代价高
部分系统子程序中包含了不易发觉的耗时的IO操作等
自己写服务 ,只实现必须的部分
让系统供应商优化其开发的被调用程序
(4)操作的相对性能消耗
多数调优情况下,通过使用低开销的操作替代高开销的操作
三、对象创建与复用的代码调优
(1)单例模式(Singleton Pattern)
某些类在概念上只有一个实例
只创建一个对象,然后复用
对管理重用的代码进行封装
确保类只有一个实例,并提供一个全局访问点
只能有一个类的实例,并且客户端可以从众所周知的访问点访问它
当唯一的实例可以通过子类扩展时,客户端应该能够使用扩展实例而不修改它们的代码
类负责管理其唯一实例
构造器private,提供static方法和字段允许对唯一实例的调用
延迟加载,不在启动时创建实例,在首次访问时创建
(2)享元模式(Flyweight Pattern)
单独为每个字符创建实例,会耗费大量内存
享元模式描述了如何共享对象,有效支持细粒度的使用而不会产生过高的成本
flyweight是一个共享对象,可在多个情境下同时使用
内在状态:不变的状态,可以共享
外在状态:状态是不固定的,使用时需要计算,不可共享
ConcreteFlyweight:保存可共享的状态,对象可共享(例如:Character)
UnsharedConcreteFlyweight:不可共享(例如:Row,Column)
FlyweightFactory
创建和管理flyweight 对象,给客户端提供flyweight对象,起到索引器的作用
通过索引获取flyweight引用
调用方法计算外在状态
Example
应用场景:
应用程序使用了大量的对象,由于对象的数量庞大,存储成本很高
对象的大多数状态是外部状态,一旦删除了外部状态,可以用相对较少的共享对象替代很多组对象
应用程序不依赖于对象标识
(3)原型模式
通过复制已有原型对象新创建对象
目标:创建或初始化对象代价高时,可通过此模式创建相似对象,降低开销
类似于文档模板,创建一次,多次被复制使用,作为撰写文档的起点。
原型模式在初始化创建第一个对象时开销大,然后将这些值作为原型存储在存储库中。
需要再次创建相同或类似对象时,只需从存储库中获取所有值已经预填充的原型副本。
应用:
当一个系统应该独立于它的 产品创建、构成和表示时
当要实例化的类是在运行时刻指定时
为了避免创建一个与产品类层次平行的工厂类层次时
当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们,可能比每次用合适的状态手工实例化该类更方便一些
Java中的Clone()
对象clone意味着创建一个具有相同内容的副本
类中要声明clone()方法
实现clone()时,要调用super.clone()(in Object)
类需要实现 java.lang.Cloneable接口
通过浅拷贝克隆
复制对象A的所有字段到对象B中
基本数据类型采用复制值的形式
对对象的引用,则复制引用,此时A和B共享对同一对象的引用
实现相对简单
通过深拷贝克隆
被 复制对象中引用了某对象时,需要创建新的被引用对象(不是共享)
A和B完全独立
实现复杂,当存在复杂的引用关系图时更加难以实现
举例
原型模式对比享元模式
原型模式用于创建本质上相似的新 对象(因此它是一种创建模式)
flyweight模式用于允许应用程序指向对象的同一个实例以节省内 存(因此它是一种结构模式)。
(4)对象池模式
重复使用,避免重新创建
已经有了一个对象,并不再使用时,可让程序其他部分继续使用
回收再利用
使用时从pool中取出,用完后归还,类似工具箱
对象池模式与享元模式
可复用的容器
有的容器我们并不需要它赋值给外部变量,仅仅是内部的操作,这时候可以采用可复用性的容器。
规范化/标准化对象
规范化的对象:用少量对象替代一个对象的大量副本
“= =” 比较比equals()快,通过规范化可实现用 “= =”比较替代equals()比较
举例:整数规范化
创建常用整数,并保留
举例:枚举常量
用整数替换常量对象
枚举需要的内存少于等效字符串,并使网络传输速度更快。
可使用== 替代 equals()
(5)避免垃圾回收
常用方法:规范化、采用pool、避免创建额外对象
原始数据类型导致较少的GC
基本数据类型与其所在对象同时回收,影响较小
临时基本数据类型在栈中,不需要垃圾回收
持有int的对象,只需要回收1个对象(持有对象); 持有Integer对象的对象,需要回收2个对象(持有对象和Integer对象)
尽量使用原始数据类型而不是其他对象数据类型
使用int来替代String、Date,但需要考虑转换的时间开销
上面两种方式,一个是通过数组,另一个使用List。
其他一般性建议
减少正在使用的临时对象的数量,特别是在循环中
用 StringBuffer替代 “+”
注意哪些方法直接更改对象而不进行复制,哪些方法会返回对象的副本
在处理基本类型时避免使用通用的类
(6)对象初始化
构造和初始化对象
利用new方法创建对象时,涉及的构造链会自动执行。开销大
应该检查构造函数层次结构以消除对实例变量的任何多重初始化
可以使用clone()替代构造方法,以避免链式构造
预分配对象
在应用程序中有空闲时间时尽早创建对象,并保留这些对象直到需要为止
延迟初始化
四、字符串的代码调优
回忆:JVM中的内存管理
字符常量池是堆中的内存区域,保存了string对象
而String s = new String(“java”)则会直接创建在堆中
内存常量池
字符串内存调优的基本原则就是最小化在堆上创建的临时对象的数量,以便最小化GC
编译时和运行时解析度
当String可以在编译时完全解析时,连接运算符比使用StringBuffer更有效。
如果在编译时无法解析String,则连接运算符比使用StringBuffer的效率较低。
字符串与字符数组
在JDK中字符串的内部实现是字符数组
private char[] a;
private int start;
private int end;
对于字符串代码调优的一些建议
使用不会复制字符串字符的 String方法,例如String.substring()
避免使用复制字符串字符的低效String方法,例如String.toUppercase() and String.toLowercase()
使用字符串连接运算符(+)在编译时创建字符串
运行时使用 StringBuffers
操作char数组中的字符,而不是使用String和StringBuffer操作
五、逻辑、循环、数据类型、表达式和函数的代码调优
(1)逻辑相关代码调优
得知答案后不要继续检验
if ( x>5 && x<10 )
C++与Java都支持“短路求值”特性,如果第一个条件已经不满足,则不会继续检查下一个条件
对于这种情况,应当加入break
为冗余的逻辑设置替换表
惰性求值
直到用到的时候再求值
类似于及时完成策略,即仅到工作必须完成的时候才去处理
在需要时再计算,同时保存结果(cache)供以后使用
(2)循环相关的代码调优
Unswitching-将判断外提
如果在循环执行时决策没有改变,可以通过在循环外进行决策来断开循环
Jamming-合并
Unrolling-展开
最大限度地减少循环内部的工作
哨兵值
可以通过使用标记来改进的复合测试的典型示例是搜索循环,它检查它是否已找到它正在寻找的值以及它是否已用完值。
把最繁忙的循环放在里面
Strength Reduct—削减强度
用多次轻量级运算代替一次代价高昂的运算
(3)数据类型相关的代码调优
尽可能使用整数而不是浮点数
尽可能降低数组维度
减少数组引用
使用辅助索引
辅助索引是添加相关数据,使得对某种数据的访问更为高效
例如:
字符串长度索引
独立的平行的索引结构
对索引排序和查找会比直接对数据进行操作要快
使用缓存
保存一些值,使最常用的值更容易被获取到
将最频繁访问的记录放入缓存,访问时首先查找缓存
缓存经常用到的字符宽度
缓存需要耗时计算的结果
缓存的成功取决于访问缓存元素的相对成本,创建未缓存的元素以及将新元素保存在缓存中
成功还取决于请求缓存信息的频率
缓存增加了复杂性并且容易出错
(4)表达式相关的代码调优
代数恒等式
通过代数恒等式,用低代价的操作替代复杂操作
not a and not b
not (a or b)
第二个式子节约了一个not操作符
再看这个例子:sqrt(x)<sqrt(y) vs. x<y
Use strength reduction—削弱运算强度
用代价低的运算代替代价高的运算
在编译时初始化
对于固定常量或值提前计算出来
使用常量的正确类型
所使用的具名常量和被赋值的相应变量具有相同的类型,避免编译器进行类型转换(额外的开销)
预计算结果
如果某结果被多次使用,计算一次且在其余时间检索并运用这个结果可节省时间
简单形式:在循环外计算一部分表达式;复杂形式:在开始前,通过表格、文件等形式记录下会多次用到的结果
删除公共表达式
(5)函数相关的代码调优
良好的子程序分解是调优的强大工具
小巧而良好定义的子程序节省空间、易于优化、易于重写
六、I/O与日志的代码调优
(参考之前章节
七、数据结构与算法的代码调优
(参考数据结构与算法的相关课程
这篇关于【软件构造】课件精译(二十一) 代码调优的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!