Швидкі WideString в Delphi. Прискорюємо операції з рядками, Різне, Програмування, статті

Трохи теорії


У Delphi є зручний механізм для роботи з рядковими даними. Для цього є кілька типів строкових змінних: AnsiString, WideString і UnicodeString. Вони зручні тим що, в операціях присвоювання та конкатенації, компілятор генерує код, який неявно виділяє або звільняє пам’ять під рядки, а також автоматично перетворює один тип даних в іншій.


AnsiString і UnicodeString – Це внутрішній формат подання рядка в Delphi . Для виділення пам’яті під рядок використовується власний, дуже продуктивний менеджер пам’яті. Також, при копіюванні рядків використовується підрахунок посилань без перерозподілу пам’яті. Таким чином, компілятор генерує максимально продуктивний код.


WideString – Це неявний формат BSTR і є стандартним рядковим типом в COM/DCOM. Це його основна перевага. Недоліком є ​​відсутність підрахунку посилань. Компілятор неявно використовує API-функції при операціях з даними цього типу. Тому операції з WideString дуже повільні.


По ряду об’єктивних причин багато проектів пишуться на старих версіях Delphi , В яких немає швидких UnicodeString. А підтримка юникода необхідна, ось і доводиться використовувати WideString.





Впроваджуємо механізм підрахунку посилань


В WideString є структура, в ній зберігається довжина рядка в байтах. Ця структура розміщена в пам’яті безпосередньо перед даними рядка. Для виділення і звільнення пам’яті під рядок замість системних API-функцій будемо використовувати власний менеджер пам’яті. При цьому ми самі можемо визначити структуру, додавши всі необхідні поля. Додамо лічильник посилань і спеціальний ідентифікатор, щоб відрізняти рядки створені нами від всіх інших рядків.





type
PWideStr = ^TWideStr;
TWideStr = record refcnt: integer; / / лічильник посилань id0: integer; / / наш ідентифікатор id1: integer; / / наш ідентифікатор id2: integer; / / наш ідентифікатор length: integer; / / розмір рядка (як і належить)
end;
const
str_id_0 = integer($96969696);
str_id_1 = integer($75757575);
str_id_2 = integer($38383838);
size_str = sizeof(TWideStr);





ПРИМІТКА

Дана структура задовольняє умові, що довжина рядка повинна бути безпосередньо перед самою рядком.


Ідентифікатор потрібний, щоб ми могли відрізняти нашу рядок від інших рядків. Тільки так ми можемо знати, для яких рядків можна використовувати підрахунок посилань.


В system.pas є безліч функцій, який компілятор викликає при операціях з рядками. Нам необхідно всього декілька.





function  _NewWideString(CharLength: Longint): Pointer;
procedure _WStrClr(var S);
procedure _WStrArrayClr(var StrArray; Count: Integer);
procedure _WStrAsg(var Dest: WideString; const Source: WideString);
procedure _WStrLAsg(var Dest: WideString; const Source: WideString);
procedure _WStrFromPWCharLen(var Dest: WideString; Source: PWideChar; CharLength: Integer);
procedure _WStrFromWChar(var Dest: WideString; Source: WideChar);
procedure _WStrFromPWChar(var Dest: WideString; Source: PWideChar);
function _WStrAddRef(var str: WideString): Pointer;

Можна замінити код цих функцій, а можна в режимі виконання програми перехопити їх і виконати свої функції. Другий метод більш універсальний, тому і виберемо його.


Щоб не було проблем з COM/DCOM, Також перехопимо системні функції:





function  SysAllocString(psz: POleStr): TBStr; stdcall;
procedure SysFreeString(bstr: TBStr); stdcall;
function SysReAllocString(var bstr: TBStr; psz: POleStr): Integer;
function SysAllocStringLen(psz: POleStr; len: Integer): TBStr;
function SysReAllocStringLen(var bstr: TBStr; psz: POleStr; len: Integer): Integer; function SysAllocStringByteLen(psz: PChar; len: Integer): TBStr; stdcall;

Базові функції


Їх усього три типи: виділення пам’яті, звільнення пам’яті та копіювання рядка.





