В наше время, когда нейронные сети бороздят просторы Big Data, а искусственный интеллект раздумывает, выгодно ли ему получать зарплату за свою работу в Bitcoin, доставшаяся мне задача поиска самой быстрой открытой кросс-платформенной библиотеки для загрузки, сохранения и перекодирования графических файлов выглядела настоящим анахронизмом. Но на самом деле эта задача актуальна как никогда – для всех технологий компьютерного зрения и машинного обучения гигабайты картинок надо обязательно загрузить, а иногда и сохранить промежуточные данные в виде изображений. Так что сделать это самым быстрым способом очень желательно. В этой статье мы найдем искомую библиотеку, а, главное, разберемся с очень полезным продуктом, сильно упрощающим подобные и многие другие задачи — Google Benchmark.

Итак, точная формулировка задачи гласит: в приложении загружаются, то есть декодируются в память файлы форматов jpeg и tiff с глубиной цвета 24 и 8 бит, а также 32-битные bmp. Размер изображений варьируется от крошечных (32х32 пикселя) до больших, с разрешением 15K. В процессе работы файлы модифицируются, после чего их требуется сохранять на диск в заданных форматах. И делать это должна кросс-платформенная библиотека с открытым кодом, обладающая максимальной производительностью на современных процессорах Intel с поддержкой векторных инструкций AVX2. Желательна также поддержка библиотекой формата сжатых текстур DirectX DXT1. За точку отсчета производительности берется Windows Imaging Component — стандартный фреймворк для работы с изображениями в Windows, то есть требуется найти библиотеку, работающую на равных или быстрее, чем WIC.

Но самое главное требование – решение нужно вот прямо сейчас, а лучше вчера.

Знакомьтесь, библиотеки для работы с bmp, tiff, jpeg


