Это глупая пропущенная оптимизация по ICC .Это не относится к AVX512;это по-прежнему происходит со стандартными / общими настройками арки.
lea ecx, DWORD PTR [16+rax]
вычисляет i+16
как часть развертывания с усечением до 32-битного (размер 32-битного операнда) и с нулевым расширением до 64-bit (подразумевается в x86-64 при записи 32-битного регистра).Это явно реализует семантику беззнакового переноса при ширине шрифта.
У gcc и clang нет проблем с доказательством того, что unsigned i
не будет переносить, поэтому они могут оптимизировать удаление нулевого расширения из 32-битногоunsigned для 64-битной ширины указателя для использования в режиме адресации, потому что известна верхняя граница цикла 1 .
Напомним, что переход без знака хорошо определен в C и C ++, но переполнение со знаком - неопределенное поведение.Это означает, что знаковые переменные могут быть переведены в ширину указателя, и что компилятору не нужно возвращать расширение знака в ширину указателя каждый раз, когда они используются в качестве индекса массива.(a[i]
эквивалентно *(a+i)
, а правила добавления целых чисел к указателям означают, что расширение знака необходимо для узких значений, где верхние биты регистра могут не совпадать.)
Переполнение знакаUB - это то, почему ICC может правильно оптимизировать счетчик со знаком, даже если он не использует информацию о диапазоне.Смотрите также http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html (о неопределенном поведении).Обратите внимание, что он использует add rax, 64
и cmp
с 64-битным размером операнда (RAX вместо EAX)
Я превратил ваш код в MCVE для тестирования с другими компиляторами.__assume_aligned
только для ICC, поэтому я использовал GNU C __builtin_assume_aligned
.
#define COUNTER_TYPE unsigned
double sum(const double *a) {
a = __builtin_assume_aligned(a, 64);
double s = 0.0;
for ( COUNTER_TYPE i = 0; i < 1024*1024; i++ )
s += a[i];
return s;
}
clang компилирует вашу функцию следующим образом ( Проводник компилятора Godbolt ):
# clang 7.0 -O3
sum: # @sum
xorpd xmm0, xmm0
xor eax, eax
xorpd xmm1, xmm1
.LBB0_1: # =>This Inner Loop Header: Depth=1
addpd xmm0, xmmword ptr [rdi + 8*rax]
addpd xmm1, xmmword ptr [rdi + 8*rax + 16]
addpd xmm0, xmmword ptr [rdi + 8*rax + 32]
addpd xmm1, xmmword ptr [rdi + 8*rax + 48]
addpd xmm0, xmmword ptr [rdi + 8*rax + 64]
addpd xmm1, xmmword ptr [rdi + 8*rax + 80]
addpd xmm0, xmmword ptr [rdi + 8*rax + 96]
addpd xmm1, xmmword ptr [rdi + 8*rax + 112]
add rax, 16 # 64-bit loop counter
cmp rax, 1048576
jne .LBB0_1
addpd xmm1, xmm0
movapd xmm0, xmm1 # horizontal sum
movhlps xmm0, xmm1 # xmm0 = xmm1[1],xmm0[1]
addpd xmm0, xmm1
ret
Я не включил AVX, это не меняет структуру цикла.Обратите внимание, что в clang используются только 2 векторных аккумулятора, поэтому узкое место в FP добавляет задержку для большинства современных процессоров, если в кеше L1d данные перегреваются.Skylake может поддерживать до 8 addpd
в полете одновременно (2 на тактовую пропускную способность с задержкой в 4 цикла).Таким образом, ICC делает намного лучшую работу для случаев, когда (некоторые из) данных горячие в L2 или особенно L1d-кэше.
Странно, что clang не использовал приращение указателя, если он собирается добавить /Все равноЭто займет всего пару дополнительных инструкций перед циклом и упростит режимы адресации, позволяющие микросинтегрировать нагрузку даже на Sandybridge.(Но это не AVX, поэтому Haswell и более поздние версии могут поддерживать микроплавление нагрузки. Режимы микроплавления и адресации ).GCC делает это, но не развертывает его вообще, что является GCC по умолчанию без оптимизации на основе профилей.
В любом случае код AVX512 ICC будет расслаиваться в отдельную загрузку и добавлять мопы на этапе выпуска / переименования (или до добавления в IDQ я не уверен).Поэтому довольно глупо, что он не использует приращение указателя, чтобы сохранить полосу пропускания внешнего интерфейса, потреблять меньше места в ROB для большего окна не по порядку и быть более дружественным к гиперпоточности.
Сноска 1:
(И даже если это не так, бесконечный цикл без побочных эффектов, таких как volatile
или atomic
, является неопределенным поведением, поэтому даже с i <= n
с переменной времени выполненияn
, компилятору будет позволено предположить, что цикл не является бесконечным, и поэтому i
не переносится. Is while (1); неопределенное поведение в C? )
На практике gcc и clang не используют это в своих интересах, и делают цикл, который на самом деле потенциально бесконечен, и не допускает автоматической векторизации из-за этой возможной странности.Поэтому избегайте i <= n
с переменной времени выполнения n
, особенно для сравнения без знака.Вместо этого используйте i < n
.
Если развернуть, i += 2
может иметь аналогичный эффект.
Так что выполнение указателя конца и увеличения указателя в источнике часто хорошо, потому что это частооптимально для асм.