Выжимаем из Delphi все возможное



Зачем нужна эта статья

В кругах низкоуровневых программистов существует устоявшееся мнение о том, что Delphi полный остой, так как не годится для системного программирования. Объясняют это обычно тем, что компилятор генерирует медленный и большой код, а средний размер пустой формы с кнопкой - 400 килобайт. Обычно этим аргументация обычно и заканчивается (иногда просто говорят что дельфи дерьмо вообще без всякой аргументации). На форумах существуют "священные" войны между поклонниками С++ и Delphi, где каждый доказывает что его любимый язык лучше. Поклонники С++ обычно кричат про супернавороченный синтаксис и мощьные возможности ООП (при этом утверждая, что в системном программировании все это незаменимо!), а поклонники Delphi опять же кричат про те возможности ООП которых нет в С++ да и еще про то, что на дельфи все пишется куда проще. Мо моему мнению - это просто крики ламеров, так как по их словам можно заключить, что ни та ни другая сторона обычно ни про Delphi ни про C++ ничего толком не знает.

Эта статья будет посвящена приемам системного программирования на Delphi, тут мы будем делать на дельфи то, что многие считают невозможным. Эта статья для тех, кто пишет программы на Delphi и при этом хочет добиться максимальной эффективности кода, но не боится вложить в это определенный труд. По оптимизации кода в С++ написано весьма немало статей, а про оптимизацию в Delphi хороших статей просто нет. Видно все считают - что никакая оптимизация здесь не нужна. Тем, кого устраивает 400 килобайтный размер пустой формы с кнопкой статью читать я не рекомендую (все равно толку то..), а тем кто упорно отстаивает мнение о ненужности дельфи лучше тоже не читать, чтобы не расстраивать нервы и не развеивать священные заблуждения.


Немного о генерируемом компилятором коде

Для начала следует проверить утверждение, что компилятор Delphi генерирует много лишнего и неэффективного кода. Для этого я напишем функцию копирующую файл с по частям, писать будем естественно с применением API. Вот что у меня получилось:

procedure CopyFile(Source, Destination: PChar); stdcall;
const
 BlockSize = 65536;
var
 sFile, dFile: dword;
 Size, dSize, Bytes: dword;
 Buff: pointer;
