Счетчик петли со знаком или без знака - PullRequest
0 голосов
/ 17 октября 2018

Я был очень удивлен различием между использованием счетчика цикла со знаком и без знака в этом простом примере:

double const* a;
__assume_aligned(a, 64);
double s = 0.0;

//for ( unsigned int i = 0; i < 1024*1024; i++ )
for ( int i = 0; i < 1024*1024; i++ )
{
    s += a[i];
}

В случае со знаком генерируется icc 19.0.0 (я показываю развернутую часть цикла):

..B1.2:
    vaddpd    zmm7, zmm7, ZMMWORD PTR [rdi+rax*8]
    vaddpd    zmm6, zmm6, ZMMWORD PTR [64+rdi+rax*8]
    vaddpd    zmm5, zmm5, ZMMWORD PTR [128+rdi+rax*8]
    vaddpd    zmm4, zmm4, ZMMWORD PTR [192+rdi+rax*8]
    vaddpd    zmm3, zmm3, ZMMWORD PTR [256+rdi+rax*8]
    vaddpd    zmm2, zmm2, ZMMWORD PTR [320+rdi+rax*8]
    vaddpd    zmm1, zmm1, ZMMWORD PTR [384+rdi+rax*8]
    vaddpd    zmm0, zmm0, ZMMWORD PTR [448+rdi+rax*8]
    add       rax, 64
    cmp       rax, 1048576
    jb        ..B1.2        # Prob 99%

В случае без знака icc использовал дополнительные регистры для адресации памяти, с соответствующими LEA s:

..B1.2:
    lea       edx, DWORD PTR [8+rax]
    vaddpd    zmm6, zmm6, ZMMWORD PTR [rdi+rdx*8]
    lea       ecx, DWORD PTR [16+rax]
    vaddpd    zmm5, zmm5, ZMMWORD PTR [rdi+rcx*8]
    vaddpd    zmm7, zmm7, ZMMWORD PTR [rdi+rax*8]
    lea       esi, DWORD PTR [24+rax]
    vaddpd    zmm4, zmm4, ZMMWORD PTR [rdi+rsi*8]
    lea       r8d, DWORD PTR [32+rax]
    vaddpd    zmm3, zmm3, ZMMWORD PTR [rdi+r8*8]
    lea       r9d, DWORD PTR [40+rax]
    vaddpd    zmm2, zmm2, ZMMWORD PTR [rdi+r9*8]
    lea       r10d, DWORD PTR [48+rax]
    vaddpd    zmm1, zmm1, ZMMWORD PTR [rdi+r10*8]
    lea       r11d, DWORD PTR [56+rax]
    add       eax, 64
    vaddpd    zmm0, zmm0, ZMMWORD PTR [rdi+r11*8]
    cmp       eax, 1048576
    jb        ..B1.2        # Prob 99%

Мне удивительно, что он не выдал тот же код(учитывая количество циклов компиляции).Это проблема оптимизации компилятора?

Опции компиляции: -O3 -march=skylake-avx512 -mtune=skylake-avx512 -qopt-zmm-usage=high

1 Ответ

0 голосов
/ 17 октября 2018

Это глупая пропущенная оптимизация по 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 может иметь аналогичный эффект.

Так что выполнение указателя конца и увеличения указателя в источнике часто хорошо, потому что это частооптимально для асм.

...