Сайт Ивана Чередниченко | Оптимизация программ

Оптимизация программ

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

Почему написана данная статья? Я занимаюсь программированием достаточно давно и меня самого волнует данный вопрос. В Интернете вы сможете найти массу информации, которая совпадает по теме; здесь же я постараюсь обобщить информацию из Интернета, из различных книг и журналов. Полученные материалы я обработал и неоднократно проверял при работе в среде разработки приложений Delphi.

В этой статье будут приводиться краткие примеры на различных языках программирования, в частности на языке Pascal (Delphi), а также на языках C\C++ и PHP.

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

§ 1. Использование операций инкремента и декремента

Очень часто в исходных кодах встречаются следующие команды.

Исходный код

  I := I + 1;
  J := J - 10;

Но рекомендуется вместо таких команд использовать операции инкремента и декремента. В языке Pascal (Delphi) предусмотрена перегруженная процедура Inc(X) и Inc(X, N), осуществляющая увеличение значение переменной X в первом случае на единицу, а во втором - на N. Также имеется операция декремента Dec(X) и Dec(X, N), которая позволяет уменьшить значение переменной X на единицу и уменьшить значение X на N соответственно.

Таким образом, вышеприведенный код будет выглядить следующим образом

Исходный код

  Inc(I);
  Dec(J, 10);

Язык С\С++ пошел еще дальше, введя постфиксные и префиксные операции инкремента и декремента (например, i++; и ++i;). Но в языке С\С++, в отличие от языка Pascal (Delphi), операции инкремента и декремента представлены в виде функций (то есть они возвращают результат), в то время, как в Pascal (Delphi) эти же операции представленны в виде процедур, поэтому они не могут выполняться в выражениях (например, J := Inc(I) + 2 - приведет к ошибке).

«Использование операций инкремента (++) и декремента (--) является очень простым и эффективным способом увеличения либо уменьшения значения на 1. Такие действия необходимо выполнять при организации циклов.

Эти два оператора компилируются непосредственно в их ассемблерные эквиваленты. Почти все компьютеры имеют среди своего низкоуровнего машинного набора команд инструкции инкремента и декремента. Если вы используете в своих программах операторы инкремента и декремента, то будьте уверены, что они будут скомпилированы в эти низкоуровневые эквиваленты.

Однако, если вы для прибавления или вычитания 1 при написании программ используете выражения типа i = i - 1, как в других языках программирования, то будьте уверены, что компилятор языка С++ не переведет такие инструкции в их эквивалент машинного кода.» [2; с. 196].

§ 2. Уменьшение усилий

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

Исходный код

 1 function SecondPos(SubStr, Str : string) : Integer;
 2 var
 3   I : Integer;
 4 begin
 5   I := Pos(SubStr, Str);
 6   Delete(Str, I, Length(SubStr));
 7   I := Pos(SubStr, Str);
 8   if ( I > 0 ) then
 9     Result := I + Length(SubStr)
10   else
11     Result := 0;
12 end;

В этой функции нет дублирования присваиваний (строки 5 и 7), так как они позволяют определить второе вхождение подстроки SubStr в строку Str. Здесь нерационально используется другое: в этой функции два раза определяется длина строки SubStr (в строках 6 и 9). Зачем? Конечно, определение длины - это не очень ресурсоемкая операция (хотя тоже, как сказать - зависит от длины строки), но если бы вместо определения длины использовалась функция содержащая в себе несколько циклов и 200 - 300 строк кода, то это значительно ударило бы по быстродействию исполнения данной функции.

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

Данную функцию лучше бы переписать следующим образом.

Исходный код

 1 function SecondPos(SubStr, Str : string) : Integer;
 2 var
 3   I, J : Integer;
 4 begin
 5   I := Pos(SubStr, Str);
 6   J := Length(SubStr);
 7   Delete(S, I, J);
 8   I := Pos(SubStr, Str);
 9   if ( I > 0 ) then
10     Result := I + J
11   else
12     Result := 0;
13 end;

Программисты на языке С\С++ тоже часто используют чрезмерное дублирование усилий. В книге [1; с. 85] дается следующий пример.

Исходный код

 1 if ( strcmp(a, b) < 0 )
 2 {
 3 }
 4 else if ( strcmp(a, b) > 0 )
 5 {
 6 }
 7 else if ( strcmp(a, b) == 0 )
 8 {
 9 }

В этой же книге дается более лучший вариант данного кода [1; с. 85].

Исходный код

 1 int cmp = strcmp(a, b);
 2 if ( cmp < 0 )
 3 {
 4 }
 5 else if ( cmp > 0 )
 6 {
 7 }
 8 else // Остается случай cmp == 0
 9 {
10 }

В качестве подтверждения моих слов приведу еще пару примеров.

Исходный код

 1 int CalculateCRC(char *pswd)
 2 {
 3   int a;
 4   int x = -1;
 5   for (a = 0; a < strlen(pswd); a++)
 6     x += *(int *)((int)pswd + a);
 7   return x;
 8 }