begin
 sFile := CreateFile(Source, GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
 if sFile <> INVALID_HANDLE_VALUE then
  begin
   Size  := GetFileSize(sFile, nil);
   dSize := BlockSize;
   Buff := VirtualAlloc(nil, BlockSize, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE);
   dFile := CreateFile(Destination, GENERIC_WRITE, 0, nil, CREATE_NEW, 0, 0);
   if dFile <> INVALID_HANDLE_VALUE then
    begin
     while Size > 0 do
      begin
       if dSize > Size then dSize := Size;
       ReadFile(sFile, Buff^, dSize, Bytes, nil);
       WriteFile(dFile, Buff^, dSize, Bytes, nil);
       Dec(Size, dSize);
      end;
     CloseHandle(dFile);
    end;
   VirtualFree(Buff, 0, MEM_RELEASE);
   CloseHandle(sFile);
  end;
end;

Этот код я вставил в программу, скомпилировал и дизассемблировал в IDA. Вот откомментированый листинг из IDA:

CopyFile	proc near	

Buff		= dword	ptr -0Ch
NumberOfBytesRead= dword ptr -8
dFile		= dword	ptr -4
Source		= dword	ptr  8
Destination	= dword	ptr  0Ch

		push	ebp
		mov	ebp, esp
		add	esp, 0FFFFFFF4h
		push	ebx
		push	esi
		push	edi
		push	0		; hTemplateFile
		push	0		; dwFlagsAndAttributes
		push	3		; dwCreationDisposition
		push	0		; lpSecurityAttributes
		push	0		; dwShareMode
		push	80000000h	; dwDesiredAccess
		mov	eax, [ebp+Source]
		push	eax		; lpFileName
		call	CreateFileA
		mov	edi, eax
		cmp	edi, 0FFFFFFFFh
		jz	@@End
		push	0		; lpFileSizeHigh
		push	edi		; hFile
		call	GetFileSize
		mov	esi, eax
		mov	ebx, 10000h
		push	4
		push	3000h
		push	10000h
		push	0
		call	VirtualAlloc
		mov	[ebp+Buff], eax
		push	0		; hTemplateFile
		push	0		; dwFlagsAndAttributes
		push	1		; dwCreationDisposition
		push	0		; lpSecurityAttributes
		push	0		; dwShareMode
		push	40000000h	; dwDesiredAccess
		mov	eax, [ebp+Destination]
		push	eax		; lpFileName
		call	CreateFileA
		cmp	eax, 0FFFFFFFFh
		jz	short F1Close
		mov	[ebp+dFile], eax
		test	esi, esi
		jbe	short F2Close

@@While:				
		cmp	esi, ebx
		jnb	short Next
		mov	ebx, esi

Next:				
		push	0		; lpOverlapped
		lea	eax, [ebp+NumberOfBytesRead]
		push	eax		; lpNumberOfBytesRead
		push	ebx		; nNumberOfBytesToRead
		mov	eax, [ebp+Buff]
		push	eax		; lpBuffer
		push	edi		; hFile
		call	ReadFile
		push	0
		lea	eax, [ebp+NumberOfBytesRead]
		push	eax
		push	ebx
		mov	eax, [ebp+Buff]
		push	eax
		mov	eax, [ebp+dFile]
		push	eax
		call	WriteFile
		sub	esi, ebx
		test	esi, esi
		ja	short @@While

F2Close:			
		mov	eax, [ebp+dFile]
		push	eax		; hObject
		call	CloseHandle

F1Close:				
		push	8000h
		push	0
		mov	eax, [ebp+Buff]
		push	eax
		call	VirtualFree
		push	edi		; hObject
		call	CloseHandle

@@End:					
		pop	edi
		pop	esi
		pop	ebx
		mov	esp, ebp
		pop	ebp
		retn	8
CopyFile	endp

Ну и где же куча лишнего кода о котором некоторые так любят говорить? Все просто и красиво. Из этого можно сделать вывод, что компилятор генерирует вполне приличный код, размер которого весьма невелик. То что сгенерировал компилятор весьма похоже на аналогичный код написанный вручную на ассемблере. Тем более, некоторые люди не знающие ассемблера, но пытающиеся что-то на нем писать иногда такое выдают, что любые ошибки компилятора покажутся мелочью :). Так почему программы написанные на дельфи такие большие? Откуда все-таки берется лишний код, если компилятор его не генерирует. Сейчас мы разберем этот вопрос подробнее.


ООП - двигатель прогресса

ООП - весьма модное в настоящее время направление развития индустрии программирования. Цель ООП - упростить написание программ и сократить сроки их разработки, и несомненно с этой целью ООП прекрасно справляется. Большинство программистов пишущих на С++ или Delphi прикладные приложения, не мыслят даже о самой возможности программирования без ООП. Если вы относитесь к их группе, немедленно бросайте читать статью, так как она не для вас.

Взглянем на ООП глазами системного программиста. ООП дает простоту написания, это плюс, но у него есть и обратная сторона - это качество генерируемого кода. Допустим, у нас есть класс который наследуется от другого класса. При создании объекта этого класса компилятор будет вынужден включить в его состав также код родительского класса полностью, так как нет возможности определить, какие методы классов использоваться не будут. А если у нас будет целое дерево наследования классов (как это обычно и бывает в реальных программах), то код этого дерева будет полностью включен в состав программы, и от этого никуда не денешься. Вызов методов класса производится через таблицу, что увеличивает время вызова. А если метод наследуется от родителя в десятом поколении, то и вызов пройдет через десять таблиц, прежде чем достигнет обрабатывающего его кода. Получается, что вместе с кучей мертвого кода мы получаем еще низкую эффективность того, что работает.

Хороший пример применения ООП - это библиотека VCL в дельфи. Этот пример в полной мере демонстрирует все достоинства и недостатки ООП. С одной стороны - черезвычайная простота написания программ, с другой - огромнейшее количество мертвого кода и ужасно низкая его производительность.

