【性能优化】4.9 [遍历技术] 列式计算

 

【性能优化】4.8 [遍历技术] 冗余分组键

4.9 列式计算

结构化数据通常以记录为基本单位,数据表则是记录的集合,针对数据表的计算通常也是以记录为基础的,即每次针对一条记录实施相应的计算,然后再针对下一条记录计算。比如前面提到的过滤或分组运算,需要针对每条记录计算过滤条件以及分组键等,然后就立即根据计算结果执行相应的过滤或分组动作。

因为记录又称为数据表的行,这种计算方式就被称为行式计算

还有一种以字段为基础的计算方式,一次性将所有记录的相关计算完成再做下一步处理。比如计算出所有记录对应的过滤条件或分组键,再来执行相应的过滤或分组动作。

因为字段又称为数据表的列,这种计算方式又称为列式计算

行式计算和列式计算在计算次数上没有实质区别,也就是理论上的算法复杂度是一样的,似乎没有必要再搞出一种方式。但在工程实现上,列式计算会有很大的优势。

行式计算也要求数据以行式存储,也就是一条记录的所有字段存储成一个记录(对象)。结构化数据计算非常灵活,随时可能计算出不同数据结构的记录 (选出、分组、连接等运算都是这样),如果每次都设计一个新的数据结构来保存,那代码就会非常繁琐了。所以,程序员会设计一种通行的数据对象来保存各种不同结构的记录。而记录的字段通常有不同类型,也要用同一种泛型对象来保存。

这在存储上就会麻烦很多,一种能存储各种数据类型的泛型对象要比简单数据复杂得多,它不仅要保存数据本身,还要多保存一个数据类型。不同数据类型占用的空间也不一样,通常还要再使用一个动态对象来保存,不仅多了一层导致取用数据时需要更多动作,占用空间也会大很多,更容易导致内存紧张时操作系统或虚拟机的倒换动作,严重拖累性能。

而且,在 CPU 层面,不同类型数据的运算要用不同的指令执行,比如整数和浮点数的加法就是不一样的。那么,实际执行计算时,就要把根据涉及数据的类型翻译成不同的 CPU 指令,这又会多一步判断。

结构化数据表中同一字段的数据类型通常是相同的,我们把满足这个条件的数据表称为纯表。对于纯数据表,可以采用列式计算来避免行式计算中多余的动作。

列式计算会采用内存中的列式存储。数据表中所有记录的同一列存储在一起,这和外存的列式存储类似。因为同一字段的类型相同,可以简单地使用一个数组对象来保存,整个数据表用多个数组就可以存储。如果数据表 n 行 m 列,列式存储只需要 m 个数组对象就可以了,而行式存储则至少需要 n*m 的泛型对象(存储字段值)+n 个数组对象(存储记录)。这在内存占用上有巨大的优势。

而且,数组成员的取用也远比泛型对象要简单得多。计算过程中,同列的数据类型相同,按列计算时,只要开始解析一次表达式根据列的数据类型生成不同的计算步骤,不再需要针对每行再判断数据类型,CPU 的动作也会少很多。
实际测试表明,对于计算密集型任务,列式计算可能会比行式计算快出数倍到数十倍。理论上的复杂度虽然一致,但工程上的优化也非常有意义。

SPL 也提供了列式计算方案。


A

1

=file("orders.btx").import@b()

2

=A1.i()

3

=A2.groups(area;sum(amount):amount)

常规序表用 i() 函数转换成纯序表,后续就会自动使用列式计算方案了。

基于列式存储的组表也可以直接产生纯游标再实施高速的列式计算:


A

1

=file("orders.ctx").open()

2

=A1.cursor@v(area,amount)

3

=A2.groups(area;sum(amount):amount)

使用 @v 选项即会产生纯游标,之后也会自动使用列式计算。
文本和集文件本身是行式存储的,不支持直接产生纯游标。

需要强调的是,列式计算只能针对纯数据表(游标)进行,而且会有些限制(计算过程中会产生不纯的或无法判断是否纯的中间结果而不能持续采用列式计算),具体信息可以参考 SPL 论坛上的文章。本书重点是解释算法复杂度,举例大都还是使用更通用的行式计算方案,读者可以根据应用环境自行改写成列式计算 。

【性能优化】5.1 [有序遍历] 有序分组汇总
【性能优化】 前言及目录