[轉]delphi 有授權許可的字串拷貝函式原始碼

Max Woods發表於2014-07-29

一段看上去“貌不驚人”的Delphi插入彙編程式碼,卻需要授權許可,但是與經典的同型別函式比較,確實“身手不凡”。

研究程式碼的目的在於借鑑,本文通過分析,並用C++重寫程式碼進行比較,再次證明這段程式碼效率較高的主要原因在於思路(或者演算法),與語言本身效率關係不大。

今天開啟Delphi2007的SysUtils.pas檔案檢視一個函式程式碼,偶爾看到字串拷貝函式StrCopy中的插入彙編程式碼,感覺與記憶中Delphi7的同名函式中的程式碼大不相同,我的彙編水平雖不算精通,但自認還過得去,但粗粗看了一下,竟沒完全看明白。找出Delphi7的StrCopy程式碼初步比較分析了一下,給我的第一印象是Delphi2007的StrCopy函式程式碼既粗燥,又難懂,拷貝速度也肯定不及Delphi7的StrCopy函式。下面分別摘入這兩段程式碼,相信不少人都會有我類似的感覺:

Delphi7的StrCopy函式程式碼:


function StrCopy(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX
MOV EDI,EDX
MOV ECX,0FFFFFFFFH
XOR AL,AL
REPNE SCASB
NOT ECX
MOV EDI,ESI
MOV ESI,EDX
MOV EDX,ECX
MOV EAX,EDI
SHR ECX,2
REP MOVSD
MOV ECX,EDX
AND ECX,3
REP MOVSB
POP ESI
POP EDI
end;
function StrCopy(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX
MOV EDI,EDX
MOV ECX,0FFFFFFFFH
XOR AL,AL
REPNE SCASB
NOT ECX
MOV EDI,ESI
MOV ESI,EDX
MOV EDX,ECX
MOV EAX,EDI
SHR ECX,2
REP MOVSD
MOV ECX,EDX
AND ECX,3
REP MOVSB
POP ESI
POP EDI
end;

Delphi2007的StrCopy函式程式碼:


function StrCopy(Dest: PChar; const Source: PChar): PChar;
asm
sub edx, eax
test eax, 1
push eax
jz @loop
movzx ecx, byte ptr[eax+edx]
mov [eax], cl
test ecx, ecx
jz @ret
inc eax
@loop:
movzx ecx, byte ptr[eax+edx]
test ecx, ecx
jz @move0
movzx ecx, word ptr[eax+edx]
mov [eax], cx
add eax, 2
cmp ecx, 255
ja @loop
@ret:
pop eax
ret
@move0:
mov [eax], cl
pop eax
end;
function StrCopy(Dest: PChar; const Source: PChar): PChar;
asm
sub edx, eax
test eax, 1
push eax
jz @loop
movzx ecx, byte ptr[eax+edx]
mov [eax], cl
test ecx, ecx
jz @ret
inc eax
@loop:
movzx ecx, byte ptr[eax+edx]
test ecx, ecx
jz @move0
movzx ecx, word ptr[eax+edx]
mov [eax], cx
add eax, 2
cmp ecx, 255
ja @loop
@ret:
pop eax
ret
@move0:
mov [eax], cl
pop eax
end;

正感嘆難怪Delphi每況日下,連庫程式碼都改得如此之差,反過來又一想,如果這段程式碼比以前的程式碼還差,為什麼要改呢?難道CodeGear的程式設計師水平如此之差?抱著疑問,又找出Delphi2010的StrCopy函式,除了PChar為PAnsiChar外,其它與Delphi2007一樣。這才想到這段程式碼肯定有它的過人之處!果然,在Delphi2007和Delphi2010的StrCopy函式前有一段註釋,被我這完全不懂英語的人給忽略了:


(* ***** BEGIN LICENSE BLOCK *****
*
* The function StrCopy is licensed under the CodeGear license terms.
*
* The initial developer of the original code is Fastcode
*
* Portions created by the initial developer are Copyright (C) 2002-2004
* the initial developer. All Rights Reserved.
*
* Contributor(s): Aleksandr Sharahov
*
* ***** END LICENSE BLOCK ***** *)
(* ***** BEGIN LICENSE BLOCK *****
*
* The function StrCopy is licensed under the CodeGear license terms.
*
* The initial developer of the original code is Fastcode
*
* Portions created by the initial developer are Copyright (C) 2002-2004
* the initial developer. All Rights Reserved.
*
* Contributor(s): Aleksandr Sharahov
*
* ***** END LICENSE BLOCK ***** *)

用網上Google的線上翻譯翻譯了一下,這才知道,原來這段程式碼還是有授權許可的!這才真是“人不可貌相”啊。

若干年前,小平同志就教導過我們:“實踐是檢驗真理的唯一標準”,照他的話辦應該沒錯。於是將這兩段程式碼摘入下來,分別改名為StrCopy7和StrCopy2007,寫了一段簡單程式碼,用80兆位元組的字串進行了一下速度測試:

const
TestSize = 80 * 1024 * 1024 + 2;
var
Dest, Source: PChar;
p, pe: PChar;
TickCount7, TickCount2007: Longword;
begin
GetMem(Source, TestSize);
GetMem(Dest, TestSize);

Randomize;
p := Source;
pe := p + TestSize - 1;
while p < pe do
begin
p^ := char(Random(255));
if p^ >= #32 then Inc(p);
end;
p^ := #0;

TickCount7 := GetTickCount;
StrCopy7(Dest, Source);
TickCount7 := GetTickCount - TickCount7;

TickCount2007 := GetTickCount;
StrCopy2007(Dest, Source);
TickCount2007 := GetTickCount - TickCount2007;

FreeMem(Dest);
FreeMem(Source);

ShowMessage(Format('StrCopy7: %d, StrCopy2007: %d', [TickCount7, TickCount2007]));
end;
const
TestSize = 80 * 1024 * 1024 + 2;
var
Dest, Source: PChar;
p, pe: PChar;
TickCount7, TickCount2007: Longword;
begin
GetMem(Source, TestSize);
GetMem(Dest, TestSize);

Randomize;
p := Source;
pe := p + TestSize - 1;
while p < pe do
begin
p^ := char(Random(255));
if p^ >= #32 then Inc(p);
end;
p^ := #0;

TickCount7 := GetTickCount;
StrCopy7(Dest, Source);
TickCount7 := GetTickCount - TickCount7;

TickCount2007 := GetTickCount;
StrCopy2007(Dest, Source);
TickCount2007 := GetTickCount - TickCount2007;

FreeMem(Dest);
FreeMem(Source);

ShowMessage(Format('StrCopy7: %d, StrCopy2007: %d', [TickCount7, TickCount2007]));
end;

測試出的結果超出我的預料:StrCopy7與StrCopy2007的拷貝速度竟然相差2.5 - 4倍!呵呵,果然是有“授權許可”的程式碼呀,還真是“身手不凡”,要知道StrCopy7採用的並非一般的單位元組拷貝,而是採用的每次4位元組拷貝,本身就是一段相當高效的字串拷貝程式碼,比它還高出2.5 - 4倍速度的程式碼,還真叫人難以相信!

為了讓有些不大懂彙編的朋友也能欣賞到這段“貌不驚人”程式碼,我給這2段程式碼逐句加上漢字註釋貼在下面(文章後面用C++重寫了這2段程式碼):


// in: eax=dest,edx=Source out: eax=Dest
function StrCopy7(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX // 儲存Dest在esi
// 計算字串Source的長度
MOV EDI,EDX // edi = Source
MOV ECX,0FFFFFFFFH // ecx = 最大無符號長整型數
XOR AL,AL // al = 0(0為C語言字串結束符)
REPNE SCASB // 在Source中查詢結束符位置
NOT ECX // ecx取反為Source長度(包括結束符在內)
// 拷貝Source到Dest(包括結束符在內)
MOV EDI,ESI // edi = Dest
MOV ESI,EDX // esi = Source
MOV EDX,ECX // 儲存Source的長度在edx
MOV EAX,EDI // eax = Dest(函式返回值)
SHR ECX,2 // ecx /= 4
REP MOVSD // 按每次4位元組進行迴圈拷貝
MOV ECX,EDX
AND ECX,3 // ecx = edx % 4(按4位元組拷貝後的剩餘位元組)
REP MOVSB // 按單位元組拷貝迴圈拷貝剩餘位元組
POP ESI
POP EDI
end;

// in: eax=dest,edx=Source out: eax=Dest
function StrCopy2007(Dest: PChar; const Source: PChar): PChar;
asm
sub edx, eax // Source地址減Dest地址
test eax, 1 // 測試Dest地址值是否為奇數
push eax // 儲存函式返回植
jz @loop
movzx ecx, byte ptr[eax+edx] // 如果Dest地址值為奇數
mov [eax], cl // 拷貝Source的一位元組到Dest
test ecx, ecx // 如果是字串結束符,返回Dest
jz @ret
inc eax // 否則Dest地址值調整為偶數
@loop: // 迴圈逐字拷貝Source到Dest
movzx ecx, byte ptr[eax+edx] // 從Source中預讀一位元組
test ecx, ecx // 如果是字串結束符,拷貝後返回Dest
jz @move0
movzx ecx, word ptr[eax+edx] // 拷貝Source的一字到Dest
mov [eax], cx
add eax, 2 // Dest地址值加2,因edx為Source與Dest之差,
// eax+edx為Source地址下一地址值
cmp ecx, 255 // 如果已拷貝字大於255,繼續下一字拷貝。
// 注:因前面已通過預讀對結束符進行判斷處理,
// 故已拷貝字低位元組不可能為0,所以已拷貝字
// <=255,說明其高位元組為0,拷貝結束
ja @loop
@ret:
pop eax
ret
@move0:
mov [eax], cl
pop eax
end;
// in: eax=dest,edx=Source out: eax=Dest
function StrCopy7(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX // 儲存Dest在esi
// 計算字串Source的長度
MOV EDI,EDX // edi = Source
MOV ECX,0FFFFFFFFH // ecx = 最大無符號長整型數
XOR AL,AL // al = 0(0為C語言字串結束符)
REPNE SCASB // 在Source中查詢結束符位置
NOT ECX // ecx取反為Source長度(包括結束符在內)
// 拷貝Source到Dest(包括結束符在內)
MOV EDI,ESI // edi = Dest
MOV ESI,EDX // esi = Source
MOV EDX,ECX // 儲存Source的長度在edx
MOV EAX,EDI // eax = Dest(函式返回值)
SHR ECX,2 // ecx /= 4
REP MOVSD // 按每次4位元組進行迴圈拷貝
MOV ECX,EDX
AND ECX,3 // ecx = edx % 4(按4位元組拷貝後的剩餘位元組)
REP MOVSB // 按單位元組拷貝迴圈拷貝剩餘位元組
POP ESI
POP EDI
end;

// in: eax=dest,edx=Source out: eax=Dest
function StrCopy2007(Dest: PChar; const Source: PChar): PChar;
asm
sub edx, eax // Source地址減Dest地址
test eax, 1 // 測試Dest地址值是否為奇數
push eax // 儲存函式返回植
jz @loop
movzx ecx, byte ptr[eax+edx] // 如果Dest地址值為奇數
mov [eax], cl // 拷貝Source的一位元組到Dest
test ecx, ecx // 如果是字串結束符,返回Dest
jz @ret
inc eax // 否則Dest地址值調整為偶數
@loop: // 迴圈逐字拷貝Source到Dest
movzx ecx, byte ptr[eax+edx] // 從Source中預讀一位元組
test ecx, ecx // 如果是字串結束符,拷貝後返回Dest
jz @move0
movzx ecx, word ptr[eax+edx] // 拷貝Source的一字到Dest
mov [eax], cx
add eax, 2 // Dest地址值加2,因edx為Source與Dest之差,
// eax+edx為Source地址下一地址值
cmp ecx, 255 // 如果已拷貝字大於255,繼續下一字拷貝。
// 注:因前面已通過預讀對結束符進行判斷處理,
// 故已拷貝字低位元組不可能為0,所以已拷貝字
// <=255,說明其高位元組為0,拷貝結束
ja @loop
@ret:
pop eax
ret
@move0:
mov [eax], cl
pop eax
end;


我仔細分析了一下StrCopy2007比StrCopy7效率高的原因,主要有三個方面:

一、StrCopy7對Source進行了2次迴圈處理,一次是為了計算Source的長度而進行的掃描迴圈,另一次是拷貝迴圈,這是一種傳統的字串拷貝函式編碼思路;而StrCopy2007則是一次性迴圈處理,雖然看上去其迴圈過程中的程式碼有些“囉嗦”,但效率確實較高,也值得我們在處理類似問題上進行借鑑,這一點與語言沒多大關係;

二、說明彙編的字串處理指令效率並不高,我將StrCopy7的2句主要的字串處理語句用“囉嗦”程式碼進行了替換,在我的機器上拷貝速度一下就提高了38%(這個與硬體有關係)。

下面程式碼中註釋掉的是原語句,小寫彙編程式碼是替換語句:


function StrCopy_(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX
MOV EDI,EDX
MOV ECX,0FFFFFFFFH
XOR AL,AL
@loop1:
inc edi
dec ecx
cmp al, [edi - 1]
jne @loop1
// REPNE SCASB
NOT ECX
MOV EDI,ESI
MOV ESI,EDX
MOV EDX,ECX
MOV EAX,EDI
SHR ECX,2
push eax
@loop2:
mov eax, [esi]
mov [edi], eax
add esi, 4
add edi, 4
loop @loop2
pop eax
// REP MOVSD
MOV ECX,EDX
AND ECX,3
REP MOVSB
POP ESI
POP EDI
end;
function StrCopy_(Dest: PChar; const Source: PChar): PChar;
asm
PUSH EDI
PUSH ESI
MOV ESI,EAX
MOV EDI,EDX
MOV ECX,0FFFFFFFFH
XOR AL,AL
@loop1:
inc edi
dec ecx
cmp al, [edi - 1]
jne @loop1
// REPNE SCASB
NOT ECX
MOV EDI,ESI
MOV ESI,EDX
MOV EDX,ECX
MOV EAX,EDI
SHR ECX,2
push eax
@loop2:
mov eax, [esi]
mov [edi], eax
add esi, 4
add edi, 4
loop @loop2
pop eax
// REP MOVSD
MOV ECX,EDX
AND ECX,3
REP MOVSB
POP ESI
POP EDI
end;

三、目標串Dest的地址偶數對齊。因為StrCopy2007是按字進行拷貝的,Dest地址的奇偶對拷貝速度有一定影響,去掉StrCopy2007中有關Dest奇偶調整的程式碼後,在我的機器上測試,奇數Dest地址與偶數Dest地址拷貝速度相差%14左右;不僅如此,Source地址的奇偶性也影響拷貝速度,其相差為7%左右;如果Dest和Source的地址都是奇數,拷貝速度則相差28%以上。StrCopy2007只調整了Dest地址的奇偶性,因為Source的奇偶性沒法調整。

很顯然,上面第一點是最主要的原因,其次是第三點,這2個原因屬於程式設計思路(或演算法)問題,與語言無多大關係,這也是我分析這段程式碼最大的收穫。為了證明這一點,按照上面2段程式碼的思路,用C++分別寫了2個拷貝函式和測試程式碼,採用BCB6編譯器編譯,我的機器上的測試結果是StrCopy2的拷貝速度是StrCopy1的1.6 - 1.9倍。把這2段C++程式碼貼在下面作為本文的結尾:

view plaincopy to clipboardprint?
char* StrCopy1(char *dest, const char *source)
{
char *pd = dest;
char *pe, *ps = (char*)source;
int ext, size;

while (*ps ++);
size = ps - source;
ext = size & 3;
ps = (char*)source;
pe = ps + (size & 0xfffffffc);
for (; ps < pe; pd += 4, ps += 4)
*(long*)pd = *(long*)ps;
for (; ext > 0; ext --)
*pd ++ = *ps ++;
return dest;
}

char* StrCopy2(char *dest, const char *source)
{
char *pd = dest;
int s = source - dest;

if ((unsigned)pd & 1)
{
*pd = *source;
if (*pd == 0)
return dest;
pd ++;
}
while (true)
{
if (*(pd + s) == 0)
break;
*(short*)pd = *(short*)(pd + s);
if (*(unsigned short*)pd <= 255)
return dest;
pd += 2;
}
*pd = 0;
return dest;
}

#define TESTSIZE (80 * 1024 * 1024 + 2)

void __fastcall TForm1::Button1Click(TObject *Sender)
{
unsigned long time1, time2;
char *dest = new char[TESTSIZE];
char *source = new char[TESTSIZE];
char *p = source;
char *pe = p + TESTSIZE - 1;

randomize();
while (p < pe)
{
*p = random(255);
if (*p >= 32) p ++;
}
*p = 0;

time1 = GetTickCount();
StrCopy1(dest, source);
time1 = GetTickCount() - time1;

time2 = GetTickCount();
StrCopy2(dest, source);
time2 = GetTickCount() - time2;

delete[] source;
delete[] dest;

ShowMessage("StrCopy1: " + String(time1) + " StrCopy2: " + String(time2));
}
char* StrCopy1(char *dest, const char *source)
{
char *pd = dest;
char *pe, *ps = (char*)source;
int ext, size;

while (*ps ++);
size = ps - source;
ext = size & 3;
ps = (char*)source;
pe = ps + (size & 0xfffffffc);
for (; ps < pe; pd += 4, ps += 4)
*(long*)pd = *(long*)ps;
for (; ext > 0; ext --)
*pd ++ = *ps ++;
return dest;
}

char* StrCopy2(char *dest, const char *source)
{
char *pd = dest;
int s = source - dest;

if ((unsigned)pd & 1)
{
*pd = *source;
if (*pd == 0)
return dest;
pd ++;
}
while (true)
{
if (*(pd + s) == 0)
break;
*(short*)pd = *(short*)(pd + s);
if (*(unsigned short*)pd <= 255)
return dest;
pd += 2;
}
*pd = 0;
return dest;
}

#define TESTSIZE (80 * 1024 * 1024 + 2)

void __fastcall TForm1::Button1Click(TObject *Sender)
{
unsigned long time1, time2;
char *dest = new char[TESTSIZE];
char *source = new char[TESTSIZE];
char *p = source;
char *pe = p + TESTSIZE - 1;

randomize();
while (p < pe)
{
*p = random(255);
if (*p >= 32) p ++;
}
*p = 0;

time1 = GetTickCount();
StrCopy1(dest, source);
time1 = GetTickCount() - time1;

time2 = GetTickCount();
StrCopy2(dest, source);
time2 = GetTickCount() - time2;

delete[] source;
delete[] dest;

ShowMessage("StrCopy1: " + String(time1) + " StrCopy2: " + String(time2));
}

當然,由於現在計算機處理速度很快,且一般程式中極少有大容量的字串拷貝,對一般字串拷貝來說,StrCopy7和StrCopy2007的拷貝速度差距可忽略不計,本文的主要目的在於對優秀程式碼的欣賞和借鑑。

由於水平有限,程式碼分析可能有錯誤,望指出,不甚感激。郵件地址:maozefa@hotmail.com

相關文章