Как сжать код для большего количества Flash и RAM? [закрыто]

14

Я работал над разработкой функции для нашего конкретного продукта. Был запрос на перенос той же функции на другой продукт. Этот продукт основан на микроконтроллере M16C, который традиционно имеет 64K Flash и 2k RAM.

Это зрелый продукт, поэтому у него осталось всего 132 байта флэш-памяти и 2 байта оперативной памяти.

Чтобы портировать запрашиваемую функцию (сама функция была оптимизирована), мне нужно 1400 байт флэш-памяти и ~ 200 байт оперативной памяти.

Кто-нибудь есть какие-либо предложения о том, как получить эти байты путем сжатия кода? Какие конкретные вещи я ищу, когда пытаюсь сжать уже существующий рабочий код?

Любые идеи действительно будут оценены.

Благодарю.

IntelliChick
источник
1
Спасибо всем за предложения. Я буду держать вас в курсе моих достижений и перечислю шаги, которые сработали, и те, которые не сработали.
IntelliChick
Итак, вот что я попробовал, что сработало: поднял версии компилятора. Оптимизация значительно улучшилась, что дало мне примерно 2K Flash. Прошел список файлов, чтобы проверить наличие избыточной и неиспользуемой функциональности (унаследованной из-за общей базы кода) для конкретного продукта и получил еще немного Flash.
IntelliChick
Для оперативной памяти я сделал следующее: прошел файл карты, чтобы проверить функции / модули, которые использовали больше оперативной памяти. Я нашел действительно тяжелую функцию (12 каналов, каждый с фиксированным объемом выделенной памяти) устаревшего кода, понял, чего он пытается достичь, и оптимизировал использование оперативной памяти, поделившись информацией между распространенными каналами. Это дало мне ~ 200 байт, которые мне были нужны.
IntelliChick
Если у вас есть файлы ascii, вы можете использовать сжатие от 8 до 7 бит. Экономит вас 12,5%. Использование zip-файла заняло бы больше кода, чтобы сжать и разархивировать его, чем просто оставить.
Sparky256

Ответы:

18

У вас есть несколько вариантов: во-первых, искать избыточный код и перемещать его в один вызов, чтобы избавиться от дублирования; во-вторых, удалить функциональность.

Внимательно посмотрите на свой файл .map и посмотрите, есть ли функции, от которых вы можете избавиться или переписать. Также убедитесь, что используемые библиотечные вызовы действительно необходимы.

Определенные вещи, такие как деление и умножение, могут принести много кода, но использование сдвигов и более эффективное использование констант может сделать код меньше. Также взгляните на такие вещи, как строковые константы и printfs. Например, каждый из них printfсъест ваш диск, но вы можете иметь пару строк общего формата вместо того, чтобы повторять эту строковую константу снова и снова.

Для памяти посмотрите, можете ли вы избавиться от глобальных переменных и использовать вместо них autos в функции. Также избегайте как можно большего количества переменных в основной функции, так как они поглощают память, как это делают глобальные переменные.

Рекс Логан
источник
1
Спасибо за предложения, я определенно могу попробовать большинство из них, кроме одного для строковых констант. Это чисто встроенное устройство без пользовательского интерфейса, поэтому в коде нет вызовов функции printf (). Надеюсь, что эти предложения приведут меня в порядок, чтобы я получил 1400 байт Flash / 200 байт оперативной памяти, которые мне нужны.
IntelliChick
1
@IntelliChick вы будете удивлены тем, сколько людей используют printf () внутри встроенного устройства для печати для отладки или отправки на периферийное устройство. Кажется, вы знаете лучше, чем это, но если кто-то написал код проекта до вас, это не помешает проверить его.
Kellenjb
5
И просто, чтобы расширить мой предыдущий комментарий, вы также будете поражены тем, сколько людей добавляют операторы отладки, но никогда не удаляют их. Даже люди, которые делают #ifdefs, время от времени становятся ленивыми.
Kellenjb
1
Круто, спасибо! Я унаследовал эту кодовую базу, поэтому плохо определился с этим. Я буду держать вас в курсе, ребята, о прогрессе и постараюсь отследить, сколько байт памяти или Flash я получил, делая то же самое, просто как справку для всех, кому, возможно, понадобится сделать это в будущем.
IntelliChick
Просто вопрос на этот счет - как насчет вложенных функций вызова прыжков от слоя к слою. Сколько накладных расходов это добавляет? Лучше ли сохранить модульность, имея несколько вызовов функций или уменьшить количество вызовов функций и сохранить несколько байтов. И это важно?
IntelliChick
8

