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

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

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

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


обработка массивов по столбцам (неоптимизированный вариант)

for(j=0;j<m;j++)

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

a[i][j] = b[i][j] + c[i][j];

Здесь три массива обрабатываются по столбцам, что крайне непроизводительно, и для достижения наивысшей эффективности циклы i и j следует поменять местами. Устоявшегося названия у данной методики оптимизации нет, и в каждом источнике она называется по-разному: «loop permutation/interchange/reversing», «rearranging array dimensions» и т. д. Как бы там ни было, оптимизированный вариант выглядит так:

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

for(j=0;j<m;j++)

a[i][j] = b[i][j] + c[i][j];

Все рассматриваемые компиляторы поддерживают данную стратегию оптимизации, однако их интеллектуальные способности очень ограничены, и со следующим примером справляется только Hewlett-Packard C++:

сложный случай обработки данных по столбцам

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

for (j=0; j<n; j++)

for (k=0; k<n; k++)

a[j][i] = a[j][i] + b[k][i] * c[j][k];

[удаление лишних обращений к памяти.]

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

пример с лишними обращениями к памяти, от которых нельзя избавиться

f(int *a, int *b)

{

int x;

x = *a + *b; // сложение содержимого двух ячеек

*b = 0x69; // изменение ячейки *b, адрес которой не известен компилятору

x += *a; // нет гарантии, что запись в ячейку *b не изменила ячейку *a

}

Компилятор не имеет права на размещение содержимого ячейки *a в регистровой переменной, поскольку, если ячейки *a и *b частично или полностью перекрываются, модификация ячейки *b приводит к неожиданному изменению ячейки *a! Бред, конечно, но ведь Стандарт этого не запрещает, а компилятор обязан следовать Стандарту, иначе его место — на свалке.

То же самое относится и к следующему примеру:

лишние обращения к памяти, от которых можно избавиться вручную

f(char *x, int *dst, int n)

{

int i;

for (i = 0; i < n; i++) *dst += x[i];

}

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

f(char *x, int *dst, int n)

{

int i,t =0;

for (i=0;i<n;i++) t+=x[i]; // сохранение суммы во временной переменной

*dst+=t; // запись конечного результата в память

}

[регистровые ре-ассоциации.]

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

Код вида «for (i = 0; I < n; i++) n+=a[i];» легко оптимизировать, если переписать его так: «for (p= a; p < &a[n]; p++) n+=*p;». Насколько известно мыщъх'у, впервые эта техника использовалась в компиляторах фирмы Hewlett-Packard, где она фигурировала под термином «register reassociation». А вот остальные рассматриваемые нами компиляторы этого делать, увы, не умеют.

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