LINUX.ORG.RU
ФорумTalks

Про состояние векторизации в последних версиях компилятора

 , , ,


0

3

Я обычно говняю ржавую мерзость за мерзотный синтаксис, но вот на этой конференции мне понравилось послушать про это вселенское зло:

Видео с конференции С++ Russia. Называется «производительность языка Rust» https://www.youtube.com/watch?v=g9mjctBfo5c

Заявляется, что проверки индексов не дают ржавому векторизовать циклы. Что вообще указывает на какой-то недостаток в архитектуре существующих компиляторов. Я погуглил что-нибудь на эту тему, нашел вот такую ссылку: https://www.nickwilcox.com/blog/autovec/ где автор пытался заставить компилятор работать по человечески на вот таком вот банальном коде

pub fn mix_mono_to_stereo(dst: &mut [f32], src: &[f32], gain_l: f32, gain_r: f32) {
    for i in 0..src.len() {
        dst[i * 2 + 0] = src[i] * gain_l;
        dst[i * 2 + 1] = src[i] * gain_r;
    }
}

Посмотрел на выхлоп компилятора, потом накидал похожий код на С++, посмотрел что выдает llvm, gcc… В общем, llvm генерит что-то совершенно демоническое. GCC делает компактный код, но к сожалению совсем не векторит, а впридачу ко всему еще и хрень какую-то творит, например грузит одно и тоже значение дважды из памяти. И проверки индексов тут явно ни при чем. Оно просто генерит дерьмо.

Что должен был сделать векторизатор здорового человека? Для начала предположить что цикл будет пердолиться долго, и подготовить операнды.

#вариант1:
    punpcklps xmm0,xmm1
    movlhps   xmm0,xmm0
#вариант2:
    pshufps   xmm0,xmm0,0
    pshufps   xmm1,xmm1,0

затем оценить сколько данных на входе:

    cmp edx,4
    jb  loop_x1
    sub edx,4
loop_x4:

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

#вариант1:
    movups    xmm2,[rsi]
    movaps    xmm3,xmm2
    pshufps   xmm2,xmm2,0b01010000
    pshufps   xmm3,xmm3,0b11111010
    pmulps    xmm2,xmm0 ;первые 2 пары
    pmulps    xmm3,xmm0 ;вторые 2 пары
    
#вариант2:
    movups    xmm2,[rsi]
    movaps    xmm4,xmm2
    pmulps    xmm2,xmm0
    pmulps    xmm4,xmm1
    movaps    xmm3,xmm2
    punpcklps xmm2,xmm4 ;первые 2 пары
    punpckhps xmm3,xmm4 ;вторые 2 пары

Кстати можете прямо щас посмотреть на выхлоп аналогичного «векторизованного» кода на С++ или расте и ужаснуться. Насколько же он, черт подери ужасен. Дальше надо данные записать. Можно сразу проверить каким-то образом что место в буфере есть, этот момент я упущу потому что мне решительно лень и вообще я уверено что разобраться сравнить число с 4 и сделать условный переход каждая собака умеет. А запись вообще-то можно сделать той же командой movdqu. Она на современных процах вполне рабочая. Дальше мы увеличиваем rsi на 16, уменьшаем edx на 4 и делаем jno loop_x4, иначе add edx,4 + jmp loop_x1.

Если бы современные компиляторы не были такими наркоманскими внутре, то реализовать подобный оптимизатор для них можно было бы например за ~20 ящиков хорошего пива. Цикл короткий, мы видим отношение выхода ко входу фиксированно 2 к 1 и повторяется монотонно и шаг итерации 8 байт, или половина регистра. Значит строительная единица - 2 умножения , один вход повторяется, другой чередуется и это повторяется два раза чтоб работать с 16 байтами. Это компилятор сделать смог бы. То есть мою логику абстрактаный компилятор повторить мог бы без каких-то запредельно сложных алгоритмов. Причем не только с массивом, но и например с матрицей и возможно даже со списком или строкой если там внутре не будет какого-то лютого ветвления и логики. Если добавить маскируемые векторные команды, которых в x86 нету в нормальном виде, дело пойдет еще проще.

В случае с проверкой индексов, можно как-то научить компилятор делать выводы, например что если индекс нарастает монотонно, то в векторизованном цикле надо сразу проверять его на N-кратное увеличение. Опять же, это не нерешаемая задача.

Ответ на: комментарий от Enthusiast

А вот по строкам ассемблера можно точнее сказать какой код будет исполняться быстрее или медленнее.

Для этого надо очень хорошо уметь в голове строить модель выполнения, хорошо понимать задержки, помнить этапы конвейера для каждой операции. Сложно получается. Конечно, если всё время работаешь с одним и тем же процессором, то быстро набъёшь руку, но стоит переключиться на другое железо, и всё, модель поплыла.

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

Поэтому если код золотой и выжать нужно максимум, то и модель следует строить, и измерять циклы явно, может ещё и perf counters придётся присобачить, а если есть возможность то и симулятор использовать, как у взрослых.

VIT ★★
()
Ответ на: комментарий от Enthusiast

Будет. Потому надо прогонять несколько раз и считать среднее время и отклонения. Статистика и теория вероятности в помощь.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от Enthusiast

А вот по строкам ассемблера можно точнее сказать какой код будет исполняться быстрее или медленнее.

Что за бред? Разница в тактах на инструкцию между различными командами - да ППЦ огромна.

lenin386 ★★★★★
()
Ответ на: комментарий от ckotctvo

А вот вариант LLVM на идиоматически верный Rust-вариант с использованием итераторов.

example[d4557ff73e3435d0]::mix_mono_to_stereo:
        shr     rsi
        cmp     rcx, rsi
        cmovb   rsi, rcx
        test    rsi, rsi
        je      .LBB0_8
        cmp     rsi, 4
        jae     .LBB0_3
        xor     eax, eax
        jmp     .LBB0_6
.LBB0_3:
        movabs  rax, 1152921504606846972
        and     rax, rsi
        movaps  xmm2, xmm0
        shufps  xmm2, xmm1, 0
        shufps  xmm2, xmm2, 216
        xor     ecx, ecx
.LBB0_4:
        movsd   xmm3, qword ptr [rdx + 4*rcx]
        movsd   xmm4, qword ptr [rdx + 4*rcx + 8]
        unpcklps        xmm3, xmm3
        mulps   xmm3, xmm2
        movups  xmmword ptr [rdi + 8*rcx], xmm3
        unpcklps        xmm4, xmm4
        mulps   xmm4, xmm2
        movups  xmmword ptr [rdi + 8*rcx + 16], xmm4
        add     rcx, 4
        cmp     rax, rcx
        jne     .LBB0_4
        cmp     rsi, rax
        je      .LBB0_8
.LBB0_6:
        unpcklps        xmm0, xmm1
.LBB0_7:
        movss   xmm1, dword ptr [rdx + 4*rax]
        unpcklps        xmm1, xmm1
        mulps   xmm1, xmm0
        movlps  qword ptr [rdi + 8*rax], xmm1
        inc     rax
        cmp     rsi, rax
        jne     .LBB0_7
.LBB0_8:
        ret
WatchCat ★★★★★
()
Ответ на: комментарий от WatchCat

ну надо же, вариант1 из моего кода на табло в чистом виде. значит всё не так плохо. надо попробовать теперь заставить С++ тоже самое сделать. видимо алиасинга боится.

ckotctvo
() автор топика
Закрыто добавление комментариев для недавно зарегистрированных пользователей (со score < 50)