您的位置:首页 > 其它

《程序设计实践》读书笔记第七至八章

2008-08-22 08:56 190 查看

第7章 性能

优化的第一要义是不做。程序是不是已经足够好了?应该了解程序将如何使用以及它将要运行于其中的环境,把它搞得更快有什么益处吗?

什么时候我们应该试图去加速一个程序?我们该如何做,又能够期望得到些什么呢?

7.1 瓶颈

一旦程序已经足够快了,我们就停止,干嘛要没事找事呢?

7.2 计时和轮廓

自动计时测量。许多系统里都有这种命令,它们可用于测量一个程序到底用了多少时间。Unix系统里有关命令的名字是time。

使用轮廓程序。除了可靠的计时方法外,在性能分析中最重要的工具就是一种能产生轮廓文件的系统。轮廓文件是对程序在哪些地方消耗了时间的一种度量。在有些轮廓文件中列出了执行中调用的各个函数、各函数被调用的次数以及它们消耗的时间在整个执行中的百分比。另一些轮廓文件计算每个语句执行的次数。执行非常频繁的语句通常对总运行时间的贡献比较大,根本没执行的语句所指明的可能是些无用代码,或者是没有合理测试到的代码。

集中注意热点。当某一个函数单独成为一个瓶颈的主导因素时,那么就只有两条路可以走了:或是采用一种更好的算法来改进这个函数;或者直接删去这个函数,重写环绕它的程序部分。

画一个图。图形特别适合用来表现性能测量的情况,它可以很好地传达信息,例如参数改变的影响、算法和数据结构的比较,有时也能指出某些没有预料到的情况。

7.3 加速策略

使用更好的算法或数据结构。要使程序运行速度快,最重要的因素是算法与数据结构的选择,有效的算法和不那么有效的算法造成的差距是巨大的。

让编译程序做优化。有一种毫不费力的改变就可能产生明显的加速效果,那就是打开编译系统的所有优化开关。

调整代码。只要数据有足够的规模,算法的正确选择就会显示它的作用。进一步说,算法方面的改进是跨机器、跨系统和跨语言的。但是,如果已经选择了正确的算法,程序的速度仍然是问题的话,下一步还能做的就是调整代码,整理循环和表达式的细节,设法使事情做得更快些。

不要优化那些无关紧要的东西。有时所做的代码调整毫无作用,这就是因为用到了一些不能产生差异的地方。

7.4 代码调整

收集公共表达式。如果一个代价昂贵的计算多次出现,那么就只在一个地方做它,并记录计算的结果。

用低代价操作代替高代价的。术语降低强度指的是用低代价操作代替高代价操作的那些优化。

铺开或者删除代码。循环的准备和运行都需要一定的开销。如果循环体本身非常小,循环次数很少,有一个更有效的方法,就是把它重写为一个重复进行的计算序列。

高速缓存频繁使用的值。以缓冲方式保存的值无须重新计算。缓存的价值来自于局部性。

写专用的存储分配程序。经常可以看到这种情况,程序里的惟一热点就是存储分配,表现为对malloc和free的大量调用。如果程序中需要的经常是同样大小的存储块,采用一个特定用途的存储分配器取代一般的分配器,有可能使速度得到实质性提高。这种特定的存储分配器调用malloc一次,取得基本存储块的一个大数组,在随后需要时一次送出去一块,这是一个代价很低的操作。释放后的存储块接在一个自由表的最后,这使它们可以立即重新投入使用。

对输入输出做缓冲。缓冲方式使数据传输操作以成批的方式完成,这能使频繁操作所造成的负担减到最小,并使代价昂贵的操作只在不可避免时才进行,使这种操作的代价能分散到许多数据值上。

特殊情况特殊处理。通过使用特殊代码去操作同样大小的对象,特殊用途的分配器可以比通用分配器节约时间和空间开销,还可能减少碎片问题。

