Lynn.Dou 发表于 2022-3-18 15:22:33

GcExcel计算引擎的工作原理及常见用户问题

本帖最后由 Lynn.Dou 于 2024-4-26 10:24 编辑

      用户在使用GcExcel的时候,常常有一些用法的问题,尤其在面对性能问题的时候,需要对计算引擎的工作原理有一些了解。为此,写这篇文章给用户介绍一下GcExcel的计算引擎,并回答一些常见的问题。

一.单元格的存储结构

      要理解计算引擎,首先需要理解单元格的存储结构,计算引擎的工作方式是基于单元格的存储结构基础上设计的。每个单元格内部有三个字段:Value、Status和Formula。
      Value用来存储值,Status用来存储状态,Formula用来存储公式。Status有两种值:Valid和Dirty, Valid表示值是有效的,Dirty表示值是脏的,无效。

      假设有这样一个表格:
      A1中存放一个数字10,B1中存放一个公式:=A1+2,C1中存放一个公式:=B1*2,对于这样一个表格,它们存储结构初始状态是这样的:
A1Value
10B1Value
0C1Value
0
Status
ValidStatus
DirtyStatus
Dirty
Fomula
Fomula
=A1+2Fomula
=B1*2

      B1和C1的初始值是0,Status是Dirty。

二.公式的计算过程

什么情况下会引发一个单元格的公式被计算?两种情况:
      1.获取一个单元格的值时(包括用户调用GetCellValue()和GcExcel的内部feature调用GetCellValue()).
      2.调用Calculate()方法。
大家可以看后面的流程图来了解详细的过程。从这个过程得到以下几点需要注意的地方:
      1.如果不调用Calculate(),一个单元格只有在获取它的值时才会引发计算。所以,即使有公式的单元格再多,如果你只取某一个单元格的值,只会计算这个单元格和它所依赖的单元格,不会引发大量的计算,除非这个单元依赖很多其它单元格。
      2.SetValue()不会引发计算,只有GetValue()才会引发计算。所以,在SetValue()的时候,一般不需要关闭计算引擎,它不会带来性能的大量提升(会有适当的提升,后面会讲到)。
      3.一个单元格只会计算一次,不会反复计算。所以,对同一个单元格,第一次取值可能消耗时间,第二次取值的时间就忽略不计。
      4.Calculate()方法可以主动引发计算,但一般情况下也不需要调用。因为很多时候,用户只会用到一部分单元格的值,其它单元格根本不用,过多的计算也是一种浪费。只有当用户觉得某些情况下CPU闲着没事,利用空闲时间先把后面需要计算的单元格提前计算了,这样后面取值就快了。另外,由于Calculate()方法是批量计算,因此它在计算过程中做的优化比较多,因此比单独计算会快一些。


在计算过程中,怎么处理单元格之间的依赖关系?如上面的例子C1依赖B1,B1依赖A1,怎么决定它们计算的先后顺序?
         在GcExcel中,计算的发起可以从任意一个单元格开始,在计算的过程中才寻找依赖对象并对它们进行计算。 假设用户调用GetValue()方法获取单元格C1的值,会引发下面的过程(详细情况参看流程图):
         1.获取单元格C1的值,发现它有公式并且状态是Dirty,调用计算引擎计算C1。
         2.计算引擎在计算C1的时候,发现C1依赖B1,于是去获取B1的值。
         3.当获取B1的值时,发现B1有公式并且状态是Dirty, 调用计算引擎计算B1。
         4.计算引擎在计算B1的时候,发现B1依赖A1,于是去获取A1的值。
         5.当获取A1的值时,发现它没有公式,直接返回Value中存储的值10,回到计算B1的过程;
         6.继续计算B1的公式,得到结果12,把这个结果存入Value中,并把Status改为Valid,然后返回12,回到计算C1的过程。
         7.继续计算C1的公式,得到结果24,把这个结果存入Value中,并把Status改为Valid,然后返回24.




三.状态同步

         当一个单元格的值发生变化后,所有依赖它的单元格都必须重新计算。但GcExcel并不会立即触发这个计算过程,只是把所有依赖它的单元格的状态改变为Dirty,后面GetValue()时才计算,我们把这个过程叫做状态同步。比如上面的例子,当A1的值发生变化后,必须把B1的状态改为Dirty, B1变为Dirty后,又需要把C1的状态变为Dirty。
         状态同步过程往往耗费时间较少,所以一般情况下,在SetValue()的时候不需要关闭计算引擎。只有当公式特别多,依赖链特别长的情况下,这个同步过程才会比较耗费时间。
          状态同步的过程是自动的,用户不需要干预。但如果用户怀疑在某些特殊情况下状态出错了,可以调用Dirty(),这个方法会把单元格的状态改为Dirty,并且会把所有依赖这些单元格的其它单元格状态也改为Dirty,这样下次去取这些单元格的值时,就会重新计算。
         当计算引擎处于关闭状态时,状态同步会停止,如果这个时候改变了单元格的值,状态就不正确了,当计算引擎被再次开启时,为了确保正确,GcExcel会把所有单元格的状态改变为Dirty。这样会带来一个后果,如果用户需要获取所有单元格的值,这些单元格都得重新计算一遍。所以,频繁关闭计算引擎,在某些情况下不一定会带来性能的提升。
      有时候,用户会在一个循环中大量设置单元格的值,比如写这样的代码:



      由于在改变值的时候需要同步状态,在这种情况下可能会带来性能问题。用户可以在这种情况下关闭计算引擎,后面再打开,代码如下:

