第5章 优化算法 摘录

本文探讨了优化算法的重要性,指出高效算法能带来指数级性能提升。介绍了时间开销的概念,如O(1)、O(n)等,并讨论了预计算、延迟计算和批量处理等优化策略。预计算用于提前计算热点代码,延迟计算推迟不必要的计算,批量处理则通过集合处理减少重复操作。这些方法旨在减少计算量,提高程序效率。

多数优化方法的性能改善效果是线性的,但是使用更高效的算法替换低效算法可以使性能呈现指数级增长。

计算机科学家之所以研究重要的算法和数据结构,是因为它们是展示如何优化代码的典型示例。

5.1 算法的时间开销

算法的时间开销是一个抽象的数学函数,它描述了随着输入数据规模的增加,算法的时间开销会如何增长。

时间开销通常使用大O表示法,例如O(f(n)),f(n)被简化为仅表示增长最快的因素。

下面概况介绍了一些常用算法的时间开销以及相对于程序运行时开销的倍数:

O(1),即常量时间:最快的算法的时间开销是常量时间,也就是说,它们的开销是固定的,完全不取决于输入数据的规模。

O(log_2N)时间开销比线性更小,它们通常足够高效,以至于许多情况下啊都无需再寻找更快的算法。

O(n),即线性时间,算法需要花费的时间与输入数据的规模成正比。通常是那些从输入数据的一端向另一端扫描,直至找到最小值或最大值的算法。这种算法并不安规,即使不断扩大程序的输入数据的规模,也不必担心占用巨大的计算资源。不过,当多种线性时间算法合并在一起时,可能会导致它们的开销变为O(n^2)或者更差。

O(nlog_2n),虽然随着n的增加,时间开销相对更大,但其增长速率是如此之慢,以至于通常情况下即使n很大,使用这类算法也没有问题。

O(n^2)O(n^3)等这类算法的时间开销的增长速度非常快,以至于让人不免有些担心。

O(2^n)时间增长太快了,它们应当只应用于n很小的情况,

5.1.3 其他开销

通过保存中间结果可以提高算法的速度。因此,这种算法不仅有时间开销,还有额外的存储开销。

5.5 优化模式

开发人员研究算法和数据结构的原因之一是其中蕴含着用于改善性能的”思想库”。

预计算:可以在程序早期,例如设计时、编译时或是链接时,通过在热点代码前执行计算来将计算从热点部分中移除。

延迟计算:通过在真正需要计算时才执行计算,可以将计算从某些代码路径上移除。

批量处理:每次对多个元素一起进行计算,而不是一次只对一个元素进行计算。

缓存:通过保存和复用昂贵计算的结果来减少计算量,而不是重复进行计算。

特化:通过移除未使用的共性来减少计算量。

提高处理量:通过一次处理大组数据来减少循环处理的开销。

提示:通过在代码中加入可能会改善性能的提示来减少计算量。

优化期待路径:以期待频率从高到低的顺序对输入数据或是运行时发生的事件进行测试。

散列法:计算可变长度字符串等大型数据结构的压缩数值映射。在进行比较时,用散列表代替数据结构可以提高性能。

双重检查:通过先进行一项开销不大的检查,然后只在必要时进行另外一项开销昂贵的减少来减少计算量。

5.5.1 预计算

预计算通过在程序中执行至热点代码之前,先提前进行计算来达到从热点代码中移除计算的目的。可以将热点代码移至程序中不那么热点的部分,也可以移至程序链接时、编译时和设计时。通常,越早进行计算。

预计算仅当被计算的值不依赖于上下文时才适用,编译器能够对以下的表达式进行预计sauna,因为它不依赖于程序中的任何东西。

int sec_per_day = 60 * 60 * 24;
int sec_per_weekend = (data_end - data_beginning + 1) * 60 * 60 * 24;

以下C++编译器是预计算的几个例子:

        C++编译器会使用编译器内检的相关性规则和运算符优先级,对常亮表达式的值自动地进行预计算。

        编译器会在编译时评估调用模板函数时所用到的参数。如果参数是常量的话,编译器会生成高效的代码。

5.5.2 延迟计算

延迟计算的目的在于将计算推迟至更接近真正需要进行计算的地方。如果没有必要在某个函数中的所有路径上都进行计算,那就只在有需要的路径上进行计算。

