Обновить
Комментарии 46
Ух какая ностальгическая заглавная картинка у поста! Статикой пробило 8-битные воспоминания молодости. Сейчас такие конденсаторы и ПЗУ с золочеными ногами безжалостно идут под нож аффинажников.
Легендарные КМ с палладием и платиной.
безжалостно идут под нож аффинажников.
у меня не идут, могу поставить куда нибудь, если попадётся под руку…
но справедливости ради — крохи остались

image
  • Optimize for size -Os
  • Place functions in their own sections --ffunction-sections

Так вам надо, всё таки, первое или второе?


When you specify these options, the assembler and linker will create larger object and executable files and will also be slower.

Ну, да -Wl,--gc-sections выкинет неиспользуемые секции, а с ними и неиспользуемые функции в своих секциях, но не лучше ли будет с LTO и без выделения собственных секций каждой функции? А в комбинации с LTO?

Если отключить только --ffunction-sections, то размер прошивки будет 17к
Если отключить только -Wl,--gc-sections. Размер прошивки увеличивается до 22к.
Если отключить и то и другое, то прошивка тоже 22к
Минимальный размер получается, когда включены все опции.

UPD.
Нашел опцию -flto. Добавил ее к компилятору и линкеру. Размер прошивки уменьшился до 7632 байт. Наличие или отсутствие --ffunction-sections при этом никак не влияет.

Это удивительно. Проверю, осталась ли прошивка работающей и обновлю статью.

UPD
С опцией -flto прошивка перестала работать
Если отключить только… Если отключить и то и другое, то прошивка тоже 22к

Я про это не спрашивал, ответ очевиден.


Размер прошивки уменьшился до 7632 байт.

Я вот про это.


Наличие или отсутствие --ffunction-sections при этом никак не влияет.

Ожидаемо. Это я на всякий случай, для чистоты эксперимента, спросил.


Это удивительно.

Это не удивительно, это LTO — link time optimization — код функций вытягивается из объектных модулей, а не линкуются секции, содержащие символ, целиком.


С опцией -flto прошивка перестала работать

Вот лучше найти, что не так в коде… А вы всё собирали с LTO? Эту опцию нужно и компилятору и компоновщику:


To use the link-time optimizer, -flto and optimization options should be specified at compile time and during the final link. It is recommended that you compile all the files participating in the same link with the same options and also specify those options at link time.

И линкуйте gcc, а не ld. Ну и посмотрите на прочие опции, связанные с LTO (и на -fwhole-program, в часности).


P.S. Только не удаляйте вариант без LTO из сравнения, если с LTO у вас получится запустить: LTO сравнительно новая функция и полезно видеть сравнение классической линковки с LTO.

Да, я поставил опцию -flto и линкеру и компилятору.
И линкуйте gcc

А это как?

Ну и посмотрите на прочие опции, связанные с LTO (и на -fwhole-program, в часности).

Я не разбираюсь в опциях gcc. На какие опции еще посмотреть?

Это как по умолчанию GNU Make делает:


$ make -p | fgrep LINK.o
make: *** No targets specified and no makefile found.  Stop.
LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)
        $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
        $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

— То есть для линковки не ld вызывается напрямую, а через gcc. Но, думаю, не в этом дело.


Вам там ниже посоветовали указывать разный LTO уровень для ARMCC, то же можно и GCC: -flto=n. Ну и стоит взять GCC поновее и попробовать с ним.

В бытность, когда был вовлечен в разработку ARMCC 6, у LTO была проблема удаления символов, которые линкер посчитал ненужными, но они на самом деле нужны. Если это этот случай, то нужно поиграть с опциями линкера developer.arm.com/documentation/101754/0615/armlink-Reference/armlink-Command-line-Options/--lto-keep-all-symbols----no-lto-keep-all-symbols
Еще можно поиграть с developer.arm.com/documentation/101754/0615/armlink-Reference/armlink-Command-line-Options/--lto-level
Хотел попробовать LTO в armcc6, но KEIL сообщает
error: use of LTO is disallowed in this variant of ARM Compiler

Наверное, дело в том, что Keil бесплатный у меня.
С опцией -flto прошивка перестала работать

я правильно понимаю, что у вас gcc7? актуальная версия gcc10, я бы начал с попытки собрать с ней.

C актуальными версиями gcc иногда странные штуки происходят. Например, актуальная версия gcc мне под cortex-a8 генерит более медленный код, чем неактуальная (старее на 1 мажорный номер).

То что странные штуки происходят ИНОГДА еще не означает что они произойдут в этот раз.

UPD
С опцией -flto прошивка перестала работать

Флаг -flto не работает вместе с HAL из-за бага который не воспринимает weak из ассемблера stm32
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83967
если убрать все ассемблерные вставки, будет работать, проверял

Тоже решал проблему «невпихуемости» прошивки, в компиляторе gcc перепробрал все опции, и в том числе -flto. Она дает очень существенный выигрыш (что то около пары килобайт на 128 Кб). Еще несколько опций, помогающих в моем проекте:
-fsection-anchors
-fno-move-loop-invariants
Добавление этих опций уменьшило прошивку на 12 байт.
Размер стал 14024