Теперь мы нашли источник всех бед, вот она причина большого размера программ написанных на деьфи, это ООП, и в частности VCL. Некоторые прочитав это зададутся вопросом, почему программа написанная на дельфи с применением VCL занимает гораздо больше места, чем программа написаная на VB, или на VC с применением MFC. Ответ прост - потому, что великая и ужасная фирма Micro$oft приложила к этому свою лапу. MFC и рунтайм библиотеки в VB занимают ничуть не меньше места, просто они скомпилены в DLL и входят в поставку Windows, а значит их код не приходится таскать с собой в программах, а программа на Delphi содержит весь необходимый для своей работы код. В защиту Borland можно сказать то, что такая возможность присутствует и в Delphi. Нужно просто в настройках проекта поставить галочку "Build with runtime packages", тогда размер программы значительно уменьшится, но она потребует наличия соответствующих Runtime библиотек. Так как Borland не выпускает Windows, то естественно эти библиотеки в поставку винды не входят, но в этом надо винить не Борланд, а монопольную политику мелкософта.

Любители ООП желающие разрабатывать программы в визуальном режиме могут использовать KOL. Это попытка сделать что-то типа VCL, но с учетом недостатков ООП. Средний размер пустой формы с кнопкой на KOL - 35кб, что уже лучше, но к сожалению для серьезных приложений эта библиотека не подходит, так как часто глючит. Да и решение это половинчатое, те кто хочет добиться действительно высокой эффективности коды должны принять другое решение - забыть про ООП и все что с ним связано раз и навсегда. Писать программы придется только на чистом API.

Из всего вышесказанного можно сделать вывод, что ООП двигатель прогресса, но с точки зрения системного программиста ООП двигает прогресс только назад.


Виновник номер два

Создадим в Delphi пустой проект, заведомо не содержащий никакого полезного кода:

program Sample;

begin

end.

После компиляции в Delphi 7 мы получаем экзешник размером в 13,5 кб. Почему так много? Ведь в программе то ничего нет. Ответ на этот вопрос опять поможет дать IDA. Дизассемблируем полученный экзешник и посмотрим, что собственно он содержит. Точка входа в программу будет выглядеть так:

		public start
start:
		push	ebp
		mov	ebp, esp
		add	esp, 0FFFFFFF0h
		mov	eax, offset ModuleId
		call	_InitExe
		; здесь мог бы быть наш код  
		call	_HandleFinally
CODE		ends

Весь лишний код находится функциях _InitExe и _HandleFinally. Связано это с тем, что к каждой Delphi программе неявно подключается код входящий в состав RTL (Run Time Library). RTL нужна для поддержки таких возможностей языка как ООП, работа со строками (string), специфичные для паскаля функции (AssignFile, ReadLn, WriteLn e.t.c.). InitExe выполняет инициализацию всего этого добра, а HandleFinally обеспечивает корректное освобождение ресурсов.

Сделано это опять же для упрощения жизни программистам, и применение RTL иногда оправдано, так как может не понизить, а повысить эффективность кода. Например в состав RTL входит менеджер кучи, который позволяет быстро выделять и освобождать маленькие блоки памяти. По своей эффективности он в три раза превосходит системный. Работа со строками реализована в RTL тоже довольно неплохо с точки зрения производительности генерируемого кода, но с точки зрения увеличения размера файла, RTL - виновник номер два (после ООП).


Уменьшаем размер

Если минимальный размер в 13.5 кб вас не устраивает, то будет убирать Delphi RTL. Весь код RTL находится в двух файлах: System.pas и SysInit.pas. К сожалению, компилятор подключает их к программе в любом случае, поэтому единственное что можно сделать - это удалить из этих модулей весь код, без которого программа может работать, и перекомпилить модули, а полученные DCU файлы положить в папку с программой.

Файл System.pas содержит основной код RTL и поддержки классов, но все это мы выбросим. Минимальное содержимое этого файла будет выглядеть так:

unit System;

interface

