SpreadJS V16 版本对CalcEngine公式计算引擎进行了全面优化。
近两年 SpreadJS 用户对前端公式计算的性能需求越来越高。在 V13 (2019年发布) 中,来自客户的最大文件包含 173,000 个公式。现在,我们经常收到包含超过 500,000 个公式的文件。而对于算法的复杂度,SpreadJS 整体是O(nlogn),大于O(n),小于O(n^2),也就是说20,000个公式相比10,000个公式,计算量增加超过一倍。因此,随着公式变得越来越多,客观上计算时间一定会增加。为了应对这一趋势,SpreadJS V16 版本对 CalcEngine 进行了全面性能优化。
注:新版本对计算引擎的优化不会带来用户交互体验及公共 API 的改变。
我们先了解一下公式计算会在哪些环节消耗时间:
公式计算性能包含三部分:解析时间、评估时间、调整时间。
1. 解析时间:SpreadJS 将“编译”公式字符串并“链接”引用。这发生在加载文件、设置公式时。我们通常使用 fromJSON 和 doNotRecalculateAfterLoad 来查看解析时间。fromJSON 包含其他操作,但这是最简单的方法。
2. 评估时间:SpreadJS 评估单元格节点并得到公式结果。我们通常使用console.time(); spread.getActiveSheet().recalcAll(); 控制台.timeEnd(); 查看评估时间。
3. 调整时间:插入/删除行/列或拖动移动单元格时,SpreadJS需要调整公式中的引用。我们可以使用 spread.commandManager().execute 来模拟操作并查看时间。
下面介绍 V16 的主要改进内容:
共享公式
什么是共享公式(Shared Formula)?
举个简单的例子:当在 Excel 中设置好一个单元格的公式后,我们通过拖拽填充的方式设置了一排公式,这些公式的差别仅仅是引用区域的不同。此时 Excel 中的记录方式并非简单复制多个相同的公式,而是把这个公式共享出来,通过引用“变量”的改变来优化性能和存储。这种使用情形在 Excel 中是普遍存在的。
想了解更多细节,可以参考这篇文章:Excel Shared Formula处理
在 SpreadJS V15 之前,如果 xlsx 文件包含共享公式,ExcelIO 会将其解析为单独的公式。然后,展开编译每个公式。
在 SpreadJS V16 中,ExcelIO 将按原样读取共享公式,并且 SpreadJS 只会编译一次,通过这个改进,减少了“解析时间”时长。
如果导出为 SpreadJS V16 新推出的 SJS 格式时,共享公式会按照实际的共享方式和结构引用来存储。
将额外的计算数据保存为新的文件格式
在此功能中,如果用户在保存到 sjs 文件时将 includeCalcModelCache 设置为 true,SpreadJS 会记下额外的数据,以便稍后打开文件时跳过大部分解析时间。
SUMIF / *IFS 重写
当 SpreadJS 发现用户文件中有大量的 SUMIF 或 SUMIFS 时,会自动为 SUMIF/SUMIFS/MAXIFS/COUNTIFS/AVERAGEIFS 函数添加 CriteriaRange 值缓存。
例如:A1:=SUMIFS(B:B, C:C, C1) A2:=SUMIFS(B:B, C:C, C2),SJS 现在将缓存 C:C 中的值,这个改进可以提高条件检查的性能。
如上所述,对于包含很多 SUMIF / SUMIFS 的文件,计算会比以前快得多。使用附件文件SUMIFS test.xlsx(总共 20,000 个带有 CriteriaRange 的 SUMIFS 包含 5000 个单元格)并将 dynamicReferences 设置为 false,评估时间将为 2.5 秒,在此重写之前为 51.7 秒。(最佳改善率为95%)
重写 MATCH / VLOOKUP / HLOOKUP 的线性搜索
我们重写了 MATCH / VLOOKUP / HLOOKUP 函数的线性搜索。
改写后,对于充满 MATCH 函数的文件,最好的改进率为 70%。
使用 Map 替换匿名对象 ({}) 以获得更快的 for-each
在 JavaScript 中,我们可以使用匿名对象或者 map 来存储(key, value)对,从下面的代码可以看出 Map 在遍历时有很大的优势。
- <div>var obj = {};
- for (var i = 0; i<1000000; i++) {
- obj[i] = i;
- }
- console.time();
- var result = 0;
- for (var t in obj) {
- result += obj[t];
- }
- console.timeEnd(); // 300ms
- var map = new Map();
- for (var i = 0; i<1000000; i++) {
- map.set(""+i, i);
- }
- console.time();
- var result = 0;
- map.forEach((value)=>result+=value);
- console.timeEnd(); // 90ms
- </div>
复制代码 我们可以使用以下代码技巧来避免 forEach:
- <div>// reset the marker before calculate
- CE.markerForCalculateLife = {marked: true};
- // ...
- // check and set the marker for only once operate
- if (!node.marker || !node.marker.marked) {
- node.marker = CE.markerForCalculateLife;
- // do the work that calculate once in one calculate cycle.
- }
- // ...
- // change the marked to false after calculate
- CE.markerForCalculateLife.marked = false;</div>
复制代码 使用此代码技巧,评估性能提升率约为 8%,在某些特殊情况下可达 25%。
|
|