Введение в POSIX'ивизм

       

Вопросы оптимизации


Сказавши "а", логично было бы добавить "б". А именно, после пересборки ядра - выполнить оптимизацию приложений под имеющееся "железо", по крайней мере - критически важных для производительности системы в данных условиях. Тем более что современные версии компилятора gcc предоставляют к тому много возможностей.

Выбор версии компилятора gcc вообще очень важен для целей оптимизации. До недавнего времени практически на равных существовали две его ветки - 2.95.X и 3.X. Причем преимущества второй были отнюдь не очевидны. Дело в том, что ветка 3.X на протяжении длительного времени развивалась в сторону удучшения генерации кода Си++, тогда как код, генерируемый из программ на чистом Си, по свидетельству знатоков, получался даже хуже, чем при использовании gcc-2.95.X. И потому эта версия рекомендовалась для сборки наиболее ответственных частей Linux- и BSD-систем, таких, как ядро, гланая системная библиотека и сам компилятор, не содержащие кода Си++. А это и есть первые претенденты на оптимизацию, не считая "тяжелых" приложений типа KDE или GNOME, а также мультимедийных и узкоспециальных программ. В BSD-системах gcc-2.95.X вообще использовался по умолчанию. Однако компилятор этой ветки не предусматривал возможности оптимизации под современные процессоры с их специализированными наборами инструкций (типа SSE и 3DNow!) - ибо появился задолго до них.

Положение изменилось с выходом gcc-3.4.X, который первым из своей линии генерировал Си-код не хуже предшественника - а подчас и лучше. И потому использование его видится предпочтительным для всех компонентов любых систем. Правда, он по сию пору входит не во все, даже весьма современные, дистрибутивы Linux, а из BSD-семейства штатно предусмотрен только в DragonFlyBSD, но это, вероятно, вопрос времени.

Оптимизация приложений достигается заданием соответствующих флагов - параметров компилятора gcc, определяемых при его вызове. Первый среди них - собственно уровень оптимизации, который задается флагом -O# и может варьировать от нулевого (флаг -O0, при котором никакой оптимизации не происходит) до максимального -O3.
Большинство пакетных дистрибутивов Linux, декларирующих свою оптимизированную природу, собирается, насколько мне известно, с флагом -O2. В BSD-семействе по умолчанию принята оптимизация уровня -O1 для всех ее компонентов - ядра, базовой системы и портов; более того, до недавнего времени выполнить операцию make world (полную пересборку системы) при более высоких уровнях оптимизации было практически невозможно.
Выбор уровня оптимизации - не столь однозначный вопрос, как может показаться. И максимально высокий уровень -O3 далеко не всегда дает лучшие результаты, а подчас производительность собранных с его использованием программ даже снижается. Не говоря уж о том, что некоторые программы вообще при этом уровне собираться отказываются. И в любом случае при уровне -O3 размер исполняемого кода оказывается больше, нежели при -O2, что ведет снижение скорости запуска приложений.
Воообще говоря, мои (и не только мои) наблюдения показывают, что самый большой скачек в быстродействии происходит при переходе от уровня -O0 (то есть отказа от оптимизации) к -O1. Флаг -O2 обычно обеспечивает лишь небольшой прирост скорости запуска и выполнения программ, а результаты применения флага -O3 неоднозначны. В частности, эксперименты с пересборкой ядра и "мира" DragonFlyBSD показали стабильное (хотя и очень маленькое численно) его оставание от уровня -O2.
Следующие флаги, весьма влияющие на производительность, задают конкретный процессор для целевой машины: -mcpu=значение или -march=значение. Различие между ними в том, что программа, собранная с флагом -mcpu, будучи оптимизированной под заданный в качестве значения "камень", сохраняет способность запуска на более младших моделях, тогда как флаг -march заоптимизирует программу до того, что она сможет запуститься только на указанном процессоре или более старшем.
Допустимые значения флагов -mcpu и -march зависят от версии компилятора gcc. Однако в современных версиях, входящих в состав большинства дистрибутивов (gcc от 3.3.X и выше) здесь могут быть заданы все используемые ныне процессоры семейства x86: pentium, pentium-mmx, pentiumpro, pentium2(а также 3 и 4), разнообразные athlon'ы (athlon просто, athlon-tbird, athlon-xp, athlon-mp), не считая всякой экзотики типа winchip etc.


Детали можно посмотреть в документации на наличную версию gcc.


