* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна!
Тема циклы
31 марта 2009
Добро пожаловать на пятый урок. Его тема циклы. Мы увидим, как компилятор реализует циклы, и какую оптимизацию мы можем сделать в них. Мы также проверим эффективность этой оптимизации. function ForLoop(Start, Stop : Integer) : Integer; var I : Integer;
begin Result := 0; for I := Start to Stop do begin Result := Result + I; end; end; В данном примере нет ничего полезного, кроме примера для изучения циклов. Посмотрим, что же компилятор наворотил нам в этом примере. В данном примере мы попробуем, что ни будь новое, и откомпилируем с отключенной оптимизацией.
function ForLoopNonOpt(Start, Stop : Integer) : Integer; var I : Integer;
begin { push ebp mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax } Result := 0; { xor eax,eax mov [ebp-$0c],eax } for I := Start to Stop do { mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl +$15 inc edx mov [ebp-$14],edx mov [ebp-$10],eax } begin Result := Result + I; { mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] } end; { dec dword ptr [ebp-$14] jnz -$0e mov eax,[ebp-$0c] } { mov esp,ebp pop ebp ret } end; Как мы видим, компилятор сгенерировал кучу кода, где или совсем нет оптимизации или ее мало. Как обычно первые три строки это установка стекового фрейма. В данном примере он на 20 байт больше (16 hex). Две следующие строки копируют переменные Start и Stop на стек. Start передается в EAX и Stop передается в EDX, в соответствии с соглашением об вызове. Следующие две строки создают значение 0 и копируют его на стек [EBP-$0C], это место для хранения переменной Result. Теперь мы готовы к выполнению тела цикла. Перед началом цикла необходимо убедиться, что цикл действительно должен выполняться. Если Stop больше чем Start, то это как раз тот случай. Start и Stop извлекаются из стека в регистры EAX и EDX. Мы вычисляем выражение Stop-Start и если результат отрицательный, то цикл не выполняется и управление передается в конец цикла инструкцией JL (jump low). В следующей строке увеличивается значение Stop, и оно копируется на стек [EBP-$14]. У нас нет имени для этой локальной переменной в данной точке. Данная особенность потребует дополнительных объяснений. Эта переменная (NoName) введена компилятором для оптимизации и это немного странно, поскольку мы отключили оптимизацию. Доступ до этой неименованной переменной есть в строке DEC DWORD PTR [EBP-$14]. Эта строка уменьшает ее значение на единицу, в конце каждой итерации и проверяется, что она не достигла нуля. Инструкция DEC устанавливает флаги, и инструкция JNZ делает переход на начало цикла, при условии, что NoName <> 0. Мы должны считать, что это используется как счетчик цикла и что она бежит от Start до Stop. Это действительно делается так, но это не используется для управления циклом. Преимущество в том, что это сохраняет инструкции при сравнении I со Stop. Но это также и увеличивает стоимость инструкции DEC NoName. На P4 latency/throughput инструкции CMP составляет 0.5/0.5 цикла, а для DEC оно 1/0.5. Поэтому это можно считать «де оптимизацией». Значения latency и throughput для P4 можно найти в «Intel Pentium 4 and Xeon Processor Optimization» руководстве от Intel. Вернемся к строке MOV [EBP-$10], EAX. Она копирует переменную I на стек. Тело цикла состоит из одной строки Паскаля Result := Result + I. Она транслируется в три строки на ассемблере. Первые две строки загружают переменную I в регистр EAX и затем прибавляют ее к переменной Result на стеке [EBP-$0C]. Третья строка увеличивает переменную I. На этом мы заканчиваем объяснения кода цикла и у нас остались только две вещи. Переменная Result должна быть скопирована в регистр EAX, который используется для возврата результата из функции, в соответствии с соглашением о вызове. Последние три строки восстанавливают фрейм стека и возвращают управление обратно в программу. В упражнении мы превратим это в ассемблерный код, так что бы это соответствовало Паскаль коду и нашему пониманию циклов. Мы начнем с преобразования в чистый ассемблерный код. Сделаем это путем закомментирования Паскаль кода и раскомментирования ассемблерного кода. Определим две метки LoopEnd и LoopStart, которые нам потребуются. Изменим два перехода так, что бы они указывали на метки. function ForLoopBASM1(Start, Stop : Integer) : Integer; asm push ebp mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl @LoopEnd inc edx mov [ebp-$14],edx mov [ebp-$10],eax //begin @LoopStart : //Result := Result + I; mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] //end; dec dword ptr [ebp-$14] jnz @LoopStart @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebp //ret end; первое, что мы сделаем, так это удалим локальную переменную NoName. function ForLoopBASM2(Start, Stop : Integer) : Integer; asm push ebp push ebx //New mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl @LoopEnd //inc edx //NoName intialize //mov [ebp-$14],edx //NoName intialize mov [ebp-$10],eax //begin @LoopStart : //Result := Result + I; mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] //end; //dec dword ptr [ebp-$14] //NoName decrement mov ebx, [ebp-$10] //New mov ecx, [ebp-$08] //New cmp ebx, ecx //New //jnz @LoopStart jbe @LoopStart //New @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebx //New pop ebp //ret end; Строка, помеченная как "New" введена, для создания переменной цикла I. Строка MOV EBX, [EBP-$10] копирует переменную I в регистр EBX. Следующая строка копирует переменную Stop в регистр ECX. Затем в строке CMP EBX, ECX они сравниваются, и инструкцией JBE @LOOPSTART управление передается в начало цикла, если I меньше или равно Stop. Поскольку мы используем регистр EBX и он не разрешен для свободного использования, поэтому мы его сохраняем его в стеке. Мы решили проверять окончания цикла в начале цикла. Данный тест разделен компилятором на две части. Перед входом в цикл проверяется, что цикл может выполниться как минимум один раз и реальное окончание цикла проверяется в конце. Такая техника оптимизации называется инверсия цикла. Теперь мы сменим цикл так, что бы такую оптимизацию. Потом мы увидим преимущества от этой оптимизации. function ForLoopBASM4(Start, Stop : Integer) : Integer; asm push ebp push ebx mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,[ebp-$08] //sub edx,eax //jl @LoopEnd mov [ebp-$10],eax //begin @LoopStart : mov ebx, [ebp-$10] mov ecx, [ebp-$08] cmp ebx, ecx ja @LoopEnd //Result := Result + I; mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] //end; //mov ebx, [ebp-$10] //mov ecx, [ebp-$08] //cmp ebx, ecx //jbe @LoopStart jmp @LoopStart @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebx pop ebp end; Проверка на окончания цикла была перемещена в начало и тест был инвертирован. На месте старой проверки теперь находится безусловный переход. Этот переход единственное, что сделано по отношению к инверсной оптимизации. В не оптимизированном цикле было два перехода, оптимизированным один. Проверка вверху, то что проверяется всегда. Start был на Stop и теперь лишнее и поэтому удалено. Перед проведением измерений по эффекту от двух оптимизаций, хорошей идеей будет оптимизировать часть или все, что возможно, стек в регистры, регистры в стек. Данный процесс называется – размещение в регистрах и это одна из самых важных оптимизаций на всех архитектурах, но это особенно важно для архитектуры Intel, поскольку в ней малое количество доступных регистров. Если нет места для всех переменных в регистрах, то важно определить какие переменные поместить в регистры. Инструкции MOV в теле цикла наиболее важные кандидаты на это. Они выполняются большое количество раз. Инструкции за пределами цикла выполняются только раз. Переменные внутри цикла первыми должны быть размещены в регистрах. Это переменные I, Stop и Result. Теперь рассмотрим использование регистров для временных переменных. Если переменная всегда копируется в тот же самый временный регистр, то ее желательно разместить в этом регистре. Переменная Stop в регистре EDX при входе в функцию и также используется как временный регистр, во всех строках, кроме двух строк. Здесь есть две строки в цикле, которые мы добавили, изменим их mov ecx, [ebp-$08] cmp ebx, ecx на mov edx, [ebp-$08] cmp ebx, edx Регистр EAX используется для Start вверху функции и как Result в остальной части функции. Если нет перекрытия по использованию, то мы можем использовать EAX для Result, как только Start прекратит его использования. После того, как Start назначен переменной I (MOV [EBP-$10], EAX), он больше нигде не используется и регистр EAX свободен для использования для Result, кроме тех строк, где EAX используется как временное хранилище для I. mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] после того, как ECX прекращает использоваться, мы можем его использовать как временное хранилище для I, вместо EAX. mov ecx,[ebp-$10] add [ebp-$0c],ecx inc dword ptr [ebp-$10] Подведем итог по первой части оптимизации по использованию регистров: Result в EAX, I в ECX и Stop в EDX. В начале заменим все строки со Stop. [EBP-$08] на использование EDX. function ForLoopBASM6(Start, Stop : Integer) : Integer; asm push ebp push ebx mov ebp,esp add esp,-$14 //mov [ebp-$08],edx mov edx,edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] //mov edx,[ebp-$08] mov edx,edx mov [ebp-$10],eax //begin @LoopStart : mov ebx, [ebp-$10] //mov edx, [ebp-$08] mov edx, edx cmp ebx, edx ja @LoopEnd //Result := Result + I; mov ecx,[ebp-$10] add [ebp-$0c],ecx inc dword ptr [ebp-$10] //end; jmp @LoopStart @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebx pop ebp end; Затем распределим ECX для I, заменив [EBP-$10] на ECX. function ForLoopBASM7(Start, Stop : Integer) : Integer; asm push ebp push ebx mov ebp,esp add esp,-$14 mov edx,edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,edx //mov [ebp-$10],eax mov ecx,eax //begin @LoopStart : //mov ebx, [ebp-$10] mov ebx, ecx mov edx, edx cmp ebx, edx ja @LoopEnd //Result := Result + I; //mov ecx,[ebp-$10] mov ecx,ecx add [ebp-$0c],ecx //inc dword ptr [ebp-$10] inc ecx //end; jmp @LoopStart @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebx pop ebp end; И на конец используем EAX для Result. Поскольку EAX также используется вверху функции для Start и как временный регистр для инициализации Result нулем, то мы должны добавить несколько строк для копирования Result в EAX после как EAX более нигде не будет использоваться для других целей. function ForLoopBASM8(Start, Stop : Integer) : Integer; asm push ebp push ebx mov ebp,esp add esp,-$14 mov edx,edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,edx mov ecx,eax mov eax, [ebp-$0c] //New //begin @LoopStart : mov ebx, ecx mov edx, edx cmp ebx, edx ja @LoopEnd //Result := Result + I; mov ecx,ecx //add [ebp-$0c],ecx add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : //mov eax,[ebp-$0c] mov eax,eax mov esp,ebp pop ebx pop ebp end; поскольку мы особо не обращали внимания при конвертировании на другие вещи, то у нас образовалось много строк типа MOV EAX, EAX. Сразу видно они излишни ;-). Просто удалим их. function ForLoopBASM9(Start, Stop : Integer) : Integer; asm push ebp push ebx mov ebp,esp add esp,-$14 //mov edx,edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] //mov edx,edx mov ecx,eax mov eax, [ebp-$0c] //begin @LoopStart : mov ebx, ecx //mov edx, edx cmp ebx, edx ja @LoopEnd //Result := Result + I; //mov ecx,ecx add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : //mov eax,eax mov esp,ebp pop ebx pop ebp end; При оптимизации ассемблерного кода есть две линии поведения, которым мы можем следовать. Мы можем думать как человек, пытаясь проявлять сообразительность, используя информацию из кода. Мы поступили так, когда распределяли регистры. Другой путь – это пытаться использовать системный подход, так как поступает компилятор/оптимизатор. Это путь разработки алгоритмов. Данный подход позже даст многое для оптимизации, так мы поступали много раз выше. Удаление лишних строк кода, MOV EAX, EAX, был примером удаления мертвого кода, что является базисом для любых оптимизаторов. Вверху функции мы еще имеем некоторые ссылки на стек. Для их удаления мы должны разместить эти переменные также в регистрах. В данное время мы выберем регистры EDI и ESI, поскольку они ни где не используются. Используем ESI для [EBP-$04] и EDI для [EBP-$0C]. Поскольку регистры ESI и EDI должны быть сохранены, мы поместим их в стек и потом восстановим. function ForLoopBASM10(Start, Stop : Integer) : Integer; asm push ebp push ebx push esi push edi mov ebp,esp add esp,-$14 //mov [ebp-$04],eax mov esi,eax //Result := 0; xor eax,eax //mov [ebp-$0c],eax mov edi,eax //for I := Start to Stop do //mov eax,[ebp-$04] mov eax,esi mov ecx,eax //mov eax, [ebp-$0c] mov eax, edi //begin @LoopStart : mov ebx, ecx cmp ebx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : mov esp,ebp pop edi pop esi pop ebx pop ebp end; Фрейм стека больше нигде не используется и поэтому нет нужды его настраивать, это также сохранит 4 инструкции. Затем заметим, что две строки mov eax,esi mov ecx,eax могут быть заменены одной. mov ecx, esi Это пример упрощения копирования с дальнейшим удалением мертвого кода. Любые другие строки не используют значения в EAX далее следующей строки, которая копирует обратно в ECX. Фактически это сразу переписывается строкой MOV EAX, EDI. Поэтому мы можем заменить вторую строку, на строку MOV ECX, ESI и удалить первую, которая становится мертвым кодом. function ForLoopBASM11(Start, Stop : Integer) : Integer; asm //push ebp push ebx push esi push edi //mov ebp,esp //add esp,-$14 mov esi,eax //Result := 0; xor eax,eax mov edi,eax //for I := Start to Stop do //mov eax,esi //mov ecx,eax mov ecx, esi mov eax, edi //begin @LoopStart : //mov ebx, ecx //cmp ebx, edx cmp ecx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : //mov esp,ebp pop edi pop esi pop ebx //pop ebp end; Строка XOR EAX, EAX присваивает начальное значение переменной Result в 0 и может быть перемещена на несколько строк ниже в место, где EAX будет использован в первый раз. Зато она никогда не должна быть помещена в тело цикла, что изменит логику функции, но может быть перед loopStart. Это убирает необходимость в копировании EAX в EDI и обратно в EAX в строке перед строкой комментария //FOR I := START TO STOP DO. function ForLoopBASM12(Start, Stop : Integer) : Integer; asm push ebx push esi push edi mov esi,eax //for I := Start to Stop do mov ecx, esi //Result := 0; xor eax,eax //mov edi,eax //mov eax, edi //begin @LoopStart : cmp ecx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : pop edi pop esi pop ebx end; После всего этого мы видим две строки MOV, которые копируют EAX в ECX через ESI. Это оставляет копию EAX в ESI, которая не используется. Поэтому одна пересылка EAX directly into ECX может заменить эти две строки. Это также уменьшение копирования и удаление мертвого кода. function ForLoopBASM13(Start, Stop : Integer) : Integer; asm push ebx //push esi push edi //mov esi,eax //for I := Start to Stop do //mov ecx, esi mov ecx, eax //Result := 0; xor eax,eax //begin @LoopStart : cmp ecx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : pop edi //pop esi pop ebx end; После удаления использования ESI, теперь нет необходимости в его сохранении и востановлении. function ForLoopBASM14(Start, Stop : Integer) : Integer; asm //push ebx //push edi //for I := Start to Stop do mov ecx, eax //Result := 0; xor eax,eax //begin @LoopStart : cmp ecx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx //end; jmp @LoopStart @LoopEnd : //pop edi //pop ebx end; Также, хоть и немного поздно мы видим, что EBX и EDI нигде не используются. После их удаления и удаления комментариев, в результате получилась следующая красивая функция. function ForLoopBASM15(Start, Stop : Integer) : Integer; asm mov ecx, eax //Result := 0; xor eax,eax //for I := Start to Stop do @LoopStart : cmp ecx, edx ja @LoopEnd //Result := Result + I; add eax,ecx inc ecx jmp @LoopStart @LoopEnd : end; Это потребовало много времени и усилий по оптимизации, поскольку мы начали с не оптимизированной версии компилятора. Данный длинный процесс послужил для иллюстрации количества работы оставленной компилятором для оптимизации. Иногда мы не используем должные алгоритмы для оптимизации, но мы можем получить тот же самый результат, используя их. Вместо проведения такого длинного пути над функцией мы можем позволить откомпилировать Паскаль функцию с включенной оптимизацией. Компилятор должен сделать всю оптимизацию, которую сделали мы. function ForLoopOpt(Start, Stop : Integer) : Integer; var I : Integer;
begin { } Result := 0; { xor ecx,ecx } for I := Start to Stop do { sub edx,eax jl +$08 inc edx xchg eax,edx } begin Result := Result + I; { add ecx,edx inc edx } end; { dec eax jnz -$06 } { mov eax,ecx } end; В данном случае Delphi действительно сделало прекрасную работу. Только две строки режут наши глаза. XCHG EAX, EDX простой обмен значениями в EAX и EDX, и MOV EAX, ECX копирует результат в EAX. Обе строки находятся за пределами цикла и не отнимают много времени. Теперь мы имеем две функции – одна без оптимизации цикла и одна с двумя. Для полного комплекта нам нужно еще две функции, одна с обратным циклом и одна с переменной NoName, только с оптимизацией. В начале урока мы видели, как удалить две оптимизации и это я сделал в двух оставшихся функциях. В Delphi оптимизированной выше функции, я оптимизировал инструкцию XCHG для обмена значений двух регистров. Поскольку мы хотим увидеть максимальный эффект только от оптимизации циклов, я удалил тело цикла Result := Result + I; Здесь четыре последние функции. function ForLoopNoLoopInverNoNoName(Start, Stop : Integer) : Integer; asm mov ecx, eax //Result := 0; xor eax,eax //for I := Start to Stop do @LoopStart : cmp ecx, edx ja @LoopEnd inc ecx jmp @LoopStart @LoopEnd : end; Цикл состоит их 4 инструкции. 1 CMP, 1 JA, 1 INC и 1 JMP. Latency и throughput для этих двух инструкции на P4 следующие: CMP 0.5/0.5, JA X/0.5, INC 0.5/1 и JMP X/0.5. X означает, что "latency is not applicable to this instruction" «Латентность не применима к данной инструкции». Дополнительная латентность, которую мы имеет: 0.5 + X + 0.5 + X = ? циклов. function ForLoopLoopInverNoNoName(Start, Stop : Integer) : Integer; asm mov ecx, eax //Result := 0; xor eax,eax //for I := Start to Stop do cmp ecx, edx ja @LoopEnd @LoopStart : inc ecx cmp ecx, edx jbe @LoopStart @LoopEnd : end; Данный цикл состоит из 3 инструкций, также с неизвестной суммой латентности. function ForLoopNoLoopInverNoName(Start, Stop : Integer) : Integer; asm //Result := 0; xor ecx,ecx //for I := Start to Stop do sub edx,eax cmp edx, 0 @LoopStart : jz @LoopEnd inc eax dec edx jmp @LoopStart @LoopEnd : mov eax,ecx end; Данный цикл состоит из 4 инструкций, также с неизвестной суммой латентности. Заметим, что две инструкции INC/DEC имеют возможность выполняться параллельно. Поскольку за DEC NoName инструкцией не следует условный переход JMP, это выглядит как преимущество, в отсутствии необходимости использования инструкций CMP или TEST для установки флагов, но инструкция JMP не изменяет значения флагов и они доступны, когда мы попадаем на инструкцию JZ в начале цикла. Только в первой итерации инструкция CMP EDX,0 необходима для этого. function ForLoopLoopInverNoName(Start, Stop : Integer) : Integer; asm //Result := 0; xor ecx,ecx //for I := Start to Stop do sub edx,eax jl @LoopEnd inc edx @LoopStart : inc eax dec edx jnz @LoopStart @LoopEnd : mov eax,ecx end; Данный цикл состоит из 3 инструкций, также с неизвестной суммой латентности. Здесь также есть независимая пара INC/DEC. Это очень простая измерительная программа (benchmark), которую я использую для проверки производительности этих четырех функций. var Starttime, Endtime, Runtime : TDateTime; I, LoopResult : Integer; RunTimeSec, NoOfLoopsPerSec, NoOfLoops, ClockCount, LoopEnd2Float, LoopEndFloat, LoopStartFloat : Double;
begin Starttime := Time; for I := 1 to LOOPEND2 do begin LoopResult := ForLoopNoLoopInverNoName(LOOPSTART, LOOPEND); end; Endtime := Time; Runtime := Endtime - Starttime; CEdit.Text := IntToStr(LoopResult); RuntimeEdit4.Text := TimeToStr(Runtime); RunTimeSec := RunTime*24*60*60; LoopEnd2Float := LOOPEND2; LoopEndFloat := LOOPEND; LoopStartFloat := LOOPSTART; NoOfLoops := LoopEnd2Float * (LoopEndFloat - LoopStartFloat); NoOfLoopsPerSec := NoOfLoops / RunTimeSec; ClockCount := CLOCK / NoOfLoopsPerSec; ClockCountEdit4.Text := FloatToStrf(ClockCount, ffFixed, 9, 1); end; результаты на P4 1920 следующие: No Loop Inversion and No NoName variable 00:00:55 (2.7 Clock cycles) Loop Inversion but No NoName variable 00:00:39 (1.9 Clock cycles) No Loop Inversion but NoName variable 00:01:02 (3.0 Clock cycles) Loop Inversion + NoName 00:00:41 (2.0 Clock cycles) результаты на P3 1400 следующие: No Loop Inversion and No NoName variable 00:01:26 (3.0 Clock cycles) Loop Inversion but No NoName variable 00:01:26 (3.0 Clock cycles) No Loop Inversion but NoName variable 00:01:55 (4.0 Clock cycles) Loop Inversion + NoName 00:01:26 (3.0 Clock cycles) Конечно, clock count числа должны быть целыми. На P4 возможны пол цикла, в связи с double-clocked ALU. Наши измерения не так хороши, как хотелось бы, но для сравнения производительности циклов они достаточны. Заключение для P4. Используйте только No Loop Inversion или loop inversion with NoName variable оптимизацией. Заключение для P3. Не используйте No Loop Inversion but NoName variable оптимизацию. Заключение для обеих платформ. Используйте обе оптимизации как делает Delphi. Также обратите внимание насколько эффективен P4 на этом коде.