[Delphi] Как оптимизировать блок проверок case
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Почему в delphi блок case работает медленно и как это исправить.
Делюсь способами оптимизации и хаками.
Почему case плох
Даже начинающие delphi-программисты знают как выглядит блок case. Однако не все знают, что для нашего процессора, он выглядит как множество if блоков.
Вот что видит программист:
procedure case_test(Index:Integer);
begin
case Index of
0: writeln('Hello');
1: writeln('Habr!');
end;
end;
А вот что видит процессор:
procedure case_test(Index:Integer);
begin
Index:=0;
if Index = 0 then
writeln('Hello')
else
if Index = 1 then
writeln('Habr!');
end;
К сожалению никакой магии за словом case не оказалось. Более того, слишком активное использование case может замедлить выполнение кода. Например если у Вас не 2 варианта проверок, а 50, 250 или ещё больше. Худшим решением для вас будет блок case.
Чем заменить case
Решение у этой проблемы есть. Само строение блока case подсказывает нам, что наши варианты должны быть достаточно прибраны, чтобы поместиться в перечисляемом типе данных например: Integer, Word, Byte, Enum или Char.
В случае когда мы используем индекс для обращения к данным через case — всё просто. Вам необходимо записать данные в массив и подставлять индекс не в case, а в массив.
const
Data:Array[0..1] of String = ('Hello', 'Habr!');
procedure case_test(Index:Integer);
begin
writeln(Data[Index]);
end;
Это работает когда в действиях внутри блока case меняется только один параметр. Но что делать если параметров несколько?
Чем заменить сложный case
Для случаев когда действия в блоке case отличаются сразу по нескольким параметрам, Вы можете расширить тип данных массива до структуры, чтобы в нём поместилось больше параметров.
type
TMyTextWord = record
Text:String;
NeedLinebreak:Boolean;
end;
const
Data:Array[0..2] of TMyTextWord = (
(Text:'Hello'; NeedLinebreak:False),
(Text:' '; NeedLinebreak:False),
(Text:'Habr!'; NeedLinebreak:True)
);
procedure case_test(Index:Integer);
var
MyTextWord:TMyTextWord;
begin
MyTextWord:=Data[Index];
write(MyTextWord.Text);
if MyTextWord.NeedLinebreak then writeln;
end;
Здесь мы заменили блок case, который мог выглядеть вот так
procedure case_test(Index:Integer);
begin
case Index of
0: write('Hello');
1: write(' ');
2:
begin
write('Habr!');
writeln;
end;
end;
end;
Таким образом мы сводим количество действий для выполнения любого из случаев в блоке case до минимума, одинакового для всех случаев. Хотя здесь не сильно видна разница т.к. один набор из 3-ех действий заменился другим. Но подумайте, что выполнится быстрее: «50 раз проверить, является ли переменная одним из чисел?» или «Получить по индексу из массива 1 параметр из 50 возможных?». Ответ очевиден.
И так мы пришли к тому что case не далеко ушёл от известного нам if.
А раз мы научились оптимизировать case почему бы не пойти дальше?
Чем заменить if
Допустим у нас case не использует настроек, которые можно было бы записать в обычный массив. Например такой case:
class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;
Data: Pointer);
var
I:Integer;
PRec:Pointer;
begin
PRec:=@Data;
for I:=0 to Length(Fields)-1 do
begin
case Fields[I].Kind of
tkUString,
tkWideString: PString(PRec)^:=PString(Fields[I].Data)^;
tkInteger: PInteger(PRec)^:=PInteger(Fields[I].Data)^;
tkInt64: PInt64(PRec)^:=PInt64(Fields[I].Data)^;
tkFloat: PDouble(PRec)^:=PDouble(Fields[I].Data)^;
tkEnumeration: PWord(PRec)^:=PWord(Fields[I].Data)^;
end;
IncPtr(PRec,Fields[I].Size);
end;
end;
Но перед главным решением я сделаю отступление, чтобы описать ход своих мыслей.
Допустим у нас есть некая процедура содержащая несколько блоков кода.
И единственное чем похожи эти блоки это номером случая, когда их нужно вызвать.
Мы знаем заведомо простое решение этой проблемы: перечислить в массиве процедуры, вот так
procedure SetString(A,B:Pointer); inline;
procedure SetInt(A,B:Pointer); inline;
procedure SetInt64(A,B:Pointer); inline;
procedure SetDouble(A,B:Pointer); inline;
procedure SetBool(A,B:Pointer); inline;
implementation
procedure SetString(A,B:Pointer); begin PString(A)^:=PString(B)^; end;
procedure SetInt(A,B:Pointer); begin PInteger(A)^:=PInteger(B)^; end;
procedure SetInt64(A,B:Pointer); begin PInt64(A)^:=PInt64(B)^; end;
procedure SetDouble(A,B:Pointer); begin PDouble(A)^:=PDouble(B)^; end;
procedure SetBool(A,B:Pointer); begin PBoolean(A)^:=PBoolean(B)^; end;
type
TTypeHandlerProc = reference to procedure (A,B:Pointer);
var
TypeHandlers:Array[TTypeKind] of TTypeHandlerProc;
class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;
Data: Pointer);
var
I:Integer;
PRec:Pointer;
begin
PRec:=@Data;
for I:=0 to Length(Fields)-1 do
begin
TypeHandlers[Fields[I].Kind](PRec, Fields[I].Data);
IncPtr(PRec,Fields[I].Size);
end;
end;
initialization
TypeHandlers[tkUString]:=SetString;
TypeHandlers[tkWideString]:=SetString;
TypeHandlers[tkInteger]:=SetInt;
TypeHandlers[tkInt64]:=SetInt64;
TypeHandlers[tkFloat]:=SetDouble;
TypeHandlers[tkEnumeration]:=SetBool;
end.
Здесь мы создали массив с inline процедурами, т.е. вместо вызова этих процедур компилятор подставит их код в строку, из которой мы к ним обращаемся. А в остальном всё как раньше, обращаемся к массиву по индексу операции, и выполняем операцию указывая в круглых скобках параметры. Обработчик представлен в сокращенном варианте, т.к. обработка всех действий с типом TTypeKind выглядит громоздкой.
Как, наверное многие уже догадались, это не последний вариант решения проблемы.
Допустим, что мы не хотим выходить за рамки одной функции, и создавать вокруг неё ворох вспомогательных процедур, тип данных для них, и инициализировать массив с процедурами в конструкторе класса или при инициализации модуля.
Если вы уверенный в себе программист и хотите максимальной оптимизации, но не хотите засорять модуль кучей примочек. Я представляю вам решение прямиком из тёмной стороны кодинга. Все кодеры вокруг говорят, что использовать goto не безопасно, что сам оператор устарел и в 90% случаях существуют решения без goto. Говорят что использование ассемблерных вставок доступно только злым хакерам. Но что будет, если мы пустимся во все тяжкие?
Я давно мечтал о таком способе переключения между блоками кода, чтобы хранить в массиве тип label, для осуществления прыжка goto. Таким образом я бы мог перемещаться между кусками одной процедуры при этом использовать для этого не case, а индекс по массиву адресов прыжка. Но возможно ли такое решение? Оказалось что да.
К сожалению в generic методах и классах нельзя использовать ассемблерные вставки, по этому решение пришлось переместить в метод другого объекта, хотя на самом решении это не отразилось. Решение представлено для x32 мода.
procedure TTestClass.Test(Fields: TArray<TField>; Data: Pointer);
label
LS,LI,LI64,LF,LE,FIN;
var
I:Integer;
PRec:Pointer;
ADR:Cardinal;
Types:Array[TTypeKind] of Cardinal;
begin
FillChar(Types,Length(Types)*4,#0);
asm
lea EDX, [EAX].Types
mov [EAX-$4C+tkUString*4], offset LS
mov [EAX-$4C+tkWideString*4], offset LS
mov [EAX-$4C+tkInteger*4], offset LI
mov [EAX-$4C+tkInt64*4], offset LI64
mov [EAX-$4C+tkFloat*4], offset LF
mov [EAX-$4C+tkEnumeration*4], offset LE
end;
PRec:=Data;
for I:=0 to Length(Fields)-1 do
begin
ADR:=Types[Fields[I].Kind];
asm jmp ADR end;
LS: PString(PRec)^:=PString(Fields[I].Data)^; goto FIN;
LI: PInteger(PRec)^:=PInteger(Fields[I].Data)^; goto FIN;
LI64: PInt64(PRec)^:=PInt64(Fields[I].Data)^; goto FIN;
LF: PDouble(PRec)^:=PDouble(Fields[I].Data)^; goto FIN;
LE: PByte(PRec)^:=PByte(Fields[I].Data)^; goto FIN;
FIN:
IncPtr(PRec,Fields[I].Size);
end;
end;
Трюк этого способа заключается в том, что компилятор delphi сам хранит адреса всех label и в ассемблерных вставках дает к ним доступ. Решение нашлось неожиданно просто[1]. Когда я искал как считать регистр EIP, в котором хранится адрес текущей исполняемой команды. Оказалось, что регистр считать нельзя, а вот адрес label'а как раз можно.
Ну а дальше всё просто, на ассемблере заполняем массив адресами нужных нам label'ов.
В цикле по массиву, берется каждый элемент. Тип элемента = индекс адреса его обработчика. Берем по индексу адрес обработчика, прыгаем на адрес, профит.
ADR:=Types[Fields[I].Kind];
asm jmp ADR end;
Остались чисто формальности: после каждого блока ставим прыжок вконец цикла «goto FIN;», чтобы не попадать на следующие label блоки.
Константа $4C — это количество байт до начала блока с памятью массива, чтобы её вычислить можете записать в массив заполненный нулями «mov [EAX], 1» и посмотреть в дебагере какая ячейка приняла это значение, количество ячеек от начала до неё * 4 и будет ваша константа.
Пишите своё мнение, и правки к статье в комментариях. Желаю успехов с оптимизацией кода.
References:
1. Хак с адресом EIP через label
===========
Источник:
habr.com
===========
Похожие новости:
- [Java, Delphi] JNI и Delphi. Использование Java методов при помощи JNI
- [Поисковая оптимизация, IT-компании] Google ввела в поисковую выдачу информацию о доступности путешествий в связи с коронавирусом
- [Тестирование веб-сервисов, DevOps] Запуск тестов JMeter в OpenShift, используя Jenkins Pipeline
- [Big Data, Машинное обучение, История IT, Искусственный интеллект] Технология распознавания лиц: тайная история (перевод)
- [Гаджеты, Носимая электроника, Звук] В iFixit оценили ремонтопригодность наушников Samsung Galaxy Buds Live на 8 из 10
- [Программирование, Системное программирование, Rust] Распространённые заблуждения о временах жизни в Rust (перевод)
- [Big Data, Разработка для интернета вещей, Управление проектами, Облачные сервисы] Мониторинг производственного оборудования: как с этим дела в России
- [Node.JS] NestJS. Загрузка файлов в S3 хранилище (minio)
- [Разработка под iOS, Swift] 6 объединяющих операторов Swift Combine, которые вам следует знать (перевод)
- [JavaScript, Java, Big Data, Data Engineering] В диких условиях. Итоги проектов Школы программистов в эпоху самоизоляции
Теги для поиска: #_delphi, #_delphi, #_optimizatsija (оптимизация), #_case, #_if, #_asm, #_delphi
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:24
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Почему в delphi блок case работает медленно и как это исправить. Делюсь способами оптимизации и хаками. Почему case плох Даже начинающие delphi-программисты знают как выглядит блок case. Однако не все знают, что для нашего процессора, он выглядит как множество if блоков. Вот что видит программист: procedure case_test(Index:Integer);
begin case Index of 0: writeln('Hello'); 1: writeln('Habr!'); end; end; А вот что видит процессор: procedure case_test(Index:Integer);
begin Index:=0; if Index = 0 then writeln('Hello') else if Index = 1 then writeln('Habr!'); end; К сожалению никакой магии за словом case не оказалось. Более того, слишком активное использование case может замедлить выполнение кода. Например если у Вас не 2 варианта проверок, а 50, 250 или ещё больше. Худшим решением для вас будет блок case. Чем заменить case Решение у этой проблемы есть. Само строение блока case подсказывает нам, что наши варианты должны быть достаточно прибраны, чтобы поместиться в перечисляемом типе данных например: Integer, Word, Byte, Enum или Char. В случае когда мы используем индекс для обращения к данным через case — всё просто. Вам необходимо записать данные в массив и подставлять индекс не в case, а в массив. const
Data:Array[0..1] of String = ('Hello', 'Habr!'); procedure case_test(Index:Integer); begin writeln(Data[Index]); end; Это работает когда в действиях внутри блока case меняется только один параметр. Но что делать если параметров несколько? Чем заменить сложный case Для случаев когда действия в блоке case отличаются сразу по нескольким параметрам, Вы можете расширить тип данных массива до структуры, чтобы в нём поместилось больше параметров. type
TMyTextWord = record Text:String; NeedLinebreak:Boolean; end; const Data:Array[0..2] of TMyTextWord = ( (Text:'Hello'; NeedLinebreak:False), (Text:' '; NeedLinebreak:False), (Text:'Habr!'; NeedLinebreak:True) ); procedure case_test(Index:Integer); var MyTextWord:TMyTextWord; begin MyTextWord:=Data[Index]; write(MyTextWord.Text); if MyTextWord.NeedLinebreak then writeln; end; Здесь мы заменили блок case, который мог выглядеть вот так procedure case_test(Index:Integer);
begin case Index of 0: write('Hello'); 1: write(' '); 2: begin write('Habr!'); writeln; end; end; end; Таким образом мы сводим количество действий для выполнения любого из случаев в блоке case до минимума, одинакового для всех случаев. Хотя здесь не сильно видна разница т.к. один набор из 3-ех действий заменился другим. Но подумайте, что выполнится быстрее: «50 раз проверить, является ли переменная одним из чисел?» или «Получить по индексу из массива 1 параметр из 50 возможных?». Ответ очевиден. И так мы пришли к тому что case не далеко ушёл от известного нам if. А раз мы научились оптимизировать case почему бы не пойти дальше? Чем заменить if Допустим у нас case не использует настроек, которые можно было бы записать в обычный массив. Например такой case: class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;
Data: Pointer); var I:Integer; PRec:Pointer; begin PRec:=@Data; for I:=0 to Length(Fields)-1 do begin case Fields[I].Kind of tkUString, tkWideString: PString(PRec)^:=PString(Fields[I].Data)^; tkInteger: PInteger(PRec)^:=PInteger(Fields[I].Data)^; tkInt64: PInt64(PRec)^:=PInt64(Fields[I].Data)^; tkFloat: PDouble(PRec)^:=PDouble(Fields[I].Data)^; tkEnumeration: PWord(PRec)^:=PWord(Fields[I].Data)^; end; IncPtr(PRec,Fields[I].Size); end; end; Но перед главным решением я сделаю отступление, чтобы описать ход своих мыслей. Допустим у нас есть некая процедура содержащая несколько блоков кода. И единственное чем похожи эти блоки это номером случая, когда их нужно вызвать. Мы знаем заведомо простое решение этой проблемы: перечислить в массиве процедуры, вот так procedure SetString(A,B:Pointer); inline;
procedure SetInt(A,B:Pointer); inline; procedure SetInt64(A,B:Pointer); inline; procedure SetDouble(A,B:Pointer); inline; procedure SetBool(A,B:Pointer); inline; implementation procedure SetString(A,B:Pointer); begin PString(A)^:=PString(B)^; end; procedure SetInt(A,B:Pointer); begin PInteger(A)^:=PInteger(B)^; end; procedure SetInt64(A,B:Pointer); begin PInt64(A)^:=PInt64(B)^; end; procedure SetDouble(A,B:Pointer); begin PDouble(A)^:=PDouble(B)^; end; procedure SetBool(A,B:Pointer); begin PBoolean(A)^:=PBoolean(B)^; end; type TTypeHandlerProc = reference to procedure (A,B:Pointer); var TypeHandlers:Array[TTypeKind] of TTypeHandlerProc; class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>; Data: Pointer); var I:Integer; PRec:Pointer; begin PRec:=@Data; for I:=0 to Length(Fields)-1 do begin TypeHandlers[Fields[I].Kind](PRec, Fields[I].Data); IncPtr(PRec,Fields[I].Size); end; end; initialization TypeHandlers[tkUString]:=SetString; TypeHandlers[tkWideString]:=SetString; TypeHandlers[tkInteger]:=SetInt; TypeHandlers[tkInt64]:=SetInt64; TypeHandlers[tkFloat]:=SetDouble; TypeHandlers[tkEnumeration]:=SetBool; end. Здесь мы создали массив с inline процедурами, т.е. вместо вызова этих процедур компилятор подставит их код в строку, из которой мы к ним обращаемся. А в остальном всё как раньше, обращаемся к массиву по индексу операции, и выполняем операцию указывая в круглых скобках параметры. Обработчик представлен в сокращенном варианте, т.к. обработка всех действий с типом TTypeKind выглядит громоздкой. Как, наверное многие уже догадались, это не последний вариант решения проблемы. Допустим, что мы не хотим выходить за рамки одной функции, и создавать вокруг неё ворох вспомогательных процедур, тип данных для них, и инициализировать массив с процедурами в конструкторе класса или при инициализации модуля. Если вы уверенный в себе программист и хотите максимальной оптимизации, но не хотите засорять модуль кучей примочек. Я представляю вам решение прямиком из тёмной стороны кодинга. Все кодеры вокруг говорят, что использовать goto не безопасно, что сам оператор устарел и в 90% случаях существуют решения без goto. Говорят что использование ассемблерных вставок доступно только злым хакерам. Но что будет, если мы пустимся во все тяжкие? Я давно мечтал о таком способе переключения между блоками кода, чтобы хранить в массиве тип label, для осуществления прыжка goto. Таким образом я бы мог перемещаться между кусками одной процедуры при этом использовать для этого не case, а индекс по массиву адресов прыжка. Но возможно ли такое решение? Оказалось что да. К сожалению в generic методах и классах нельзя использовать ассемблерные вставки, по этому решение пришлось переместить в метод другого объекта, хотя на самом решении это не отразилось. Решение представлено для x32 мода. procedure TTestClass.Test(Fields: TArray<TField>; Data: Pointer);
label LS,LI,LI64,LF,LE,FIN; var I:Integer; PRec:Pointer; ADR:Cardinal; Types:Array[TTypeKind] of Cardinal; begin FillChar(Types,Length(Types)*4,#0); asm lea EDX, [EAX].Types mov [EAX-$4C+tkUString*4], offset LS mov [EAX-$4C+tkWideString*4], offset LS mov [EAX-$4C+tkInteger*4], offset LI mov [EAX-$4C+tkInt64*4], offset LI64 mov [EAX-$4C+tkFloat*4], offset LF mov [EAX-$4C+tkEnumeration*4], offset LE end; PRec:=Data; for I:=0 to Length(Fields)-1 do begin ADR:=Types[Fields[I].Kind]; asm jmp ADR end; LS: PString(PRec)^:=PString(Fields[I].Data)^; goto FIN; LI: PInteger(PRec)^:=PInteger(Fields[I].Data)^; goto FIN; LI64: PInt64(PRec)^:=PInt64(Fields[I].Data)^; goto FIN; LF: PDouble(PRec)^:=PDouble(Fields[I].Data)^; goto FIN; LE: PByte(PRec)^:=PByte(Fields[I].Data)^; goto FIN; FIN: IncPtr(PRec,Fields[I].Size); end; end; Трюк этого способа заключается в том, что компилятор delphi сам хранит адреса всех label и в ассемблерных вставках дает к ним доступ. Решение нашлось неожиданно просто[1]. Когда я искал как считать регистр EIP, в котором хранится адрес текущей исполняемой команды. Оказалось, что регистр считать нельзя, а вот адрес label'а как раз можно. Ну а дальше всё просто, на ассемблере заполняем массив адресами нужных нам label'ов. В цикле по массиву, берется каждый элемент. Тип элемента = индекс адреса его обработчика. Берем по индексу адрес обработчика, прыгаем на адрес, профит. ADR:=Types[Fields[I].Kind];
asm jmp ADR end; Остались чисто формальности: после каждого блока ставим прыжок вконец цикла «goto FIN;», чтобы не попадать на следующие label блоки. Константа $4C — это количество байт до начала блока с памятью массива, чтобы её вычислить можете записать в массив заполненный нулями «mov [EAX], 1» и посмотреть в дебагере какая ячейка приняла это значение, количество ячеек от начала до неё * 4 и будет ваша константа. Пишите своё мнение, и правки к статье в комментариях. Желаю успехов с оптимизацией кода. References: 1. Хак с адресом EIP через label =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:24
Часовой пояс: UTC + 5