Всегда стоит посмотреть вывод списка файлов (ассемблера), чтобы найти вещи, в которых ваш конкретный компилятор особенно плох.

Например, вы можете обнаружить, что локальные переменные очень дороги, и если приложение достаточно простое, чтобы оправдать риск, перемещение нескольких счетчиков циклов в статические переменные может сэкономить много кода.

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

Но смотреть на ассемблере - это первый шаг.


источник
3
Очень важно, чтобы вы знали, что делает ваш компилятор. Вы должны увидеть, что разделить на мой компилятор. Это заставляет детей плакать (включая меня).
Кортук
8

Оптимизация компилятора, например, -Osв GCC дает лучший баланс между скоростью и размером кода. Избегайте -O3, так как это может увеличить размер кода.

Томас О
источник
3
Если вы сделаете это, вам нужно будет повторно протестировать ВСЕ! Оптимизация может привести к тому, что рабочий код не будет работать из-за новых допущений, которые делает компилятор.
Роберт
@ Роберт, это верно только в том случае, если вы используете неопределенные операторы: например, a = a ++ будет компилироваться по-разному в -O0 и -O3.
Томас О
5
@ Томас не правда. Если у вас есть цикл for для задержки тактовых циклов, многие оптимизаторы поймут, что вы ничего не делаете в нем, и удалите его. Это всего лишь 1 пример.
Kellenjb
1
@thomas O, Вы также должны убедиться, что вы внимательно относитесь к определениям изменчивых функций. Оптимизаторы взрывают тех, кто думает, что хорошо знает C, но не понимает сложности атомных операций.
Кортук
1
Все хорошие моменты. Изменчивые функции / переменные по определению НЕ должны быть оптимизированы. Любой оптимизатор, который выполняет оптимизацию на этом (включая время вызова и вставку), не работает.
Томас О
8

Для оперативной памяти проверьте диапазон всех ваших переменных - используете ли вы целочисленные значения, где можно использовать символ? Буферы больше, чем они должны быть?

Сжатие кода очень зависит от приложения и стиля кодирования. Ваши оставшиеся суммы указывают на то, что, возможно, код уже ушел, хотя какое-то сжатие, что может означать, что осталось немного.

Также внимательно посмотрите на общую функциональность - есть ли что-то, что на самом деле не используется и может быть отброшено?

mikeselectricstuff
источник
8

Если это старый проект, но компилятор был разработан с тех пор, возможно, более поздний компилятор может производить меньший код

mikeselectricstuff
источник
Спасибо, Майк! Я пробовал это в прошлом, и полученное место уже было использовано! :) Перешел с компилятора IAR C с 3.21d на 3.40.
IntelliChick
1
Я поднял еще одну версию, и мне удалось получить еще немного Flash, чтобы соответствовать этой функции. Однако я действительно борюсь с оперативной памятью, которая осталась неизменной. :(
IntelliChick
7

Всегда стоит обратиться к руководству по компилятору за вариантами оптимизации пространства.

Для GCC -ffunction-sectionsи -fdata-sectionsс--gc-sections флагом компоновщика хороши для удаления мертвого кода.

Вот некоторые другие отличные советы (ориентированные на AVR)

Тоби джеффи
источник
Это на самом деле работает? Документы говорят: «Когда вы укажете эти параметры, ассемблер и компоновщик создадут большие объекты и исполняемые файлы, а также будут работать медленнее». Я понимаю, что наличие отдельных разделов имеет смысл для микро с разделами Flash и RAM. Разве это утверждение в документах не применимо к микроконтроллерам?
Кевин Вермеер
Мой опыт
показывает,
1
Это не очень хорошо работает в большинстве компиляторов, которые я использовал. Это как использовать ключевое слово register. Вы можете сказать компилятору, что переменная входит в регистр, но хороший оптимизатор сделает это намного лучше, чем человек (как утверждают некоторые, на практике это считается неприемлемым).
Кортук
1
Когда вы начинаете назначать местоположения, вы заставляете компилятор размещать вещи в определенных местах, что очень важно для расширенного кода загрузчика, но ужасно иметь дело с оптимизатором, так как вы принимаете решения для него, вы отнимаете шаг его оптимизации может сделать. В некоторых компиляторах они разрабатывают его так, чтобы в нем были разделы, для которых используется код. Это случай, когда компилятор сообщает больше информации для понимания вашего использования, это поможет. Если компилятор этого не предлагает, не делайте этого.
Кортук
6

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

Я думаю, для проекта , который влезает в 2к ОЗУ , чтобы начать с нет динамического распределения памяти (использование malloc, callocи т.д.). Если это так, вы можете полностью избавиться от кучи, если исходный автор оставил часть оперативной памяти, выделенную для кучи.

Вы должны быть очень осторожны, уменьшая размер стека, так как это может привести к ошибкам, которые очень трудно найти. Может быть полезно начать с инициализации всего пространства стека известным значением (что-то отличное от 0x00 или 0xff, поскольку эти значения обычно уже встречаются), а затем запустить систему на некоторое время, чтобы увидеть, сколько места в стеке не используется.

semaj
источник
Это очень хороший выбор. Я отмечу, что вы никогда не должны использовать malloc во встроенной системе.
Кортук
1
@Kortuk Это зависит от вашего определения встроенных и выполняемой задачи
Тоби Джаффей
1
@joby, да, я понимаю это. В системе с 0 перезапусками и отсутствием ОС, такой как linux, Malloc может быть очень и очень плохим.
Кортук
Нет динамического выделения памяти, нет места, где используется malloc, calloc. Я также проверил распределение кучи, и оно уже было установлено на 0, поэтому нет распределения кучи. В настоящее время выделенный размер стека составляет 254 байта, а размер стека прерываний - 128 байтов.
IntelliChick
5

В вашем коде используется математика с плавающей запятой? Возможно, вам удастся повторно реализовать ваши алгоритмы, используя только целочисленную математику, и устранить издержки использования библиотеки C с плавающей запятой. Например, в некоторых приложениях такие функции, как sine, log, exp можно заменить на целочисленные полиномиальные приближения.

Использует ли ваш код большие справочные таблицы для каких-либо алгоритмов, таких как вычисления CRC? Вы можете попробовать заменить другую версию алгоритма, которая вычисляет значения на лету, вместо использования справочных таблиц. Предостережение заключается в том, что меньший алгоритм, скорее всего, медленнее, поэтому убедитесь, что у вас достаточно циклов ЦП.

Содержит ли ваш код большие объемы постоянных данных, таких как строковые таблицы, HTML-страницы или пиксельная графика (значки)? Если он достаточно большой (скажем, 10 кБ), возможно, стоит реализовать очень простую схему сжатия, чтобы уменьшить объем данных и распаковывать их на лету, когда это необходимо.

Крейг МакКуин
источник
Есть 2 маленьких справочных таблицы, ни одна из которых, к сожалению, не будет составлять 10K. И математика с плавающей запятой тоже не используется. :( Спасибо за предложения, хотя. Они хорошие.
IntelliChick
2

Вы можете попытаться изменить код, чтобы сделать его более компактным. Это во многом зависит от того, что делает код. Ключ заключается в том, чтобы найти похожие вещи и повторно реализовать их в терминах друг друга. Экстремальным вариантом было бы использование языка более высокого уровня, такого как Forth, с которым легче достичь более высокой плотности кода, чем в C или ассемблере.

Вот Forth для M16C .

Профессор Фалькен нарушил контракт
источник
2

Установите уровень оптимизации компилятора. Многие IDE имеют настройки, которые позволяют оптимизировать размер кода за счет времени компиляции (или, возможно, даже времени обработки в некоторых случаях). Они могут выполнить сжатие кода, перезапустив свой оптимизатор пару раз, выполнив поиск менее распространенных оптимизируемых шаблонов и других приемов, которые могут не понадобиться для случайной / отладочной компиляции. Обычно по умолчанию компиляторы настроены на средний уровень оптимизации. Покопайтесь в настройках и вы сможете найти некоторую целочисленную шкалу оптимизации.

Джоэл Б
источник
1
В настоящее время оптимизирован до максимального размера. :) Спасибо за предложение, хотя. :)
IntelliChick
2