Ну так в контрольник шьётся не 'executable file', а бинарник, который собственно из 'executable file' получается мержем всего-всего в, собственно, бинарник.


Версия gcc тоже указана как-то криво (обычно версия gcc — это число вида x.y.z, например 9.3.0, можно получить запустив gcc с опцией --version).


Ещё я бы попробовал -fno-pie -no-pie, если почему-то дефолт в gcc был генерить перемещаемый код, то таким макаром ещё размер чуть сократится.

Версия gcc, поставляемая вместе с STM32 IDE 7.3.1
Нет, по дефлоту генерится неперемещаемый код
А теперь скормите софтине readelf выхлоп gcc:
$readelf firmware.elf -a


Там вы увидите много интересного в коде, ниже выдержки:
Таблица символов «.symtab» содержит 192 элемента:
   Чис:    Знач   Разм Тип     Связ   Vis      Индекс имени
08000561   188 FUNC    GLOBAL DEFAULT    2 __call_exitprocs
...
08000629     4 FUNC      GLOBAL DEFAULT    2 __retarget_lock_[...]
...
0800049d   196 FUNC    GLOBAL DEFAULT    2 __register_exitproc
...
080003d9    40 FUNC    GLOBAL DEFAULT     2 exit
...


Это большая часть того, что мне не нужно, там ещё мелочи полно. Итого минимум 500 байт не нужного в микроконтроллере. От части можно избавится ключем -nostartfiles, написав замену __libc_init_array(). Можно ещё использовать -nostdlib, написав замену memcpy, memset и некоторым другим функциям, либо подтянув другую реализацию стандартной библиотеки, но это жестковато.

Результаты примерно такие:
text data bss dec hex filename
1628 224 1760 3612 e1c blink_stm32f407_disco.elf


После выкидывания ненужного
text data bss dec hex filename
716 104 1576 2396 95c blink_stm32f407_disco.elf


Размер таких маленьких проектов сильно зависит от реализации Run-Time библиотеки. Показательными будут проекты побольше, например LWIP впихнуть и графическую либину.
Также нужно понимать что уровни оптимизации являются набором отдельных флагов, совершенно разных для разных компиляторов. При правильном наборе флагов разница будет 1.5-2%, может и меньше.
Неа, нет там ничего интересного. Похоже, что все интересное там вырезал линкер, удалив неиспользуемые секции.
Из статьи похоже тоже, но фотка зачётная. Ещё 133 серия красивая была.

Можно еще попробовать линкеру сказать --specs=nosys.specs или nano.specs
UPD: пардон, не заметил, что уже.

Это с закомментированным
#define USE_FULL_ASSERT 1
в
stm32...xx_hal_conf.h?

Помнится, у IAR был как-то отдельно включаемый агрессивный multi-file compilation режим, рассматривающий все файлы проекта как единое целое и инлайнящий всё и вся.

Есть такая опция.
С включенной опцией Multi-file compilation IAR ужал прошивку до 12746 байт и прошивка осталась рабочей.
Ожидаемый и совсем не удивительный результат. Да, примерно так дело и обстоит.
А вот выводы… Я бы не стал вот так брать и ставить gcc на последнее место в компиляторостроении.

По факту USB Device + Masstorage Class, которые тянут аж на 14K это крутой результат. Единственное чего не хватает для полного ужаса — это требований по RAM. Но это уводит нас в другую область. А уходить туда совсем не хочется.

По вопросу статьи скажу так — результат вышел очень приближенным. По сути сравнивать работу именно кодогенератора компилятора надо вычислив размер скомпилорованного кода скажем по MAP файлу. Тут на результат сильно влияет runtime библиотека.

Я не скажу ничего про armcc (слишко мало с ним работал). Но между жестко посаженной на диету и сильно оптимизированной библиотекой IAR и нацеленной на переносимость (включая кроссплатформенную) библиотекой GCC есть огромная разница.

Что использовать решает программист (если ему дадут — тут бюджет разработки подвязан в виде цены компилятора) и руководители. У меня последнее время золотое правило — IAR и GCC проект должны собирать. Работающим. Потому как IAR штука хорошая, но… Думаю это «но» достаточно хорошо известно Российским разработчикам. А любая разработка рано или поздно должна вырасти до того момента, когда это «но» оказывается существенным. А опыт учит, что случается это сильно раньше чем планируется.
Я постарался свести к минимуму использование стандартной библиотеки, отключив весь отладочный вывод.
Вот кусок map файла из IAR. Тут из стандартной библиотеки немного осталось.

dl7M_tln.a: [2]
exit.o 4
------------------------------------------------
Total: 4

rt7M_tl.a: [3]
ABImemclr4.o 6
ABImemcpy_small.o 24
ABImemset48.o 50
XXexit.o 12
cexit.o 10
cmain.o 30
cstartup_M.o 12
data_init.o 40
lz77_init_single.o 122
zero_init3.o 58
------------------------------------------------
Total: 364


