TADODataSet的Next方法性能探讨

摘要:本文探讨了DelphiADOExpress组件中存在一个性能问题:TADODataSet组件的Next方法的时间性能会中随着游标的向后移动而变差。通过分析TADODataSet组件的源代码,找到的导致这个性能问题的原因。并且,提出的两种解决此问题的方法。

关键字:ADOVCLTADODataSet、性能

中图法分类号:      TP311.52;312DE   文献标识码: A

 

Discussion about the performance of TADODataSet’s Next mothed

ABSTRACT: This thesis attempts to explore a performance problem of Delphi ’s ADOExpress component: With the cursor moving backward, the time performance of TADODataSet component’s Next method will worsen. Based on the analysis of TADODateSet component’s original code, this thesis discusses the factor influencing its performance, and some approaches to solve this problem have been suggested.

Key Words: ADOVCLTADODataSetPerformance

ADOExpress组件

如何存取数据一直是软件技术开发的重心之一,因为大部分的应用程序都需要存取各种不同的数据并根据这些数据进行运算。随着数据类型不断地复杂和多样化,应用程序程序员必须花费更多的时间和成本撰写存取数据的程序代码。再加上新操作系统平台的出现,应用系统的需求增加以及数据库系统持续进步,都使数据存取的工作更加困难,并超过了程序员能够负担的范围,因此使用一种统一的标准让程序员存取数据便成为一个非常重要的需求。这便是ODBCBDE/I DAPIADO等标准数据存取技术出现的重要原因之一。

ADOMicrosoft存取通用数据源的标准引擎。ADO主要是让应用程序或Web应用程序存取各种不同的数据源。ADO封装了OLE-DB复杂的接口,以极为简单的COM接口存取数据。图1ADO的架构图。从图1我们可以看到,ADO能够藉由OLE-DB存取传统的关系数据库,或Flat-File类型的数据库;也可以存取非传统的数据,例如文字、Email、声音、图形、影像等。更可以通过OLE-DB,藉由Connector来存取大型的数据源,例如CICS等。但是不管应用程序要存取哪一种数据源,应用程序都只需要使用ADO,而不需要使用各种不同的复杂API来存取不同的数据,这样就可以大大简化应用程序员的工作。

                                  

     图 1

原生ADO对象的架构图如图2,主要对象有这么几个:Connection对象负责和数据源建立连接,因此ADO应用程序一开始应该使用Connection对象来连接数据源。Command对象代表ADO应用程序向数据源下达的命令。Recordset对象可以说是原生ADO对象中的灵魂对象,它提供了访问结果集的能力。

2

Delphi中,为了和原来VCL的数据访问和数据感知组件相配合,DelphiADOExpress组件封装了原生ADO对象。ADOExpress几乎是以一对一的方式来封装原生ADO对象,例如ADOConnection对象是以TADOConnection组件封装,ADOCommand对象是以TADOCommand组件封装。但是ADORecordset对象在ADOExpress中却使用了数个不同的VCL组件来封装,它们分别是TADODataSetTADOQueryTADOTableTADOStoredProc组件。

3ADOExpress组件架构图。在图中我们可以看到TADODataSet继承自TCustomADODataSet,而TCustomADODataSet又继承自TDataSetTDataSetDelphi中数据访问的核心组件,Delphi中的数据感知组件都可以通过TDataSet工作。这样,通过ADOExpress就把ADO融入了DelphiVCL体系。

 

3

Next方法的低效现象

笔者在利用Delphi开发一个应用程序时,碰到这么一个现象:使用TADODataSet 打开一个结果集,然后调用其Next方法移动游标一行一行地遍历整个结果集,刚开始速度很快,越到结果集的后部越慢。如果结果集比较小,比如几千行的话,这种速度的变慢不易察觉,但是随着结果集的变大,这种速度的变缓会变得不可承受。为了说明这个现象,笔者作了一个实验。

实验是这样的:使用TADODataSet 打开一个10万行左右的结果集,然后反复调用Next方法进行遍历(除了Next方法,没有调用任何其他方法),以10000次调用为单位进行计时。实验结果如下表(表格1)所示:

计时次数

