Delphi Copy函数效率的问题

技术交流,DH讲解.

最近和肥鸟交流了下关于字符串方面的知识,而这篇文章是很久以前写的,现在发出来吧.

我们写两段代码来对比下:
第一个用Copy函数:

procedure TForm1.Button1Click(Sender: TObject); 
var 
  a,c:Cardinal; 
  n:Integer; 
  D:Double; 
  i:Integer; 
  b:string; 
begin 
  c:=0; 
  for n:=0 to 99 do 
  begin 
    a:=GetTickCount; 
    for i:=0 to 999999 do 
    begin 
      b:=Copy(s,1,20); 
    end; 
    a:=GetTickCount-a; 
    C:=C+A; 
  end; 
  D:=C/100; 
  Label1.Caption:=FloatToStr(D); 
end; 

第二个用MoveMemory函数:

procedure TForm1.Button2Click(Sender: TObject); 
var 
  a,c:Cardinal; 
  n:Integer; 
  d:Double; 
  i:Integer; 
  b:string; 
begin 
  c:=0; 
  for n:=0 to 99 do 
  begin 
    a:=GetTickCount; 
    for i:=0 to 999999 do 
    begin 
      SetLength(b,20); 
      MoveMemory(@b[1],@s[1],20); 
    end; 
    a:=GetTickCount-a; 
    C:=C+A; 
  end; 
  D:=C/100; 
  Label2.Caption:=FloatToStr(D); 
end; 

其中:

procedure TForm1.FormCreate(Sender: TObject); 
begin 
  s:='HuangJackyJackyHuang'; 
end; 

看下实验数据:
1用了 264
2用了 169
当然这是在运行了9999900次才看出来的,如果我们把s赋值成一个很长的字符串看看
当字符串s有100个字符,Copy 100个:
1用了266
2用了181
继续 S有200个字符,Copy 200个的情况
1   244
2   186
可以看出来在短字符串的情况下MoveMemory肯定要快一些,后面字符串增长Copy效率没有下降,
但是SetLength + MoveMemory就下降了
最后s有400个字符Copy 400个,但是我把SetLength放在循环外面了,也就是只SetLength一次,也就只比较Copy和MoveMemory这两个了.
1      216
2       89
哈哈今天换在台式机了,所以2个运行时间都减少了很多.
再测试下SetLength在循环体里面的情况 用了147.
这下子我们可以看出来一个SetLength耗了多少资源.

OK,我们看见了这个结果,可是为什么会这样呢?有朋友想了解没有?
要了解就要深入函数内部去:
我们先看1的情况:

Unit1.pas.46: b:=Copy(s,1,400);
lea eax,[ebp-$14]
push eax
mov ecx,$00000190
mov edx,$00000001
mov eax,[esi+$00000308]
call @LStrCopy
//跟进去
-------------@LStrCopy---------
push ebx
test eax,eax
jz +$2d
mov ebx,[eax-$04]
test ebx,ebx

jz +$26
dec edx
jl +$1b
cmp edx,ebx
jnl +$1f
sub ebx,edx
test ecx,ecx

jl +$19
cmp ecx,ebx
jnle +$11
add edx,eax
mov eax,[esp+$08]
//这里会跳走我们继续跟踪

call @LStrFromPCharLen
jmp +$11
xor edx,edx
jmp -$1b
mov ecx,ebx
jmp -$15
mov eax,[esp+$08]
call @LStrClr
pop ebx
ret $0004
ret
---------@LStrFromPCharLen------------
push ebx
push esi
push edi
mov ebx,eax
mov esi,edx
mov edi,ecx
mov eax,edi
//这里还要跟

call @NewAnsiString
mov ecx,edi
mov edi,eax
test esi,esi
jz +$09
mov edx,eax
mov eax,esi
//这里

call Move
mov eax,ebx
//这里

