SPL 代码优化技巧

         SPL是一种面向结构化数据计算的程序设计语言,集算器是SPL语言的java实现,采用网格式编程形式提供了编码和调试的IDE环境,语法比JavaSQL更为简单易懂,开发效率更高。本文将从集算器的实现原理出发列举一些可以提升计算性能的小技巧。

1数据类型

1.1 数值

        SPL里的数值类型有IntegerLongDoubleBigDecimal。其中BigDecimal虽然能够表示任意精度的数据,但计算速度比其它数类型慢很多,占用的内存也大很多,因此在其它数字类型能够满足精度要求时,使用其它数类型代替BigDecimal能够显著提升计算效率。

        实际案例中,在使用JDBC读取数据库数据时,有些数据库的JDBC对于低精度数值也返回BigDecimal,这样,在做性能优化时就可以检查一下是否可以转为其它类型,从而提升性能。

1.2 字符串

         Java的字符串对象String占用空间较大,一个长度为0的字符串占用40多个字节,而IntegerLong只占用16个字节。同时字符串的比较运算、哈希运算也比IntegerLong慢。

         另外,数据从硬盘读入生成java对象,其占用的内存大小往往是其占用的硬盘大小的数倍甚至十倍以上(如果硬盘存储使用了压缩技术差距会更大)。这种情况可能直接导致不太大的数据文件在读成java对象时发生内存溢出,这时如果不能减少内存占用量就只能使用外存计算了。而通常外存计算的复杂度远大于内存计算,同时也导致性能会下降很多。

        那么,有没有什么方法能够减少内存占用同时又能提高计算效率呢?

        一个常用的方法就是枚举串序号化,比如下面一个事实表的数据:

地区

性别

姓名

北京

赵大

北京

钱二

上海

李四

北京

孙三

上海

周五

        性别、地区这类枚举型的字段,可以建立一个对应表把性别、地区值转换为序号12,这样事实表中性别字段就可以只保存对应的序号,地区也是一样。转换后数据如下:

1

北京

2

上海

 

1

2

 

地区

性别

姓名

1

1

赵大

1

1

钱二

2

1

李四

1

2

孙三

2

2

周五

        这样一来,我们就可以做到减少内存占用,并且提高计算效率,因为数字的比较、分组等操作比字符串的要快很多。在输出结果时,可以根据需要再把序号转化为串,也就是使用序号直接按位置到代码表中找到相应的记录进行替换。

2序表结构

2.1 行追加

         序表类似数据库中的表,但是却是有顺序的。序表数据在内存中用一个连续的数组保存。一般情况下,为序表分配内存时会多预留一些空间来应付可能的增长,以免每次追加数据时都重新分配内存,不过也不可能预留太多空间而浪费内存。

        基于这个原来,为序表频繁地追加记录,会导致这个数组长度不断地变长,原先为这个数组分配的空间也要扩大。而扩大内存分配不是一件很简单的事情,需要分配一块更大的空间,然后将原空间内的数据复制过来。寻找空间和复制数据都要占用 CPU 时间,而且常常比运算本身的消耗都大。

        因此,如果事先知道行数,一次性把序表创建出来,那只需要在一开始分配一次内存就行了。即便序表中的字段值需要一些步骤才能计算出来,那也应该先new出序表后再去修改记录的字段值,而不要计算一行插入一行。而对于修改记录字段值的方法SPL提供了很多途径。

        假设我们想生成一个20 2列的斐波那契数列序表,第一列key为行号,即 1,2,3,…;第二列 value 为值。斐波那契数列数列的规则是:第 1、第 2 行取值为 1,从第 3 行起,取值为前两行之和。这个运算需要一步步实现,动态追加数据就是很自然的想法了:


A

B

1

=create(key,value)


2

>a=0

>b=1

3

for 20

>A1.insert(0,A3,b)

4


>b=a+b,a=b-a

不过,序表一次性产生性能更好,即使计算本身仍然需要一步步实现:


A

B

1

=20.new(~:key,:value)


2

>a=0

>b=1

3

for A1

>A3.value=b

4


>b=a+b,a=b-a

 

2.2 列追加

        扩充序表,除了一行行追加数据,还有可能会改变数据的结构,增加每行数据中的字段,也就是所谓的列追加。列追加比行追加要更为复杂,序表本身是一个大数组,其中的每一行是一条记录,物理实现上也是一个数组。因为数据结构很少改变,创建序表时不会在生成每行的数组时预留空间,否则内存浪费就太多了(因为每一行都要预留)。基于这种实现原理,如果出现列追加,就会发生前面说的重新分配空间的情况,而且要针对每一行记录进行,再将原记录数据抄过来,可以想见,这个动作的时间成本有多大,甚至经常会远远超过追加那个列后要做的计算。

        SPL为序表提供了追加列的功能,这会带来方便性,但在关注性能时却要慎用。不得不用时,也应该如上所述,一次性把需要追加的列都加上,不要一遍遍地追加。对于当时无法计算出字段值的列可以先填成空值,以后再用其它函数去修改字段值。

        最常见的情况,从数据库取出的序表后,如果事先知道要再derive出新的一列xxx,那么可以在写SQL时多写一个null as xxx,这样在query时就直接把所需的字段都产生了,不用再做一次derive了。

        例如,要从数据表sales中取出字段ORDERDATE,AMOUNT并按ORDERDATE排序,然后追加一列计算AMOUNT的累计值。一般先读出再追加列的自然写法:


A

1

=demo.query("select ORDERDATE,AMOUNT from sales order by ORDERDATE")

2

=A1.derive(CUMULATE[-1]+AMOUNT:CUMULATE)

而用 SQL 语句先把列生成好的写法:


A

1