Да, у IAR есть классная штука. Он пакует данные для инициализации. lz77_init_single.o — это распаковщик. Не знаю, умеют ли так делать другие компиляторы
Упакованные инициализационные данные скорее всего не позволят «выжать» килобайт, но свою копейку добавят. Думаю это очень важно уточнение в пользу IAR'а. Достойное того, чтобы быть упомянутым в статье. А за одно и проверить результат без этой опции. Она отключается в настройках. Вот только не спрашивайте где именно.
Помню раньше в старых версиях она включалась специальный параметром компилятора, а сейчас она включена по умолчанию. Не видел галочки для ее отключения.
Это вроде в опциях линкера настраивается, но только через конфигурационный файл (.icf) в директиве initialize, не через IDE. Параметр algorithm:
Specifies how to handle the initializers. Choose between:
none — Disables compression of the selected section contents. This is the default method for initialize manually.
zeros — Compresses consecutive bytes with the value zero.
packbits — Compresses with the PackBits algorithm. This method generates good results for data with many identical consecutive bytes.
lz77 — Compresses with the Lempel-Ziv-77 algorithm. This method handles a larger variety of inputs well, but has a slightly larger decompressor.
auto — ILINK estimates the resulting size using each packing method (except for auto), and then chooses the packing method that produces the smallest estimated size. Note that the size of the decompressor is also included. This is the default method for initialize by copy.
smallest — This is a synonym for auto.
Точно-точно. А теперь, видимо, по дефлоту вклчюено.
Ну да, написано, что по дефолту включено auto, то есть наименьший размер :)

Подтверждаю, причём алгоритм тот же, самодельный скрипт для IDA Pro распаковывает данные от обоих компиляторов.

В рамках одного большого проекта по пересборке дизассемблированного кода получился вот такой эксперимент:


  • прошивку размером порядка 28КБ, изначально собранную неизвестной версией IAR, отдизассемблировали в большой монолитный asm и «доработали напильником» до сборки средствами gcc/ac5/ac6/свежего IAR байт в байт (само собой, под каждый компилятор выходной файл генерировался специфическим для него способом, синтаксис разный, особенно у IAR)
  • затем из получившегося asm полностью выкорчевали STM32 SPL, объявили всё пропавшее как extern и включили в сборку исходники SPL.
  • собрали каждым компилятором (уже как asm + C, SPL компилировалась из С, а остальное было неизменным из asm, включая стандартную библиотеку, стартовый код и упакованную секцию данных)
    Результаты были такие: свежий IAR выдал ещё немного меньший код, чем оригинал, а остальные в старые размеры не поместились несмотря ни на какие игры с настройками.
ARMCC v6.14.1 (среда Keil uVision 5.32)

Вроде бы формально он называется не armcc, а armclang.


Надо отметить, что опция KEIL «Use cross module optimization» Значительно увеличила время компиляции, но ни чуть не уменьшила размер кода.

Попробуйте (с armcc5) компилятору и линкеру вручную указать опцию --feedback=unused и дважды перекомпилировать все.

Вроде бы формально он называется не armcc, а armclang.

Да, действительно

Размер прошивки не изменился.

Можно еще попробовать галку (тоже для armcc) One ELF section per function, в некоторых ситуациях помогает.

Я бы порекомендовал переписать инициализацию ядра и периферии на регистрах по рефмануалу, а не через ХАЛ. Сразу сэкономятся 2...5 кБ, т.к. те же GPIO через регистры можно за раз 8...16 штук инициализировать, а не по одному, и без всех этих лишних вычислений, которые внутри ХАЛ. А каждая сэкономленная операция — это 4...20 байт ПЗУ. Да, это читать надо, но когда хочется втиснуться в проц — выбора нет.
Зашел за фотки РФок лайкнуть, но ещё добавлю что в gcc -Os не самый лучший вариант вместе с -flto, лучше набрать оптимизации разными ключами.

-flto мощный инструмент, дающий выигрыш по размеру и скорости выполнения. Выводы Вашей статьи могут кардинально измениться :).
GCC в связке с CubeMX требует немножко танцев с бубном.
Основные возможные проблемы связаны с ".weak" и возможной "переоптимизацией" глобальных переменных.
В файле (например) startup_stm32g431xx.s закомментите все(!) ".thumb_set" типа:
.weak NMI_Handler
//.thumb_set NMI_Handler,Default_Handler
Чаще всего этого будет достаточно.
Подозреваю, что есть более элегантное решение, пока поборол только так.
Удачи.

Неистово плюсую!

Итак, в startup_xx файле надо убрать weak объявления используемых векторов. В данном случае используются только два вектора:

// .weak USB_LP_CAN1_RX0_IRQHandler
// .thumb_set USB_LP_CAN1_RX0_IRQHandler,Default_Handler
// .weak SysTick_Handler
// .thumb_set SysTick_Handler,Default_Handler

В результате размер прошивки стал 11722.
достаточно добавить __attribute__((used)) в c/cpp файлах, для тех функций которые помечены weak.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.