Это связано, но не то же самое, что и этот вопрос: Оптимизация производительности сборки x86-64 - Согласование и предсказание ветвей и немного связано с моим предыдущим вопросом: Неподписанное 64-битное преобразование: почему этот алгоритм из g++
Ниже приведен тестовый пример не в реальном мире. Этот алгоритм тестирования примитивности не является разумным. Я подозреваю, что любой алгоритм реального мира никогда не будет выполнять такой маленький внутренний цикл довольно много раз (num
- это просто размер около 2 ** 50). В С++ 11:
using nt = unsigned long long;
bool is_prime_float(nt num)
{
for (nt n=2; n<=sqrt(num); ++n) {
if ( (num%n)==0 ) { return false; }
}
return true;
}
Затем g++ -std=c++11 -O3 -S
выдает следующее: RCX содержит n
и XMM6, содержащие sqrt(num)
. См. Мой предыдущий пост для оставшегося кода (который никогда не выполняется в этом примере, поскольку RCX никогда не становится достаточно большим, чтобы рассматриваться как подписанный отрицательный результат).
jmp .L20
.p2align 4,,10
.L37:
pxor %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd %xmm0, %xmm6
jb .L36 // Exit the loop
.L20:
xorl %edx, %edx
movq %rbx, %rax
divq %rcx
testq %rdx, %rdx
je .L30 // Failed divisibility test
addq $1, %rcx
jns .L37
// Further code to deal with case when ucomisd can't be used
Я использую это время, используя std::chrono::steady_clock
. Я продолжал получать странные изменения производительности: просто добавляя или удаляя другой код. В конечном итоге я отследил это до вопроса о выравнивании. Команда .p2align 4,,10
пыталась выровнять по границе 2 ** 4 = 16 байтов, но для этого используется не более 10 байтов заполнения, я думаю, чтобы сбалансировать выравнивание и размер кода.
Я написал Python script, чтобы заменить .p2align 4,,10
на ручное число команд nop
. Следующий график рассеяния показывает самые быстрые 15 из 20 прогонов, время в секундах, количество отступов байтов по оси x:
Из objdump
без заполнения, команда pxor будет выполняться со смещением 0x402f5f. Запуск на ноутбуке, Sandybridge i5-3210m, turboboost отключен, я обнаружил, что
- Для заполнения 0 байтов, низкой производительности (0,42 с)
- Для заполнения 1-4 байта (смещение 0x402f60 до 0x402f63) получается немного лучше (0,41 с, видимое на графике).
- Для заполнения 5-20 байт (смещение 0x402f64 до 0x402f73) получают быструю производительность (0,37 с)
- Заполнение от 21 до 32 байт (смещение 0x402f74 до 0x402f7f) медленная производительность (0,42 с)
- Затем циклы по 32-байтовому образцу
Таким образом, выравнивание по 16 байт не дает лучшей производительности - оно помещает нас в немного лучшую (или только меньшую вариацию, из области разброса). Выравнивание 32 плюс 4-19 дает наилучшую производительность.
Почему я вижу эту разницу в производительности? Почему это, похоже, нарушает правило выравнивания цепей ветки на 16-байтовую границу (см., Например, руководство по оптимизации Intel).
Я не вижу проблем с прогнозированием ветвлений. Может ли это быть quopk кэша uop?
Изменив алгоритм С++ на кеширование sqrt(num)
в 64-битовом целое, а затем сделайте цикл чисто целочисленным, я удалю проблему - выравнивание теперь не имеет никакого значения.