Ускорение операций в 2.5 раза по сравнению с Pillow и в 10 по сравнению с ImageMagick
Pillow-SIMD — это «форк-последователь» библиотеки работы с изображениями Pillow (которая сама является форком библиотеки PIL, ныне покойной). «Последователь» означает, что проект не становится самостоятельным, а будет обновляться вместе с Pillow и иметь ту же нумерацию версий, только с суффиксом. Я надеюсь более-менее оперативно выпускать версии Pillow-SIMD сразу после выхода версий Pillow.
Почему SIMD
Есть несколько способов улучшения производительности обработки изображений (да и всех остальных вещей, наверное, тоже).
- Можно использовать более хорошие алгоритмы, которые дают такой же результат.
- Можно сделать более быструю реализацию существующего алгоритма.
- Можно подключить больше вычислительных ресурсов для решения той же задачи: дополнительные ядра 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)
ToSHiC
24.05.2016 17:36Кстати, в убунте по-дефолту сборка имейджмейджика с Q=16 (что логично, двойная точность на время обработки, потом обратно 8-бит на канал в результате). Вы могли бы посравнивать с ним, или в PIL нету такого режима? И я понимаю, что для этого нужно будет понаписать ещё немножко кода на ассемблере :)
homm
24.05.2016 18:26Я специально для тестов собрал последнюю версию ImageMagick, а не ту которая идет в пакетах, и специально собрал с Q8, чтобы сравнение было более честным. Из пакетов работала еще чуть медленее.
К сожалению, в Pillow сейчас нет режима 16-битного RGB, есть только 32-битный одноканальный режим. То есть теоретически можно было разбить картинку на 3 отдельных канала и обрабатывать их по отдельности, но такой режим сейчас не ускорен SIMD и на практике вряд ли кто-то так делает.
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 каждый собран в отдельном единице трансляции.
ToSHiC
Флаги компилятора типа -mavx2 ведь передаются при сборке объектного файла, а не библиотеки целиком. Вы можете сделать отдельные .c/.cpp файлы для оптимизированных функций, и компилировать с оптимизацией только их, в основной же функции только проверять флаги и вызывать ускоренные, если процессор и флаги компиляции позволяют.
homm
Спасибо, очень дельное замечание, так действительно будет намно проще. Причем даже не нужно разные файлы, можно один и тот же с разными опциями комплировать и давать объектным файлам разные имена. Осталост только понять, можно ли это как-то встроить в систему сборки setuptools.