procedure _HandleFinally;

type
 TGUID = record
  D1: LongWord;
  D2: Word;
  D3: Word;
  D4: array [0..7] of Byte;
 end;

 PInitContext = ^TInitContext;
 TInitContext = record
    OuterContext:   PInitContext;   
    ExcFrame:       Pointer;          
    InitTable:      pointer;     
    InitCount:      Integer;          
    Module:         pointer;       
    DLLSaveEBP:     Pointer; 
    DLLSaveEBX:     Pointer; 
    DLLSaveESI:     Pointer;   
    DLLSaveEDI:     Pointer;   
    ExitProcessTLS: procedure;     
    DLLInitState:   Byte;             
 end;

implementation

procedure _HandleFinally;
asm
end;

end.

Описания структуры TGUID компилятор требует в любом случае, и без ее наличия компилировать модуль отказывается. TInitContext понадобиться, если мы будем компилировать DLL, без описания этой структуры линкер собирать DLL отказывается. HandleFinally - процедура освобождения ресурсов RTL, компилятору она тоже необходима, хотя может быть пустой.

Теперь урежем файл SysInit.pas, который содержит код инициализации и завершения работы RTL и управляет поддержкой пакетов. Минимальный файл SysInit.pas будет выглядеть так:

unit SysInit;

interface
procedure _InitExe;
procedure _halt0;
procedure _InitLib(Context: PInitContext);

var
  ModuleIsLib: Boolean;        
  TlsIndex: Integer = -1;       
  TlsLast: Byte; 

const
  PtrToNil: Pointer = nil;   

implementation

procedure _InitLib(Context: PInitContext);
asm
end;

procedure _InitExe;
asm
end;

procedure _halt0;
asm
end;

end. 

InitExe - процедура инициализации RTL для EXE файлов, InitLib - для DLL, halt0 - завершение работы программы. Все остальное же просто требуется компилятором.

Но те лишние структуры и переменные, которые пришлось оставить в выходной файл включаться не будут и никак не повлияют на его размер. Теперь положим эти два файла в папку с проектом и скомпилируем их из коммандной строки.

dcc32.exe -Q system.pas sysinit.pas -M -Y -Z -$D- -O

Теперь мы наконец избавились от RTL, попробуем скомпилировать пустой проект, и получаем экзешник размером в 3,5 кб. Откуда взялся такой размер в пустом проекте? Борландовский линкер создает в исполнимом файле 6 секций, и при выравнивании секций в файле по 512 байт + размер PE заголовка, мы как раз получаем размер в 3.5 кб.

Но в добавок к малому размеру мы получаем определенные неудобства, так как заголовочные файлы на WinAPI идущие с Delphi мы использовать не сможем, вместо них придется писать свои. Но это не трудно, так как описания используемых API можно брать с борландовских хедеров и переносить в свои по мере необходимости. С проблемой увеличения размера можно столкнуться тогда, когда в составе проекта есть несколько PAS файлов. Борландовский линкер в такой ситуации может для выравнивания кода вставлять в файл пустые участки. Чтобы этого избежать - нужно всю программу (включая определения API) помещать в один файл. Это весьма неудобно, поэтому лучше воспользоваться директивой препроцессора $INCLUDE и разнести код на несколько inc файлов. Тут может встретиться еще одна проблема - повторные вставки одного и того же кода (когда несколько inc файлов подключают один и тот же inc), компилятор в таких случаях компилировать откажется. Чтобы этого избежать нужно воспользоваться директивами условной компиляции, после чего любой inc файл будет иметь вид:

{$ifndef win32api}
{$define win32api}
 
 // здесь идет наш код  

{$endif}

Таким образом можно писать без RTL достаточно сложные программы и забыть о неудобствах.


Можно еще меньше!