В этом цикле for выполняется подсчет длины строки на каждой итерации. В этом случае гораздо лучше, будет, если вынести функцию strlen за пределы цикла.

Исходный код

  int length;
  length = strlen(pswd);
  for (a = 0; a < length; a++)

В [5] такое изменение функции (и цикла) позволило увеличить производительность (скорость) цикла в два с половиной раза. Это очень хороший результат при оптимизации программы по скорости выполнения. Смотрите также раздел "Эффективное использование цикла for".

Код стандартных модулей Delphi - это конечно отдельный разговор, там везде, где только можно используются чрезмерные усилия. Например, следующая процедура из модуля StdCtrls.

Исходный код

 1 procedure TCustomListBox.DeleteSelected;
 2 var
 3   I: Integer;
 4 begin
 5   if MultiSelect then
 6   begin
 7     for I := Items.Count - 1 downto 0 do
 8       if Selected[I] then
 9         Items.Delete(I);
10   end
11   else
12     if ItemIndex <> -1 then
13       Items.Delete(ItemIndex);
14 end;

Лучше переписать данную процедуру следующим образом.

Исходный код

 1 procedure TCustomListBox.DeleteSelected;
 2 var
 3   I, Max: Integer;
 4 begin
 5   if MultiSelect then
 6   begin
 7     Max := Items.Count - 1;
 8     for I := Max downto 0 do
 9       if Selected[I] then
10         Items.Delete(I);
11   end
12   else
13     if ( ItemIndex <> -1 ) then
14       Items.Delete(ItemIndex);
15 end;

Посмотрите процедуры из этого же модуля, например, TCustomListBox.CopySelection, TCustomListBox.ClearSelection. По поводу таких промохов исходного кода стандартных модулей Delphi хорошо написано в [6]. Отсюда вообще можно заключить то, что эффективный код на Delphi сделать невозможно по крайней мере до тех пор, пока используются стандартные модули. Хотя остается вопрос к разработчикам Delphi: зачем они использовали такой подход при создании стандартных модулей?

Приведем еще один пример функции на языке Pascal (Delphi):

Исходный код

 1 function CharAt(S : string; Index : Integer) : Char;
 2 begin
 3   if ( ( Index > 0 ) and ( Index <= Length(S) ) ) then
 4     Result := S[Index]
 5   else
 6     Result := ' ';
 7 end;

Здесь выполняется проверка условия, в котором определяется длина строки S. Если условие выполняется, то функция возвращает символ под номером Index из строки S, если же условие окажется ложным, то возвращается пробел. Также можно продемонстрировать следующую функцию.

Исходный код

 1 function IsFreeString(S : string) : Boolean;
 2 var
 3   I, Len : Integer;
 4   C : Char;
 5 begin
 6   Len := Length(S);
 7   for I := 1 to Len do
 8   begin
 9     C := S[I];
10     if C = ' ' then
11       Result := True
12     else
13       Result := False;
14   end;
15 end;

Вы видите в строке 9 происходит выполнение C := S[I], но ведь это можно сделать с помощью функции CharAt - почему же тогда здесь не используется данная функция? Ответ весьма прост. Данная функция проверяет содержатся ли в строке S какие-либо символы кроме пробела, если в строке только пробелы, она вернет True. Если вместо строки 9 записать C := CharAt(S, I);, то в теле цикла будет использоваться дополнительная нагрузка в виде функции CharAt, которая при каждом обращении к ней выполняет вычисление длины строки S. Здесь же можно безопасно использовать присваивание (строка 9), так как цикл переберет все символы начиная с первого и до последнего символа строки S.

Ваши процедуры и функции должны быть также оптимизированы с точки зрения логики и не использовать чрезмерных усилий (тем более, когда их можно легко обойти).

§ 3. Убираем лишние модули

Данному параграфу должны уделить максимальное внимание пользователи языка Pascal (Delphi). Скажем сразу следующее: чем меньше модулей подключено к файлу - тем меньшего размера будет он.

Например, при создании библиотеки DLL в проекте уже подключены модули SysUtils и Classes. Если скомпилировать шаблон библиотечного модуля, то DLL-файл будет размером 85 KB. Если оставить только SysUtils (наша библиотека не использует функции и процедуры модуля Classes), то размер библиотеки будет только 39 KB. А если оставляем модуль Classes, то наша библиотека будет занимать опять 85 KB.

Чтобы ответить на вопрос почему, заглянем внутрь модуля Classes и найдем там следующий блок кода.

Исходный код

{$IFDEF MSWINDOWS}
uses Windows, Messages, SysUtils, Variants, TypInfo, ActiveX;
{$ENDIF}
{$IFDEF LINUX}
uses Libc, SysUtils, Variants, TypInfo, Types;
{$ENDIF}

Это означает, что DLL-библиотека для Windows уже ссылается на модуль SysUtils, таким образом, при использовании модуля Classes мы сразу в него включаем аж целых шесть модулей. Именно поэтому добавлением одного модуля Classes возрастает размер библиотеки.