call @LStrClr
mov [ebx],edi
pop edi
pop esi
pop ebx
ret
mov eax,eax
push ebp
mov ebp,esp
push $00
push $00
push edx
push eax
mov eax,[ebp+$08]
------------@NewAnsiString------------
test eax,eax
jle +$24
push eax
add eax,$0a
and eax,-$02
push eax
//继续,这里分内存了,快要到尽头了
call @GetMem
pop edx
mov word ptr [edx+eax-$02],$0000
add eax,$08
pop edx
mov [eax-$04],edx
mov [eax-$08],$00000001
ret
xor eax,eax
ret
------------@GetMem------------
push ebx
push ecx
mov ebx,eax
test ebx,ebx
jle +$1a
mov eax,ebx
//这里调用SysGetMem,我们不用跟了,因为SetLength肯定也会用到这个函数,抵消,哈哈
call dword ptr [MemoryManager]
mov [esp],eax
cmp dword ptr [esp],$00
jnz +$0e
mov al,$01
call Error
jmp +$05
xor eax,eax
mov [esp],eax
mov eax,[esp]
pop edx
pop ebx
ret
lea eax,[eax+$00]
-------------接下来是Move 但是MoveMemory也是调用这个函数,抵消,不看了----------
-----------@LStrClr这个要进去看,不看对不起观众--------------
mov edx,[eax]
test edx,edx
jz +$1c
mov [eax],$00000000
mov ecx,[edx-$08]
dec ecx
jl +$10
lock dec dword ptr [edx-$08]
jnz +$0a
push eax
lea eax,[edx-$08]
//这里又FreeMem,不看了.
call @FreeMem
pop eax
ret
nop
----------------整体过程--------------
Copy -> @LStrFromPCharLen -> @NewAnsiString -> @GetMem 
   -> Move
    -> @LStrClr -> @FreeMem
好了这个流程走完.
基本上是3步,分空间,复制数据,收尾

接下来看看2的情况:
Unit1.pas.71: SetLength(b,400);
lea eax,[ebp-$14]
mov edx,$00000190
//这个看一下
call @LStrSetLength
Unit1.pas.72: MoveMemory(@b[1],@s[1],400);
lea eax,[esi+$00000308]
//这个干什么的
call @UniqueStringA
push eax
lea eax,[ebp-$14]
call @UniqueStringA
mov ecx,$00000190
pop edx
//这里
call MoveMemory
--------------@@LStrSetLength----------
push ebx
push esi
push edi
mov ebx,eax
mov esi,edx
xor edi,edi
test edx,edx
jle +$48
mov eax,[ebx]
test eax,eax
jz +$23
cmp dword ptr [eax-$08],$01
jnz +$1d
sub eax,$08
add edx,$09
push eax
mov eax,esp
call @ReallocMem
pop eax
add eax,$08
mov [ebx],eax
mov [eax-$04],esi
mov byte ptr [esi+eax],$00
//这里直接跳过去,然后就到MoveMemory这句了
jmp +$28
mov eax,edx
//这个是不会被执行的.
call @NewAnsiString
mov edi,eax
mov eax,[ebx]
test eax,eax
jz +$10
------------@ReallocMem------------------
mov ecx,[eax]
test ecx,ecx
jz +$32
test edx,edx
jz +$18
push eax
mov eax,ecx
call dword ptr [MemoryManager + $8]
pop ecx
or eax,eax
jz +$19
mov [ecx],eax
ret
--------------@UniqueStringA----------
jmp InternalUniqueString
ret
mov eax,eax
----------InternalUniqueString----------
mov edx,[eax]
test edx,edx
jz +$38
mov ecx,[edx-$08]
dec ecx
jz +$32
mov eax,edx
ret
---------MoveMemory---------------
xchg eax,edx
call Move
ret
-------------------------整个过程------------------
SetLength -> @LStrSetLength -> @ReallocMem 
  MoveMemory -> 2次@UniqueStringA -> Move
------------------------------------------------------------

对比2个过程
我们可以看到SetLength + MoveMemory 比 Copy 少了第三步.
从我们上面的实验结果我们也能看到 第2种方法比第1种方法 少用了1/3左右的时间.

感觉用PChar然后GetMem分配空间会跟快一些.这里就不测试了,有兴趣的朋友可以测试一下.

今天就到这里了,我是DH.

原文地址:https://www.cnblogs.com/huangjacky/p/1625073.html