算法(逻辑)优劣评估方法

评估代码实现优劣的几个方法

工程实现到算法实现在性能成为瓶颈的时候应该去排查代码,从什么角度去评价一段代码的优劣显得非常有重要,高级程序员都该清醒认识,这才可能写出优质的代码。下面列举几个通常衡量算法性能的维度,并逐一分析实现的策略。其实所有的有逻辑的代码都可以认为是算法,并不单单指狭义的数学概念中的“算法”,我这里称之为侠义的代码质量。其实代码质量还要包含编程风格等因素,这里暂时不涉及。


传统算法评估维度

算法,即解决问题的方法。算法是解决某个问题的想法、思路;而程序是在心中有算法的前提下编写出来的可以运行的代码。概括来说算法相当于是程序的雏形。

同一个问题,使用不同的算法,虽然得到的结果相同,但是耗费的时间和资源是不同的。条条大路通罗马”,解决问题的算法有多种,这就需要判断哪个算法“更好”。算法复杂度常从两个维度来评估:时间复杂度和空间复杂度。我们使用时间复杂度来度量算法运行的性能,使用空间复杂度来度量算法要使用的存储器空间大小。

算法评估应该包括算法的准确性/健壮性/运行效率。但本文中重点要说明的算法运算效率的评估。

1.1 时间复杂度

时间复杂度用O(f(n))表示,函数f(n)描述了算法计算量随着计算规模n的变化而变化的趋势,当n趋于无穷大时,通过f(n)我们就能知道算法在极端(最坏)情况下的计算(时间)消耗情况。

与时间复杂度关联紧密的一个量是时间频度T(n),它用于描述一个算法中的语句执行次数。我们可以认为,对时间频度T(n)做渐进行为表示,得到的就是时间复杂度O(f(n))。因此,在求算法的时间复杂度时,通常是先计算得到算法的T(n)表达式,再进一步求得算法的O(f(n))表示。

以如下的矩阵与向量乘法为例:

for(i=0; i<row_len; i++);
{
	temp = 0;
	for(j=0; j<col_len; j++)
	{
		temp += matrix[i][j] * vecter[j];
	}
	product[i] = temp;
}

设row_len = col_len = n,并不考虑循环跳转所需的指令时间。在内层循环中,包含一次乘法,一次加法,两次读操作和一次写操作;在外层循环中,包含两次写操作。因此该算法的时间频度可表示为T(n) = 5n^2 + 2n。考虑到时间复杂度忽略低阶和常数项的影响,该算法的时间复杂度为O(n^2)。

需要注意的一点是,对于T(n)仅含一个常数项的算法,它的时间复杂度表示为O(1)。
几种常见的实际复杂度排序:

O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n2)平方阶 < O(n3)(立方阶) < O(2n) (指数阶)

1.2 空间复杂度

空间复杂度是指算法在计算机内执行时所需存储空间的度量,与时间复杂度的分析思想类似,空间复杂度也用O(f(n))表示。

一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。空间复杂度不考虑存储算法占用的空间,同时,由于算法的输入输出数据所占用的存储空间是由要解决的问题决定的,它不随算法的不同而改变,在分析空间复杂度时一般也不予考虑。因此,我们一般所讨论的空间复杂度,是用于衡量算法正常占用内存开销外的辅助存储单元规模。

以上面时间复杂度分析所用的例子来说,临时的存储空间只有一个temp变量,而matrix、vecter、product三者均为输入输出变量,固该算法的空间复杂度为O(1)。


综合实现复杂度

尽管算法复杂度能够在抽象层面上衡量算法的性能,但在分析算法在具体处理器平台上的表现时,它却不是一个很好的度量标准。原因有:

  1. 时间复杂度忽略低阶和常数项的影响,而两个算法复杂度相同的算法有可能在实现上会表现出成倍的性能差异,这些差异和编程语言、编译器平台、处理器的指令集、处理器的功能架构等有关;
  2. 空间复杂度也忽略低阶和常数项的影响,且不能表征算法访问存储器的次数,更没有考虑到处理器访存层次结构的影响;
  3. 不同算法在其实现时所关注的限制因素不同,仅关注时间复杂度或空间复杂度有时是没有意义的。比如,从来没有优秀的开发人员使用时间复杂度来衡量网络服务器或数据库服务器的性能;
  4. 时间复杂度不能很好地度量并行算法,并行算法需要的数据/任务划分、通信等都超出了时间复杂度的考量范围。因此一旦并行算法的这些特性成为了影响性能的重要因素,时间复杂度分析通常会得出与实际不符的结论。

