Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #71, ОКТЯБРЬ 2006 г.

акробатика для программиста

КРИС КАСПЕРСКИ

Спецвыпуск: Хакер, номер #071, стр. 071-056-4


Рассмотрим типичный пример:

for(a=0;a<strlen(s);a++) b+=s[a];

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

t = strlen(s);

for(a=0;a<t;a++) b+=s[a];

[нормализация циклов.]

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

Рассмотрим типичный цикл:

for (a = from; a < to; i+=(-step))

{

// тело цикла

}

Алгоритм нормализации выглядит так:

for (NCL = 0; i < (to - from + step)/step - 1; 1)

{

i = step*NLC + from;

// тело цикла

}

i = step * _max((to - from + step)/step, 0) + from;

Наибольшую отдачу нормализация дает на циклах с заранее известным количеством итераций, то есть когда выражение (to from + step)/step представляет собой константу, вычисляемую еще на стадии трансляции.

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

[разворот циклов.]

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

Компактные циклы вида for(a=0;a<n;a++) *dst++= *src++; исполняются крайне медленно и должны быть развернуты (unrolled). Под «разворотом» в общем случае понимается многократное дублирование цикла, которое в классическом случае реализуется так:

for(i=1; i<n;i+)

k += (n % i);

цикл, развернутый на 4 итерации (меньший размер, большая скорость)

for(i=1; i<n;i+=4)

{

k += (n % i) +\

(n % i+1) + \

(n % i+2) + \

(n % i+3);

}

// выполняем оставшиеся итерации

for(i=4; i<n;i++) k += (n % i);

За исключением Microsoft Visual C++, все остальные рассматриваемые компиляторы умеют разворачивать циклы и самостоятельно, но... делают это настолько неумело, что вместо ожидаемого увеличения производительности сплошь и рядом наблюдается ее падение, поэтому автоматический разворот лучше сразу запретить и оптимизировать программу вручную, подбирая подходящую степень разворота опытным путем (вместе с профилировщиком).

Тут ведь как — чем сильнее разворот, тем больше места занимает код, и появляется риск, что в кэш первого уровня он может вообще не влезть, вызывая обвальное падение производительности! (Подробнее о влиянии степени разворота на быстродействие можно прочитать в моей «технике оптимизации», электронная копия которой, как обычно, лежит на моем мыщъхином ftp://nezumi.org.ru).

Назад на стр. 071-056-3  Содержание  Вперед на стр. 071-056-5