Наверняка минимальный размер экзешника в 3.5кб удовлетворит не всех, но если постараться, то можно добиться еще большего уменьшения размера. Для этого нужно отказаться от удобств работы с борландовским линкером и собирать исполнимые файлы линкером от Microsoft. Но к сожалению, здесь нас ждет одна загвоздка. Проблема в том, что основным рабочим форматом мелкософтовского линкера является COFF, но он может понимать и интеловский OMF. Но программисты борланда (видать с целью создать несовместимость с микрософт) в версиях Delphi выше третьей изменили генерируемый формат obj файлов так, что теперь он несовместим с Intel OMF. Тоесть теперь существуют два вида OMF: Intel OMF и Borland OMF. Программы способной конвертировать объектные файлы из формата Brland OMF в COFF или Intel OMF я не нашел. Поэтому придется использовать компилятор от Delphi 3, который генерирует стандартный Intel OMF объектный файл. Импорт используемых API нам тоже придется описывать вручную, но его описание несколько отличается. Для начала возьмем библиотеку импорта user32.lib из состава Visual C++ и откроем ее в HEX редакторе. Имена функций библиотеки имеют такой вид: "[email protected]", где после @ идет размер передаваемых параметров. Следовательно объявлять функции мы будем таким образом:

function MessageBoxA(hWnd: cardinal; lpText, lpCaption: PChar; uType: Cardinal): Integer; 
 stdcall; external 'user32.dll' name '[email protected]';

Попробуем теперь написать HelloWorld как можно меньшего размера. Для этого создаем проект такого типа:

unit HelloWorld;

interface

Procedure Start;

implementation

function MessageBoxA(hWnd: cardinal; lpText, lpCaption: PChar; uType: Cardinal): Integer;
 stdcall; external 'user32.dll' name '[email protected]';

Procedure Start;
begin
  MessageBoxA(0, 'Hello world!', nil, 0);
end;

end.

Тип модуля UNIT нужен для того, чтобы компилятор генерировал в объектном файле символьные имена объявленных процедур. В нашем случае это будет процедура Start, которая будет являться точкой входа в программу. Теперь компилируем проект следующей строкой:

dcc32.exe -JP -$A-,B-,C-,D-,G-,H-,I-,J-,L-,M-,O+,P-,Q-,R-,T-,U-,V-,W+,X+,Y- HelloWorld.pas

После компиляции получим файл HelloWorld.obj, который откроем в HEX редакторе и посмотрим во что превратилась наша точка входа. У меня получилось Start$qqrv. Это имя нужно указать как точку входа при сборке исполнимого файла. И наконец выполним сборку:

link.exe /ALIGN:32 /FORCE:UNRESOLVED /SUBSYSTEM:WINDOWS 
/ENTRY:Start$qqrv HelloWorld.obj user32.lib /out:Hello.exe

В результате мы получаем работающий HelloWorld размером в 832 байта! Я думаю, что этот размер удовлетворит любого. Попробуем теперь дизассемблировать этот файл в IDA и поискать лишний код:

; Attributes: bp-based frame
; char Text[]
Text		db 'Hello world!',0  

		public start
start		proc near
		push	0		; uType
		push	0		; lpCaption
		push	offset Text	; lpText
		push	0		; hWnd
		call	MessageBoxA
		retn
start		endp

Как мы видим - ни одного байта лишнего кода!

Пишем драйвер на Delphi

Используя эту методику можно не только писать очень маленькие программы, можно даже сделать то, что раньше считалось невозможным - написать на Delphi драйвер режима ядра. Об этом даже есть статья на RSDN, и всем интересующимся рекомендую ее прочитать. Здесь же я приведу пример простейшего драйвера и содержимое make.bat для его сборки.

Файл Driver.pas:

unit Driver;

interface

function DriverEntry(DriverObject, RegistryPath: pointer): integer; stdcall;

implementation

function DbgPrint(Str: PChar): cardinal; cdecl; external 'ntoskrnl.exe' name '_DbgPrint';

function DriverEntry(DriverObject, RegistryPath: pointer): integer; 
begin
 DbgPrint('Hello World!');
 Result := -1;
end;

end.

Файл make.bat:

