Clang генерирует худший код для 7 сравнений, чем для 8 сравнений - PullRequest
5 голосов
/ 23 сентября 2019

Я был заинтригован способностью clang преобразовывать множество сравнений малых целых чисел в одну большую SIMD-инструкцию, но затем я заметил кое-что странное.Clang генерировал «худший» код (в моей любительской оценке), когда у меня было 7 сравнений по сравнению с кодом, когда у меня было 8 сравнений.

bool f1(short x){
    return (x==-1) | (x == 150) |
           (x==5) | (x==64) | 
           (x==15) | (x==223) | 
           (x==42) | (x==47);
}

bool f2(short x){
    return (x==-1) | (x == 150) |
           (x==5) | (x==64) | 
           (x==15) | (x==223) | 
           (x==42);
}

Мой вопрос - это небольшая ошибка производительности, или у clang оченьвеская причина не желать вводить фиктивное сравнение (то есть делать вид, что есть еще одно сравнение с одним из 7 значений) и использовать еще одну константу в коде для достижения этого.

ссылка Годболта здесь:

# clang(trunk) -O2 -march=haswell
f1(short):
    vmovd   xmm0, edi
    vpbroadcastw    xmm0, xmm0             # set1(x)
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI0_0]  # 16 bytes = 8 shorts
    vpacksswb       xmm0, xmm0, xmm0
    vpmovmskb       eax, xmm0
    test    al, al
    setne   al           # booleanize the parallel-compare bitmask
    ret

против

f2(short):
    cmp     di, -1
    sete    r8b
    cmp     edi, 150
    sete    dl
    cmp     di, 5             # scalar checks of 3 conditions
    vmovd   xmm0, edi
    vpbroadcastw    xmm0, xmm0
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI1_0]  # low 8 bytes = 4 shorts
    sete    al
    vpmovsxwd       xmm0, xmm0
    vmovmskps       esi, xmm0
    test    sil, sil
    setne   cl                # SIMD check of the other 4
    or      al, r8b
    or      al, dl
    or      al, cl            # and combine.
    ret

quickbench, похоже, не работает, потому что IDK предоставил ему флаг -mavx2.(Примечание редактора: простой подсчет мопов для входной стоимости показывает, что это явно хуже для пропускной способности. И также для задержки.)

1 Ответ

4 голосов
/ 24 сентября 2019

Похоже, что оптимизатор clang не задумывался о дублировании элемента, чтобы довести его до удобного для SIMD числа сравнений.Но вы правы, это было бы лучше, чем делать дополнительную скалярную работу. Очевидно, что пропущенная оптимизация должна быть зарегистрирована как ошибка оптимизатора clang / LLVM.https://bugs.llvm.org/


Asm для f1() явно лучше, чем f2(): vpacksswb xmm имеет такую ​​же стоимость, как и vpmovsxwd xmm на основных процессорах Intel и AMD, как и другие однопользовательские тасовки,И если что-нибудь vpmovsx -> vmovmskps могло бы иметь задержку обхода между целочисленными доменами и доменами FP 1 .


Сноска 1. Вероятно, нет дополнительной задержки обхода на основных процессорах Intel сAVX2 (семья Sandybridge);целочисленные тасования между операциями FP, как правило, в порядке, IIRC.(https://agner.org/optimize/). Но для версии SSE4.1 в Nehalem, да, может быть дополнительный штраф, который не будет иметь целочисленная версия.

Вам не нужен AVX2, но трансляция в слова водна инструкция без управляющего вектора pshufb делает ее более эффективной, и clang выбирает pshuflw -> pshufd для -march=nehalem


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

Вместо test al, al, можно выбрать, какие биты вы хотите проверить, например, с помощью test sil, 0b00001010, чтобы проверить биты1 и 3, но игнорируют ненулевые биты в других позициях.

pcmpeqw устанавливает оба байта одинаково внутри элемента слова, поэтому можно pmovmskb получить результат и получить целое число с парами битов.

Существует также нулевое преимущество использования регистра байтов вместо регистра dword: test sil,sil следует избегать префикса REX и использовать test esi,esi.

Так что даже без дублирования одним из условий, f2() может быть:

f2:
    vmovd           xmm0, edi
    vpbroadcastw    xmm0, xmm0             # set1(x)
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI0_0]
    vpmovmskb       eax, xmm0
    test    eax, 0b011111111111111    # (1<<15) - 1 = low 14 bits set
    setne   al
    ret

То, что test установит ZF в соответствии с младшими 14 битами результата pmovmksb, потому что старшие биты очищаются в маске TEST.ТЕСТ = И это не записывает свой вывод.Часто полезно для выбора частей маски сравнения.

Но так как в первую очередь нам нужна 16-байтовая константа в памяти, да, мы должны дублировать один из элементов, чтобы заполнить его до 8 элементов.Тогда мы можем использовать test eax,eax как нормальный человек.Сжатие маски до 8-битового AL - это пустая трата времени и размера кода.test r32, r32 так же быстро, как test r8,r8 и не нуждается в префиксе REX для SIL, DIL или BPL.

Интересный факт: AVX512VL позволит нам использовать vpbroadcastw xmm0, edi для объединения movdи транслировать.


Или для сравнения только 4 элементов, вместо дополнительной перестановки для movmskps, нам нужен только SSE2 здесь.И использование маски действительно полезно.

test_4_possibilities_SSE2:
    movd            xmm0, edi
    pshufd          xmm0, xmm0, 0             # set1_epi32(x)
    pcmpeqw         xmm0, [const]             # == set_epi32(a, b, c, d)
    pmovmskb        eax, xmm0
    test    eax, 0b0001000100010001     # the low bit of each group of 4
    setne   al
    ret

Мы выполняем трансляцию слова и игнорируем результат сравнения в старших 16 битах каждого 32-битного элемента.Использование маски для test позволяет нам делать это дешевле, чем любая дополнительная инструкция.

Без AVX2 широковещательная передача SIMD-меча с pshufd дешевле, чем необходимость трансляции слова.

Другим вариантом является imul с 0x00010001 для трансляции слова в 32-битный регистр, но с задержкой в ​​3 цикла, поэтому она потенциально хуже, чем punpcklwd -> pshufd

Внутри циклатем не менее, стоило бы загрузить управляющий вектор для pshufb (SSSE3) вместо использования 2 перемешиваний или imul.

...