由于以上分析的这些原因,实际应用中,算法复杂度提高但是实现更快的例子很多。为更好地度量算法在实现时的性能,文献2的作者刘文志提出了实现复杂度的概念。鉴于某个特定的处理器上算法的实际性能基本上由运行时的计算,仿存和指令决定,他将实现复杂度分三个维度来评估:计算复杂度、访存复杂度和指令复杂度。

在做实现复杂度分析时,应注意以下几点:

1.脱离抽象回归具体,评估实现复杂度时已不能像算法复杂度那样做渐进行为分析,而应该尽可能地还原处理器实现的细节

2.对于具体的代码而言,依据在某种处理器上运行的性能瓶颈不同而采用实现复杂度的不同方面来度量。对于一个计算限制算法,应当使用计算复杂度和指令复杂度。对于一个存储器限制的算法,应当使用访存复杂度来分析。

2.1 计算复杂度

计算复杂度衡量的是每一个控制流的计算数量,它评估处理器对指定算法的计算能力。在并行算法上应用计算复杂度分析时,如果控制流之间的计算量并不均衡,同时考虑控制流之间最大计算复杂度和最小计算复杂度,它们的比例就可大致估计负载不均衡的程度。

与时间复杂度只考虑指令数量不同的是,计算复杂度还需要考虑指令的吞吐量或延迟,将它们加权平均。计算复杂度权重可基于延迟或吞吐量确定,但实际都可转化为吞吐量。

通常可以把处理器分为为延迟优化设计和为吞吐量优化设计两类。比如ARM cortex-r系列处理器,它比较注重对事件的响应速度,因此单条指令的延迟是很短的,我们可以把它归为基于延迟设计的处理器。又比如NVIDIA的GPU,它拥有众多的并行处理核心,单次可以做多个并行计算,但它每次计算的延迟相对又是比较长的,我们可以把它归为基于吞吐量优化的处理器。但有时这两种归类方法也会变得模糊,因为越来越多延迟性很好的处理器也加入了指令级并行能力。总之,究竟是以延迟还是吞吐量来考量计算复杂度,应该结合处理器的特点以及算法的需求来共同决定,选择最贴近于现实目的那一个。

例如我们要实现下面的计算:

int a, b, c;
c = a + b;

其计算复杂度公式为O(2nα + nβ + n*γ),其中α表示加载指令的吞吐量倒数,β表示加法指令的吞吐量倒数,γ表示存储指令的吞吐量倒数。如果代码运行在为延迟优化的处理器上,只需将α、β、γ替换成时钟周期数即可。

以Intel Haswell架构为例,假设数据均在一级缓存中,则α=3,β=0.25,γ=3,固计算复杂度为O(9n + 0.25n),可以看出计算复杂度主要由访存带来,这意味着这个算法的瓶颈是访存。

2.2指令复杂度

指令复杂度衡量执行代码关键路径所需要的时钟周期数。它与计算复杂度的区别是:计算复杂度没有考虑处理器能并行执行多个不同指令的情况,而指令复杂度考虑;计算复杂度适合用于同一处理器平台的不同算法或不同控制流之间的比较,而指令复杂度适合于同一算法的不同实现策略间的比较,因此常用于指导实现性能优化。

代码计算效率优化的过程,就是指令复杂度降低的过程!

2.3访存复杂度

访存复杂度衡量算法访问缓存层次的字节数量。如在《计算机系统中与存储有关的那些事》中介绍的,计算机系统中的存储空间往往是一个多层次结构,访存复杂度并不是对算法在各层中的访问性能做加和,而是由瓶颈所在的存储层次决定。如果算法是一级缓存密集型,则需要分析一级缓存访问字节数量;如果算法是多核心共享缓存密集型,则需要分析所有处理器核心访问共享缓存的总数量。

访存复杂度通常使用缓存层次的带宽来表示。计算访存复杂度时,由于缓存缺失(cache miss)带来的额外开销也应该计算在内。

有四种与存储有关的带宽值得介绍下:

  1. 存储器的峰值带宽:内存访问速度
  2. 可获得的存储器带宽:可获得的存储器带宽通常要小于存储器的峰值带宽,其大小一般由程序测试获得
  3. 程序发挥的存储器带宽:特定程序在硬件上发挥的带宽,该值可以通过硬件计数器获得
  4. 程序的有效访存带宽:程序最小要访问的数据量与程序计算时间的比值,可由算法计算获得。该值能表示访存优化能达到的上限。

结论

评估的步骤总结如下

1. 确定程序中需要关注的维度,并找到此维度对应的理论极限

2. 如果多个维度,设计各个维度的权值,并计算维度加权和


参考资料:https://zhuanlan.zhihu.com/p/55132581

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值