耗时(ms

比上次增加耗时

1

1983

-

2

5948

3965

3

9954

4006

4

13500

3546

5

17255

3755

6

21080

3825

7

24876

3796

8

28711

3835

9

32537

3826

10

36332

3795

表格

从结果中我们可以发现以下现象:

随着游标向后的移动,调用Next方法的耗时变得越来越大。从第1行到第10000行,遍历的时间,也就调用10000next方法的耗时为1.9秒;而从第90000行到第100000行,遍历的时间膨胀到36.3秒。同样是10000次调用next,两者时间差距达30多倍。而且这种增长几乎是线性的,每次移动10000行,都要比前一个10000行多耗时4秒左右。

可以预见,随着结果集的变大,调用Next方法(游标移动)的速度会越变越慢。而这种现象是要避免出现的,理想情况是,Next调用速度不应该随着游标的向后移动而变慢,也就是说,在第1行上调用Next方法的耗时,应该和在第10000行调用Next方法的耗时是一样的。只有这样,应用程序的效率才能得到保证。

探寻低效的根源

为什么会出现这种现象呢?到底是ADO原生组件的问题,还是Delphi封装的问题呢?带着这些疑问,笔者对TADODataSet 的源代码进行分析。分析的源代码主要在两个单元:ADODBDB。出于篇幅考虑,笔者仅列出特别需要说明的代码。

前面提到过,TADODataSet继承自TCustomADODataSet,而TCustomADODataSet又继承自TDataSet。我们的分析也基本上在这三个组件中进行。

TADODataSet没有覆盖Next方法,而TCustomADODataSet也没有覆盖Next方法。所以观察TDataSetNext 方法,其内容很简单,仅仅是调用了MoveBy(1)。我们继续跟踪MoveBy方法。MoveBy方法比较长,其中关键的调用是:GetNextRecord。而在GetNextRecord中关键的一句是GetRecord(GetBuffer(FRecordCount), GetMode, True);其中GetMode是一个变量,已经在方法的开头设置为gmNext GetRecord方法是一个虚方法,是TDataSet留给其子类去实现的。那么,问题关键转移TCustomADODataSet是如何实现GetRecord方法的。

TCustomADODataSet GetRecord方法中,除去一些维护FilterState的一些代码,它把真正的移动动作交给了另一个方法 InternalGetRecord。移动结果集游标的最为核心,真正工作的代码正是隐藏在这里。

InternalGetRecord 方法的基本工作原理如下:首先它判断移动的方向,若是要求是向后移动,便调用TCustomADODataSet 内含的原生ADORecordSetMoveNext方法。若是向前,便调用TCustomADODataSet 内含的原生ADORecordSetMovePrevious方法。如果调用成功,便设置书签状态和行位置。

笔者分析,实际上真正的移动动作,在调用好MoveNext方法(或MovePrevious)已经完成。而问题应该出现在其后的那些代码中。这部分的代码摘抄入下

 

if Result = grOK then

    begin

       with PRecInfo(Buffer)^ do

      begin

        RecordStatus := Recordset.Status;

        if (BookmarkSize > 0) and ((adRecDeleted and RecordStatus) = 0) then

        begin

          BookmarkFlag := bfCurrent;

          Bookmark := Recordset.Bookmark;

          if ControlsDisabled then

            RecordNumber := -2 else

            RecordNumber := Recordset.AbsolutePosition;

        end else

          BookmarkFlag := bfNA;

      end;

      Finalize(PVariantList(Buffer+SizeOf(TRecInfo))^, Fields.Count);

      GetCalcFields(Buffer);

    end;

代码段 I

为了证明这种猜测,笔者将这段代码注释掉(也就是不做那些多余的动作),重新编译程序,程序运行的结果如下:

计时次数

耗时(ms

1

40

2

40

3

30

4

40

5

40

6

30

7

30

8

40

9

40

10

40

从结果看,Next方法的耗时大大缩减了,并且也不会随着游标的移动而变慢!果然,效率低下的原因正是出现这段代码中。

到底这段代码中的哪一行语句是罪魁祸首呢?笔者经过简单的实验,发现影响最大的是RecordNumber := Recordset.AbsolutePosition; 若是注释掉这一语句,next方法的调用时间是基本固定的,不会随着游标的移动而变慢。正是这句导致了Next方法的低效。这一语句的要去取原生RecordSet方法的AbsolutePosition 属性,而获取这个属性,在原生ADO对象中也是比较慢的会随着游标的向后移动变慢的操作。

问题的结论是这样的,DelphiADOExpress的实现者,为了维护结果集当前游标的位置,在InternalGetRecord中不恰当的取用了原生ADO对象的AbsolutePosition属性导致Next方法性能会随着游标的移动而变差。

改进

问题的根源找到了,但是修改是却要考虑全面。如果我们简单的注释掉那一语句(RecordNumber := Recordset.AbsolutePosition;),会导致TADODataSet的组件中使用到RecordNumber数据成员的代码工作不正常,所以我们应该找到所有使用RecordNumber数据成员的地方,经过搜寻,发现只有在GetRecNo方法和DoRecordsetDelete方法中使用到RecordNumber。解决的办法也很简单,把GetRecNo方法和DoRecordSetDaelete方法中对RecordNumber的取用,改为直接取用RecordSet..AbsolutePosition

TADODataSet的源代码做了以上修改后,重新编译,再运行上面的实验,结果如下:

计时次数

耗时(ms

1

50

2

60

3

50

4

50

5

50

6

60

7

60

8

60

9

60

10

50

这样修改后,虽然耗时比完全注释掉代码段 I要增加一点,但是耗时不再随着游标的移动而变大。

还有另一种修改的方法:如果在代码中,仅仅是读取数据,那么向下移动游标时,可以选择不调用TADODataSet.Next 方法;而是直接调用TADODataSet内含的原生ADO Recordset对象的moveNext方法。这样做的好处是不用修改VCL的源代码,风险要小的多。

参考书目

[1]. DELPHI 7.0 英文版 帮助文档 CP/DK

[2].    (美)Steve Teixeira /Xavier PachecoDelphi5 开发人员指南》机械工业出版社 2001

[3].    (美) LischnerDelphi技术手册》[M] 中国电力出版社2001

 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值