dcc32.exe -JP -$A-,B-,C-,D-,G-,H-,I-,J-,L-,M-,O+,P-,Q-,R-,T-,U-,V-,W+,X+,Y- Driver.pas
link.exe /DRIVER /ALIGN:32 /BASE:0x10000 /SUBSYSTEM:NATIVE /FORCE:UNRESOLVED 
/ENTRY:DriverEntry$qqspvt1 Driver.obj ntoskrnl.lib /out:Driver.sys

Для компиляции нам понадобиться файл ntoskrnl.lib из DDK. После компиляции мы получим драйвер размером 1 кб, который выводит сообщение Hello World в отладочную консоль и возвращает ошибку, а поэтому в памяти не остается, что не требует определения функции DriverUnload. Для запуска драйвера можно использовать KmdManager от Four-F, посмотреть на результаты его работы можно в софтайсе или DbgView.

При наличии некоторых знаний можно писать на Delphi вполне полноценные драйвера. Но здесь есть одна большая проблема - отсутствие DDK. Для написания драйверов нужны заголовочные файлы на API ядра и описания большого количества системных структур. Все это богатство есть как для С (от Microsoft), так и для MASM32 (от Four-F). Есть слух, что DDK для паскаля уже существует, но его автор продает его за деньги и сильно этот факт не афиширует. Но я думаю, что найдутся энтузиасты, которые перепишут DDK на паскаль и выложат для всеобщего использования. Другой проблемой является то, что большинство примеров связанных с системным программированием написаны на си, поэтому на каком бы языке вы не писали свои программы, а си знать придется. Это конечно не означает, что придется изучать С++ в полном его объеме. Для понимания системных программ хватит базовых знаний синтаксиса си, а все остальное используется в только в прикладных програмах которые нас совершенно не интересуют.


Переносимость кода

При программировании на стандартных Delphi компонентах, в добавок к куче недостатков мы получаем еще одно достоинство - некоторую переносимость кода. Если программа использует только возможности языка, но не возможности системы, то она будет легко компилироваться в Kilix и работать в Linux. Вся проблема в том, что без использования возможностей системы мы получим настоящее глюкалово (Шедевр Ламерского Искусства), тяжелую и неэффективную программу. Тем не менее, при написании серьезных программ по вышеописанным методикам все-таки хочется иметь некоторую независимость от системы. Получить такую независимость очень просто, достаточно писать код не использующий ни API функций ни возможностей языка вообще. Такой код в некоторых случаях писать совершенно невозможно (например в играх), но иногда функции системы абсолютно не нужны (например в математических алгоритмах). В любом случае, следует четко разделять машинно-зависимую и машинно-независимую (если такая есть) части кода. При соблюдении вышеописанных правил машинно-независимая часть будет совместима на уровне исходных текстов с любой системой, для которой есть компилятор паскаля (а он есть даже для PIC контролеров). Независимый от API код можно смело компилировать в DLL и использовать например в драйвере режима ядра. Также такую длл не составит труда использовать и в других ОС, для этого нужно просто посекционно отмапить длл в адресное пространство процесса, настроить релоки и можно смело пользоваться ее функциями. Код на паскале это осуществляющий занимает около 80 строк. Если же DLL все-таки использует некоторые API функции, то их наличие можно проэмулировать, заполнив таблицу импорта DLL адресами заменяющих их функций в своей программе.

Общие приемы оптимизации

Старайся везде, где можно использовать указатели. Никогда не передавай данные в функцию таким образом:

procedure FigZnaet(Data: TStructure);

Всегда передавай указатели на структуры:

procedure FigZnaet(pData: PStructure); где PStructure = ^TStructure;

Такой вызов происходит быстрее и экономит немалое количество кода. Старайся не пользоваться типом данных string, вместо него всегда можно использовать PChar и обрабатывать строки вручную. Если нужен временный буфер для хранения строки, то его следует объявить в локальных переменных как array of Char. Старайся передавать в функцию не более трех параметров. Это объясняется тем, что первые три параметра согласно методу вызова fastcall (который по умолчанию применяется в Delphi) передаются в регистрах, а все последующие через стек, что замедляет доступ к ним и увеличивает размер кода. Экономь память, если например у тебя есть массив чисел, диапазон которых укладывается в байт, то не нужно объявлять его как dword. Никогда не стоит писать повторяющийся код. Если какие-либо действия следует делать повторно, то их нужно вынести в функцию, но тем не менее не стоит делать функции содержащие 2 строчки кода, так как вызов такой функции может занимать куда больше места, чем она сама. И помни главное - эффективность кода в первую очередь определяется не компилятором, а примененным алгоритмом, при этом разница в эффективности может составлять сотни раз!

