关于填充DataTable的效率问题

缘起

最近在用AQTime分析一个功能节点的性能问题时,注意到AQTime给出的性能数据存疑:

image

BulkCopyTool是程序员写的一个工具类,利用Ado.net的SqlBulkCopy特性来快速地将数据批量插入数据库中。 每次操作的记录数大致在5千左右,因此SqlBulkCopy的开销在2.92秒是合理的。但是GetDataTable的开销就很有疑问,顾名思义,这应该是一个构造内存DataTable的方法,开销怎么会是数据库开销的2倍还不止呢?

初步分析

查看GetDataTable的消耗,大量时间消耗在setItem触发的BeginEdit/EndEdit上:

image

检查代码,也就是很普通的填充DataTable的代码而已:

image

反复检查几遍后,认为疑点在AddRow的时机上

问题确认

使用Reflector Pro反编译Ado.net相关代码,调试进去,发现将DataRow添加到DataTable的Rows属性中会导致DataRow的部分内部变量发生变化:

内部变量

AddRow前

AddRow后

RowState

Detached

Added

newRecord

-1

0

rowID

-1

1

tempRecord

0

-1

RBTreeNodeId

0

1

而这些变量控制了一些处理逻辑。如果是在AddRow之前做DataRow.SetItem,则很快可以返回:

image

  如果大家有过WinForm编程的经验,应该记得像Tree或者Grid这类可以显示大量记录的UI控件,一般都会提供BeginEdit/EndEdit方法,用于控制在批量添加记录的过程中关闭掉UI的同步刷新。仅当所有记录添加完毕后,再一次刷新最后的结果。这个效率差距早已在无数的实践中证明过。批量添加DataRow到DataTable中,背后的原理是一样的。

  考虑DataTable并未明确提供BeginEdit/EndEdit机制,或许用JavaScript批量添加DOM节点的技巧来类比更为合适。这个技巧就是先在离线方式下生成所有DOM节点,最后才把它Attach到DOM树上,同样是为了避免不停刷新界面的开销。

总结

填充DataTable正常的步骤是这样的:

1. 构造新的DataRow对象:row = new DataRow

2. 对各个列赋值:row[xxx] = yyy, row[aaa] = bbb, ......

3. 最后将DataRow加入到DataTable中:myDataTable.Rows.Add(row)

这是MSDN中所有示例代码给出的次序。如果搜索一下网络上的文章,也都是这个次序。可以说100个人中有99个,不,应该说1万个人中有9999个都是这么做的。但实现BulkCopyTool的程序员采用了另一种次序,将步骤3提到步骤2前面。一旦先AddRow,那么对每个列的赋值都会触发BeginEdit/EndEdit开销。这个案例有趣的地方在于,明明设计BulkCopyTool的目的是为了提高性能,却选择了一种性能不佳的写法。当更正这一失误后,新的AQTime数据表明GetDataTable的开销由8.42秒大幅下降到不足0.5秒

 

最后想探讨一下的是疑点在性能分析工作中的作用。从个人经验看,疑点往往能快速指示正确的工作方向。一般我们会使用工具捕获性能数据,或者通过代码调试,来寻找疑点后的真相。有时候,更大的乐趣在于仅仅通过一个纯粹的逻辑推理,就能够合理解释所有疑点,当然,那基本上就会成为问题的答案。可以说分析这个案例毫无难度,能帮助我们揭开这个小秘密的,就在于对异常数据是否有足够的敏感。

原文地址:https://www.cnblogs.com/hbzhang/p/2328599.html