查看原文
其他

内存数据集产生的隐性成本

蒋步星 数据蒋堂 2023-02-25

当我们要对数据做一些非常规的复杂运算时,通常要将数据装入内存。现在也有不少程序设计语言提供了内存数据集对象及基本的运算方法,可以较方便地实现这类运算。不过,如果对内存数据集的工作原理了解不够,就可能写出低效的代码。

我们看数据集的产生。比如要生成一个100行2列的数据集,第一列x为序号,第二列xx是第一列的平方。

第一种方法,先生成一个空数据集,再一行一行地追加数据进去。


A

B

1

=create(x,xx)


2

for 100

>A1.insert(0,A2,A2*A2)

第二种方法,直接产生相应行数的数据集。


A

B

1

=100.new(~:x,~*~:xx)


这两种方法产生的结果集相同,实质的循环次数和每次循环的计算内容看起来也相同,但执行效率却不一样,后者要比前者更快。

数据集在内存中会保存成一个连续的数组。动态追加的数据集,会导致这个数组长度不断地变长,原先为这个数组分配的空间也要扩大。而内存分配不是一件很简单的事情,已经分配好的空间的后面内存很可能已经被占用而不能再继续扩大,只能重新分配一块更大的空间,然后将原空间内的数据复制过来。寻找空间和复制数据都要占用CPU时间,常常比运算本身的消耗都大。一般情况下,分配内存时会多预留一些空间来应付可能的增长,这样不致于每次追加都导致重新内存分配,但也不可能预留太多而浪费内存,如果追加次数过多,仍然还会有不少时间消耗到内存分配上。而如果事先知道数据集行数一次性创建出来后,则只需要在开始做一次内存分配即可。

动态追加的数据集虽然使用起来更灵活,但性能却赶不上静态数据集,在关注性能时要习惯性地避免。


其实,用这个例子来对比并不很恰当,因为有100.new(~:x,~*~:xx)这样的简单写法时,很少人会采用动态追加的数据集了,这里用这个例子主要是为了突出关键差异。而实际应用中会有不少情况下很难用一句话写出数据集生成,程序员就可能习惯性地采用动态追加的数据集了。

举一个更恰当的例子,我们想生成一个20行2列的Fibonacci数据集,第一列key为行号,即1,2,3,…;第二列value为值。Fibonacci数列的规则是:第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(0:key,0:value)


2

>a=0

>b=1

3

for A1

>A3.key=#A3,A3.value=b

4


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

先生成一个都填满0的数据集(随便别的什么数也可以),然后再用循环把正确的值填进去。实质计算量仍然一样,但避免了动态分配。


除了行方向动态追加外,还有列方向的问题,即在数据集上增加新的列。

列追加比行追加要更为复杂。内存数据集中的一行是一条记录,物理上也是一个数组。因为数据结构很少改变,大多数内存数据集不会在生成这个数组时预留空间,否则内存浪费就太多了(每一行都有)。这时候每次追加列时都会发生前面说的重新分配空间,而且要针对每一行记录进行,再将原记录数据抄过来,这个动作的时间成本经常远远超过追加的那个列本身的计算。

内存数据集一般都有提供追加列的功能,这会带来方便性,但在关注性能时却要慎用。能不用则不用,一定需要时,也是如上所述,最好是一次性把需要追加的列都加上,而不要一遍遍地追加。

比如我们想在第一个例子中的数据集上再追加两个列y=x+xx和yy=y*y。看起来较自然的两步写法:


A

B

3

=A1.derive(x+xx:y)

=A3.derive(y*y:yy)

一次性产生列的写法:


A

B

3

=A1.derive(x+xx:y,0:yy)

>A3.run(yy=y*y)

相比之下,后者的性能要更好。


类似技巧还可以用在从数据库中取出的数据集上。

比如要从数据表T中取出字段month和amount并按month排序,然后追加一列计算amount的累计值。先读出再追加列的写法:


A

B

1

=db.query(“select month,amount from T order by month)


2

=A1.derive(0:acc)

>a=0

3

for A1

>A3.acc=(a=a+A3.amount)

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


A

B

1

=db.query(“select month,amount,0 as acc from T order by month)

>a=0

2

for A1

>A2.acc=(a=a+A2.amount)

对于关注性能的程序员,后一种写法会成为习惯。


数据蒋堂 第二年原创文章

应对报表没完没了的五个步骤

“后半”有序的分组

前半有序的大数据排序

- 如何将数据热导出到文件

时序数据从分表到分库

- BI系统的前置计算

性能优化是个手艺活

数据分布背后的逻辑

- 从一道招聘考题谈起

为什么我们需要C程序员

报表工具的SQL植入风险

内置的数据无法实现高性能

怎样生成有关联的测试数据

遍历复用

- 一些数据压缩手段

用HBase做高性能键值查询?

BI系统中容易被忽视的数据源功能

这个产品能支持多大数据量?

最简单的大数据性能估算方法

大清单报表应当怎么做?

大清单报表的打印?

大数据技术的4个E

做基础软件很悲壮?

做基础软件要投入很多钱?

- 国产操作系统还能怎么做?

- 国产数据库通通都没戏!

- 人工智能中的“人工”

- 存储和计算技术的选择

- 区块链技术的一些疑问

- 数据蒋堂新一年




润乾软件创始人、首席科学家

中国大数据产业生态联盟 专家委员

1989年国际奥林匹克数学竞赛团体冠军成员,个人金牌

清华大学计算机硕士

发明了非线性报表模型,并著《非线性报表模型原理》

创建离散数据集模型,颠覆四十年关系代数理论体系!

2016、2017年中国软件和信息服务业 • 十大领军人物

2017年度中国数据大工匠

数据领域专业技术讲堂《数据蒋堂》创办者

点“阅读原文”,在乾学院与作者交流

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存