Старайся чаще использовать битовые маски. Например, несколько boolean переменных относящихся к одному объекту можно заменить одним двордом каждый бит которого определяет булеву переменную. Например для представления маски префиксов команды в дизассемблере я использовал битовую маску с определенную следующим образом:

Bit: 10 9 8 7 6 5 4 3 2 1 0
      ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ L 66h
      ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ L-- 67h
      ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ L---- cs:
      ¦ ¦ ¦ ¦ ¦ ¦ ¦ L------ ds:                    
      ¦ ¦ ¦ ¦ ¦ ¦ L-------- es:
      ¦ ¦ ¦ ¦ ¦ L---------- ss:
      ¦ ¦ ¦ ¦ L------------ fs:
      ¦ ¦ ¦ L-------------- gs:
      ¦ ¦ L---------------- rep
      ¦ L------------------ repnz
      L-------------------- lock

Для такой маски объявляем следующие константы представляющие ее отдельные биты:

const
 OPX_66    = 1 shl 00;
 OPX_67    = 1 shl 01;
 OPX_CS    = 1 shl 02;
 OPX_DS    = 1 shl 03;
 OPX_ES    = 1 shl 04;
 OPX_SS    = 1 shl 05;
 OPX_FS    = 1 shl 06;
 OPX_GS    = 1 shl 07;
 OPX_REP   = 1 shl 08;
 OPX_REPNZ = 1 shl 09;
 OPX_LOCK  = 1 shl 10;

Проверка любого бита маски будет выглядеть так:

if (Mask and MASK_BIT <> 0) then...

А установка бита маски:

Mask := Mask or MASK_BIT;

Никогда не используй if там, где без этого можно обойтись. Условные переходы очень плохо сказываются на производительности кода, поэтому их следует по мере возможности избегать. Например следующий код очень неоптимален:

if x < (y * 2) then bVar := false else bVar := true;

Это следует заменить следующим кодом:

bVar := x > (y * 2);

Этот прием неплохо комбинируется с битовыми масками. Вот пример хорошо оптимизированного кода установки битовой маски по результатам проверки нескольких условий:

Mask := dword(boolean(x < y * 2)) * MASK_BIT1 +
        dword(boolean(z + y <> x * 4)) * MASK_BIT2 +
        dword(boolean(y + z - 2 = e + r - x)) * MASK_BIT3 +
        dword(boolean(z * 4 > e)) * MASK_BIT4 +
        dword(boolean(y * z < x)) * MASK_BIT5;

Этот код содержит пять условий и ни одного условного перехода и он будет скомпилирован всего в несколько команд. Если подобный код делать без применения битовых масок и с использованием if (как и делает большинство программистов), то его размер будет в разы больше, а производительность меньше. В общем, все что можно сделать с помощью логических выражений - следует делать именно так, а не с помощью условных переходов.


Приложение:

Итак, я думаю, что прочитав эту статью ты станешь писать на дельфи весьма маленькие и быстрые программы. Также я думаю, что ты обязательно расскажешь об этом всем, кто в такой возможности сомневается. Пора покончить с мнением, что дельфи дерьмо, и я думаю, ты в это дело вложишь долю своего труда. Как всегда здесь вы можете скачать все примеры к статье.

ФайлОписание
Driver.rar (1.3 кб) Пример простейшего драйвера на Delphi.
Hello.rar (1.3 кб) Пример самого маленького Hello World на Delphi.
inclides.rar (15 кб) Файлы System.pas, SysInit.pas и маленький набор инклудов на Win32 API для написания маленьких программ.


© Copyright by Ms-Rem ALL RIGHTS RESERVED.