Ускорение операций в 2.5 раза по сравнению с Pillow и в 10 по сравнению с ImageMagick



Pillow-SIMD — это «форк-последователь» библиотеки работы с изображениями Pillow (которая сама является форком библиотеки PIL, ныне покойной). «Последователь» означает, что проект не становится самостоятельным, а будет обновляться вместе с Pillow и иметь ту же нумерацию версий, только с суффиксом. Я надеюсь более-менее оперативно выпускать версии Pillow-SIMD сразу после выхода версий Pillow.


Почему SIMD


Есть несколько способов улучшения производительности обработки изображений (да и всех остальных вещей, наверное, тоже).


  1. Можно использовать более хорошие алгоритмы, которые дают такой же результат.
  2. Можно сделать более быструю реализацию существующего алгоритма.
  3. Можно подключить больше вычислительных ресурсов для решения той же задачи: дополнительные ядра CPU, GPU.

Самое классное, когда вы можете использовать более быстрый алгоритм, как когда в Pillow 2.7 Гауссово размытие на основе сверток было заменено размытием последовательностью box-фильтров. К сожалению, число таких фокусов весьма ограничено. Также очень заманчива идея использовать больше вычислительных ресурсов. Но к сожалению, часто их либо нет, либо они стоят дополнительных денег (как в случае с арендуемыми серверами). Использовать же GPU для вычислений вообще нетривиальная задача, связанная с подбором определенного железа и правильной настройкой драйверов. Остается самый надежный способ — попытаться заставить существующий код работать быстрее на существующем железе. И тут SIMD-инструкции подходят как нельзя лучше.


SIMD означает: «одна инструкция, много данных» (single instruction, multiple data). В классических программах мы берем операнды, выполняем операцию, сохраняем результат. В случае SIMD мы берем сразу пачку операндов, делаем одно и то же действие над всеми разом и сохраняем пачку результатов. Для процессора это проще, чем несколько раз выполнить одинаковые действия. Существует огромное количество расширений команд процессоров с SIMD-инструкциями, например: MMX, SSE-SSE4, AVX, AVX2, AVX512, NEON.


В текущей версии Pillow-SIMD может быть скомпилирован с использованием расширений SSE4 (по умолчанию), либо AVX2.


Статус проекта


Pillow-SIMD годится для продакшена. Различные версии Pillow-SIMD уже больше года работают на серверах Uploadcare. Uploadcare — это сервис для хранения и обработки пользовательского контента и главный спонсор Pillow-SIMD.


На текущий момент следующий операции ускорены в SIMD-версии:


  • Ресайз (ресемплинг на основе сверток): SSE4, AVX2
  • Гауссово размытие и box-фильтры: SSE4

Производительность


Цифры означают количество обработанных мегапикселей исходного изображения в секунду. Например, если ресайз изображения размером 7712?4352 был выполнен за 0.5 секунд, производительность будет 67.1 Mpx/s.


Уже в процессе редактирования я понял, что у меня кажется путаница и в мегапикселе для ImageMagick 10^6 пикселей, а в мегапикселе для Pillow — 2^20. Но это не сильно влияет на общую картину.


Протестированы:


  • ImageMagick 6.9.3-8 Q8 x86_64
  • Pillow 3.2.0
  • Pillow-SIMD 3.2.0.post2

Source Operation Filter IM Pillow SIMD SSE4 SIMD AVX2
7712?4352 RGB Resize to 16x16 Bilinear 27.0 217 437 710
Bicubic 10.9 115 232 391
Lanczos 6.6 76.1 157 265
Resize to 320x180 Bilinear 32.0 166 410 612
Bicubic 16.5 92.3 211 344
Lanczos 11.0 63.2 136 223
Resize to 2048x1155 Bilinear 20.7 87.6 229 265
Bicubic 12.2 65.7 140 171
Lanczos 8.7 41.3 100 126
Blur 1px 8.1 17.1 37.8
10px 2.6 17.4 39.0
100px 0.3 17.2 39.0
1920?1280 RGB Resize to 16x16 Bilinear 41.6 196 426 750
Bicubic 18.9 102 221 379
Lanczos 13.7 68.6 140 227
Resize to 320x180 Bilinear 27.6 111 303 346
Bicubic 14.5 66.3 164 230
Lanczos 9.8 44.3 108 143
Resize to 2048x1155 Bilinear 9.1 20.7 71.1 69.6
Bicubic 6.3 16.9 53.8 53.1
Lanczos 4.7 14.6 40.7 41.7
Blur 1px 8.7 16.2 35.7
10px 2.8 16.7 35.4
100px 0.4 16.4 36.2

Pillow всегда быстрее, чем ImageMagick, а Pillow-SIMD быстрее чем Pillow примерно в 2-2.5 раза для SSE4-версии. В основном, AVX2-версия оказывается быстрее чем ImageMagick в 10-15 раз.


Тесты выполнялись на Ubuntu 14.04 64-bit, запущенной на процессоре Intel Core i5 4258U с AVX2. Все тесты использовали только одно ядро процессора.


Производительность ImageMagick была измерена утилитой командной строки convert с аргументами -verbose и -bench. Выбранные фильтры в точности соответствуют существующим в Pillow фильтрам:


  • PIL.Image.BILINEAR == Triangle
  • PIL.Image.BICUBIC == Catrom
  • PIL.Image.LANCZOS == Lanczos

Для тестирования были использованы такие скрипты.


Почему Pillow такой быстрый


