容器中的int

来源:互联网 发布:c语言延时1秒函数delay 编辑:程序博客网 时间:2024/04/28 02:33
这篇讨论下larva在容器含int的优化处理方式,仅对内置类型list和dict做了相关优化,具体以list为例


如上篇末尾所说,由于相当多的程序并非简单的整数计算,而是需要数据结构,因此larva的规定“下标运算返回object类型”会在这些程序中使得类型推导派不上用场,例如,我们做一个矩阵运算,即便矩阵中都是整数,也不得不采用object计算的方式,严重影响效率,而如果采用拆箱装箱的办法,也只是在外部计算量远大于存储操作量的时候有很大作用,否则效率还是很慢,java的泛型就是一个典型的例子,具体原因以前的blog已经分析过了


在优化这一点的时候,我主要是从C++和java的实现异同做切入点来分析问题,首先注意到的一点是存储上的差别,以元素为整数的vector为例,C++是vector<int>,java是Vector<Integer>,即C++存储的是整数,而java存储的是对象,且Integer是一个不可变对象,这导致了两个问题:
1 虽然Integer的运算可直接优化成int类型的计算,但程序运行过程中可能会反复new大量对象
2 即便不会新增很多对象,若容器中维持的小对象太多,会给GC带来额外负担(标记的时候,以及分代GC)
不过在java中,这两个问题都可以很好地缓解,做法很简单,搞一个VectorOfInt对象即可,算法同泛型容器Vector,但接口和元素都是int类型,实际上就是手工实现了C++模板的代码生成步骤,把int数组包装成可变的,这样一来的确能提高效率,就是很多相关的东西要自己实现一套了
于是很自然地会想到,类似的办法是否能在larva中实现,比如我们搞一个ListOfInt的内置类,如果一个列表只存int元素,那么就不用“[]”建立,改用“ListOfInt()”,比如代码:
l = ListOfInt()l.add(123)l.add(456)l[0] += l[1]
然而这里存在一个问题,就是从l中取元素的时候,编译期是无法得知l就是ListOfInt类型的,因为larva是动态类型语言,l的类型只能运行时知道,所以对于l的存取元素操作,编译器只能按object处理,这个容器可以解决上述问题2,即内部只需要维护一个int数组,而非一个object的数组,在存元素的时候,对传入的object判断,确保是LarObjInt,然后直接存其value即可。如果传入的就是一个int,则直接存
但是,它解决了问题2,却加剧了问题1,在取元素的时候,由于需要返回object,因此需要new一个LarObjInt,对象的创建会更加频繁,甚至频繁很多,因为很多时候对list的元素的读取比计算和写都多很多


假如一个容器有一个读取接口,编译器知道读出的结果就是int,那么就可以优化了,其实类似的优化前面提过,就是针对len和hash的优化,譬如给ListOfInt一个方法get_item_as_int,上面例子最后一句写成:
l[0] += l.get_item_as_int(1)
编译器看到get_item_as_int这个方法,知道它返回就是一个int即可,这就解决了问题


但是,这个解决方法在设计上是不合理的,同样是因为编译器不知道调用这个方法的对象是什么类型,为了防止其他类自己定义一个同名的方法,就需要强制规定名为get_item_as_int的方法必须返回int,这就太霸王条款了,不够自然。但为何len和hash能这样优化呢,原因在于它们调用了“内置”方法__len__和__hash__,这两个被强制规定返回int了,内置方法的名字是不允许被用户随意使用的(所以内置方法以两头的双下划线标示,一般也不会有人使用这么奇怪的名字)
所以,这个手段是行得通的,关键就在于语法不能和习惯冲突,语法就是霸王条款,但要尽可能不要和常见的编程习惯冲突,比如上面的方法可改成内置函数:
l[0] += get_item_as_int(l, 1)
这样一来就跟len一样,编译器可以规定这个内置函数就返回int,用户也不会有什么意见,因为larva的函数等静态名字是可以在模块内覆盖的,有一定的灵活性


虽然这样能解决问题,但不够完美,为了这么一个优化引入内置函数,看着比较别扭,考虑上篇的一个例子:
a = int(b+c)
由于存在int类型转换,在推导的时候,无论b+c是什么,a在这个表达式中都被赋值了一个int,用在上面的例子:
l[0] += int(l[1])
如果只是按照代码字面意思编译和执行,l[1]返回一个object,然后被转为int,这就只是换汤不换药。关键在于,编译器在这里知道右边的计算是取出一个元素后立即转为int,则可以将两步合成一步,即本来的计算过程:
convert_to_int(l.op_get_item(1))
被合并为:
l.op_get_item_int(1)
op_get_item_int这个操作方法返回int类型,这样一来,就省掉了临时对象的创建,对ListOfInt的存取就像对一个java的整数数组存取一样了(稍慢一点,因为有方法调用和larva自己的越界检查等)


