|
|
|||||||||||||||||||||||||||||
|
Написание оптимального кода под DelphiИсточник: delphimaster
В данной статье рассмотрены принципы, помогающие компилятору Delphi генерировать более оптимальный с точки зрения скорости код. Если Вы не хотите вникать в подробности, в конце статьи есть "свод правил", которые рекомендуется соблюдать при написании программ. Компилятор Delphi относится к разряду оптимизирующих. Но насколько качественно проводится оптимизация? Как "помочь" компилятору создать более быстрый код? Давайте разберемся с этим на экспериментах. Оптимизация константных выраженийПример 1: const z = 15616; ... var a,b: integer; ... a := $abcd6123; b := z+a; .... С точки зрения оптимизации код можно упростить еще на этапе компиляции до b:=15616+$abcd6123; или того проще: b:=$ABCD9E23; Но написанный выше листинг преобразуется в mov eax, $abcd6123 lea ebx, [eax + $00003D00] С одной стороны компилятор не "сообразил", что значение переменной "a" можно преобразовать в константу и сложить с другой константой (которая, заметим, подставлена именно как константа) на этапе компиляции, с другой стороны был применен весьма хитрый трюк с LEA (об этом ниже). Тем не менее, код mov ebx, $ABCD9E23 в любом случае быстрее и короче. Пример 2: ... b:=random(maxint); // b - заведомо не константа ! a:=$abcd6123; if b>a then b:=a; ... Скомпилированный код будет выглядеть mov eax, $7fffffff // MaxInt call @RandInt mov ebx,eax mov eax, $abcd6123 cmp eax, ebx jnl +$02 ... А ведь значение, присвоенной переменной "а" являлось константой и наш пример можно было бы переписать как: b:=random(maxint); a:=$abcd6123; if b>$abcd6123 then b:= $abcd6123; Пример 3: ... a:=$abcd6123; b:=$abc34233; c:=b-a; ... После компиляции получаем: mov eax, $abcd6123 mov ebx, $abc34233 mov ebx, edx sub ebx, eax Т.е. компилятор преобразовал код так, как он был написан, а ведь можно было бы просто записать: mov ebx, $fff5e110 т.е. c:= $fff5e110; Оптимизация алгебраических выраженийПример 4: ... var a,b,c,d: integer; p: pointer; begin p:=nil; a:=0; ... {далее по коду эти присвоения не используются} После компиляции эти переменные будут удалены, причем с предупреждением Value assigned to ... never used Пример 5: ... a:=0; b:=a; showmessage(inttostr(b)); ... Код скомпилируется как есть! Таким образом мы обманули компилятор псевдо использованием переменных. Delphi не исправляет нашей "кривости", поэтому эта задача ложится исключительно на плечи программиста. Пример 6: ... b:=random(maxint); a:=b; func(a,b); a:=a+1; func(a,b); ... Данный код можно оптимизировать до ... b:=random(maxint); func(b,b); a:=b+1; func(a,b); ... И этого Delphi за нас не сделает. Пример 7: ... c:=a div b; c:=a*b; ... В данном примере первую строчку можно безболезненно удалить, что Delphi делать умеет. Пример 8: if ((a*b)<$3d00) and (a*b)>0)) then ... В данном случае можно избавится от одной операции умножения, присвоив значение выражения a*b временной переменной. Анализ ассемблерного листинга показывает, что компилятор именно так и поступает. Тем не менее, поменяв второе подвыражение на ((b*a)>0), компилятор принимает выражения за разные и генерирует умножение для обоих случаев, не смотря на то, что результат одинаков. Оптимизация арифметических операцийСложение и вычитаниеПрименение инструкции LEA вместо ADD позволяет производить сумму 3х операндов (двух переменных и одной константы) за один такт. Трюк заключается в том представление ближних указателей эквивалентно их фактическому значению, поэтому результат, возвращенный LEA равен сумме ее операндов. При возможности Delphi производит такую замену. ДелениеОперация деления требует гораздо больше тактов процессора, нежели умножение, поэтому замена деления на умножение может значительно ускорить работу. Существуют формулы, позволяющие выполнять такое преобразование. Тем не менее, Delphi не использует такую оптимизацию. Деление на степень двойки можно заменять сдвигом вправо на n бит, но даже в этом случае получаем следующий код: mov esi,edi sar esi,1 jns +$03 adc esi, $00 Здесь учитывается особенность самой операции div - округление в большую сторону. Поэтому, если можно пренебрегать округлением, используйте c:=a shr 1 вместо с:=a div 2. УмножениеУмножение на степень двойки можно заменять сдвигами битов. Delphi заменяет умножение сдвигами при умножении на 4,8,16 итд. При умножении на 2 производится суммированием переменной с собой. Умножать на 3,5,6,7,8,10 и т. д. можно и без операции умножения - расписав выражение по формуле (a shl n)+a, где n - показатель степени двойки. Например, при умножении на 3 n=1. Delphi при возможности прибегает к этому трюку. Заметим, операнд LEA умеет умножать регистр на 2,4,8, что также при возможности используется компилятором. Например, умножение на 3 преобразуется в инструкцию lea esi, [ebx + ebx*2] Оптимизация case ofАнализ скомпилированного кода показывает, что Delphi проводит утрамбовку дерева. Т.е. значения case сортируются и выбор нужного элемента производится при помощи двоичного поиска. В случае, если элементы case of выстраиваются в арифметической прогрессии, компилятор формирует таблицу переходов. Т.е. создается массив указателей с индексами элементов, поэтому выбор нужно элемента выполняется за одну итерацию независимо от количества элементов. Оптимизация цикловРазворачивание циклов - не производится. Разворачивание циклов весьма спорный момент в оптимизации, поэтому принять грамотное решение может только человек. Delphi не производит разворачивания ни больших, ни маленьких циклов. Слияние циклов - не производится. Если два цикла, следующие друг за другом имеют одинаковые границы итерационной переменной, разумно оба цикла объединить в один. Вынесение инвариантного кода за пределы цикла - не выносится. Наиболее распространенный недочет - условие цикла записывается как: for i:=0 to memo1.lines.count - 1 do... Delphi будет при каждой итерации вызывать метод count, вычитать из результата 1 и потом уже сверять. Настоятельно рекомендуется переписывать подобный код как lin := .lines.count - 1; for i:=0 to lin do... Весь код VCL написан с нарушением этого правила. Очевидно, что проще подобного рода оптимизацию встроить в компилятор, нежели переписывать VCL :) Замена циклов с предусловием на циклы с постусловием - производится. Циклы с постусловием имеют главное преимущество над другими видами циклов (с предусловием и с условием в середине) - они содержат всего одно ветвление. Delphi производит такую замену. Замена инкремента на декремент - не производится. Более того, даже декрементный цикл компилируется в неоптимальный код, т.к. не используется флаг ZF. Вместо этого происходит сравнивание значения регистра с 0. Удаление ветвлений - не производится. Вывод:
Сравнивая Delphi с компиляторами Visual C++, WATCOM, Borland C++ (тестирование данных компиляторов приведено в [1]) приходим к выводу, что Delphi по своим оптимизирующим свойствам аналогичен Borland C++ (а кто сомневался? ;) ). Учитывая, что Borland C++ по итогам сравнения оказался последним, делаем несложный вывод. Весьма печален и тот факт, что большинство кода VCL написано с точки зрения "красоты" кода, а не его оптимальности с точки зрения скорости. Например, не соблюдается правило 12. Ссылки по теме
|
|