二、 从TList开始分析……
为了写一个更好的性能ISAPI Filter,我需要更快速地从TList中删除部分连续的Item。比如这样的一段代码:
var p : pChar = 'abcdefgh';
procedure TestDelFromTList;
var t1 : TList;
i : integer;
maxI : integer;
begin
t1 := tlist.create;
t1.count := 100000;
for i:=0 to t1.count-1 do t1[i] := p;
maxI := t1.count-10-1; //最后一个结点
for i:= maxI downto 10 do t1.delete(i);
//正向删除
//for i:=10 to t2.count-10-1 do t2.delete(10);
end;
这段代码是初始化一个100000个结点的List,然后删除其中的第10个到倒数第10个。这段代码是逆向的,这样的删除速度比较快。如果换成正向删除(已经注释掉),则速度就慢得非常多了。
这样的删除是正常的算法。用测效率的程序测试:逆向删除算法的耗时是21.66个毫秒,则正向删除的耗时却能达到58099.02个毫秒。速度慢了2680倍!!!
但这样就很快了么?不是!我认为就算是逆向删除的速度也并不是快的。
分析TList这个类的源码,我们可以看到,它是这样写的(我加入了注释):
procedure TList.Delete(Index: Integer);
var
Temp: Pointer;
begin
if (Index < 0) or (Index >= FCount) then //判定Index值是否超界
Error(@SListIndexError, Index);
Temp := Items[Index]; //取待删除结点
Dec(FCount); //Count减一
if Index < FCount then //将待删除结点后的Buffer提前
System.Move(FList^[Index + 1], FList^[Index],(FCount - Index) *
SizeOf(Pointer));
if Temp <> nil then //发通告
Notify(Temp, lnDeleted);
end;
由于在TList类是将全部的结点指针存放在FList这个动态数组的指针中,所以只需要将Index+1之后的内存块向前移4个字节,即SizeOf(Pointer),即可实现Index结点的删除。
但是,如果使用这样来删除成批连续的(N个)结点,则要实现N次system.move()操作,操作的内存块的大小决定了system.move()操作的耗时,而Index值越小的的结点在FList中越靠前,则system.move()要操作的内存块也就越大。这就是我认为上述成批删除效率不高的原因,也是正向删除比逆向删除的耗时慢了慢了2680倍的原因。
对于成批删除,理想的算法是从index+len结点开始位置,向前移动count-index-len个结点,这样,就能够一次完成全部的结点移动,实现删除操作。这个思路非常好,至少我认为是这样。为此,我实现了下面的代码:
procedure CutList(aList:TList; left,len:integer);
begin
with aList do begin
System.Move(List^[left+len], List^[left], (Count-left-len) *
SizeOf(Pointer));
count := count-len;
end;
end;
这段代码的功能是在TList.List这个Buffer中,将删除后的剩余结点直接移动到Left这个位置上,从而完成全部的移动操作。
然后,我们再设count := count-len;来使用个数减少,从而完成了成批量的删除。
好的,如果一切正常,算法的速度将大幅度提升!OHHH,美妙的想法!
但是,真的是这样么?我再用效率测试程序来测试了一轮,结果是这样的:
1. 测试数据为10万个结点,则逆向删除算法耗时为20.56毫秒,CutList()函数耗时9.69毫秒;
2. 测试数据为100万个结点,则逆向删除算法耗时为209.13毫秒,CutList()函数耗时98.01毫秒。
速度比逆向算法提高了一倍,而且应该注意到,CutList()的耗时仍然随数据量的增大而等比例的增大!!!而从CutList()函数的实现来看,数据量增大,算法耗时应该只增加极少才对。
要知道,只加快一倍速度的CutList(),并不是我所想要的!!!但为什么CutList()函数得不到更高的性能呢???