然而这个方案还是不完美,因为int(x)这种转换可以将其他类型转成int,而上面代码所需要的,是“从l中拿一个整数”,比如,若l是一个普通的list,l[1]是一个字符串"123",则上述代码会正常运行,l[0]会自增123,但用户这时候需要的是一个报错“l[1]不是整数”,也就是说,用户需要一个断言转换,但上面的写法是显示转换
断言转换是golang的一个概念,我就直接借鉴了其语法,不过去掉了断言类型的括号,上面的例子写成:
l[0] += l[1].int
编译器碰到[].int,就知道要调用op_get_item_int了,同时,“.int”对其他表达式也可以实现int的断言转换,也就是说不但解决了容器中取int的问题,还扩展了一个语法
熟悉golang的朋友知道,go中的断言转换类型必须加括号,是为了防止和属性语法混淆,不过在larva中,由于只有对int的断言转换,且很少有人用int做标识符,于是我规定int为一个关键字,这样语法也简洁些(下面会讲到,这个规定还有一个好处)


list元素的问题可以用ListOfInt来解决(相当于泛型list<int>),但是dict可能就麻烦点了,可能要定义DictOfIntInt,DictOfIntObj,DictOfObjInt三种,不但麻烦而且语法上更复杂,其实光一个ListOfInt都不够简洁,larva其实并没有引入这几个类型,在语法不变的前提下也能实现上述方案,它基于这样一个事实:即便是动态类型语言,一个正常的程序代码是不会有类型不一致的bug的,比如有人定义了一个ListOfInt对象,虽然在存元素的时候类会自己检查传入的是不是int,但一个正常的程序不应该在这里异常;另一方面,虽然动态类型语言允许list中存的元素类型可以互不相同,但绝大多数程序的list中存的都是同一类型的元素,也就是说,要么都是int,要么都不是
于是我们就可以修改list的结构,让它同时支持两种情况,具体做法就是,若一个list从建立开始存入的元素都是int,则内部保持一个int数组的结构,而只有当它被设置其他object的时候,改变其内部数据结构为对象数组,当然此时里面已有的int元素都转成LarObjInt,根据上述理由,这个转换极少发生


对于dict,也是一样的道理,larva的dict中的key和value并不像一般的实现那样是绑在一个entry(或node)对象上(大量entry对象也会导致上述的GC问题),而是用两个等大且元素对应的数组来实现,这样key和value在运行时根据其是否int可各自调整。这样一来,对于key和value都是int的情况,larva的dict比java的HashMap速度就会快很多


对容器中的int的优化,目前的larva实现就是这样了,虽然效率上去了,但它在形式上依然存在一些让人不爽的地方。例如,假如我们在一段代码中存在对一个int list的大量取值操作,则每个取值的地方都得加上.int后缀,这个很繁琐;另一方面,这个语法只有在通过下标取元素的时候使用,而在其他情况下就没用了,比如:
for i in l:    //work with i
这个代码的i就被认为是object,当然如果增加新的语法,比如for i in l.(int[]),表示l断言转换为int列表,这样编译器就知道i是一个int,但太复杂的语法也不是好事。如果用临时变量转:
for tmp in l:    i = tmp.int    //work with i
这又回到上面说过的问题,迭代的时候会产生大量tmp对象,所以目前的办法也只能是:
for idx in range(len(l)):    i = l[idx].int    //work with i
显然也很繁琐


这个问题跟代码运行效率关系不大,而是需要一个相对简洁的语法来实现同样的功能,目前的一个想法是,由于int是一个关键字,所以可以像静态类型语言一样支持一些声明,比如在函数开头用int i声明i是一个int,则所有对i的赋值的右值,编译器都自动加一个int断言转换,若i是for语句迭代的左值,则for调用被迭代对象的int元素迭代方法;再比如声明int l[]表示范围内所有对l的下标取值都自动加上int断言转换,而所有对l的下标赋值的右值也都自动加int断言转换等等。也就是说给动态类型语言增加一些静态类型的声明语法元素,不过现在的版本没有做这个
0 0
原创粉丝点击