预先算出某些值。有时可以让程序预先计算出一些值,需要时拿起来就用,这也可能使程序运行得更快些。

使用近似值。如果精度不太重要,那么就尽量使用具有较低精度的数据类型。

在某个低级语言里重写代码。低级语言程序的效率可能更高,不过这样做也要付出代价,那就是程序员的时间。

7.5 空间效率

使用尽可能小的数据类型以节约存储。提高空间效率的一个步骤是做些小修改,使现有存储能使用得更好,例如使用能满足工作需要的最小数据类型。

不存储容易重算的东西。与代码调整类似,这方面的修改也不太重要。最重要的改进应该是来自好的数据结构,或许还要伴随着算法的修改。

7.6 估计

要预先估计一个程序能运行得多么快,通常是非常困难的,而要估计某个特殊程序语句或者机器指令的时间代价,那就是双倍的困难了。然而,为一个语言或者系统做一次代价模拟却是比较简单的,它至少能使你对各种重要操作花费的时间有一个粗略的概念。

第8章 可移植性

8.1 语言

盯紧标准。得到可移植代码的第一步当然是使用某种高级语言,应该按照语言标准(如果有的话)去写程序。二进制不可能很容易地移植,但是源代码可以。

在主流中做程序设计。某些编译系统不能辨识上面的错误,这当然很不幸,也说明了与可移植性有关的一个重要问题。任何语言都有黑暗的角落,在那里实践会出现分歧。

警惕语言的麻烦特性。我们已经提过,标准里常常有意遗留下一些东西,不给以定义或者不加以清楚的说明,通常这是为了给写编译系统的人更大的自由度。

用多个编译系统试验。人们很容易认为自己已经理解了可移植性,但是编译系统能看出某些你没有看到的问题。

8.2 头文件和库

使用标准库。在这里,应该提出与核心语言同样的建议:盯紧标准,特别是其中比较成熟的、构造良好的成分。

8.3 程序组织

只使用到处都可用的特征。我们建议采用取交集的方式,即:只使用那些在所有目标系统里都存在的特性,绝不使用那些并不是到处都能用的特征。

避免条件编译。使用#ifdef和其他类似预处理指示写的条件编译是很难管理的,因为在这种情况下有关信息趋向于散布在整个源文件里。

8.4 隔离

把系统依赖性局限在独立文件里。如果不同系统需要不同的代码,应该使这种差异局限在独立的文件里,一个文件对应一个系统。

把系统依赖性隐藏在接口后面。抽象是一种强有力的技术,应该通过它划清程序的可移植部分与不可移植部分之间的界限。大部分程序设计语言所附带的I/O库就是一个很好的例子,它们使用可供打开/关闭、读和写的文件概念,从不提及任何物理位置或结构,为二级存储器提供了一种抽象。使用这些接口的程序将能在任何实现了它们的系统上运行。

8.5 数据交换

用正文做数据交换。正文容易用各种工具操作,以原来未曾预计到的方式去处理。

8.6 字节序

8.7 可移植性和升级

如果改变规范就应改变名字。

维护现存程序与数据的相容性。

8.8 国际化

不要假定是ASCII。

不要假定是英语。

8.9 小结

可移植代码是一个非常值得去追求的理想,因为有如此多的时间被浪费在修改程序方面,无论是把程序从一个系统移到另一个系统,还是为了它本身演化的需要,或是因为它运行的系统发生了变化,在这些情况下需要设法维持程序的继续运行。

我们已经指出了追求可移植性的两种途径,即联合和交集。联合途径相当于为在每个目标系统上工作而写一个版本,利用条件编译一类的机制,把这些代码尽可能地汇集在一起。交集途径是设法以一种形式写出尽量多的代码,使它能在每种系统上运行而不需要做任何修改。把无法逃避的系统依赖性封装在独立的源文件里,其作用就像是程序与基础系统之间的界面。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: