Gcc vs Intel C++ Compiler: собираем FineReader Engine for Linux

    Предпосылкой к написанию данной статьи было вполне естественное желание улучшить производительность FineReader Engine.

    Существует мнение, что компилятор от Intel производит гораздо более быстрый код, чем gcc. И ведь было бы неплохо увеличить скорость распознавания ничего не сделав просто собрав FR Engine другим компилятором.

    На данный момент FineReader Engine собирается не самым новым компилятором – gcc 4.2.4. Пора переходить на что-то более современное. Мы рассмотрели две альтернативы – это новая версия gcc – 4.4.4, и компилятор от Интел – Intel C++ Compiler (icc).

    Портирование большого проекта под новый компилятор может оказаться не самым простым делом, поэтому для начала мы решили протестировать компиляторы на бенчмарке.

    Вот краткие результаты на процессоре Intel Core2 Duo:
    image
    Время povray-теста на процессоре Intel (сек)

    На AMD тоже было интересно посмотреть:
    image
    Время povray- теста на процессоре AMD (сек)

    Кратко по поводу флагов:
    -O1, -O2 и -O3 – различные уровни оптимизации.
    Для gcc наилучшей опцией считается –O3, и она используется почти везде при сборке FineReader Engine.
    Для icc оптимальной опцией считается -O2, а -O3 включает в себя некоторые дополнительные оптимизации циклов, которые, впрочем, могут не сработать.
    -mtune=core2 -march=core2 -msse3 – оптимизация под конкретный процессор, в данном случае под Core2 Duo.
    -xT – аналогичный флаг для компилятора от Intel.
    -fprofile-use – PGO

    Тесты с оптимизациями под конкретный процессор приведены just for fun. Бинарник, собранный с оптимизацией под один процессор, может не запуститься на другом. FineReader Engine не должен привязываться к определенному процессору, поэтому использовать подобные оптимизации нельзя.

    Итак, судя по всему, прирост производительности возможен: icc очень значительно ускоряет на процессоре Intel. На AMD он ведет себя скромнее, но все равно дает неплохой прирост по сравнению с gcc.
    Пришло время переходить к тому, ради чего мы все это затеяли, – к сборке FineReader Engine. Мы собрали FineReader Engine разными компиляторами, запустили распознавание на пакете изображений. Вот результаты:
    image
    Время работы FREngine, собранного различными компиляторами (секунд на станицу)

    Неожиданный результат. Соотношение gcc-4.4.4 / gcc-4.2.4 вполне согласуется с замерами на бенчмарке, и даже слегка превышает ожидания. Но что с icc? Он проигрывает не только новому gcc, но и gcc двухлетней давности!

    Мы отправились за правдой к oprofile, и вот что удалось выяснить: в некоторых (достаточно редких) случаях icc испытывает проблемы с оптимизацией циклов. Вот код, который удалось написать, основываясь на результатах профайлинга:
    static const int aim = ..;
    static const int range = ..;

    int process( int* line, int size )
    {
        int result = 0;
        for( int i = 0; i < size; i++ ) {
            if( line[i] == aim ) {
                result += 2;  
            } else {
                if( line[i] < aim - range || line[i] > aim + range ) {
                    result--;
                } else {
                    result++;   
              }
            }
        }
        return result; 
    }


    * This source code was highlighted with Source Code Highlighter.

    Запустив функцию на разных входных данных, мы получили примерно такой результат:
    image
    Время выполнения цикла (мс)

    Добавление других флагов оптимизации не дало ощутимых результатов, поэтому я привел только основные.
    К сожалению, Intel не указывает явно, что включают в себя -O1, -O2 и -O3, поэтому выяснить, какая именно оптимизация заставила код тормозить, не удалось. На самом деле в большинстве случаев icc оптимизирует циклы лучше, но в некоторых особых случаях (подобных приведенному выше) возникают трудности.

    С помощью профайлинга удалось обнаружить еще одну, более серьезную проблему. В отчете профайлера для версии FineReader Engine, собранной icc, на достаточно высоких позициях находилась подобная строчка:

    Namespace::A::GetValue() const (На всякий случай все имена изменены на ничего не значащие)

    в то время как в версии от gcc такой функции вообще не было.
    A::GetValue() возвращает простую структуру, содержащую несколько полей. Чаще всего этот метод вызывается для глобального константного объекта в конструкции вида

    GlobalConstObject.GetValue().GetField()

    которая имеет тип int. Все приведенные методы Get..() тривиальны – они просто возвращают какое-то поле объекта (по значению или ссылке). Таким образом, GetValue() возвращает объект с несколькими полями, после чего GetField() вытаскивает одно из этих полей. В этом случае вместо постоянного копирования всей структуры с целью вытащить всего одно поле, вполне возможно цепочку превратить в один вызов, возвращающий нужное число. Кроме того, все поля объекта GlobalConstObject известны (могут быть вычислены) на этапе компиляции, поэтому цепочку этих методов можно заменить на константу.

    gcc так и делает(по крайней мере, он избегает лишнего конструирования), однако icc с включенными оптимизациями -O2 или -O3 оставляет все, как есть. А учитывая количество таких вызовов, это место становится критическим, хотя здесь не выполняется никакой полезной работы.

    Теперь о способах лечения:

    1) Автоматический. У icc есть замечательный флаг -ipo, который просит компилятор выполнять межпроцедурную оптимизацию, к тому же между файлами. Вот что написано по этому поводу в мануалах Intel по оптимизации:

    IPO allows the compiler to analyze your code to determine where you can benefit from a variety of optimizations:
    • Inline function expansion of calls, jumps, branches and loops.
    • Interprocedural constant propagation for arguments, global variables and return values.
    • Monitoring module-level static variables to identify further optimizations and loop invariant code.
    • Dead code elimination to reduce code size.
    • Propagation of function characteristics to identify call deletion and call movement
    • Identification of loop-invariant code for further optimizations to loop invariant code.

    Кажется, это то, что нужно. Все было бы хорошо, но FineReader Engine содержит огромное количество кода. Попытка запустить его собираться с флагом -ipo привела к тому, что линковщик (а именно он выполняет ipo) занял всю память, весь swap и один только модуль распознавания страницы (впрочем, самый крупный) собирался несколько часов. Безнадежно.

    2) Ручной. Если везде в коде вручную заменить цепочку вызовов на константу, на компиляторе Intel получается неплохой прирост производительности относительно старых результатов.

    image

    Последняя строчка – версия, собранная icc, где в самых критичных местах цепочка вызовов была заменена на константу. Как видно, это позволило версии icc работать быстрее gcc-4.2.4, но все еще медленнее gcc-4.4.4.
    Можно попытаться выловить все подобные критические места и вручную поправить код. Очевиден недостаток — на это уйдет огромное количество времени и сил.

    3) Комбинированный. Можно собирать с флагом -ipo не весь модуль, а только некоторые его части. Это даст приемлемое время компиляции. Однако какие именно файлы следует компилировать с этим флагом, придется определять вручную, что опять же потенциально приводит к большим трудозатратам.

    Итак, резюмируем. Intel C++ Compiler потенциально хорош. Но из-за описанных выше особенностей и большого количества кода мы решили, что он не дает значительного выигрыша по скорости – достаточно значительного, чтобы оправдать трудозатраты по портированию и «заточке» кода под этот компилятор.
    ABBYY
    167,00
    Решения для интеллектуальной обработки информации
    Поделиться публикацией

    Комментарии 55

      0
      Это еще ладно, что скорость в реальных приложениях зачастую не улучшается, так еще в редких случаях icc приводит к странным падениям и непонятным ошибкам.
        +1
        В gcc они тоже случаются. Поэтому на каждую новую версию лучше переходить, когда у неё в конце будет хотя бы 4: 4.2.4, 4.4.4 и т.д.
          0
          Очень интересно, страшно интересно было бы взглянуть на результат CLang-а в даной ситуации
        +3
        Кстати gcc имеет еще несколько хитростей:
        ну например некоторые оптимизации не включенные даже в -O3 а также штуки навроде fastmath, которые понижают точность вычислений с плавающей точкой, но значительно их ускоряют.
        Ну и еще в gcc есть такая штука, как gprof
        www.network-theory.co.uk/docs/gccintro/gccintro_80.html
        Суть её в том, что при первом проходе gcc в код добавляет некоторые счетчики и тому подобные вещи, потом приложение запускается, а на выходе появляется результирующий файл, после чего уже вторым прогоном можно по этому файлу дополнительно заоптимизировать приложение.
          0
          У ICC тоже есть возможность использовать оптимизацию вычислений с плавающей точкой: strict (без оптимизации), safe (оптимизация, не влияющая на результат), fast (с потерей точности). Причем именно последняя опция стоит по дефолту при включении оптимизации.
          +2
          Это PGO? icc, кстати, тоже умеет.
          PGO пробовали, большого выигрыша не было, поэтому здесь я его почти нигде не приводил.
            +1
            Извиняюсь, промахнулся.
            –19
            А можете провести тесты производительности серверного ПО под разными компиляторами.

            Было бы интересно посмотреть mysql, apache, php, и nginx.
              +17
              Да-да, ABBYY больше нечем заняться.
              +3
              Для gcc наилучшей опцией считается –O3


              Это не совсем так, а иногда совсем не так. -O3 дает на выходе файлы куда большего размера, а значит приводит к повышенному потреблению памяти со всеми вытекающими.
              Далеко не весь код хорошо оптимизируется с -O3 и бывает что результат оказывается хуже, чем -O2. А бывает что и вообще не собирается.
              Так что в общем случае оптимальным вариантом все же считается -O2.
                0
                Почему тогда не -Os? Он использует почти все те же флаги оптимизации, что и -O2, кроме тех, что увеличивают размер итогового файла.
                  0
                  Начиная с четвертой версии gcc -Os направлен чисто на уменьшение размера и очень существенно проигрывает в производительности.
                  +1
                  Пожалуй, это правда.
                  Большая часть кода FREngine собирается с флагом -O3, это меня заставило поверить, что он оптимальный.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      +1
                      Не совсем, -O3 это в первую очередь направление на оптимизацию скорости выполнения кода, в ущерб всему остальному. По типу как -Os оптимизация на уменьшение размера в ущерб всему остальному.
                      А -O2 компромиссный вариант. Поэтому многие методики оптимизации, которые вполне стабильны, так и останутся в -O3.
                    0
                    А попробуйте за одно и clang…
                      +2
                      clang разве уже с «плюсами» нормально работает? Не так давно у него только С и Obj-C нормально получались.
                        +1
                        Там поддержка плюсов уже относительно вменяемая, но всё равно даже не самый извращенный qutIM он уже не собирает
                          0
                          Boost собирает, и это главное )
                          0
                          Вроде как они писали, что да
                          0
                          У него поддержка C++ пока совсем новая. Пока она устаканится, пройдёт ещё пару лет. Выгребать сейчас проблемы из-за того, что компилятор неправильно/по-другому поддерживает стандарт — не самый хороший способ потратить своё время.
                            0
                            clang пока хорошо юзать как статистический анализатор кода, на большее он не годится еще в случае с плюсами.
                              +1
                              да ну, мы им успешно собираем достаточно тяжелые проекты для iOS, написанные на плюсах. И, вы знаете, работает.
                                0
                                Речь идёт о примерно 2 миллионах строк кода. Работает?
                                  0
                                  это вы у меня спрашиваете? у нас 500к строк, никаких проблем при переходе на clang вообще замечено не было.
                                  0
                                  Много строчек не означает, что проект тяжелый, если много шаблонов юзать, то clang иногда ну очень странные ошибки кидает, например не может найти деструктор, хотя он есть точно и все другие компиляторы: gcc,icc,msvs этот код нормально едят.
                                    0
                                    ну тут, как говорится, your mileage may vary…
                              –1
                              Приветствую вас, Евгений,
                              ну, и ты, Аби заходи. ;-)

                              Рад, что Аби стала портировать на линюкс, прогресс, однако; мало наших компаний на пигвина обращают внимание.

                              Хочу показать вот эту ОЧЕНЬ важную статью:
                              habrahabr.ru/blogs/hardware/80050/
                              (там есть ссылка на метод преодоления даже)

                              Очень странно, что у вас в тестировании на АМД не выявилось ухудшения по сравнению с gcc.
                              Зато по сравнению с интелом в два раза хуже, как и предсказано.

                              Предположения:
                              а) возможно у вас процессор АМД ниже классом и явно слабее;

                              если же нет, то:
                              б) возможно в gcc встроена такая же гадость, или не встроена, а просто недопилен
                              в) возможно компилятор интела настолько хитёр, что на тривиальных задачах не портит код для АМД
                                +4
                                Эту статью мы видели, даже сослались на неё :-)

                                По поводу AMD vs Intel. ICC обычно производит лучший код, чем gcc, просто это улучшение на архитектуре AMD не такое существенное, как улучшение на intel.
                                +2
                                Всегда считалось что для гцц лучше всего -О2. Первый раз здесь слышу про -О3
                                  +1
                                  -O3 нынче ещё и векторизацию в себя включает, а она может дать неслабый прирост производительности при правильной организации циклов.
                                  +3
                                  А почему нету тестов для ветки gcc 4.5 он же теперь current?
                                  + интересно было бы посмотреть на результат работы FineReader Engine при сборке gcc с турбонадувом, привязкой под процессор и т.д. Возможно какие-то флаги из 4.5 ветки позволили бы увеличить скорость.
                                  Поскольку FineReader Engine скорее всего будет собран для домашнего использования, включение подобных флагов допустимо.
                                    +1
                                    1. В 4.2.1 напоролись на баг в компиляторе, так что теперь используем только хорошо протестированные и пропатченные версии компилятора.

                                    2. См. первую таблицу. FR Engine — это серверный продукт. Затачиваться под конкретный процессор имеет смысл только под конкретного заказчика с большим кошельком, т.к. требует дополнительный билд и дополнительное тестирование.
                                      +1
                                      Ну хотя бы ради интереса.
                                        0
                                        ждем ебилдов.
                                      +3
                                      В gcc-4.5 появилась link time optimization, включается флагом -flto.
                                        +1
                                        А почему недопустима сборка под конкретный процессор? ну или семейство?
                                          0
                                          Плохо совместимо с таблицей системных требований. Имеет смысл затачиваться на самый массовый процессор (флаг -mtune), но использовать набор инструкций, доступный практически всем современным процессорам.
                                            +1
                                            ну может имеет смысл делать один общий билд

                                            и еще парочку: для корки, i7 и тд — самые распространенные архитектуры
                                          0
                                          Было бы интересно еще и LLVM посмотреть, если это возможно.
                                            +6
                                            ICC известны тем, что плюёт на стандарты IEEE о числах с плавающей точкой (п. 4.3.2). Поэтому следует сравнивать скорость компиляции с флагами gcc -mrecip и -ffast-math (конкретно — -funsafe-math-optimizations). Также gcc не делает -funroll-loops даже на уровне O3.

                                            Если вам так нравится межпроцедурная оптимизация, советую попробовать скомпилировать в gcc 4.5 с флагом -flto. На практике может уменьшить размер исполняемого файла в разы (да-да), но мне не удалось ни разу заметить ускорение выполнения, отличное от погрешности измерения.

                                            Если надо скомпилировать под текущий процессор, используйте флаг -march=native (компилятору виднее, какой набор инструкций у процессора).

                                              0
                                              Любопытно было бы узнать, какой версией ICC вы пользовались?
                                                0
                                                11.1
                                                  0
                                                  Спасибо. И за пост — отдельное спасибо, впечатляющее исследование.
                                                    0
                                                    Было бы лучше, если в итоге этого исследования мы перешли на icc. Тестовые замеры были очень многообещающими.
                                                      0
                                                      А уж нам бы как этого хотелось ;).
                                                      Но, полагаю, все понимают сложности подобного перехода. Сейчас главное, что ваш пост заставил задуматься… Будем посмотреть, что еще можно сделать.
                                                +5
                                                Вообще, лучше чем O2 или O3 для gcc — итеративная компиляция. Дело в том, что в компиляторе несколько сотен оптимизаций, агрессивных и не очень. Очень важен порядок, в котором они применяются, ну и, конечно, применяются ли вообще (например, если бы в gcc была неудачная оптимизация циклов наподобие icc, то ее просто следует отключить). Ручками находить такую оптимальную последовательность — адский труд. Применяя эвристики вроде генетического алгоритма в acovea, можно достигнуть значительного прироста производительности программ, найдя нечто похожее на оптимум. Подробнее см. www.coyotegulch.com/products/acovea
                                                  +1
                                                  Интересно, спасибо! Можно попробовать поставить такую штуку работать на неделю, вдруг найдёт оптимум.
                                                    +1
                                                    Нашим компиляторщикам делают как раз такие заказы по ускорению кода (причем иногда пишем свои оптимизации для конкретных продуктов). Типичное время работы acovea составляет несколько дней, через 30 поколений получаются хорошие результаты
                                                  0
                                                  Интересная идея.
                                                  Насколько я понял, для поиска применяется генерический алгоритм, где роль особи выполняет, судя по всему, билд с конкретными опциями. Набор опций — это набор хромосом, который и подвергается скрещиванию/мутации. Функция приспособленности, очевидно, — время работы билда на каком-то(каком?) наборе тестов. Таким образом, всего полных компиляций и запусков происходит N * M, где N — количество особей в поколении, M — количество поколений(пусть 30). Особей в поколении, я полагаю, несколько десятков? Я правильно все понял?
                                                  Если да, то, боюсь, к FREngine'у это применить будет достаточно тяжело, потому что время полной сборки составляет около двух часов, а если их количество измеряется сотнями…
                                                  В любом случае, интересная идея, спасибо.
                                                    0
                                                    Тьфу ты, опять не туда! Никак не могу привыкнуть к древовидной структуре, а еще эта открытая формочка внизу страницы с приглашением к комментарию…
                                                      0
                                                      Гыгыгы :-)
                                                    0
                                                    Дык, а зря Вы открещиваетесь от возможности сборки версии под конкретный процессор. Вот например, если я сканирую в промышленных масштабах, то рад 5% производительности мне будет не влом скачать и поставить версию именно под мой процессор.
                                                      0
                                                      Очень интересные результаты. Надо отметить, что вы отлично поработали. Я обязательно покажу ваши результаты дизайнерам компилятора. Было бы интересно посмотреть на результаты последнего Intel® Parallel C++ Composer XE (т.е. Intel® С++ Compiler Professional edition). Предлагаю пообщаться для этого уже в кулуарах, а по завершению совместной работы опубликовать результаты.
                                                        0
                                                        Ответил личным сообщением.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое