一段看上去“貌不惊人”的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

[转]delphi 有授权许可的字符串拷贝函数源码的更多相关文章

  1. OAuth2.0 授权许可 之 Authorization Code

    写在前面: 在前一篇博客<OAuth2.0 原理简介>中我们已经了解了OAuth2.0的原理以及它是如何工作的,那么本篇我们将来聊一聊OAuth的一种授权许可方式:授权码(Authoriz ...

  2. C语言字符串拷贝

    C语言字符串拷贝利用指针操作,要清楚知道指针的指向 代码如下: #include <stdio.h> #include <assert.h> #include <stri ...

  3. C++内存问题大集合(指针问题,以及字符串拷贝问题,确实挺危险的)

    作者:rendao.org,版权声明,转载必须征得同意. 内存越界,变量被篡改 memset时长度参数超出了数组长度,但memset当时并不会报错,而是操作了不应该操作的内存,导致变量被无端篡改 还可 ...

  4. 字符串拷贝函数strcpy写法_转

    Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> ...

  5. 【转】parallels desktop 11 授权许可文件删除方法

    原文网址:http://www.macappstore.net/tips/parallels-desktop-uninstall/ 很多同学在安装parallels desktop 11破解版后显示还 ...

  6. Com组件的内存分配和释放,CredentialProvider SHStrDup 字符串拷贝问题

    一.简单介绍 熟悉CredentialProvider的同学应该知道,他为一个Com组件,于是,在这里的内存分配(字符串拷贝)的一系列操作就要依照con的标准来. 二.Com组件的内存分配和释放 CO ...

  7. delphi string.split 按照任意字符串分割语句

    delphi string.split 按照任意字符串分割语句 1.就是把一个指定的字符串用指定的分割符号分割成多个子串,放入一个 TStringList 中 function ExtractStri ...

  8. C语言——常用标准输入输出函数 scanf(), printf(), gets(), puts(), getchar(), putchar(); 字符串拷贝函数 strcpy(), strncpy(), strchr(), strstr()函数用法特点

    1 首先介绍几个常用到的转义符 (1)     换行符“\n”, ASCII值为10: (2)     回车符“\r”, ASCII值为13: (3)     水平制表符“\t”, ASCII值为 9 ...

  9. 实现字符串检索strstr函数、字符串长度strlen函数、字符串拷贝strcpy函数

    #include <stdio.h> #include <stdlib.h> #include <string.h> /* _Check_return_ _Ret_ ...

随机推荐

  1. vue-router两种模式,到底什么情况下用hash,什么情况下用history模式呢?

    转:https://segmentfault.com/q/1010000010340823/a-1020000010598395 为什么要有 hash 和 history 对于 Vue 这类渐进式前端 ...

  2. LeetCode(30):与所有单词相关联的字串

    Hard! 题目描述: 给定一个字符串 s 和一些长度相同的单词 words.在 s 中找出可以恰好串联 words 中所有单词的子串的起始位置. 注意子串要与 words 中的单词完全匹配,中间不能 ...

  3. 步步为营-68-asp.net简单练习(get set)

    1 加法计算器 using System; using System.Collections.Generic; using System.Linq; using System.Text; using ...

  4. C++传值、传引用

    C++传值.传引用 C++的函数参数传递方式,可以是传值方式,也可以是传引用方式.传值的本质是:形参是实参的一份复制.传引用的本质是:形参和实参是同一个东西. 传值和传引用,对大多数常见类型都是适用的 ...

  5. (转载)关于一些对location认识的误区

    原文:https://www.cnblogs.com/lidabo/p/4169396.html 关于一些对location认识的误区 1. location 的匹配顺序是“先匹配正则,再匹配普通”. ...

  6. .NetCore下使用IdentityServer4 & JwtBearer认证授权在CentOS Docker容器中运行遇到的坑及填坑

    今天我把WebAPI部署到CentOS Docker容器中运行,发现原有在Windows下允许的JWTBearer配置出现了问题 在Window下我一直使用这个配置,没有问题 services.Add ...

  7. hdu 1372 骑士从起点走到终点的步数 (BFS)

    给出起点和终点 求骑士从起点走到终点所需要的步数 Sample Inpute2 e4 //起点 终点a1 b2b2 c3a1 h8a1 h7h8 a1b1 c3f6 f6 Sample OutputT ...

  8. codeforces 750D New Year and Fireworks【DFS】

    题意:烟花绽放时分为n层,每层会前进ti格,当进入下一层是向左右45°分开前进. 问在网格中,有多少网格至少被烟花经过一次? 题解:最多30层,每层最多前进5格,烟花的活动半径最大为150,每一层的方 ...

  9. mvn2gradle

    mvn项目根目录下,运行 gradle init --type pom 备注: 1)确保build.gradle, settings.gradle不存在 2)gradle 3.1测试通过 3)修改bu ...

  10. 【Java】 剑指offer(22) 链表中倒数第k个结点

    正文 本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 输入一个链表,输出该链表中倒数第k个结点.为了符合大多数人的 ...