Если вы уже используете компилятор профессионального уровня, такой как IAR, я думаю, что вам будет сложно добиться какой-либо серьезной экономии за счет незначительной низкоуровневой подстройки кода - вам нужно больше смотреть на удаление функциональности или выполнение основных переписывает детали более эффективным способом. Вам нужно быть умнее кодером, чем тот, кто написал оригинальную версию ... Что касается оперативной памяти, вам нужно очень внимательно взглянуть на то, как она используется в настоящее время, и посмотреть, есть ли возможности для перекрытия использования той же оперативной памяти для разные вещи в разное время (для этого пригодятся союзы). Размеры кучи и стека в IAR по умолчанию в ARM / AVR, которые у меня были, как правило, слишком великодушны, так что это будет первое, на что стоит обратить внимание.

mikeselectricstuff
источник
Спасибо майк Код уже использует объединения в большинстве мест, но я посмотрю в некоторых других местах, где это все еще может помочь. Я также посмотрю на выбранный размер стека и посмотрим, можно ли его вообще оптимизировать.
IntelliChick
Как узнать, какой размер стека подходит?
IntelliChick
2

Что еще нужно проверить - некоторые компиляторы на некоторых архитектурах копируют константы в ОЗУ - обычно используются, когда доступ к константам флэш-памяти медленный / трудный (например, AVR), например, компилятору IAR AVR требуется квалификатор _ _flash, чтобы не копировать константу в RAM)

mikeselectricstuff
источник
Спасибо майк Да, я уже проверил это - он называется опцией «Записываемые константы» для компилятора IAR C M16C. Копирует константы из ПЗУ в ОЗУ. Эта опция не отмечена для моего проекта. Но действительно действительный чек! Благодарю.
IntelliChick
1

Если ваш процессор не имеет аппаратной поддержки для параметра / локального стека, но компилятор все равно пытается реализовать стек параметров времени выполнения, и если ваш код не нуждается в повторном вводе, вы можете сохранить код пространство путем статического распределения авто переменных. В некоторых случаях это должно быть сделано вручную; в других случаях директивы компилятора могут это сделать. Эффективное ручное распределение потребует разделения переменных между подпрограммами. Такое совместное использование должно быть сделано осторожно, чтобы гарантировать, что ни одна процедура не использует переменную, которую другая процедура считает находящейся «в области видимости», но в некоторых случаях преимущества размера кода могут быть значительными.

Некоторые процессоры имеют соглашения о вызовах, которые могут сделать некоторые стили передачи параметров более эффективными, чем другие. Например, на контроллерах PIC18, если подпрограмма принимает один однобайтовый параметр, она может быть передана в регистр; если требуется больше, все параметры должны быть переданы в ОЗУ. Если подпрограмма будет принимать два однобайтовых параметра, может быть наиболее эффективно «передать» один из них в глобальную переменную, а затем передать другой в качестве параметра. Благодаря широко используемым подпрограммам можно сэкономить. Они могут быть особенно значительными, если параметр, передаваемый через global, является однобитовым флагом или если он обычно будет иметь значение 0 или 255 (поскольку существуют специальные инструкции для хранения 0 или 255 в ОЗУ).