Особенно богатые возможности для оптимизации под современные процессоры предсотавляет gcc-3.4.X. В нем имеется поддержка и AMD64, и Prescott с его набором новых инструкций и улучшенным HyperThreading), а главное, судя по имеющимся сообщениям, существенно выросло качество оной (в том числе и поддержка HT). И если раньше (в версиях 3.3.X), сборка, например, под Pentium-4 подчас давала худшие результаты, нежели чем под абстрактный i686, то теперь для процессоров от Intel имеет смысл подбирать наиболее близкое значение флага -march (для процессоров AMD точное указание процессора всегда давало хорошие результаты).
Следующие процессорно-специфичные флаги оптимизации позволяют задействовать специальные наборы инструкций современных "камней" - от MMX до 3DNow и SSE всех номеров. Для этого сначала определяем, куда в этих случаях должен обращаться процессор - к стандартному сопроцессору или соответствующим блокам специализированных инструкций. Делается это с помощью флага -mfpmath=значение, в качестве какового могут выступать 387 (стандартный сопроцессор) или sse (блок SSE-расширений); согласно документации, можно задать оба параметра, правда смысл этого остается мне не вполне ясным.
Насколько я понимаю, при компиляции под Athlon'ы более оправданным будет флаг -mfpmath=387, учитывая максимально мощный сопроцессор этого "камушка". А вот в случае с Pentium-III и, особенно, Pentium4 резонным выбором будет скорее -mfpmath=sse. К чему следует приложить еще парочку флагов - -mmmx плюс -msse (для "пятой трешки") или -msse2 (для "пятой четверки").
Впрочем, флаги для задйствования специальных наборов инструкций имеют смысл только для программ, которые в принципе способны их использовать. По имеющимся у меня сведениям, они дают хорошие результаты для некоторых видеоприложений и вообще всякого рода multimedia. Ожидать же от них выигрыша в быстродействии того же компилятора вряд ли оправданно.


Процессорно- привязанными флагами возможности оптимизации не исчерпываются. Есть еще флаги, специфическим образом обрабатывающие код для достижения максимальной производительности вне зависимости от типа процессора (вплоть до нарушений стандартов POSIX/ANSI, типа -ffast-math, благодаря чему теоретически можно собрать максимально быстрый код). Детальное их описание далеко выходит за рамки данной заметки. Тем более, что его можно найти в фундаментальном руководстве по gcc, написанном Ричардом Столлменом с соавторами (русский перевод доступен, например, на здесь - часть 1 и часть 2 - и номером его версии, весьма древним, смущаться не след, актуальности оно ничуть не потеряло).
Как уже говорилось, при использовании флагов оптимизации, особенно достаточно жестких (типа -O3 -march=значение, не говоря уже о -ffast-math), необходимо учитывать, что отнюдь не все программы гарантированно соберутся с ними. А те, что соберутся, вовсе не обязаны безукоризненно функционировать (даже, возможно, функционировать вообще - от ошибок сегментации гарантировать не может никто). А уж то, что при этом будет достигнут большой выигрышь в быстродействии - приходится только надеяться.
Я достаточно много экспериментировал с различными флагами оптимизации, в том числе и весьма жесткими, при разных версиях компилятора, и на разных системах (Gentoo, CRUX, Archlinux, FreeBSD 5-й ветви, DragonFlyBSD). И в конце концов пришел к весьма умеренному решению:
CFLAGS="-O2 -march=pentium4" CXXFLAGS="$CFLAGS"
Вероятно, рекордных результатов при нем не добиться, но оно дает гарантированно стабильный результат. По крайней мере, и ядро, и "мир" DragonFlyBSD (при gcc-3.4.X) собираются без проблем, как и большинство используемых мной портов.
Тем не менее, любители экстремальной сборки могут попробовать нечто вроде этого:
CFLAGS="-O3 -march=pentium4 \ -fomit-frame-pointer \ -funroll-loops -pipe \ -mfpmath=sse -mmmx -msse2" CXXFLAGS="$CFLAGS"
А при желании еще и подобрать специальные флаги оптимизации для программ Си++, чем я никогда не озадачивался вообще.


К слову сказать, флаги оптимизации задаются различными способами:
  • в командной строке при конфигурировании программы:

  • CFLAGS="значения" ./configure
  • в виде переменных окружения в данном сеансе шелла:
    export CFLAGS="значения"
    в sh-совместимы оболочках и
    setenv CFLAGS "значения"
    в csh и tcsh;

  • глобально, раз и навсегда, в профильном файле (например, в /etc/profile или /etc/cshrc);

  • в конфигурационном файле системы портов, если таковая используется (типа /etc/make.conf в BSD).
    Осталось определить, что дает столь жесткая оптимизация? Компилятор gcc, пересобранный указанным мной ранее образом, на сборке ядра демонстрирует быстродействие на 10-15% выше, чем собранный по умолчанию. Учитывая, что на современных машинах ядро собирается минут за 10-15, казалось бы, немного. Однако при сборке монстров типа оконной системы Икс, интегрированной среды KDE или, скажем, OpenOffice, игра вполне стоит свеч. Да и прикладные программы, собранные с флагами оптимизации, работают существенно быстрее: Mozilla, для примера - чуть не в полтора раза, хотя для других Исковых программ столь высоких результатов получить не удается.

    Содержание раздела