关闭计算引擎有两个缺点 :
      1.再次打开时,会导致所有有公式单元格的状态都变成Dirty,如果后面需要获取所有单元格的值,全部需要重新计算一遍。
      2.在计算引擎关闭中间,获取一个公式的值,取出来的是最近一次计算的结果,很可能是错误的。
关于第二个缺点,写下面这样的代码就可能发生:



       如果C1是一个公式,而它依赖的单元格的值被改变了,那么上面的代码就会出现错误。为了避免这类错误,GcExcel提供了一个方法:setDeferUpdateDirtyState(boolean value)。
       这个方法会把同步状态这个过程推迟,在SetValue()的时候,不同步状态,只是把发生变化的单元格放入一个ChangeSet中,等到GetValue()的时候,再从ChangeSet中取出所有变化的单元格,同步所有的状态。这样SetValue()变成很快,无需关闭计算引擎,也就不会出现上面的错误。
      于是,GetValue()的流程就增了一个同步状态环节,变成这下面这个图的样子。虽然每个单元格的GetValue()中都会同步状态,但真正的同步只会发生在第一次,同步完后会清空ChangeSet,后面的同步状态过程就无事可做了。
         setDeferUpdateDirtyState()虽然解决了一些关闭计算引擎带来的问题,但也有一个问题:就是状态同步延迟后,一些依赖状态通知的feature就会出问题,最典型的是Chart。比如一个Chart引用了单元格B1的值,而B1是一个公式,引用了A1的值,当用户改变A1后,如果没有状态同步,就不会知道B1发生了变化,就不会通知Chart引用的值失效了。如果这个时候用户去取Chart的值,得到的是旧值。
       要想避免这样的问题,用户需要谨慎使用setDeferUpdateDirtyState()。只有在出现性能问题的地方才调用,并且要尽快的调用setDeferUpdateDirtyState(false)改回来。在这之间,除了setvalue()和getValue(),用户要避免去用其它的feature。不仅是Chart,任何直接或间接依赖公式的Feature,都会受到setDeferUpdateDirtyState()的影响。
       如果使用setDeferUpdateDirtyState()来优化性能,前面的For循环的代码,用户可以写成这样:



四.常见的一些问题

1.优化For循环中大量写的性能问题


      在For循环中大量写值是最容易出现性能问题的地方,前面说了一些优化方式,这里再说一些需要注意的地方。
      优化这个问题有三种方法:
      (1)      把值写在一个数组中,然后把数组赋值给GcExcel,这是最快最好的方法。代码如下:



      (2)      调用setDeferUpdateDirtyState()来改善性能问题。
         在这种情况下,要尽量避免setValue和getValue()同时进行,比如下面的代码。
         如果第一列都是公式,那么这个代码会带来很大的性能问题。因为它的setvalue()和getValue()在不断的交替进行,这样就会导致不断的同步状态,不断的运算。
而且这种情况下,关闭计算引擎也不行,会获取错误的数据。这种情况只能从业务逻辑上去想办法,避免出现这种GetValue()和SetValue()交替出现的情况。
也可以通过设置公式来解决,比如这个例子,就给B1:B10000的单元格设置一个公式:=A1。


      (3)      通过关闭计算引擎来改善性能问题,这是最后的手段。

2.打开文件时如何优化公式的性能?


         Excel文件中存储有公式和它的计算结果,结构如下图: C节点表示单元格,f节点表示公式,v节点表示值。v节点不是必须出现的,如果没有它,表示这个公式还没有被计算。


         GcExcel在读Excel文件时,会把公式和值都存进单元格的存储结构中,但读完所有数据时,GcExcel调用了Workbook.Dirty(),这个函数会把所有单元格的状态改成Dirty。后面取单元格的值时,需要重新计算。
         GcExcel为什么要这么做呢?因为我们在测试的时候,发现很多用户的Excel文件,数据并不完全正确,原因可能是这些文件不是Excel生成的。为了确保正确,GcExcel需要重新计算一遍。
         如果用户确信文件数据是正确的,可以在打开文件的时候,告诉GcExcel不要重新计算,这样GcExcel打开文件后,就不会取调用Workbook.Dirty()。用户要写的代码如下:





      如果一个公式在文件中没有存放值,GcExcel会把它的状态标记为Dirty。如果所有的公式都没有存放值时,不管用什么样的打开方式,所有单元格的状态都是Dirty。

3.保存文件需要注意的地方

         在保存文件的时候,GcExcel不会对公式进行计算,当它发现一个单元格的状态是Dirty时,不会写值,只把公式写进文件。所以就会出现这样一个现象,当用户写下面的代码:


      在xml里面查看时会发现test2.xlsx文件中的公式的值全丢了(Excel和GcExcel能显示正常值是因为它发现没有值,会马上计算一个值,所以看的时候总是有值的)!要避免这一点,需要这样写:

      这样test2.xlsx和test.xlsx中的值是一样的。也可以这样写:

      这样test2.xlsx中每一个公式都有值,但是保存的时候需要消耗计算时间

4. 一个小问题,调用Workbook.Calculate()会强制重新计算所有单元格吗?


      不管调用GetValue()或者调用Calculate(),都只计算状态为Dirty的单元格,所以调用Workbook.Calculate()不会重新计算所有单元格,只计算那些状态为Dirty的单元格,只有先调用Workbook.Dirty(),再调用Workbook.Calulate()才会重新计算所有的单元格。


Clark.Pan 发表于 2022-3-18 18:05:48

:i0tw:
页: [1]
查看完整版本: GcExcel计算引擎的工作原理及常见用户问题