Решение начинается с очевидного и несложного, хотя и не очень быстрого шага – тщательного изучения Wikileaks github, stackoverflow и прочего google в поисках подходящих кандидатов на роль искомой библиотеки. Таковых оказалось немного:

  • FreeImage. Оболочка над известными библиотеками LibJPEG, LibPNG, LibTIFF. Поддержка DXT1 присутствует посредством плагина. Недостаток – качество сохранения jpeg в API задается слишком дискретно — 100, 75,50 и 25%. Для изменения этого параметра придется разбираться и править код. Проект живой и развивающийся – последяя версия 3.18.0 выпущена 31 июля 2018. Сборка под Windows тривиальна, все компоненты строятся автоматически.
  • Cimg Представляет собой заголовочный С++ файл-обертку над древним артефактом пакетом ImageMagick. Пакет требует отдельной сборки-установки, возможно также прямое его использование, минуя Cimg. Обладает массой возможностей по работе с изображениями: фильтры, преобразования, определение морфологии и т.п. Поддерживает HDR, не поддерживает DXT1.
  • DevIL (Developer's Image Library). Очень простая библиотека с С интерфейсом в стиле OpenGL. Содержит оболочку над LibJPEG, LibPNG, LibTIFF, но также имеет обширный встроенный функционал, дополнительно поддерживает массу форматов изображений, в том числе DXT1. Для сборки использует CMake. Большинство зависимостей, в том числе и LibJPEG, LibPNG, LibTIFF не входят в состав DevIL и должны быть самостоятельно загружены и собраны отдельно. Последнее обновление DevIL, касающееся системы сборки, датировано 01.2017, а предыдущее – вообще случилось в 2014 году, так что в случае возможных проблем с библиотекой – возможны проблемы с их решением.
  • OpenImageIO. Позиционируется как инструмент разработчика профессионального софта для работы с изображениями. Поддерживает в форме плагинов работу с многочисленными экзотическими форматами фото и даже видео. Сборка для Windows требует предкомпилированных Boost и Qt 4. Готовой собранной версии для тестирования нет.
  • Boost GIL (Generic Image Library) Boost и этим все сказано. Хотя, не все. Эта библиотека также содержит оболочку над LibJPEG, LibPNG и LibTIFF.
  • SDL_image 2.0 Используется вместе с библиотекой SDL и, вы будете смеяться, но также содержит оболочку над LibJPEG, LibPNG и LibTIFF.

Все найденные библиотеки были собраны под Windows с использованием максимального уровня оптимизации компилятора Visual Studio и ключом /arch:AVX2.

То же самое относится и к библиотекам LibJPEG, LibPNG и LibTIFF, для ускорения работы взятым из свежего пакета библиотеки OpenCV.

Знакомьтесь, Google Benchmark


Следующий шаг решения также очевиден – создание бенчмарка для сравнения производительности найденных библиотек, а несложным и быстрым его делает использование широко известной в узких кругах библиотеки для микробенчмаркинга Google Benchmark.
Google Benchmark умеет достаточно точно измерять производительность кусков кода, вставленных вами в тело цикла C++11.

static void BM_foo1(benchmark::State& state) {
//Этот кусок кода не измеряется
Init_your_code();
for (auto _ : state){
//А этот - измеряется
    your_code_to_benchmark();
}

в функциях, зарегистрированных в качестве бенчмарка

// Регистрируем функцию выше в качестве бенчмарка
BENCHMARK(BM_foo1);

И запускать их:

BENCHMARK_MAIN();

После чего выдавать отчет в заданном формате — консольный вывод, json, csv.

Отчет будет содержать данные о системе исполнения (процессор, конфигурация кэш-памяти), общее глобальное время работы каждой из измеряемых функций, а также время, которое они занимают процессор. Эти времена в общем случае отличаются – в первое, например, входит задержка на чтение\запись, а второе для многопоточных бенчмарков складывается из времени работы всех ядер.

Последний выводимый Google benchmark параметр — количество выполненных итераций функции, необходимое для статистически корректного точного измерения времени ее работы. Система выбирает его самостоятельно, автоматически, осуществляя предварительные измерения.

Что такое «точное измерение» времени работы? На эту тему можно писать диссертации, но в данном случае достаточно сказать, что:

  • по умолчанию измерение идет в процессорных клок-тиках, то есть, теоретический порядок точности именно такой. Выдача результата по умолчанию — в наносекундах;
  • результаты во всех виденных мной тестах очень стабильны от запуска к запуску;
  • по моим агентурным данным Google benchmark используют и полностью доверяют его результатам разработчики софта бортовых компьютеров одного крупнейшего мирового автомобильного концерна. Так что поверим и мы.

Единственный момент, на который стоит обратить внимание: Google benchmark не обеспечивает «чистку» кэш-памяти между запусками итераций бенчмарка. Об этом при необходимости вам следует позаботиться самостоятельно.

Зато Google benchmark может много всего другого:

  • посчитать асимптотическую сложность алгоритма (О);
  • корректно работать с многопоточными бенчмарками, измеряя их продолжительность не в процессорных тиках, а в режиме «реальное время» (wall clock);
  • использовать свою собственную функцию «ручного» измерения времени, что может оказаться полезным, например, при измерениях работы на GPU;
  • по заданному телу измеряемой функции автоматически сгенерировать бенчмарки с разными наборами аргументов;
  • показывать среднее значение, медиану и стандартное отклонение при многократных запусках бенчмарка;
  • Задавать собственные счетчики и метки, которые будут отражены в отчете Google benchmark.

Google benchmark загружается из репозитория на github, собирается для соответствующей платформы с использованием Cmake (для Windows доступна сборка Visual Studio), получившаяся библиотека линкуется к вашему проекту (в случае Windows кроме этого потребуется линковка с библиотекой shlwapi), в ваш код добавляется заголовочный файл benchmark.h, после чего все работает, как описано выше.

Если не работает, то единственное место, помимо уже указанного сайта, где можно получить хоть какую-нибудь информацию и помощь по Google benchmark – это специализированный форум по продукту.

В нашем случае все заработало без проблем. После общения с заказчиками были определены 4 бенчмарка, представляющие собой загрузку и сохранение под другим именем:

  • 8-битного jpeg файла с разрешением 15k
  • 24-битного jpeg файла с разрешением 15k
  • 24-битного tiff файла с разрешением 15k
  • 32-битного bmp файла с разрешением 32x32

Знакомьтесь, результаты


Изначально планировалось, что в тестировании-сравнении с Windows Imaging Component (WIC) примут участие все найденные библиотеки, т.е., FreeImage, Cimg, DevIL, OpenImageIO, Boost GIL и SDL_image 2.0. Но три последние библиотеки, зависящие от таких «монстров» как Boost и SDL, заказчики попросили оставить в запасе на крайний случай, если нужная библиотека не найдется среди первых трех. И, к счастью, она нашлась. Хотя и не сразу.

Ниже приведен сгенерированный Google benchmark отчет, из которого видно, что:

  • FreeImage полностью с разгромным счетом проигрывает WIC во всех тестах, так что его можно больше не рассматривать.
  • Cimg проигрывает WIC вчистую везде, кроме загрузки tiff, где он слегка (меньше чем на 5%) быстрее. Увы, его также придется вычеркнуть. Причем, это относится и к прямому использованию пакета ImageMagick

Остается библиотека DevIL. Она показывает отличные результаты в случаях загрузки bmp и tiff (в 3 и 2.8 раза соответственно превосходит WIC!), черно-белого jpeg (в 1.75x лучше WIC), но немного тормозит на загрузке обычного 24-битного jpeg – делает это аж на 3% медленнее WIC.
08/15/18 11:15:44
Running c:\WIC\WIC_test\Release\WIC_test.exe
Run on (8 X 4008 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 262K (x4)
L3 Unified 8388K (x1)
Benchmark Time CPU Iterations
BM_WIC8jpeg 72 ms 70 ms 11
BM_cimg8jpeg 562 ms 52 ms 10
BM_FreeImage8jpeg 147 ms 144 ms 5
BM_devIL8jpeg 41 ms 41 ms 17
BM_WIC24jpeg 266 ms 260 ms 3
BM_cimg24jpeg 656 ms 128 ms 6
BM_FreeImage24jpeg 594 ms 594 ms 1
BM_devIL24jpeg 276 ms 276 ms 3
BM_WIC24tiff 844 ms 844 ms 1
BM_cimg24tiff 808 ms 131 ms 5
BM_FreeImage24tiff 953 ms 938 ms 1
BM_devIL24tiff 305 ms 305 ms 2
BM_WIC32 3 ms 3 ms 236
BM_cimg32 71 ms 7 ms 90
BM_FreeImage32 6 ms 5 ms 112
BM_devIL32 1 ms 1 ms 747
Конечно, на этом этапе можно было бы забраковать и DevIL, но тут в кадре появляется еще одна библиотека — Libjpeg-turbo.

Ее выход можно смело встречать аплодисментами — Libjpeg-turbo — это кросс-платформенная библиотека, полностью реализующая функциональность (API) libjpeg и добавляющая к нему собственную функциональность (например, работу с 32-битными буферами). При этом, для x86 архитектуры Libjpeg-turbo активно использует векторные инструкции (SSE2, AVX2) и по утверждению ее создателей, превосходит по скорости libjpeg в 2-6 раз (!)

Поэтому следующий шаг – это сборка DevIL c Libjpeg-turbo вместо libjpeg. Libjpeg-turbo с помощью CMake без проблем собирается Visual Studio, после чего почти сразу (с заменой единственного #define, определяющего версию libjpeg в заголовочном файле DevIL) начинает работать в составе DevIL.

В результате отчет Google benchmark выглядят так:
Benchmark Time CPU Iterations
BM_WIC8jpeg 72 ms 68 ms 9
BM_cimg8jpeg 565 ms 39 ms 10
BM_FreeImage8jpeg 148 ms 141 ms 5
BM_devIL8jpeg 31 ms 31 ms 24
BM_WIC24jpeg 269 ms 266 ms 2
BM_cimg24jpeg 675 ms 131 ms 5
BM_FreeImage24jpeg 604 ms 594 ms 1
BM_devIL24jpeg 149 ms 150 ms 5
BM_WIC24tiff 833 ms 828 ms 1
BM_cimg24tiff 785 ms 138 ms 5
BM_FreeImage24tiff 943 ms 938 ms 1
BM_devIL24tiff 318 ms 320 ms 2
BM_WIC32 4 ms 3 ms 236
BM_cimg32 74 ms 8 ms 56
BM_FreeImage32 6 ms 5 ms 100
BM_devIL32 1 ms 1 ms 747
Конечно же, улучшения производительности работы с jpeg даже в два раза в сравнении с libjpeg здесь не видно, но так и должно быть – ведь превосходство в скорости относится только к кодированию\декодированию jpeg, а тест включает накладные расходы на чтение\запись файла.

Зато видно, что в среднем DevIL работает быстрее WIC в случае 8-бит jpeg в 2.3 раза, 24-бит jpeg в 1.8 раз, 24-бит tiff – в 2.7 раз, 32-бит bmp — 3.5 раз.

Задача решена. На решение было полностью потрачено три летних предотпускных рабочих дня. Конечно, если бы их было чуть больше, возможно, что нашлась бы библиотека с еще более впечатляющими результатами, а если значительно больше, то, возможно, я бы написала искомую библиотеку сама.

Но даже то, что есть – впечатляет. Поэтому, если вы ищете быструю и легкую во всех смыслах кросс-платформенную библиотеку для работы с графическими файлами, то обратите внимание на DevIL, а если вам нужно быстро и качественно произвести сравнительные измерения кода, то к вашим услугам – Google benchmark.

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


  1. DjOnline
    04.10.2018 12:29

    Добавить бы в сравнение Pillow из соседней темы habr.com/post/301576


    1. vikky13 Автор
      04.10.2018 12:42

      Да, я ждала, что в комментариях еще библиотек напредлагают. Может, кто-нибудь когда-нибудь за это и возьмется. Задача — очень несложная, студенческая :). Плюс интересно не только и не сколько загрузка-сохранение, но и фильтрация, конвертация цвета и другие операции (хотя там наверняка победит Intel IPP)


  1. Nomad1
    04.10.2018 13:25
    +1

    В целом, сравнение разных оберток над одинаковыми libPNG/libJPEG/libTIFF должно было показать почти одинаковый результат. Если это не так, то либо дело в накладных расходах (читай — ошибках реализации), либо в ошибках тестирования. Вам обязательно надо было попробовать напрямую замерить скорость самих этих библиотек, без оберток. Отдельно в TIF файле может быть сжатие RLE, JPEG и LZW (+ чуть более редкие варианты), поэтому libTIFF использует libJPEG и в идеальном варианте скорость открытия TIFF-JPEG должна совпасть со скоростью чистой libJPEG.
    Так же замерять скорость чтения BMP файла размером 32х32 откровенно несерьезно — после прогрева кеша его чтение должно быть менее 1мс для абсолютно любой реализации и если это не так, то вы что-то делаете не так.

    P.S. Я же надеюсь, вы в курсе, что кодирование в DXT1 отличается в разных реализациях в разы по качеству и скорости? Простенький кодер есть и в ImageMagik, который вы почему-то обозвали древним артефактом, хотя он развивается не в пример лучше того же DevIL. Просто, его задачей никогда не была производительность или удобство встраивания, это универсальный комбайн для всех возможных и невозможных операций с картинками. А в целом, сжатие в DXT1 может быть существенно оптимизировано и разбито на потоки, что хорошо сделали ребята из Unity в своей версии crunch.


    1. vikky13 Автор
      04.10.2018 13:47
      -1

      Это удивительно, но от вашего комментария есть польза. Я увидела, что забыла упомянуть в тексте, что Tiff по условиям-не сжатый. Спасибо!


      1. Nomad1
        04.10.2018 14:06
        +1

        Это удивительно, но от вашей статьи тоже есть польза — сравнение разных библиотек весьма полезно. Но все перечёркивается очень странными бенчмарками.
        FreeImage считывает в память 4220 байт картинки за 5мс? Даже для 1000 итераций это нереально долго, скорость памяти исчисляется в тысячах мегабайт в секунду. Вызов jpeg_start_decompress/jpeg_read_scanlines/jpeg_finish_decompress занимает разное время в разных обертках вокруг одной библиотеки? Почему?
        В конце-концов, раз речь шла о кросс-платформенности, где результаты других платформ?


        1. vikky13 Автор
          04.10.2018 14:22
          -1

          Хороший вопрос, простой ответ — значит, кроме считывания (и записи, которая также входит в тест) происходит что-то еще. Что именно — в данном случае неважно. Предпоследний абзац исходного текста, однако. Другие платформы — любопытно, да. Но мы сравниваем с WIC, которого там (пока) нет


  1. lieff
    04.10.2018 15:14

    Давно уже пользуюсь github.com/nothings/stb
    Интересно было бы сравнить и его. Так же кроме ImageMagiсk есть еще GraphicsMagick, чья цель как раз была оптимизация — его тоже стоит проверить.


  1. Serge78rus
    04.10.2018 15:41

    Но три последние библиотеки, зависящие от таких «монстров» как Boost и SDL, заказчики попросили оставить в запасе на крайний случай, если нужная библиотека не найдется среди первых трех.
    Данное утверждение касательно Boost GIL противоречит фразе из туториала:
    GIL consists of header files only and does not require any libraries to link against. It does not require Boost to be built and including boost/gil.hpp will be sufficient for most projects.


  1. Nomad1
    04.10.2018 16:23

    А что такое 8-бит JPEG?


    1. Serge78rus
      04.10.2018 16:28

      Очевидно — монохромный (8 бит на пиксель)


      1. Nomad1
        04.10.2018 16:34

        Честно говоря, я думал, там только YCbCr сжатие бывает. Есть модификация только для Y (luminance) компоненты?


        1. Serge78rus
          04.10.2018 17:00
          +1

          Если верить Википедии, то стандарт JPEG не обязывает использовать пространство YCbCr


          1. Nomad1
            04.10.2018 17:39

            Да, весьма интересно. По какой-то причине не сталкивался с ним раньше — декодеры спокойно дают на выходе 24-битное изображение, не думая об оригинале. А с энкодерами я мало сталкивался по разной причине. Да и инфрмации на эту тему весьма мало.
            Imagemagick пишет в такой формат, если ему принудительно это указать. Что интересно, при этом параметр quality не работает вообще, PSNR остается неизменным (51.1808 в моем случае), но это не Lossless сжатие, где-то еще есть внутренние преобразования.

            P.S. В Википедии как раз ошибка, формат JFIF подразумевает и Y, и YCbCr:

            The RGB components calculated by linear conversion from YCbCr shall not be gamma corrected (gamma = 1.0). If only one component is used, that component shall be Y.


  1. kovserg
    04.10.2018 22:55

    Здорово, а как обстоят дела с векторными форматами изображений?