/ / Ініціалізація рядка.
function doWStrAlloc(len: Integer): PWideStr; inline;
begin
GetMem(result, size_str + len + 2);
result.refcnt := 1;
result.Id0 := str_id_0;
result.Id1 := str_id_1;
result.Id2 := str_id_2;
result.length := len;
PWideChar(@PAnsiChar(result)[size_str+len])^ := #0;
end;
/ / Звільнення рядки
procedure doWStrFree(s: PWideStr); inline;
begin
if (s.Id2 = str_id_2) and
(s.Id1 = str_id_1) and
(s.Id0 = str_id_0)
then
if InterlockedDecrement(s.refcnt) = 0 then
FreeMem(s);
end;
procedure WStrFree(s: PWideStr); inline;
begin
if Assigned(s) then begin
Dec(s);
if (s.Id2 = str_id_2) and
(s.Id1 = str_id_1) and
(s.Id0 = str_id_0)
then
if InterlockedDecrement(s.refcnt) = 0 then
FreeMem(s);
end;
end;
/ / Копіювання рядка
function doWStrCopy(s: PWideStr): PWideStr; inline;
begin
if (s.Id2 = str_id_2) and
(s.Id1 = str_id_1) and
(s.Id0 = str_id_0)
then begin
InterlockedIncrement(s.refcnt);
result := s;
end
else begin
result := doWStrAlloc(s.length);
Move(PAnsiChar(s)[size_str], PAnsiChar(result)[size_str], s.length);
end;
end;
function WStrCopy(s: PWideStr): PWideStr; inline;
begin
if s = nil then
result := nil
else begin
Dec(S);
if (s.Id2 = str_id_2) and
(s.Id1 = str_id_1) and
(s.Id0 = str_id_0)
then begin
InterlockedIncrement(s.refcnt);
result := @PAnsiChar(s)[size_str];
end
else begin
result := @PAnsiChar(doWStrAlloc(s.length))[size_str];
Move(PAnsiChar(s)[size_str], result^, s.length);
end;
end;
end;
function WStrLCopy(s: PWideStr; len: integer): PWideStr; inline;
begin
result := doWStrAlloc(len);
Inc(result);
if Assigned(s) then
Move(s^, result^, len);
end;

Підставні функції


Всі підставні функції є обгортками над базовими функціями. Для зручності сприйняття імена підставних функцій будуть починатися на букву “х”.





// system.pas
function xWStrClr(var S: PWideStr): PWideStr;
begin
result := @S;
WStrFree(s);
S := nil;
end;
procedure xWStrAsg(var Dest: PWideStr; Source: PWideStr);
var
t : PWideStr;
begin
t := Dest;
if t <> Source then begin
WStrFree(t);
if Source = nil then
Dest := nil
else begin
Dec(Source);
t := doWStrCopy(Source);
Dest := @PAnsiChar(t)[size_str];
end;
end;
end;
function xWStrAddRef(var s: PWideStr): Pointer;
begin
result := WStrCopy(s);
end;
procedure xWStrArrayClr(s: PPWideStr; Count: Integer);
var
t : PWideStr;
begin
while Count > 0 do begin
t := s^;
WStrFree(t);
Inc(s);
Dec(count);
end;
end;
procedure xWStrFromPWCharLen(var Dest: PWideStr; Source: PWideStr; Len: Integer);
begin
WStrFree(Dest);
Dest := WStrLCopy(Source, Len*2);
end;
procedure xWStrFromWChar(var Dest: PWideStr; Source: WideChar);
var
t : PWideStr;
begin
if (Dest = nil) or (PWideChar(Dest)^ <> Source) then begin
WStrFree(Dest);
t := doWStrAlloc(2);
Inc(t);
Move(Source, t^, 2);
Dest := t;
end;
end;
procedure xWStrFromPWChar(var Dest: PWideStr; Source: PWideStr);
var
t : PWideStr;
begin
t := WStrLCopy(Source, WStrSize(PWideChar(Source)));
WStrFree(Dest);
Dest := t;
end;
function xNewWideString(Len: Longint): PWideStr;
begin
result := doWStrAlloc(Len*2);
Inc(result);
end;

// oleaut32.dll
procedure xSysFreeString(s: PWideStr); stdcall;
begin
WStrFree(s);
end;
function xSysAllocString(s: PWideStr): PWideStr; stdcall;
begin
result := WStrLCopy(s, WStrSize(PWideChar(s)));
end;
function xSysAllocStringLen(s: PWideStr; len: Integer): PWideStr; stdcall;
begin
result := WStrLCopy(s, len * 2);
end;
function xSysAllocStringByteLen (s: pointer; len: Integer): PWideStr; stdcall;
begin
result := WStrLCopy(s, len);
end;
function xSysReAllocStringLen(var p: PWideStr; s: PWideStr; len: Integer): LongBool; stdcall;
begin
if s <> p then begin
WStrFree(p);
p := WStrLCopy(s, len * 2);
end;
result := true;
end;


Код перехоплення


Перехоплення функцій здійснюватиметься методом сплайсингу. Це коли в початок коду перехоплюваних функції вставляємо перехід на нашу функцію. Зазвичай це команда jmp offset.





type
POffsJmp = ^TOffsJmp;
TOffsJmp = packed record
code : byte; //$E9
offs : cardinal;
end;
procedure HookCode(Src, Dst: pointer); inline;
begin
if Assigned(Src) then begin
poffsjmp(Src).code := $E9;
poffsjmp(Src).offs := cardinal(Dst) – cardinal(Src) – 5;
end;
end;
procedure HookProc(handle: cardinal; Name: PAnsiChar; Hook: pointer); inline;
begin
HookCode(GetProcAddress(handle, Name), Hook);
end;

Адреси функцій в system.pas можна дізнатися, тільки використовуючи вставки асемблера.





function pWStrClr: pointer;
asm
mov eax, OFFSET System.@WStrClr
end;
function pWStrAddRef: pointer;
asm
mov eax, OFFSET System.@WStrAddRef
end;
function pWStrAsg: pointer;
asm
mov eax, OFFSET System.@WStrAsg
end;
function pWStrLAsg: pointer;
asm
mov eax, OFFSET System.@WStrLAsg
end;
function pWStrArrayClr : pointer;
asm
mov eax, OFFSET System.@WStrArrayClr
end;
function pWStrFromPWCharLen : pointer;
asm
mov eax, OFFSET System.@WStrFromPWCharLen
end;
function pWStrFromWChar : pointer;
asm
mov eax, OFFSET System.@WStrFromWChar
end;
function pWStrFromPWChar : pointer;
asm
mov eax, OFFSET System.@WStrFromPWChar
end;
function pNewWideString : pointer;
asm
mov eax, OFFSET System.@NewWideString
end;

Перед перехопленням необхідно дати дозвіл на запис пам’ять, де знаходяться перехоплюваних функції.





procedure FastWideStringInit;
var
handle : cardinal;
protect : cardinal;
mem : TMemoryBasicInformation;
begin / / Отримати початковий адресу і розмір секції пам’яті
VirtualQuery(pWStrAddRef, mem, sizeof(mem)); / / Дозволити запис
VirtualProtect(mem.AllocationBase, mem.RegionSize, PAGE_EXECUTE_READWRITE, protect);
HookCode(pWStrClr, @xWStrClr);
HookCode(pWStrAsg, @xWStrAsg);
HookCode(pWStrLAsg, @xWStrAsg);
HookCode(pWStrAddRef, @xWStrAddRef);
HookCode(pWStrArrayClr, @xWStrArrayClr);
HookCode(pWStrFromPWCharLen, @xWStrFromPWCharLen);
HookCode(pWStrFromWChar, @xWStrFromWChar);
HookCode(pWStrFromPWChar, @xWStrFromPWChar);
HookCode(pNewWideString, @xNewWideString);
/ / Відновити атрибут захисту пам’яті
VirtualProtect(mem.AllocationBase, mem.RegionSize, protect, protect);
handle := GetModuleHandle(oleaut);
if handle = 0 then
handle := LoadLibrary(oleaut);
VirtualQuery(GetProcAddress(handle, “SysAllocString”), mem, sizeof(mem));
VirtualProtect(mem.AllocationBase, mem.RegionSize, PAGE_EXECUTE_READWRITE, protect);
HookProc(handle, “SysAllocString”, @xSysAllocString);
HookProc(handle, “SysAllocStringLen”, @xSysAllocStringLen);
HookProc(handle, “SysAllocStringByteLen”, @xSysAllocStringByteLen);
HookProc(handle, “SysReAllocStringLen”, @xSysReAllocStringLen);
HookProc(handle, “SysFreeString”, @xSysFreeString);
VirtualProtect(mem.AllocationBase, mem.RegionSize, protect, protect);
end;

Для ініціалізації нашого механізму досить викликати FastWideStringInit(). І чим раніше, тим краще.



Тестування


Для тестування потрібен код, в який в основному складається з операцій з рядками. Під рукою опинилася часто використовувана бібліотека WideStrings.pas. Там є чудовий клас TWideStringList. А в ньому властивість





property Text: WideString read GetTextStr write SetTextStr;

Засікти час виконання TWideStringList.GetTextStr() і TWideStringList.SetTextStr() до і після ініціалізації швидких WideString. Ось частина коду.





const
rep_count := 40;
procedure TestWideString(var s: widestring);
var
i : integer;
begin
with TWideStringList.Create do
try
for i := 0 to rep_count do begin
Text := s;
s := Text;
end;
finally
Free;
end;
end;

Приріст швидкості становить близько 80%. І це тільки за рахунок механізму підрахунку посилань.



Підводні камені


Розглянемо по кроках наступний приклад.





procedure Test1;
var
s1, s2 : WideString;
begin
s1 := “test”; // 1
s2 := s1; // 2
s2[1] := “b”; // 3
end;


  1. Привласнюючи s1 := “test” , виділяємо пам’ять.
  2. Привласнюючи s2 := s1, виділяємо пам’ять.
  3. Міняємо значення першого символу s2[1] := “B”. У підсумку s2 = best “, а s1 = test” .

А що буде, коли включимо підрахунок посилань?





procedure Test2;
var
s1, s2 : WideString;
begin
FastWideStringInit; // 1
s1 := “test”; // 2
s2 := s1; // 3
s2[1] := “b”; // 4
end;


  1. Ініціалізували швидкі WideString
  2. Привласнюючи s1 := “test” , виділяємо пам’ять.
  3. Привласнюючи s2 := s1, ми тільки збільшуємо лічильник. s2 вказує на ту саму ділянку пам’яті, що і s1.
  4. Міняємо значення першого символу s2[1] := “B”. У підсумку s2 = best “, і s1 = best” .

Ось цього ми і не чекали.


Розглянемо реальний приклад з життя і варіант його рішення.





const
shlwapi32 = “SHLWAPI.DLL”;
{Функція виділяє шлях з імені файлу, шляхом заміни подальшого за шляхом символу на # 0}
function PathRemoveFileSpecW(pszPath: PWideChar): BOOL; stdcall; external shlwapi32;
{А це наша зручна обгортка}
function MyPathRemoveFileSpec(s: WideString): WideString;
begin
result := s;
if PathRemoveFileSpecW(PWideChar(result)) then
result := PWideChar(result);
end;
var
a : widestring;
b : widestring;
begin
FastWideStringInit;
a := “c:myfoldermyfile.txt”;
b := MyPathRemoveFileSpec(a);
end;

Функція PathRemoveFileSpecW() якщо вдало відпрацює, модифікує рядок result “C: myfoldermyfile.txt” на “c: myfolder” # 0 “myfile.txt”;


Операція result := PWideChar(result) виділить нову пам’ять, і скопіює в неї “c: myfolder”.


В результаті, b = “c:myfolder” , а = “c:myfolder”#0″myfile.txt” .


Мінлива a зіпсована і якщо її використання далі приведе до невизначених ситуацій. А все тому, що на момент виконання PathRemoveFileSpecW() змінні a, s і result вказували на одну і ту ж саму рядок в пам’яті. Значить, нам треба вміти копіювати без використання підрахунку посилань. А робиться це просто, от так.





function MyPathRemoveFileSpec(s: WideString): WideString;
begin result: = s + “”; / / при конкатінаціі завжди сода нова копія рядки
if PathRemoveFileSpecW(PWideChar(result)) then
result := PWideChar(result);
end;

Дана реалізація функції буде працювати без вищеописаної проблеми.



Примітки


Даний код писався на Delphi  2007. Для інших версій, можливо, доведеться код трохи модифікувати. Це стосується інструкцій inline і назв функцій з system.pas.


Зауваж, деініціалізаціі механізму немає. Якщо він запущений, то повинен працювати до кінця, поки є остання WideString в пам’яті. Також бажано, щоб ініціалізація була якомога раніше. Наприклад, додайте в секції initialization того юніта, який раніше за всіх ініціалізується.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*