Тут нет никаких трюков, для тестов использовались высококачественные методы ресайза и размытия. Результаты практически попиксельно совпадают с небольшой погрешностью. Разница только в эффективности самих алгоритмов. В Pillow 2.7 ресемплинг был переписан с использованием предварительно вычисленных коэффициентов, меньшим использованием чисел с плавающей точкой и транспонированием, эффективно использующим кэш процессора.


Почему Pillow-SIMD еще быстрее


Конечно же из-за использования SIMD-команд. Но у меня еще несколько мыслей, как можно улучшить этот результат.


  • Эффективная работа с памятью В настоящий момент каждый пиксель загружается в SSE-регистр из памяти по отдельности, в то время как в один SSE-регистр возможно прочитать 4 пикселя за раз.
  • Вычисления на целых числах Несмотря на то, что современные процессоры очень эффективно работают с числами с плавающей точкой, есть две причины полагать, что работа с целыми числами будет эффективнее: операции над целыми алгоритмически проще; для работы с ними не требуется дополнительных ковертаций.
  • Выравнивание данных в памяти загрузка и выгрузка данных из SIMD-регистров выполняется быстрее, если адреса в памяти, с которыми идет обмен, выровнены.

Почему бы не влить изменения обратно в Pillow


Если коротко — это очень сложно. Pillow поддерживает большое количество архитектур, не только x86. Но даже на x86 Pillow для некоторых платформ распространяется в виде скомпилированных исполняемых файлов. Чтобы иметь возможность использовать SIMD-команды в коде, нужно передавать компилятору аргументы, разрешающие использование самых продвинутых инструкций, которые мы хотим использовать: -mavx2. После этого нужно делать проверку возможностей процессора во время выполнения и включать ту или иную ветку кода в зависимости от них. Проблема в том, что такие аргументы автоматически компируют код, спрятанный под условия препроцессора if (__AVX2__) и ниже, который может ни иметь никаких проверок времени выполнения. Самое печальное, что такой код действительно находится, по крайней мере при компиляции GCC, и исполняемые файлы без явного использования AVX2, но собранные с -mavx2, начинают вылетать. Разумеется, можно собирать разные версии библиотеки с разными опциями компилятора и динамически их подключать, но это [см. начало этого параграфа].


Установка


Хорошие новости, что для установки SSE4 версии достаточно написать как обычно pip install pillow-simd, и если ваш процессор умеет в SSE4 (думаю, вероятность этого около 95%), все пойдет замечательно. Не забудьте удалить оригинальный пакет Pillow.


Если вы хотите собрать AVX2 версию, то нужно передать компилятору дополнительные флаги. Проще всего это сделать, задав переменную окружения CC во время установки и компиляции.


$ pip uninstall -y pillow-simd ; CC="cc -mavx2" pip install pillow-simd

Иногда бывает, что зависимость от Pillow есть не только у вас, но и у других пакетов, которые вы используете. И даже если эти пакеты не особо нуждаются в быстром ресемплинге, они все равно устанавливают Pillow без SIMD, который может импортироваться первым. Для этого может пригодиться такой хак при установке с Гитхаба:


$ pip install -e git+https://github.com/uploadcare/pillow-simd.git@v3.2.0.post3#egg=pillow

Тогда во время установки другого пакета с зависимостью от Pillow, еще одна версия Pillow ставиться не будет:


$ pip install xhtml2pdf -e git+https://github.com/uploadcare/pillow-simd.git@v3.2.0.post3#egg=pillow
Поделиться с друзьями
-->

Комментарии (5)


  1. ToSHiC
    24.05.2016 13:32
    +7

    Флаги компилятора типа -mavx2 ведь передаются при сборке объектного файла, а не библиотеки целиком. Вы можете сделать отдельные .c/.cpp файлы для оптимизированных функций, и компилировать с оптимизацией только их, в основной же функции только проверять флаги и вызывать ускоренные, если процессор и флаги компиляции позволяют.


    1. homm
      24.05.2016 14:00
      +2

      Спасибо, очень дельное замечание, так действительно будет намно проще. Причем даже не нужно разные файлы, можно один и тот же с разными опциями комплировать и давать объектным файлам разные имена. Осталост только понять, можно ли это как-то встроить в систему сборки setuptools.


  1. ToSHiC
    24.05.2016 17:36

    Кстати, в убунте по-дефолту сборка имейджмейджика с Q=16 (что логично, двойная точность на время обработки, потом обратно 8-бит на канал в результате). Вы могли бы посравнивать с ним, или в PIL нету такого режима? И я понимаю, что для этого нужно будет понаписать ещё немножко кода на ассемблере :)


    1. homm
      24.05.2016 18:26

      Я специально для тестов собрал последнюю версию ImageMagick, а не ту которая идет в пакетах, и специально собрал с Q8, чтобы сравнение было более честным. Из пакетов работала еще чуть медленее.

      К сожалению, в Pillow сейчас нет режима 16-битного RGB, есть только 32-битный одноканальный режим. То есть теоретически можно было разбить картинку на 3 отдельных канала и обрабатывать их по отдельности, но такой режим сейчас не ускорен SIMD и на практике вряд ли кто-то так делает.


  1. apro
    24.05.2016 23:45

    Не совсем понял вашу проблему с SIMD и gcc.

    Ведь в gcc (начиная с 4.9) можно указывать флаги оптимизации и target опции для куска исходного кода
    с помощью `pragma`, а также можно указывать эти флаги для каждой функции отедльно, например так `static void calculate_sse(float *data, float scale, int size ) __attribute__ ((__target__ («no-avx»)));`
    подробности можно найти здесь:

    gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Function-Attributes.html#Function-Attributes
    gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Function-Specific-Option-Pragmas.html

    можно также просто как glibc делать — в нем есть всякие memcpy_avx каждый собран в отдельном единице трансляции.