Теперь попробуем удалим блок uses из нашей библиотеки и скомпилируем ее. Размер 14.5 KB. Это уже куда не шло. Таким образом, мы доказали, что чем меньше модулей - тем меньший размер файла мы получим после компиляции.

Итак, для уменьшения размеров исполняемых файлов и библиотек (особенно библиотек) следует учитывать то, что чем меньше мы подключим модулей, тем меньший размер мы получим. Попробуйте не подключать модули в блоке uses, а просто помещать в файл проекта необходимые функции, таким образом можно добиться желаемого эффекта - уменьшения размера файла. Но такие действия не подойдут для уменьшения размера исполняемого EXE-файла, так как там нельзя выкинуть секцию Forms, которая значительно увеличивает размер файлов.

Приведу практический пример. Допустим мы создаем библиотеку DLL на Delphi. Допустим, мы работаем со строками в этой библиотеке. Чтобы преобразовывать строки string к типу PChar и обратно нам необходимо подключить подуль SysUtils. Но я предлагаю несколько более простой вариант - скопировать в исходный код вашей библиотеки все используемые функции из других модулей. Конечно, такая библиотека должна потом будет долго тестироваться, ведь мало ли что могло быть нарушено при переносе функций. Но если она скомпилировалась без ошибок, то вероятнее всего, что вы получили отличную библиотеку.

Итак, этой цели нам понадобятся функции LowerCase и StrPas, а также функция PChar из модуля System. Найдите в соответствующих модулях данные функции и скопируйте их в код библиотеки. Теперь скомпилируйте библиотеку и протестируйте. Если все работает, то мы получили библиотеку, которая работает со строками и занимает где-то 20 KB.

Но надо быть в этом случае очень внимательным, так как иногда такой фокус не удается, и приводит к тому, что код нельзя скомпилировать, либо он будет работать совершенно непредсказуемо. Но пробывать всегда можно. И если вдруг вместо монстра в 400 KB вы получите миниатюрную библиотеку размером в 60 KB, то фокус удался (если конечно все потом работает правильно).

§ 4. Эффективное использование цикла for

Циклы - это слабое место многих программ и программистов, составляющих программы. Циклы могут значительно увеличить время обработки подпрограммы, а также при обработке неграмотно использованных циклов компьютер может впасть в кому.

Например, часто встречаются следующие примеры использования циклов:

Исходный код

for I := 0 to Memo1.Lines.Count - 1 do
  ...

Здесь используется код с чрезмерными усилиями (о чем говорилось выше), так как при очередной итерации будет происходить расчет количества строк в Memo1, то есть будет выполняться код Memo1.Lines.Count - 1. А это тоже занимает время.

Поэтому такой пример лучше переписать следующим образом.

Исходный код

 1 var
 2   I, Max : Integer;
 3 begin
 4   Max := Memo1.Lines.Count - 1;
 5   for I := 0 to Max do
 6     ...
 7 end;

В нем при очередном прохождении цикла (последующей итерации) не будет происходить расчет количества строк в Memo1, а будет подставляться значение переменной Max, значение которой равно количеству строк в Memo1 и посчитано вне (до) цикла в строке 4.

В исходных кодах на языке С\С++ часто встречаются следующие коды [1; с. 97].

Исходный код

for ( i = 0; i < max; ++i )

Как известно, «циклы являются одним из тех мест, где малое повышение эффективности значительно улучшает выполнение программы, потому что их код выполняется многократно. Так как сравнение с нулем обычно более эффективно, чем сравнение с определенным числом, то цикл с уменьшающимся счетчиком, как правило, выполняется быстрее» [1; с. 97].

Таким образом, вместо вышеприведенного кода цикла, лучше использовать следующий код [1; с. 97].

Исходный код

for ( i = max; --i >= 0 )

§ 5. Используйте оператор выбора

Часто начинающие (и не очень) программисты используют десятки, а порой и сотни операторов if идущих подряд.

Исходный код

 1 if ( I = 0 ) then
 2   J := 10;
 3 if ( I = 1 ) then
 4   J := 13;
 5 if ( I = 2 ) then
 6   J := 16;
 7 if ( I = 3 ) then
 8   J := 23;

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

Вместо таких кодов используйте оператор выбора case.

Исходный код

 1 case I of
 2   0 : J := 10;
 3   1 : J := 13;
 4   2 : J := 16;
 5   3 : J := 23;
 6 end;

Здесь мы уменьшили количество строк с восьми до шести и значительно улучшили читаемость данного фрагмента кода. Помимо этого, многие компиляторы хорошо оптимизируют операторы выбора [6], что позволит повысить скорость выполнения такого кода.

§ 6. Использование Ассемблера

Такие среды разработки, как Delphi и C++ Builder поддерживают внутренний Ассемблер. Поэтому рекомендуется использовать ассемблерные функции. Но не следует смешивать два языка, например, Pascal (Delphi) и Ассемблер в теле одной процедуры или функции. Лучше вынести ассемблерный блок кода в отдельную функцию.

