01.09.2022

Оптимизация. Меньше насколько лучше?

Предыдущий вариант оптимизации арифметического сжатия с 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 такое: $7FFF
7FFF7FFF7FFF.
Спросите меня, зачем я в командах общего пользования использую регистры с константами вместо указания констант в самих командах? Ответ очень прост: в 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-битный алфавит. 

2 комментария:

  1. > использовать SIMD оправдано только в случае несколько более сложных вычислений

    Я правильно понимаю что Вы развернули цикл по два для SISD, но оставили не развернутым для SIMD? Как я понимаю процессор умеет выполнять по два сложения за такт хоть для SISD, хоть для SIMD, так как у Piledriver 2 128-bit ALU, а у Zen-3 вроде еще больше. Я бы попробовал развернуть цикл для SIMD, вдруг еще ускориться :)

    > то удастся выйти на современных процессорах за 20 МиБ/с при сжатии данных.

    Круто! А я погряз в бинарном сжатии, но до 20 МиБ/с очень далеко, уже на C++ переписал, получил 11 МиБ/с для сжатия и 14.7 МиБ/с для распаковки, маловато :(((

    ОтветитьУдалить
  2. > Я правильно понимаю что Вы развернули цикл по два для SISD...
    Нет, по две команды для SIMD, это получается по 4 для SISD. Вроде пробовал сделать больше, но эффекта нет.

    >...процессор умеет выполнять по два сложения за такт хоть...
    Это вроде с обоими операторами в регистрах? Если один из них в памяти, то все сложнее. Ну и с SSE тоже. Там сначала приходится делать загрузку из памяти, потом уже операция, а затем выгрузку. Насколько проц умеет это паралеллить - отдельный вопрос. Арифметические операции с память появились только в AVX, но я их не юзаю по понятным причинам. Там может быть приход от дальнейшего разворачивания, так как арифметическая операция может начаться параллельно с чтением из памяти.

    ОтветитьУдалить