=demo.query("select ORDERDATE,AMOUNT, null as CUMULATE from sales order by ORDERDATE")

2

>A1.run(CUMULATE=CUMULATE[-1]+AMOUNT)

        

2.3 引用记录

        针对前面两种调整序表结构的优化思路,出发点都是减少newderive函数中抄字段值的动作。除此之外,SPL还支持对象引用,字段取值可以是另一条记录。这样,在SPL中,大多数情况没必要像SQL那样在新结果集中把字段抄一遍,为了保持原有整条记录一起参与运算,只要用引用方式来写就可以了。这样不仅性能更好而且空间占用也少。

        上面用derive追加AMOUNT累计值的要求可以用new函数实现,new创建一个新序表,SRC字段引用原纪录,CUMULATE字段存储累计值,写法如下:


A

1

=demo.query("select ORDERDATE,AMOUNT from sales order by ORDERDATE")

2

=A1.new(~:SRC,CUMULATE[-1]+AMOUNT:CUMULATE)

 

3循环函数

3.1 用循环函数代替循环语句

        SPL的网格程序提供了循环语句for和分支语句if来实现复杂的运算逻辑。运行时,由于网格的执行次序是动态解释的,因此大量使用循环,会导致执行的网格过多,在网格的动态解释上就要花费大量的时间。

        除了循环语句,SPL还提供了循环函数,可以对付大多数需要使用for语句的场景。对于计算步骤不太复杂,对性能要求高的运算应该尽量使用循环函数来完成。类似地,能用if 函数的场景也尽量不要用if语句。

        1.2节中列举的计算斐波那契数列的例子可以改写为如下:


A

1

=20.new(~:key,:value)

2

=A1.run(value=if(#>2,value[-2]+value[-1],1))

        其中#表示当前循环到哪条记录,第一条记录对应的#1,依次递增。value[-1]表示上一条记录的value值,value[-2]表示上前数第二条记录的value值。

 

        eval函数每次执行都要把参数指定的表达式字符串解析成表达式,然后再执行,如果eval函数在循环里执行,过多地把表达式字符串解析成表达式会花费大量的时间,如果表达式字符串不是变的则可以使用宏替换代替eval

3.2 常量放在循环外

        把循环里常量的产生放到循环外,也可以对性能优化提供帮助。例如选出北京, 上海, 深圳地区的销售记录,比较“自然”的写法是:


A

1

=demo.query("select * from 供应商")

2

=A2.select(["北京","上海","深圳"].contain(城市))

        因为SPL的序列是可以被修改的,所以表达式["北京","上海","深圳"]每计算一次都会产生一个新序列。如果像上面这样把["北京","上海","深圳"]放在循环函数select里,那么在执行时将会产生A2长度个序列。如果循环次数多,这些不必要的运算将消耗大量时间。因此,注重性能的写法应该如下:


A

1

=["北京","上海","深圳"]

2

=demo.query("select * from 供应商")

3

=A2.select(A1.contain(城市))

 

3.3 警惕循环套循环

        警惕循环函数中再有循环函数,这些代码看起来很简单,但几层循环下来,实际计算量会以几何级数放大。这虽然是个常识,但有时也会被忽略,因此能在循环外做的事不要放到循环内。特别地,尤其要警惕在循环内读文件和访问数据库这种超级耗时的动作。

4代码习惯

4.1 释放内存

         Java在内存不足时性能会急剧下降。所以要及时释放内存,SPL没有删除变量释放内存的语句,只需把变量或单元格的值设为空即可,也可以用clear语句清除一片格子。例子如下:


A

1

=file("PART").cursor@b(P_PARTKEY,P_BRAND,P_CONTAINER)

2

>A1.select(P_BRAND == "Brand#33" && P_CONTAINER == "LG DRUM")

3

=A1.fetch()

4

=file(path+"LINEITEM").cursor@b(L_PARTKEY,L_QUANTITY)

5

>A4.join@i(L_PARTKEY,A3:P_PARTKEY)

6

=A4.groups@u(L_PARTKEY;avg(L_QUANTITY):quantity)

7

>A3=null

8

……

         =开头单元格是计算格,表达式的返回值会保存在单元格上,以>开头的单元格是执行格,表达式的返回值不会保存。cs.selectcs.join是给游标附加运算,不会产生新的游标所以返回值可以不用保存,A7格为释放读出的PART数据,也可以用clear语句把A1A5之间的单元格值都清空,只需要把A7格代码替换如下:


A

7

clear A1:A5

 

4.2 代码紧凑

         for if的代码块,可以直接写到同一行上,没有必要像Java那样换一行再写。SPL的网格已经能够清晰地拆分出这些语句了。解释器扫描空白格也需要时间,因此对于含有循环语句的程序,如果循环次数特别多,应该让代码紧凑一些,删除空白的行和列以减少格子数量,从而提高解释器的效率。

         下面以获取每天第一条销售记录为例,介绍一下SPL的代码块规则,sales是销售记录游标参数,按ORDERDATE有序。


A

B

C

D

E

F

1

=[]






2

for

if A1.len()>1

return A1(1)

>A1=A1.to(2;)



3


else

=sales.fetch(1000)

if C3==null || C3.len()==0

if A1.len()==1

return A1(1)

4





break


5



>A1 = (A1|C3).group@1(ORDERDATE)




        单元格的代码块为单元格所在行及其正下和左下单元格都为空白格的行,上面例子中A2for的代码块为[B2:F5]B2if的代码块为[C2 : F2]if代码块的下一行和if所在格同列的单元格B3else,并且B3左面的格子都是空白格,则B3格为B2格的else分支,B3格的代码块为[C3 : F5]else也可以和对应的if同行,写在if右面的单元格上。