Исходный код

 1 function Add(X, Y : Integer) : Integer;
 2 asm
 3   mov eax, X
 4   add eax, Y
 5 end;

Используем данную функцию следующим образом: Z := Add(X, Y);, что равнозначно Z := X + Y;. Хотя существуют спорные мнения по поводу таких ассемблерных функций. Конечно, может быть это слишком простой пример и поэтому неэффективен, но если вместо функции сложения двух целых чисел будет идти функция обработки цвета, то возможно достичь более продуктивных результатов при использовании Ассемблера.

§ 7. Больше циклов, меньше рекурсии

Как уже говорилось выше, циклы являются тем местом, благодаря которому быстродействие программы может значительно сократиться. При создании оптимальных по размеру и скорости исполнения программ, вам следует правильно и наиболее простым (наименее затратным) способом задавать тело цикла.

Как-то я слышал фразу примерно следующего содержания "цикл for - самый медленный цикл в Delphi". Я решил это проверить. И результат был одинаковым для любого из циклов for, while, repeat. Любой цикл выполняется на одном и том же компьютере с одинаковой скоростью.

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

Тем более, что правильно оформленный цикл и верная рекурсия будут выполняться одно и то же время.

Исходный код

 1 function Factorial_Iteration(A : Integer) : Int64;
 2 var
 3   I : Integer;
 4 begin
 5   Result := 1;
 6   for I := 1 to A do
 7     Result := Result * I;
 8 end;
 9
10 function Factorial_Recursive(A : Integer) : Int64;
11 begin
12   if A = 1 then
13     Result := 1
14   else
15    Result := A * Factorial_Recursive(A - 1);
16 end;

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

Аналогичные коды определения факториала с помощью цикла и с помощью рекурсии на языке C++ [3; с. 156, 170, 175].

Исходный код

 1 int Factorial_Iteration(int n) {
 2   int Result = 1;
 3   int i;
 4   for (i = 1; i <= n; i++) {
 5     Result *= i;
 6   }
 7   return Result;
 8 }
 9
10 b Factorial_Recursive(int Number) {
11   if ( Number > 1 )
12     return Number * Factorial_Recursive(Number - 1);
13   return Number;
14 }

Еще небольшой пример на языке PHP [4].

Исходный код

 1 <?
 2   function degree($x,$y)
 3   {
 4     if($y)
 5     {
 6       return $x*degree($x, $y - 1);
 7     }
 8     return 1;
 9   }
10   echo(degree(2, 4)); // выводит 16
11 ?>

Здесь мы использовали рекурсивный метод для возведения числа в степень. Теперь приведем итерационный метод определения числа в степени [4].

Исходный код

 1 <?
 2   function degree($x,$y)
 3   {
 4     for($result = 1; $y > 0; --$y)
 5     {
 6       $result *= $x;
 7     }
 8     return $result;
 9   }
10   echo(degree(2, 4));  // выводит 16
11 ?>

«Кроме того, что этот код намного легче понять, он еще и более эффективен, поскольку проход цикла обходится "дешевле" вызова функции.» [4].

§ 8. Оптимизация программ Delphi и C++Builder

Мы уже убирали модули из программ на Delphi. Аналогичное действие по удалению лишних (неиспользуемых) модулей на C++Builder приведет также к уменьшению размера исполняемого файла.

Но такие средства, как Delphi и C++Builder предоставляют еще более лучший вариант оптимизации программ. Если вы работаете с Delphi, то включите параметр: Project | Options | Packages | Build with runtime packages и скомпилируйте программу заново. Теперь программа занимает килобайты. Но вместе с такой маленькой программой вам необходимо будет распространять специальные библиотеки. И если их у пользователя не окажется, то ваш продукт работать не будет.

Если вы работаете с C++Builder, то включите параметры: Project | Options | Linker | Use dynamic RTL и Project | Options | Packages | Build with runtime packages По умолчанию в C++Builder эти параметры выключены, поэтому он и создает килобайтные программы.

Если сравнить пустой проект, созданный в Delphi (с включенным параметром Build with runtime packages), то файл будет 16.5 KB (использовалась Borland Delphi 7). Теперь пустой проект создадим в C++Builder версии 6 (с выключенными вышеперечисленными параметрами) - и получаем исполняемый файл размером 24.5 KB. И где здесь выгода?

Теперь попробуем эти же проекты создать с выключенными параметрами для Delphi и включенными для C++Builder. Для Delphi 7 получаем EXE-файл размером 359 KB, а C++Builder 6 сгенерировал монстра размером 440 KB. Вот здесь и понятна разница между языком Pascal (Delphi) и C++. Хотя это, конечно, во многом зависит от самого C++Builder, который базируется на компонентах среды Delphi.

Здесь может встать вопрос о том, что стоит ли переходить с Delphi на C++? Или хотя бы с Delphi на C++Builder? Но отвечать на этот вопрос я не стану, а лишь дам вам возможность прочитать статью Какой язык программирования предпочесть?.

§ 9. Оптимизация двухмерных массивов

При работе с массивами всегда используются циклы - необходимо перебрать значения массивов, найти сумму или среднее значение массива - все это циклы. А как вы уже знаете - циклы - весьма слабое место любого кода.

В этом разделе мы попытаемся разработать такой метод работы с двухмерными массивами, который позволял бы работать с ними наиболее эффективно. В [5] дается следующие примеры кода на C++.

Исходный код

 1 int b[200][120];
 2 void xmpl17(int *a)
 3 {
 4   int i, j;
 5   for (i = 0; i < 120; i++)
 6     for (j = 0; j < 200; j++)
 7       b[j][i] = b[j][i] + a[2 * j];
 8 }

«Двухмерные массивы лучше обрабатывать по строкам, а не по столбцам». И там же приводится следующий код.

Исходный код

 1 int b[200][120];
 2 void ympl17(int *a)
 3 {
 4   int i, j;
 5   for (j = 0; j < 200; j++)
 6     for (i = 0; i < 120; i++)
 7       b[j][i] = b[j][i] + a[2 * j];
 8 }

Я проводил собственные эксперименты в Delphi для следующих функций.

Исходный код

 1 procedure Init1();
 2 var
 3   I, J : Integer;
 4 begin
 5   for I := 0 to 1200 do
 6     for J := 0 to 2000 do
 7       Form1.StringGrid1.Cells[J, I] := IntToStr(I + J);
 8 end;
 9
10 procedure Init2();
11 var
12   I, J : Integer;
13 begin
14   for J := 0 to 2000 do
15     for I := 0 to 1200 do
16       Form1.StringGrid1.Cells[J, I] := IntToStr(I + J);
17 end;

Я получил практически одинаковые результаты: функция Init1 выполнялась 7..8 секунд, а функция Init2 - 7..8 секунд. Затем я попробывал добавить ключевое слово register; и получил следующие результаты: Init1 выполнялась 6..7 секунд, а Init2 - уже 8..9 секунд. Конечно, это может быть слишком простые и примитивные тексты, но результаты были таковыми и с ними не поспоришь.

Выполнение следующего кода также длится 7 секунд.

Исходный код

 1 var
 2   I, J : Integer;
 3 begin
 4   for J := 0 to 2000 do
 5     for I := 0 to 1200 do
 6       Form1.StringGrid1.Cells[J, I] := '0';
 7 end;

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

§ 10. Убираем лишнюю математику

Мы уже рассмотрели множество различных вариантов оптимизации приложения. Но в этом разделе поговорим о том, как можно просто улучшить программу. Во-первых, если вы производите математические действия, то в большинстве случае часть используемых переменных можно объявить как константы.

Исходный код

  for x := 0 to 100 do
    a := a + x * sin(45 * pi / 180);

Этот код лучше заменить более удачным кодом.

Исходный код

const
  PI45 = 0.785375;  { PI45 = 45 * 3.1415 / 180 }
...
  for x := 0 to 100 do
    a := a + x * PI45;

Такие же действия лучше применить и в отношении следующей функции.

Исходный код

 1 function RGB(Red, Green, Blue : Byte) : Cardinal;
 2 begin
 3   Result := Blue + Green * 256 + Red * 256 * 256;
 4 end;

При каждом обращении к данной функции будет производиться умножение 256 на 256 (а это 65536). А это все занимает время, конечно не очень много, но все же. Итак, лучше переписать данную функцию следующим образом.

Исходный код

 1 function RGB(Red, Green, Blue : Byte) : Cardinal;
 2 begin
 3   Result := Blue + Green * 256 + Red * 65536;
 4 end;

Здесь я даже не стану использовать число 65536 как константу, а запишем его сразу в функцию. Во многих стандартных модулях Delphi такая "оптимизация" не проводилась, об этом говорилось в разделе "Уменьшение усилий".

«Если в условии несколько раз проверяется одно и тоже выражение, следите, чтобы оно было выражено во всех конструкциях одинаково. В противном случае скомпилированный код будет не оптимален. Например, if ( (a * b) > 0 ) and ( (a * b) < 1024 ) then. При перестановке во втором случае b * a смысл выражения не изменится, но код будет иметь уже не одну операцию умножения, а две. Можно временно присвоить проверяемое выражение временной переменной, а затем уже проверять полученное значение.» [6]. Самым лучшим, на мой взгляд, будет следующий код.

Исходный код

  X := A * B;
  if ( ( X > 0 ) and ( X < 1024 ) ) then

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

Исходный код

 1 function Divide(X, Y : Integer) : Integer; stdcall;
 2 asm
 3   mov ebx, Y
 4   cdq
 5   idiv ebx
 6 end;

Посмотрим в модуль SysUtils среды Delphi. Там найдем следующее объявление констант.

Исходный код

 1  HoursPerDay   = 24;
 2  MinsPerHour   = 60;
 3  SecsPerMin    = 60;
 4  MSecsPerSec   = 1000;
 5  MinsPerDay    = HoursPerDay * MinsPerHour;
 6  SecsPerDay    = MinsPerDay * SecsPerMin;
 7  MSecsPerDay   = SecsPerDay * MSecsPerSec;

Интересно зачем происходит операция умножения в строках 5, 6 и 7? Неужели число минут в дне может измениться? Я бы заменил эти строки следующим фрагментом.

Исходный код

  MinsPerDay    = 1440;      // 24 * 60
  SecsPerDay    = 86400;     // 24 * 60 * 60
  MSecsPerDay   = 86400000;  // 24 * 60 * 60 * 1000

Часто такие "ошибки" приходят из книг, в которых в основном материал выкладывается наиболее понятным и наиболее читабельным способом, а не с точки зрения оптимизации и рациональности. Вот например код из книги Нейла Рубенкинга «Программирование в Delphi для "чайников"» (стр. 275).

Исходный код

 1 var
 2   X, Y, Delta : LongInt;
 3 begin
 4   with Canvas do
 5   begin
 6     Delta := Random(8) + 3;
 7     X := Random(ClientWidth - 16 * Delta) + 4 * Delta;
 8     Y := Random(ClientHeight - 8 * Delta) + 4 * Delta;
 9     Brush.Color := RGB(Random(256), Random(256),
                          Random(256));
10     Pen.Width := 3;
11     Ellipse(X - 4 * Delta, Y - 4 * Delta,
               X + 4 * Delta, Y + 4 * Delta);
12     Ellipse(X + 4 * Delta, Y - 4 * Delta,
               X + 12 * Delta, Y + 4 * Delta);
13     Brush.Color := clBlack;
14     Ellipse(X + 2 * Delta, Y - Delta,
               X + 4 * Delta - 2, Y + Delta);
15     Ellipse(X + 4 * Delta + 2, Y - Delta,
               X + 6 * Delta, Y + Delta);
16     MoveTo(X + 2 * Delta, Y - 4 * Delta);
17     LineTo(X + 4 * Delta, Y - 2 * Delta);
18     LineTo(X + 6 * Delta, Y - 4 * Delta);
19   end;
20 end;

Что может не нравиться в этом коде? С виду очень хороший код, но посмотрите сколько раз здесь используется выражение 4 * Delta? Давайте выделим непосредственно это выражение красным цветом, а выражения, которые можно получить из этого выражения оранжевым.

Исходный код

 1 var
 2   X, Y, Delta : LongInt;
 3 begin
 4   with Canvas do
 5   begin
 6     Delta := Random(8) + 3;
 7     X := Random(ClientWidth - 16 * Delta) + 4 * Delta;
 8     Y := Random(ClientHeight - 8 * Delta) + 4 * Delta;
 9     Brush.Color := RGB(Random(256), Random(256),
                          Random(256));
10     Pen.Width := 3;
11     Ellipse(X - 4 * Delta, Y - 4 * Delta,
               X + 4 * Delta, Y + 4 * Delta);
12     Ellipse(X + 4 * Delta, Y - 4 * Delta,
               X + 12 * Delta, Y + 4 * Delta);
13     Brush.Color := clBlack;
14     Ellipse(X + 2 * Delta, Y - Delta,
               X + 4 * Delta - 2, Y + Delta);
15     Ellipse(X + 4 * Delta + 2, Y - Delta,
               X + 6 * Delta, Y + Delta);
16     MoveTo(X + 2 * Delta, Y - 4 * Delta);
17     LineTo(X + 4 * Delta, Y - 2 * Delta);
18     LineTo(X + 6 * Delta, Y - 4 * Delta);
19   end;
20 end;

В этом коде выражение 4 * Delta встречается 14 раз, выражения 16 * Delta, 8 * Delta и выражение 12 * Delta встречаются по одному разу. Выражение 6 * Delta встречается два раза, а выражение 2 * Delta три раза. Мы можем только догадываться, произведет ли компилятор оптимизацию выражений (хотя бы выражения 4 * Delta). Чтобы быть увереным в этом, давайте перепишем данный код следующим образом.

Исходный код

 1 var
 2   X, Y, Delta, Delta2, Delta4, Delta6 : LongInt;
 3 begin
 4   with Canvas do
 5   begin
 6     Delta := Random(8) + 3;
 7     Delta2 := Delta * 2;  // Delta2 := Delta shl 1;
 8     Delta4 := Delta * 4;  // Delta4 := Delta shl 2;
 9     Delta6 := Delta * 6;  // Delta6 := Delta2 * 3;
10     X := Random(ClientWidth - 4 * Delta4) + Delta4;
11     Y := Random(ClientHeight - 2 * Delta4) + Delta4;
12     Brush.Color := RGB(Random(256), Random(256),
                          Random(256));
13     Pen.Width := 3;
14     Ellipse(X - Delta4, Y - Delta4,
               X + Delta4, Y + Delta4);
15     Ellipse(X + Delta4, Y - Delta4,
               X + 2 * Delta6, Y + Delta4);