两段构建 two-part construction

        当实例能够被静态地构建时,经常会缺少构建对象所需的信息。在构建对象时,我们并不是一气呵成,而是仅在构造函数中编写建立空对象的最低限度的代码。稍后,程序在调用该对象的初始化成员函数来完成构建。将初始化推迟至有足够的额外数据时,意味着被构建的对象总是高效的、扁平的数据结构。

写时复制

        写时复制是当一个对象被复制时,并不复制它的动态成员变量,而是让两个实例动态变量。只在其中某个实例要修改该变量时,才会真正进行复制。

5.5.3 批量处理

批量处理的目标是收集多份工作,然后一起处理它们。批量处理可以用来移除重复的函数调用或是每次只处理一个项目时会发生的其他计算。当有更高效的算法可以处理所有输入数据时,也可以使用批量处理将计算推迟至更多的计算资源可用时。举例如下:

        缓存输出是批量的一个典型例子。输出字符会一直被缓存。

        多线程的任务队列是通过批量处理高效地利用计算资源的一个例子。

        在后台保存或更新是使用批量处理的一个例子。

5.5.4 缓存

缓存是指通过保存和复用昂贵计算的结果来减少计算量的方法。这样就可以避免在每次计算结果时都重新进行计算。

        就像用于解引用元素的计算一样,编译器也会缓存短小的,重复的代码块的结果。

        高速缓存指的是计算机中使处理器可以更快地访问那些需要频繁访问的内存地址的特殊电路。

        在每次需要知道C风格字符串的长度时,都必须计算字符的数量。

        线程池缓存了那些创建开销很大的线程。

        动态规划是一项算法技术,通过计算子问题并缓存结果来提高具有递归关系的计算的速度。

5.5.5 特化

特化与泛化相对。特化的目的在于移除某种情况下不需要执行的昂贵的计算。

通过移除那些导致计算变得昂贵的特性可以简化操作或是数据结构。

5.5.6  提高处理量

提高处理量的目标是减少重复操作的迭代次数,削减重复操作带来的开销。这些策略如下:
        向操作系统请求大量输入数据或是发送大量输出数据。来减少内存块或是独立的数据项调用内核而产生的开销。提高处理量的副作用是,当程序崩溃,特别是在写数据时崩溃时,损失的数据量更大。

        在移动缓存或是清除缓存时,不要以字节为单位,而是要以字或是长字为单位。这项优化仅在两块内存对齐至相同大小的边界时才能改善性能。

        以字或是长字来比较字符串。这项优化仅适用于大端计算机,不适用于小端的x86架构。这种依赖计算机架构的技巧可能会非常危险,因为它们是不可移植的。

        在唤醒线程时执行更多的工作。在唤醒线程后,不要只让处理器执行一个工作单元后就放弃它,应当让它处理多个工作单元。这样可以节省重复唤醒线程的开销。

        不要在每次循环中都执行维护任务,而应当每循环10次或100次再执行一次维护任务。

5.5.7 提示

使用提示来减少计算量,可以达到减少单个操作的开销的目的。例如,std::map中有一个重载的insert成员函数,它有一个提示最优插入位置的可选参数,最优提示可以让插入操作的时间开销变为O(1),而不使用最优提示时的时间开销则是O(log_2n)

5.5.8 优化期待路径

存在许多else-if分支的代码块中,如果有一种情况的发生几率是95%,那应首先对它进行条件测试。

5.5.9 散列法

大型数据结构或长字符串会被一种算法处理为一个称为散列值的整数值。通过比较两个输入数据的散列值,可以高效地判断它们是否相等。散列法可与双重检查一起使用,以优化条件判断处理的性能。

5.5.10 双重检查

双重检查是指首先使用一种开销不大的检查来排除部分可能性,然后在必要的时候再使用开销很大的检查来测试剩余的可能性。例如:

        双重检查常与缓存同时使用。当处理器需要某个值时,首先会去检查该值是否在缓存中,如果不在,则从内存中获取该值或是通过一项开销很大的计算来得到该值。

        当比较两个字符串是否相等时,通常需要对字符串中的字符逐一进行比较。不过首先比较这两个字符串的长度可以很快地排除它们不相等的情况。

        双重检查也可用于散列表中。首先比较两个输入数据的散列值,可以高效地判断它们是否不相等。

5.6 小结

        注意那些推销常量时间算法的陌生人, 这些算法的时间开销可能是O(n);

        混合使用多种高效算法可能会导致它们的整体运行时间变为O(n^2)

        对于表项小于4的表,所有查找算法所检查的表项的数量几乎都是相同的;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值