Оптимизация производительности

При переходе приложения, расширения ядра или другого кода к 64-разрядному, можно заметить снижение производительности, в зависимости от кода. В то время как в некоторых случаях это вызывается путем простого перемещения большего количества данных, во многих случаях, они вызываются довольно незначительными проблемами расположения структуры данных и другими легко исправленными проблемами. Эта глава кратко описывает некоторые из этих проблем и объясняет, как исправить их.

Чрезмерное увеличение размера структуры данных

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

Как отмечено в Подсказках по Типу данных и Выравниванию, выравнивание структуры данных отличается для 64-разрядных исполнимых программ. Следующий отрывок показывает небольшую структуру данных:

struct mystruct {
    char member_one[4];
    void *member_two;
    int member_three;
    void *member_four;
    int member_five;
}

В 32-разрядной исполнимой программе эта структура данных берет 20 байтов. Указатели 4 байта длиной, и выровненные на 4-байтовой границе. Если структура будет выделена динамично, даже при том, что Вы только запрашиваете 20 байтов, то средство выделения памяти фактически присвоит ее область памяти, которая 32 байта длиной (самая близкая power-2 граница).

В 64-разрядной исполнимой программе эта структура данных чрезмерно увеличивается в размерах к огромным 36 байтам и дополняется к 40 байтам (потому что это должны быть выровненных 8 байтов), что означает, что, если выделено динамично, это занимает 64 байта.

Для объяснения почему предположите, что переменная запускается в нуле (0) адреса. Переменная member_one берет 4 байта, таким образом, следующий элемент обычно запускал бы в байте 4.

Поскольку member_two указатель, он должен быть выровненный на 8-байтовой границе. Таким образом его адрес должен быть делимым 8. Поскольку 4 не является делимым 8, структура должна быть дополнена. Результатом является 4-байтовый разрыв между member_one и member_two.

Переменная member_three не проблема — она должна быть выровненная на 4-байтовой границе и обычно начиналась бы при смещении 16, таким образом, она не генерирует разрыв.

Переменная member_four, однако, должен быть выровненный на 8-байтовой границе. Компилятор генерирует другой 4-байтовый разрыв, и эта переменная начинается при смещении 24. Наконец, переменная member_five начинается при смещении 32 и заканчивается при смещении 36.

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

struct mystruct {
    char member_one[4];
    int member_three;
    void *member_two;
    void *member_four;
    int member_five;
}

Путем смещения третьего элемента перед вторым элементом, переменными member_two и member_three теперь естественно падение на 8-байтовых границах. В результате эта структура сокращена до простых 28 байтов и дополняется к 32 байтам компилятором для создания его 8 байтами выровненный. Это также берет то же 32-байтовое динамическое выделение памяти, как это сделало в 32-разрядной версии кода.

Самый простой способ гарантировать максимальную эффективность структуры данных состоит в том, чтобы запуститься с элементов, выровненных к самой большой границе байта, и проложить себе путь вниз элементам, выровненным к самому маленькому. Поместите выровненных 8 байтов long и pointer элементы сначала, сопровождаемый на 4 байта выровнялись int значения, сопровождаемые на два байта, выровнялись short значения, и наконец заканчиваются выровненным байтом char значения.

struct mystruct {
    void *member_two;
    void *member_four;
    int member_five;
    int member_three;
    char member_one[4];
}

Строка кэша промахи

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

for (i=0; i<columns; i++) {
    for (j=i; j<rows; j++) {
        arr[j][i] = ...
    }
}

Этот код выполняет серию операций на массиве. Однако это выполняет итерации через массив довольно неэффективным способом. C массивы хранилищ в главном строкой порядке, но этот код выполняет итерации способом, более подходящим для главного массива столбца — выполняющий итерации посредством первой записи в каждой строке, тогда второй записи в каждой строке, и т.д.

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

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

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

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

Предотвращение невыровненных доступов

При создании структур данных с упакованным выравниванием можно видеть регрессию производительности, вызванную невыровненными штрафами доступа. Например:

#pragma pack(1)
typedef struct
{
        long a;
        int b;
        int b1;
        int b2;
        long double c;
} myStruct;
#pragma options align=reset

Идеально, переменная c должен быть выровненный на 16-байтовой границе. В 32-разрядной среде это заканчивается выровненное правильно потому что переменная a 4 байта длиной. В 64-разрядной среде эта переменная заканчивается смещенная на 4 байта от ее идеальной позиции потому что переменная a 8 байтов длиной.

В то время как единственный невыровненный штраф доступа является относительно маленьким, кумулятивные эффекты могут быть значительными. Учитывая цикл, присваивающий нуль значения (0) неоднократно к переменным a, b, и c, можно улучшить производительность цикла больше чем на 20% путем простого перемещения переменной b2 после переменной c. Конечно, это разрушает производительность в 32-разрядной версии потому что переменная c тогда не выравнивается.

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

Для максимальной производительности необходимо также переупорядочить эту структуру для размещения переменной c во-первых, сопровождаемый переменной a, сопровождаемый остающимися переменными (в порядке от самого большого размера выравнивания до самого маленького). Это достигает оптимальной эффективности упаковки при предотвращении штрафов выравнивания независимо от архитектуры ЦП и часто устраняет потребность в упаковке прагм.