В ARM размещение глобальных переменных, которые часто используются вместе, в структуре может значительно уменьшить размер кода и повысить производительность. Если A, B, C, D и E являются отдельными глобальными переменными, то код, который использует все из них, должен загрузить адрес каждого в регистр; если регистров недостаточно, может потребоваться перезагрузить эти адреса несколько раз. Напротив, если они являются частью одной и той же глобальной структуры MyStuff, то код, который использует MyStuff.A, MyStuff.B и т. Д., Может просто один раз загрузить адрес MyStuff. Большая победа.

Supercat
источник
1

1. Если ваш код опирается на множество структур, убедитесь, что элементы структуры упорядочены от тех, которые занимают большую часть памяти, до минимума.

Пример: "uint32_t uint16_t uint8_t" вместо "uint16_t uint8_t uint32_t"

Это обеспечит минимальное заполнение структуры.

2. Используйте const для переменных, где это применимо. Это гарантирует, что эти переменные будут в ПЗУ, а не израсходуют ОЗУ.

AkshayImmanuelD
источник
1

Несколько (возможно, очевидных) приемов, которые я успешно использовал при сжатии клиентского кода:

  1. Уплотните флаги в битовые поля или битовые маски. Это может быть полезно, так как обычно логические значения хранятся в виде целых чисел, тратя впустую память. Это сэкономит как ОЗУ, так и ПЗУ и обычно не выполняется компилятором.

  2. Ищите избыточность в коде и используйте циклы или функции для выполнения повторных операторов.

  3. Я также сохранил некоторые ПЗУ, заменив многие if(x==enum_entry) <assignment>операторы из констант на индексированный массив, позаботившись о том, чтобы записи enum могли использоваться в качестве индекса массива.

клабаккио
источник
0

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

AngryEE
источник
1
Любой достойный компилятор должен делать это автоматически для функций, которые вызываются только один раз.
mikeselectricstuff
5
Я обнаружил, что встраивание обычно более полезно для оптимизации скорости, и обычно за счет увеличения размера.
Крейг МакКуин
встраивание, как правило, увеличивает размер кода, за исключением таких тривиальных функций, как int get_a(struct x) {return x.a;}
Дмитрий Григорьев
0

Измените локальные переменные, чтобы они соответствовали размеру ваших регистров процессора.

Если процессор 32-битный, используйте 32-битные переменные, даже если максимальное значение никогда не превысит 255. Если вы использовали 8-битную переменную, компилятор добавит код для маскировки верхних 24 бит.

Первое, на что я бы обратил внимание - это переменные цикла for.

for( i = 0; i < 100; i++ )

Это может показаться хорошим местом для 8-битной переменной, но 32-битная переменная может производить меньше кода.

Роберт
источник
Это может сохранить код, но он будет кушать оперативную память.
mikeselectricstuff
Он будет использовать оперативную память только в том случае, если этот вызов функции находится в самой длинной ветви трассировки вызовов. В противном случае он повторно использует пространство стека, в котором уже нуждается какая-то другая функция.
Роберт
2
Обычно верно, если это локальная переменная. Если не хватает оперативной памяти, размер глобальных переменных, особенно массивов, является хорошим местом для начала поиска экономии.
mikeselectricstuff
1
Интересно, что есть еще одна возможность заменить неподписанные переменные на подписанные. Если компилятор оптимизирует беззнаковое короткое замыкание для 32-разрядного регистра, он должен добавить код, чтобы его значение обернулось от 65535 до нуля. Однако, если компилятор оптимизирует подписанное замыкание на регистр, такой код не требуется. Поскольку нет гарантии того, что произойдет, если короткое замыкание будет превышено 32767, компиляторам не требуется выдавать код, чтобы справиться с этим. По крайней мере, на двух ARM-компиляторах, на которые я смотрел, код со знаком без подписи может быть меньше кода без знака по этой причине.
суперкат