16     Brush.Color := clBlack;
17     Ellipse(X + Delta2, Y - Delta,
               X + Delta4 - 2, Y + Delta);
18     Ellipse(X + Delta4 + 2, Y - Delta,
               X + Delta6, Y + Delta);
19     MoveTo(X + Delta2, Y - Delta4);
20     LineTo(X + Delta4, Y - Delta2);
21     LineTo(X + Delta6, Y - Delta4);
22   end;
23 end;

Получился неплохой код. Комментарии к строкам 7, 8 и 9 даны с учетом предыдущих разделов данной статьи.

Но посмотрите сколько все еще осталось выражений, типа Y - Delta4, Y + Delta4, X - Delta4 и выражение X + Delta4? Много... Давайте их также выделим: красным, зеленым, синим и оранжевым цветами соответственно. В этом случае получаем следующий код.

Исходный код

 1 var
 2   X, Y, Delta, Delta2, Delta4, Delta6 : LongInt;
 3 begin
 4   with Canvas do
 5   begin
 6     Delta := Random(8) + 3;
 7     Delta2 := Delta * 2;  // Delta2 := Delta shl 1;
 8     Delta4 := Delta * 4;  // Delta4 := Delta shl 2;
 9     Delta6 := Delta * 6;  // Delta6 := Delta2 * 3;
10     X := Random(ClientWidth - 4 * Delta4) + Delta4;
11     Y := Random(ClientHeight - 2 * Delta4) + Delta4;
12     Brush.Color := RGB(Random(256), Random(256),
                          Random(256));
13     Pen.Width := 3;
14     Ellipse(X - Delta4, Y - Delta4,
               X + Delta4, Y + Delta4);
15     Ellipse(X + Delta4, Y - Delta4,
               X + 2 * Delta6, Y + Delta4);
16     Brush.Color := clBlack;
17     Ellipse(X + Delta2, Y - Delta,
               X + Delta4 - 2, Y + Delta);
18     Ellipse(X + Delta4 + 2, Y - Delta,
               X + Delta6, Y + Delta);
19     MoveTo(X + Delta2, Y - Delta4);
20     LineTo(X + Delta4, Y - Delta2);
21     LineTo(X + Delta6, Y - Delta4);
22   end;
23 end;

Что из этого сделует? Естественно необходимо выделить данные повторяющиеся элементы в качестве переменных. В этом случае получаем следующий код.

Исходный код

 1 var
 2   X, Y, Delta, Delta2, Delta4,
     Delta6, XpDelta4, YmDelta4, YpDelta4 : LongInt;
 3 begin
 4   with Canvas do
 5   begin
 6     Delta := Random(8) + 3;
 7     Delta2 := Delta * 2;  // Delta2 := Delta shl 1;
 8     Delta4 := Delta * 4;  // Delta4 := Delta shl 2;
 9     Delta6 := Delta * 6;  // Delta6 := Delta2 * 3;
10     X := Random(ClientWidth - 4 * Delta4) + Delta4;
11     Y := Random(ClientHeight - 2 * Delta4) + Delta4;
12     XpDelta4 := X + Delta4;
13     YmDelta4 := Y - Delta4;
14     YpDelta4 := Y + Delta4;
15     Brush.Color := RGB(Random(256), Random(256),
                          Random(256));
16     Pen.Width := 3;
17     Ellipse(X - Delta4, YmDelta4,
               XpDelta4, YpDelta4);
18     Ellipse(XpDelta4, YmDelta4,
               X + 2 * Delta6, YpDelta4);
19     Brush.Color := clBlack;
20     Ellipse(X + Delta2, Y - Delta,
               XpDelta4 - 2, Y + Delta);
21     Ellipse(XpDelta4 + 2, Y - Delta,
               X + Delta6, Y + Delta);
22     MoveTo(X + Delta2, YmDelta4);
23     LineTo(XpDelta4, Y - Delta2);
24     LineTo(X + Delta6, YmDelta4);
25   end;
26 end;

Вот мы получили код, в котором используется меньшее количество операций умножения. Конечно, не могу гарантировать того, что данный код быстрее (или лучше) исходного кода, тем более что количество строк увеличилось на шесть до 26 строк.

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

§ 11. Команды shl и shr

Мы уже говорили об использовании Ассемблера, но иногда вместо вставок на языке Ассемблера можно использовать операции shl и shr.

Приведем примеры данных команд и аналогичные решения без использования этих команд.

Исходный код

  Result := X shr Y;  // Result := X / Power(2, Y);
  Result := X shl Y;  // Result := X * Power(2, Y);

А [6] дает еще один пример работы с данными командами.

Исходный код

  Result := X shr 1;  // Result := A DIV 2;

Немного поэкспериментировав я пришел к следующем выводам.

Исходный код

 1 Result := X shl 1;  // Result := X + X;
 2 Result := X shl X;  // Result := X * Power(2, X);
 3 Result := X shr X;  // Result := 0;
 4 Result := X shl 2;  // Result := X * 4;

