Предыдущий вариант оптимизации арифметического сжатия с 8-битным алфавитом использовал таблицу накопленных частот с элементами UInt32. Идея ускорения состояла в том, что бы хранить элементы в формате UInt16, что в два раза уменьшит размер памяти и количество элементов, обрабатываемых одной командой процессора.
Давайте посмотрим, что это даст. Предыдущий вариант показывал производительность 10.0 и 6.7 МиБ/с на AMD FX-4350.
Новый вариант ускорился, но все же не так сильно, как я ожидал: 10.7 и 7.4 МиБ/с соответственно.
Что будет, если вместо классических команд SIMD (в моем случае на технологии SSE) использовать в этом режиме команды SISD?
А вот что: 10.6 и 7.2 МиБ/с, то есть в общем-то особого смысла использования команд SIMD в самых простых случаях нет.
На уровне ассемблера вкратце это выглядит так: команды
movdqu XMM0, [RCX+Limits+R11*2];
paddw XMM0, XMM3;
movdqu XMM0, [RCX+Limits+R11*2];
заменятся на
add [RCX+Limits+R11*2], R8;
add [RCX+Limits+R11*2+8], R8;
Здесь в регистрах XMM3 и R8 хранятся упакованные массивы из единиц длиной в слово: $1000100010001 для R8 и то же самое, только в два раза длиннее, для XMM3.
Проблемы переноса старших разрядов в младшие у соседних элементов не возникает, так как модель адаптируется, как только последний элемент с максимальным значением достигнет значения $FFFF.
При адаптации мы просто делим все элементы массива на два, после чего дополнительным проходом гарантируем монотонное увеличение всех элементов от первого до последнего.
Деление просто реализуется как на SIMD, так и на инструкциях общего пользования (GPI). Команда на ассемблере
psrlw XMM0, 1;
заменятся на две пары из двух команд вида
shr R9, 1;
and R9, R8;
Вторая команда AND нужна для того, что бы очистить случайно залетевшие младшие биты элементов с большими индексами в старшие биты с элементами с меньшими индексами. Значение для R8 такое: $7FFF7FFF7FFF7FFF.
Спросите меня, зачем я в командах общего пользования использую регистры с константами вместо указания констант в самих командах? Ответ очень прост: в X64 нет возможности указывать константы в командах длиной 64 бита.
В принципе, таким же образом можно действовать и на языке высокого уровня, поддерживающим указатели и ссылки.
Ладно, хорошо, но мой FX-4350 уже достаточно старенький процессор. Возможно, SSE на нем реализовано не так уж оптимально?
Сравним с Ryzen 7 5800X: SIMD - 19.56 и 12.61 МиБ/с, GPI - 19.63 и 12.46 МиБ/с. Как видим, разницы никакой, следовательно, использовать SIMD оправдано только в случае несколько более сложных вычислений, которые никак не укладываются в инструкции общего назначения.
Ну и кстати, вариант с уменьшенным количеством делений и короткой таблицей чуть-чуть обошел вариант с пирамидой (17.8 и 13.3 МиБ/с). Есть подозрение, что если реализовать вариант с пирамидой с уменьшенным количеством делений, то удастся выйти на современных процессорах за 20 МиБ/с при сжатии данных.
А вот при распаковке, видимо нет, даже при переходе на 16-битный алфавит.
> использовать SIMD оправдано только в случае несколько более сложных вычислений
ОтветитьУдалитьЯ правильно понимаю что Вы развернули цикл по два для SISD, но оставили не развернутым для SIMD? Как я понимаю процессор умеет выполнять по два сложения за такт хоть для SISD, хоть для SIMD, так как у Piledriver 2 128-bit ALU, а у Zen-3 вроде еще больше. Я бы попробовал развернуть цикл для SIMD, вдруг еще ускориться :)
> то удастся выйти на современных процессорах за 20 МиБ/с при сжатии данных.
Круто! А я погряз в бинарном сжатии, но до 20 МиБ/с очень далеко, уже на C++ переписал, получил 11 МиБ/с для сжатия и 14.7 МиБ/с для распаковки, маловато :(((
> Я правильно понимаю что Вы развернули цикл по два для SISD...
ОтветитьУдалитьНет, по две команды для SIMD, это получается по 4 для SISD. Вроде пробовал сделать больше, но эффекта нет.
>...процессор умеет выполнять по два сложения за такт хоть...
Это вроде с обоими операторами в регистрах? Если один из них в памяти, то все сложнее. Ну и с SSE тоже. Там сначала приходится делать загрузку из памяти, потом уже операция, а затем выгрузку. Насколько проц умеет это паралеллить - отдельный вопрос. Арифметические операции с память появились только в AVX, но я их не юзаю по понятным причинам. Там может быть приход от дальнейшего разворачивания, так как арифметическая операция может начаться параллельно с чтением из памяти.