GCC x86, как уменьшить размер кода

          Времена, когда программисты пытались выжать максимум из размера своего приложения, безвозвратно ушли. Основной причиной является существенное увеличение объемов оперативной памяти и дискового пространства на современных компьютерах. Немногие помнят, как при загрузке приложения с кассеты можно было пойти покушать. Или как можно было считать моргания дисковода, косвенно определяя размер приложения. Пожалуй, только разработчики програмного обеспечения под встраиваемые системы до сих пор заботятся о размере кода и потребляемой памяти. Могут ли таблетки и смартфоны вернуть разработчиков «назад в будущее»?
          Данная статья призвана помочь разработчикам програмного обеспечения, использующим GCC компилятор, уменьшить размер кода своих приложений. Все данные в статье получены при помощи x86 GCC компилятора версии 4.7.2 на операционной система Fedora 17 для архитектуры Intel Atom.

          Довольно существенный выигрыш с точки зрения размера GCC дает динамическая линковка (включенная по умолчанию). То, насколько динамическая линковка выигрывает у статической, сильно зависит от используемых библиотек.
          Чаще всего, когда речь заходит об оптимизации размера, используется опция “-Os”. Ниже приведена табличка со средними геометрическими размера кода по набору приложений для смартфонов и таблеток.
          Результаты в табличке показаны относительно “-Os”. Меньший результат говорит о меньшем размере кода. “-m32, -mfpmath=sse, -march=atom” включены по-умолчанию во всех случаях.
    -O2 6%
    -O2 -flto -5%
    -Ofast 11,5%
    -Ofast -flto 3%
    -Ofast -funroll-loops 19%
    -Ofast -funroll-loops -flto 10,5%

          “-Ofast” (или “-O3”) и “-funroll-loops”, очевидно, увеличивают размер кода. Опция “-flto”, за счет более агрессивной подстановки функций (inline), также должна увеличивать размер кода. Однако, результат противоположный. Почему?
          “-flto” делает возможным удаление неиспользуемых функций. Функция может стать таковой, если в конкретной конфигурации приложения она не вызывается или была полностью и во всех местах вызова подставлена в код. Для того, чтобы удалить неиспользуемые функции без “-flto” можно воспользоваться “-ffunction-sections -Wl,--gc-sections”. Данная техника дает хороший результат, если в приложении используются внутренние статические библиотеки.
          Приложение все еще слишком большое? Есть еще несколько техник для уменьшения размера. По умолчанию GCC использует опцию “-fasynchronous-unwind-tables”, что увеличивает размер EH (exception handling) секции, даже при компиляции приложений на языке “C”. Это облегчает процесс отладки, однако может существенно увеличить размер кода. Для отключения надо добавить “-fno-asynchronous-unwind-tables” к опциям компиляции.
          “-Wl,--strip-all” сообщит линкеру, что надо удалить всю символьную информацию. Это сделает процесс отладки еще сложнее. И все же, если размер кода критичен, опция приемлема.
          Ниже приведена табличка отражающая эффект от добавления:
    • “-ffunction-sections -Wl,--gc-sections” (+ сборщик мусора)
    • “-ffunction-sections -Wl,--gc-sections -fno-asynchronous-unwind-tables” (+ без таблиц раскрутки)
    • “-ffunction-sections -Wl,--gc-sections -fno-asynchronous-unwind-tables -Wl,--strip-all” (+ удаление символов)

          к различным опциям оптимизации.

          Результаты в табличке показаны относительно “-Os”. Меньший результат говорит о меньшем размере кода. “-m32, -mfpmath=sse, -march=atom” включены по-умолчанию во всех случаях.

    по умолчанию + сборщик мусора + без таблиц раскрутки + удаление символов
    -Os - -5% -10,5% -22,5%
    -O2 6% 0,5% -3,5% -13,5%
    -O2 -flto -5% -5% -8% -17%
    -Ofast 11,5% 6% 2% -6,5%
    -Ofast -flto 3% 2,5% 0,5% -6,5%
    -Ofast -funroll-loops 19% 12,5% 9,5% 3%
    -Ofast -funroll-loops -flto 10,5% 10% 8,5% 2,5%

          Ниже представлено описание используемых в статье опций компилятора. Полное описание (на английском): gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Optimize-Options.html
    • "-Ofast" аналогично "-O3 -ffast-math" включает более высокий уровень оптимизаций и более агрессивные оптимизации для арифметических вычислений (например, вещественную реассоциацию)
    • "-flto" межмодульные оптимизации
    • "-m32" 32 битный режим
    • "-mfpmath=sse" включает использование XMM регистров в вещественной арифметике (вместо вещественного стека в x87 режиме)
    • "-funroll-loops" включает развертывание циклов
    • "-ffunction-sections" размещает каждую функцию в отдельной секции
    • "-Os" оптимизирует производительность и размер
    • "-fno-asynchronous-unwind-tables" гарантирует точность таблиц раскрутки только в пределах функции

          Ниже представлено описание используемых в статье опций линковщика. Полное описание (на английском): sourceware.org/binutils/docs/ld/Options.html
    • “--gc-sections” включает удаление неиспользуемых секций
    • “--strip-all” удаляет символьную информацию
    Intel
    209,00
    Компания
    Поделиться публикацией

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

      +11
      Немногие помнят, как при загрузке приложения с кассеты можно было пойти покушать.
      Ну, фотошоп и некоторые другие тяжёлые приложения и до сих пор так грузятся :)
        0
        Это конечно перебор, на современных компьютерах даже с HDD успеваешь только чайник включить :)
          0
          Пока я не поставил SSD, вполне успевал съесть бутерброд или два, пока загрузятся ОС и приложения в автозагрузке.
            +7
            Выходит, SSD полезны для здоровья? :)
              +14
              Наоборот — приложения стартуют за секунду, на еду не остаётся времени, и от нерегулярного питания можно заработать гастрит.
                +1
                Время на еду должно оставаться независимо от скорости работы компьютера. А вот бутерброды есть в ожидании загрузки — точно не оно. Поэтому SSD полезны. Just my 2 cents.
              • НЛО прилетело и опубликовало эту надпись здесь
                  +1
                  Вроде в прессе о таких случаях не пишут почти никогда. Правда, из этого можно другой вывод сделать — что реально быстрых машин мало в народе. :)
          0
          Цифры интересные, но несколько не понятно, какой будет результат на разных проектах. Уж точно не те же проценты от объема бинарников. Думаю, будет полезно показать код, который оптимизировался в ходе работа на статьей.
            0
            Кода в работе было много. Приведенные цифры — среднее, и конечно, в конкретных случаях будут другие результаты. Однако, порядок и знак, скорее всего, сохранятся. Можно взять конкретное приложение и поработать над ним, рассмотрев его особенности. Думаю, хватит на отдельную статью.
            +1
            С месяц назад пытался выжать из своего мини-приложения самый маленький размер. При включенной опции "-О2" выжал 3.5kb, при "-О1" — 3kb. Все опции из статьи использованы. + Ещё склеивал секции .text и .rdata с помощью скрипта линковщика. Но компилятор от Visual Studio тот же код скомпилировал в 2.5kb. Очень обидно, что с помощью любимого компилятора не удалось достичь тех же результатов
              +1
              Попробуйте подрезать «лишние» секции. Например:
              strip FILENAME -R .comment
                0
                Из секций и так остались только .text и .idata. Их в одну слить не получилось
                SECTIONS { .text ALIGN(16): { *(.text) *(.rdata) *(SORT(.rdata$*)) } }
                  0
                  .text у кого меньше?
                  попробовать -Os; -fjump-tables и -fno-jump-tables; -fif-conversion -fif-conversion2;
                  в зависимости от версии GCC можно попробовать поработать с планировщиком и распределителем регистров.
                    0
                    У EXEшника от Visual Studio только одна секция — .data.
                    Манипуляции с jump-tables и if-conversion не помогли.
                    GCC у меня 4.7.1:
                    Скрытый текст
                    c:\Program Files (x86)\CodeBlocks\MinGW\bin>gcc -v
                    Using built-in specs.
                    COLLECT_GCC=gcc
                    COLLECT_LTO_WRAPPER=c:/program files (x86)/codeblocks/mingw/bin/../libexec/gcc/mingw32/4.7.1/lto-wra
                    pper.exe
                    Target: mingw32
                    Configured with: ../../src/gcc-4.7.1/configure --build=mingw32 --enable-languages=c,c++,ada,fortran,
                    objc,obj-c++ --enable-threads=win32 --enable-libgomp --enable-lto --enable-fully-dynamic-string --en
                    able-libstdcxx-debug --enable-version-specific-runtime-libs --with-gnu-ld --disable-nls --disable-wi
                    n32-registry --disable-symvers --disable-build-poststage1-with-cxx --disable-werror --prefix=/mingw3
                    2tdm --with-local-prefix=/mingw32tdm --enable-cxx-flags='-fno-function-sections -fno-data-sections'
                    --with-pkgversion=tdm-1 --enable-sjlj-exceptions --with-bugurl=http://tdm-gcc.tdragon.net/bugs
                    Thread model: win32
                    gcc version 4.7.1 (tdm-1)
                      0
                      К сожалению, нет под рукой GCC под Windows. Подскажите опции при которых секция .text была минимальна (принимая в расчет только оптимизации самого .text). Посмотрю в каком месте GCC можно подкрутить.
                        0
                        Лучший случай:
                        Скрытый текст
                        -s -O1 -Os

                        -nodefaultlibs
                        -nostartfiles
                        -mconsole
                        -lkernel32
                        -luser32
                        -lgcc
                        -Wl,ld_script_merge_rdata_and_text.x

                        .text
                        Physical Address 2024 $7E8 %11111101000
                        Virtual Address 4096 $1000 %1000000000000
                        Size of Raw Data 2048 $800 %100000000000
                        Pointer To Raw Data 512 $200 %1000000000
                        Pointer To Relocations 0 $0 %0
                        Pointer To Linenumbers 0 $0 %0
                        Number Of Relocations 0 $0 %0
                        Number Of Linenumbers 0 $0 %0
                        Characteristics 1613758560 $60300060 %1100000001100000000000001100000
                        Если добавить следующие опции, то размер секции .text не меняется.
                        Скрытый текст
                        -ffunction-sections -Wl,--gc-sections
                        -fno-asynchronous-unwind-tables -Wl,--strip-all
                        -fvisibility=hidden -fvisibility-inlines-hidden
                        -fno-jump-tables
                        -fif-conversion2
                        -fno-asynchronous-unwind-tables
                        Если -O1 заменить на -O3, то получим:
                        Скрытый текст
                        .text
                        Physical Address 2196 $894 %100010010100
                        Virtual Address 4096 $1000 %1000000000000
                        Size of Raw Data 2560 $A00 %101000000000
                        Pointer To Raw Data 512 $200 %1000000000
                        Pointer To Relocations 0 $0 %0
                        Pointer To Linenumbers 0 $0 %0
                        Number Of Relocations 0 $0 %0
                        Number Of Linenumbers 0 $0 %0
                        Characteristics 1613758560 $60300060 %1100000001100000000000001100000
                        Если убрать скрипт линкера, то секция .text вообще не появляется
                          0
                          Crinkler ужал разницу до 0.23kb: 1.14kb studio vs. 1.37kb GCC. Но сам факт пока остался тем же — догнать студию пока не получилось…
                0
                Демосцена 4k? :)
                  0
                  Демосцена — тру! Надеюсь, когда-нибудь смогу поучаствовать. А сейчас так, тренируюсь. Вопрос заинтересовал в принципиальном плане, вот и ковырялся
                    0
                    Ну если ориентироваться только на винду и маленькие файлы, то почему бы не попробовать кринклер? www.crinkler.net/
                      0
                      Спасибо за наводку! Стандартный линковщик студии выдал EXE-файл размером 2.5kb, Crinkler на объектных файлах студии — 1.1kb. Позже попробую прикрутить его к GCC
                        0
                        GCC + Crinkler — полёт нормальный. ЕХЕ 1.37kb, т.е. всего на 0.2kb больше, чем у студии. С таким готов мириться. Спасибо огромное!
                        Если ещё кому-нибудь нужно: при настройке очень помог Makefile.
                +3
                Иногда возможные оптимизации скрываются там, где их не ждешь.
                GCC по умолчанию делает все функции экспортируемыми из динамической библиотеки. Недавно получилось уменьшить размер библиотеки на 24% (с 16.6 до 12.6 Mb) за счет опций "-fvisibility=hidden -fvisibility-inlines-hidden"
                  0
                  FYI: Есть люди, которые ради развлечени пытаются в ограниченное число байт впихнуть по-максимуму. Например, 64K Intro. Правда там совсем другими способами сжимается код :)
                    0
                    64к? Ну, батенька, вы загнули. Этот монстр не поместится у меня в оперативке, максимум — 4к.
                    0
                    Почему нет варианта -Os -flto?
                      0
                      В статье обозначен вывод, что польза от "-flto", с точки зрения размера кода, в основном засчет удаления неиспользуемых функций. То есть можно воспользоваться "-ffunction-sections -Wl,--gc-sections".
                      "-Os -flto" редкое сочетание, может оказаться и не надежным.
                      +4
                      Прежде всего, котаны, надо писать хороший код — не злоупотреблять шаблонами в крестах и т.п.
                      Далее, выигрыш от конкретных опций gcc зависит от приложения, стиля кода, модной в этом сезоне версии gcc и фазы луны. Поэтому надо брать GC Masher, который автоматически грубой силой переберет все возможные параметры gcc с целью нахождения глобального минимума.
                      Ну и напоследок можно попробовать сжать бинарник каким-нибудь exe-пакером, типа UPX, или вовсе на коленке соорудить gzip-дроппинг.
                      В этом месте можно убедиться, что все предыдущие оптимизации размера сказываются на размере сжатого файла не монотонно, и, господи, зачем вообще я пишу этот комментарий.
                        0
                        «хороший код» реально писать, если пишешь его в одиночку и с нуля. Зачастую некоторые интерфейсы уже написаны.
                        Грубая сила предполагает высокую скорость компиляции или наличие крутого сервера.
                        Что-то мне подсказывает, что 90% эффекта от «грубой силы» можно добиться «здравым смыслом».
                        Про архивацию — правда, но уже не про GCC/LD.
                          +1
                          На неконтролируемом входном коде можно вообще схватить подчеркнутое той или иной опцией неопределенное поведение в стандарте языка.
                          Да, здравый смысл конечно 90% покрывает, а грубая сила нужна только для случаев «о боже, я вылез на 35 байт за пределы дозволенного!», да и вообще предполагает прежде всего крохотулечные проекты, потому что никаких серверов не хватит хромиум перебирать.
                          Кстати, -mfpmath=sse не круто с точки зрения размера — там водятся толстенные 7-байтные инструкции, тогда как олдовые fpu'шные укладываются в 2.
                            0
                            Зато по скорости исполнения выигрывают. Это тоже немаловажный фактор.
                              0
                              Это так, но выбор между размером и скоростью зависит от задачи. В той же сцене интра может грузиться хоть минуту, но самое главное — влезть в ограничение по размеру.
                              0
                              Кстати, -mfpmath=sse не круто с точки зрения размера

                              Согласен. Особенно если использовать встроеный sin, например. Правда, и вещественная арифметика почти не представлена в тех приложения, что были использованны для получения цифр (для мобилных приложений более свойственны целочисленные вычисления, вещественные завернуты в библиотеки).
                                0
                                С sin вообще забавно получается — если не выпендриваться, gcc на место sin (когда я в последний раз смотрел году в 2009) вместо того, чтобы православно заинлайнить fsin, генерирует вызов в libm, который там у себя внутри считает синус самостоятельно (емнип, оно даже в этот самый fsin не упирается в итоге, но не буду утверждать).
                                  0
                                  Дело в требуемой точности. У библиотечного и встроенного sin она разная. Насколько я знаю текущий вариант в libm точнее встроенного.
                            0
                            Код написан. Старался писать так, чтоб результирующий файл имел минимальный размер. GCC генерирует результирующий код размером 3kb, Visual Studio — 2.5kb (подробности выше). Стандартный набор опций GC Masher'a уменьшить размер не помог — всё так же 3kb. По-моему, цель данной статьи скорее помочь найти нужные ключи GCC, которые помогут уменьшить размер… И да, при таком размере UPX не ужал ни на байт.
                              0
                              Для такой мелочи и win32-специфики лучше вообще использовать crinkler, но он не понимает объектники, генерируемые gcc (по крайней мере года четыре назад не понимал).
                              Впрочем, как вы правильно заметили, мы отдаляемся от темы этой статьи.
                                0
                                Спасибо за наводку. Всё прекрасно работает с GCC, подробнее отписал выше
                                  0
                                  О, это хорошо. Спасибо за информацию — попробую в следующих своих интрах использовать gcc, а то замучался уже с абы какой поддержкой чистых сей студийным компилятором.
                            0
                            Очень интересно, ждем про x86_64!
                              0
                              Да особых отличий не должно быть. Будут те же опции и выводы. Ну разве что "-x32" потенциально добавится для экономии памяти и регистров. Только для x86_64 размеры намного реже критичны.
                              0
                              Размер важен для демосцены, там — да, наслаждаешься безумством «упертых»
                              Скорость важна для технологических применений (встраиваемые решения, рендеринг и т.п.)
                              Гораздо важнее КАЧЕСТВО кода. Вымораживает, когда современный софт срыгивает чем-нибудь вроде:
                              «не могу открыть файл.»
                              КАКОЙ, блджад, файл ты не можешь открыть, чудо ты корпоративное! Неужели трудно в исходниках прописать конкретно ошибку, сэкономив персоналу ЧАСЫ времени на поиски неисправности…

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

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