当前位置: 首页 > 文档资料 > larva 中文文档 >

非随机行为和代码优化

优质
小牛编辑
114浏览
2023-12-01

语言的实现是一个很复杂的过程,这个复杂并非说它很难,虽然的确有一些难点,但总的来说并不是那么难以理解,复杂和简单相对,是指细节很多,很多事情需要具体到情况来讨论,比如龙书讲优化的部分,很多都是小的优化点;还有一些可能矛盾的地方,需要多种方案配合处理,比如前述静态类型推导,虽然纯粹的静态和动态类型都很容易实现,但要想各取所长就很麻烦了

幸运的是,至少在计算机领域,很多东西是非随机性的,语言也不例外,比如前述的栈虚拟机字节码switch结构,每个字节码的出现不是随机的,而且很可能是极不平衡,load,add之类的占绝大多数,因此,只需要合理安排顺序,结合指令预测,运行速度不比threaded code慢多少,非随机行为符合二八定律,在这个例子中,可以认为20%的字节码承担了80%的运行,这样一来我们只需要将优化重点放在那20%即可。计算机的局部性原理也是二八定律的例子,不过局部性原理偏向动态性,和运行时相关,这里用非随机行为来概括动静态

比如编译器的代码优化就是静态的,不考虑具体代码,编译器优化有很多方向,但并非每个方向的收益都是平均的,比如说C语言编译成可执行文件的过程中,寄存器指派、指令级并发相关的优化就比较重要,可以成倍提高运算速度,而其他一些方案,比如死代码消除,可能作用就有限,如果一个编译器优化不考虑寄存器和流水线,那其他优化做的再好,可能也还不如那些只做了重点优化的编译器

和二八定律相对应的,是长尾定律,长尾定律认为,80%的长尾虽然平均权重不高,但某些情况下由于数量多,整体权重占比也很可观,当然这是在一定情况下。如果长尾和20%的重点部分不冲突,那自然是多多益善,比如上面说的优化,如果一个编译器把重点的优化做好,其他优化也做了很多,那就更好了,如果优化的点之间是正交的,那长尾部分更不能小看,比如A方案优化到原来的5倍,B方案优化到原来的3倍,则正交情况下,AB同时起作用,速度可以提高到原来的15倍。然而考虑到资源有限,很多时候不可能覆盖到所有长尾,这里的资源有限的概念比较广泛,比如时间、人力不够,比如问题难度太大等

具体到语言上,一个语言是从设计开始的,它可能有很多特性,但一般来说也符合二八定律,即绝大多数情况下,我们仅使用其较少的特性

例如,静态类型语言提供了整数和浮点两种类型,但据统计整数比浮点使用到的几率大得多,在C语言中,地址和指针相关运算实际都是整数,这样一来整数权重就更大了。浮点数一般只有在一些计算密集的程序中使用较为频繁

再举个动态语言的例子,在python中:

class A:   
    pass   
A.x = 1   
a = A()   
a.y = "hello"

我们可以给类动态增减属性,python的这个特性提供了很高的灵活性,往往被人看做是一个优势,但平心而论,如果不是一个很简单的脚本,这种做法会给代码可读性和维护带来很大麻烦,我是不推荐使用的,一个类的定义和实现的地方应该完整展示其数据元素和方法逻辑,即便有些需要后续确定的属性,至少也在类定义中设置为None,占个位置吧

这两个例子都说明了语言在实际使用中对于特性使用的非随机行为,但它们还是有些不同的,对于整数和浮点类型,这两个特性并不冲突,如果一个程序中没有用到浮点数,那么编译器根本不会考虑浮点数相关的处理逻辑;而对于第二个例子,即便我的代码中没用到这个动态性,但语言实现还是按照动态性来实现的,因为编译器无法得知在将来的代码修改中会不会用到动态性,具体到python语言,编译器很难判断哪些类的属性不涉及动态性,这需要全文数据流分析,在动态性比较高的语言中(甚至只是相对高一点)是很困难的,编译器为了降低算法难度,统一按照最普适的方式来实现了,结果就有了短板效应,一个可以高效运转的代码不得不以低效方案运行

对于这种情况,如果我们不想提高编译器本身的复杂性,还想得到一个执行速度快的目标代码,那么就可以从语言设计上下手,最简单的,上面这个例子中不允许这种动态性即可,即类属性必须至少在类的定义过程中(可以不选择C++和java那种显式定义格式)全部体现,以告知编译器这个类具体的数据结构,如此一来不但提高了运行速度,还增加了可读性和可维护性,只是没那么“自由”罢了(自由是有代价的,何况这种自由意义真心觉得不大)

就程序运行速度来说,动态性是一个主要的杀手,反过来说,代码的效率提升可以通过静态化来实现,这里的动态和静态是指行为上的(类型使用也是行为,因此也涵盖动静态类型)。从总体上说,代码速度优化就是将一段代码转换成另一段完成同样功能且更快的代码,这里最重要的是功能相同,编译器多少得“理解”程序员这段代码的意图,不过这里的理解还上升不到人工智能那个程度,其实只要一些特定分析即可,比如python代码(用3.x,print是函数):

for i in range(n):   
    print(i)

这段代码会被python翻译为类似如下步骤:

load range   
load n   
call func 1   
get iter   
loop:   
if iter.end() goto out   
load iter.next()   
store i   
load print   
load i   
call func 1   
goto loop   
out:

实际上在99.999999%的情况下,程序员写这段代码的意图只是从0到n-1来打印而已,之所以会这么麻烦是因为动态性使得python无法做这种预测,比如在这段代码之前我们写:

xrange = f   
print = g

这样一来上述代码就不一定是从0到n-1来打印了,python这种实现可以使程序员在不修改业务代码的情况下修改行为,比如说我们需要给一个class A扩展功能,但是又无法修改A的代码(或修改起来很麻烦),就可以:

class B(A):   
    ... #扩展的代码   
A = B

然后不用修改任何代码即可达到升级的目的了,但付出的代价就是整体运行效率低下

如果改动一下这个特性,规定所有函数、类都不能修改(不是对象或是只读对象),则在range和print固定的情况下,编译器可以对上述代码做行为分析,转成:

for (i = 0; i < n; ++ i)   
{   
    print(i);   
}

这样一来,就省略了range和反复iter.next()获取元素。还可以再进一步,由于python规定range的范围不能超出C语言的long的范围,因此:

long tmp = n.as_long(); //如果n不是整数类型或无法转为long,则这里会异常   
long i;   
for (i = 0; i < tmp; ++ i)   
{   
    print_long(i);   
}

这几乎就是C或java代码了,可以运行得很快

其实前面也讲过,python对于函数局部变量是静态化处理的,而如果函数里面有了exec,则存在动态给局部变量赋值的可能,这时候python就会将这个函数编译成动态行为的,这样就兼顾了两种情况,如果要做一个特性多,运行还要比较快的语言,是需要像这样考虑不同情况下使用不同策略来处理。python的这个处理是静态的,判断方式也很简单,但实际中碰到的情况和处理方式会比较复杂