Следует заметить, что например эти команды следует использовать в библиотеках, так как они являются командами языка Pascal, а не функциями, таким образом, вам не нужно будет подключать дополнительные модули.

Когда я разбирался с этими командами, я нашел одну интересную особенность.

Исходный код

 1 procedure SomeInterestProc;
 2 var
 3   I, J, K, L : Integer;
 4   S : string;
 5 begin
 6   I := 10;
 7   I := not I;
 8   J := 0;
 9   J := not J;
10   K := -10;
11   K := not K;
12   L := 100;
13   L := not L;
14   S := IntToStr(I) + ',' + IntToStr(J) + ',' +
          IntToStr(K) + ',' + IntToStr(L);
15 end;

Знаете, что будет содержать строка S? Строка S будет содержать следующий текст '-11,-1,9,-101'. Что же здесь происходит, вызов команды I := not I; равносилен вызову I := - I - 1.

Исходный код

 1 function NotEquiv(I : Integer) : Integer;
 2 begin
 3   Result := - I - 1;  // Result := not I;
 4 end;
 5
 6 procedure SomeInterestProc2;
 7 var
 8   I, J, K, L : Integer;
 9   S : string;
10 begin
11   I := NotEquiv(10);
12   J := NotEquiv(0);
13   K := NotEquiv(-10);
14   L := NotEquiv(100);
15   S := IntToStr(I) + ',' + IntToStr(J) + ',' +
          IntToStr(K) + ',' + IntToStr(L);
16 end;

Заключение

Зачем нужна оптимизация, когда существуют многоядерные процессоры, объемы оперативной памяти в два и более гигабайта, огромные жесткие диски, способные поместить миллионы картинок, тысячи видео- и аудиофайлов? А оптимизация нужна. Нужна для того, чтобы вы могли работать со своими миллионами картинок, тысячами видеофайлов, прослушивать музыку, играть в игры, заниматься программированием или Web-дизайном.

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

Конечно, сейчас вопросы минимизации размеров исполняемых файлов не стоят на первом ряду, но все же бывает так, что минимальный размер EXE-файла может стать большим плюсом при распространении программы в Интернете. Если программа написана на VisualBasic, то она будет размером где-то около двух мегабайтов (со специальной библиотекой), в Delphi такая программа будет где-то 400 - 600 килобайтов. А на C++ такую программу можно сделать еще меньше, а на Ассемблере можно сделать просто размером со спичечный коробок. Если вы размещаете собственные программы в Интернете или скачиваете их с Интернета, то гораздо приятнее быстрее и дешевле скачать программу размером в пару сотен килобайт, чем в несколько мегабайт. Гораздо приятнее, когда программу можно скинуть на дискету или флешку и быстро работать с ней с данного носителя. А ведь это все и есть оптимизация.

Не следует отвергать и тот факт, что чем меньше размер исполняемой программы (EXE-файла), тем быстрее работает программа. Например, размер исполняемого файла Microsoft Internet Explorer версии 6 всего 91 KB. Впечатляет? Конечно, Internet Explorer будет обращаться к другим модулям и библиотекам, размер которых может значительно превосходить размер исполняемого файла. Исполняемый файл Opera версии 9.22 всего 77.5 KB (а вот десятая версия этого знаменитого браузера прибавила в размере исполняемого файла очень существенно - теперь он весит 812 KB)... А размер EXE-файла браузера Minefield (Firefox) составляет 74 KB.

Если вы не согласны с некоторыми принципами, приводимыми в данной статье - то это ваше право. Ну, например, поговорим о разделе "Убираем лишнюю математику". Допустим, в программе используется команда Сохранить, которая выполняется 3 секунды, оптимизировав ее на 10% мы получаем 2.7 секунд, то есть выигрышь в какие-то 0.3 секунды. А теперь вспомните, сколько раз за сеанс использования программы вы выполняете команду Сохранить? 10 раз? 20? 100 раз? Вот раз 20 в среднем, я думаю, выполняет команду Сохранить каждый пользователь. Итак 20 раз умножим на 3 секунды - получаем 60 секунд, то есть одну минуту нашего времени занимает операция сохранения. Если мы используем оптимизированный вариант команды Сохранить, то это уже 54 секунды. А если эта команда выполнялась 1.5 секунды, то 20 сохранений у нас заняли бы всего 30 секунд. Это уже существенная разница. А если мы сохраняемся в течение часа 100 раз? 300 секунд, 270 секунд и 150 секунд!

Таким образом мы можем тратить на одну и ту же операцию в два раза меньше времени - вместо пяти минут, 2.5 минуты!

Теперь поговорим о размере файла, с которого и было начато заключение данной статьи. Если программа состоит из одного модуля (исполняемого файла), то размер более 400 KB в принципе допустим (например, в случае, если при разработке этого файла использовалась среда Delphi). А если один продукт использует 10 дополнительных модулей, то уже они будут занимать не менее 4 MB. В этом случае лучше компилировать программы используя внешние модули - об этом подробно описано в разделе "Оптимизация программ Delphi и C++Builder".

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

Источники