Каков наилучший подход при написании функций для встроенного программного обеспечения для повышения производительности? [закрыто]


13

Я видел некоторые библиотеки для микроконтроллеров, и их функции выполняют одну вещь за раз. Например, что-то вроде этого:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Затем идут другие функции, которые используют этот однострочный код, содержащий функцию, для других целей. Например:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

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

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

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


РЕДАКТИРОВАТЬ 2:

  1. Кажется, многие люди поняли этот вопрос так, будто я пытаюсь оптимизировать программу. Нет, я не собираюсь этого делать . Я позволяю компилятору делать это, потому что он будет всегда (надеюсь, что нет!) Лучше меня.

  2. Все обвинения в том, что я выбрал пример, представляющий некоторый код инициализации . Вопрос не имеет намерения касаться вызовов функций, выполняемых для целей инициализации. Мой вопрос заключается в том, имеет ли какое-либо преимущество разбиение определенной задачи на маленькие функции многострочных ( так что inline-строк ), работающих внутри бесконечного цикла, над написанием длинной функции без вложенной функции?

Пожалуйста, учитывайте читабельность, определенную в ответе @Jonk .


28
Вы очень наивны (не подразумеваете оскорбление), если считаете, что любой разумный компилятор будет слепо превращать код, как написано, в двоичные файлы, как написано. Большинство современных компиляторов достаточно хороши для определения, когда подпрограмма лучше встроена, и даже когда для хранения переменной следует использовать место в регистре или в памяти. Следуйте двум правилам оптимизации: 1) не оптимизируйте. 2) сделать оптимизируют не еще . Сделайте ваш код читабельным и обслуживаемым, и ТОГДА только после профилирования работающей системы, посмотрите на оптимизацию.
akohlsmith

10
@akohlsmith IIRC Три правила оптимизации: 1) Не надо! 2) Нет, действительно нет! 3) Сначала профиль, а затем и только потом оптимизируйте, если необходимо - Michael_A._Jackson
esoterik

3
Просто помните, что «преждевременная оптимизация - корень всего зла (или, по крайней мере, большей его части) в программировании», - Кнут,
говорит Моуг, верните Монику

1
@ Mawg: оперативное слово там преждевременно . (Как объясняется в следующем абзаце этого документа. Буквально следующее предложение: «Тем не менее, мы не должны упускать наши возможности в эти критические 3%».) Не оптимизируйте, пока вам это не нужно - вы не найдете медленных Немного о том, что у вас есть что профилировать, но также не занимайтесь пессимизацией, например, используя явно неправильные инструменты для работы.
Чао

1
@ Mawg Я не знаю, почему я получил ответы / отзывы, связанные с оптимизацией, так как я никогда не упоминал слово и собираюсь это сделать. Вопрос гораздо больше о том, как писать функции во встроенном программировании для достижения лучшей производительности.
MaNyYaCk

Ответы:


28

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

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

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

Кроме того, как уже отмечалось другими, стоимость (и значение «стоимости») вызова функции различаются в зависимости от платформы, компилятора, настроек оптимизации компилятора и требований приложения. Будет огромная разница между 8051 и cortex-m7, а также кардиостимулятором и выключателем.


6
ИМО второй абзац должен быть жирным шрифтом и вверху. Нет ничего плохого в правильном выборе правильных алгоритмов и структур данных, но беспокоиться о накладных расходах при вызове функций, если вы не обнаружили, что это фактическое узкое место, безусловно, преждевременная оптимизация, и ее следует избегать.
Фонд Моника иск

11

Я не могу придумать никакого преимущества (но см. Примечание к JasonS внизу), заключая одну строку кода в функцию или подпрограмму. За исключением, возможно, того, что вы можете назвать функцию как-нибудь «читабельную». Но вы также можете прокомментировать строку. А поскольку завершение строки кода в функции стоит памяти кода, места в стеке и времени выполнения, мне кажется, что это в основном контрпродуктивно. В учебной ситуации? Это может иметь какой-то смысл. Но это зависит от класса учеников, их предварительной подготовки, учебной программы и учителя. В основном, я думаю, что это не очень хорошая идея. Но это мое мнение.

Что подводит нас к сути. Ваша широкая область вопросов в течение десятилетий была предметом некоторых дискуссий и до сих пор остается предметом некоторых дискуссий. Итак, по крайней мере, когда я читаю ваш вопрос, мне кажется, что это вопрос, основанный на мнении (как вы его задали).

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


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

  1. Будьте последовательны в своем подходе, чтобы другое чтение вашего кода могло развить понимание вашего подхода к процессу кодирования. Быть непоследовательным, вероятно, худшее из возможных преступлений. Это не только усложняет задачу для других, но и затрудняет возвращение к коду спустя годы.
  2. Насколько это возможно, постарайтесь организовать все так, чтобы инициализация различных функциональных разделов могла выполняться без учета порядка. Там, где требуется упорядочение, если оно связано с тесной связью двух тесно связанных подфункций, рассмотрите возможность одной инициализации для обоих, чтобы можно было изменить порядок, не причиняя вреда. Если это невозможно, задокументируйте требования к порядку инициализации.
  3. Инкапсулируйте знания в одном месте, если это возможно. Константы не должны дублироваться повсюду в коде. Уравнения, которые решают для некоторой переменной, должны существовать в одном и только одном месте. И так далее. Если вы обнаружите, что копируете и вставляете некоторый набор строк, которые выполняют необходимое поведение в разных местах, подумайте о том, как собрать эти знания в одном месте и использовать их там, где это необходимо. Например, если у вас есть древовидная структура, которую нужно пройти определенным образом, выполните нереплицируйте код обхода дерева в каждом месте, где вам нужно перебирать узлы дерева. Вместо этого соберите метод прогулки по дереву в одном месте и используйте его. Таким образом, если дерево меняется и метод ходьбы меняется, вам остается беспокоиться только об одном месте, а весь остальной код «просто работает правильно».
  4. Если вы разложите все свои подпрограммы на огромном плоском листе бумаги со стрелками, соединяющими их так, как они вызываются другими подпрограммами, вы увидите, что в любом приложении будут «группы» подпрограмм, которые имеют много-много стрелок между собой, но только несколько стрел вне группы. Таким образом, будут естественные границы тесно связанных подпрограмм и слабосвязанных соединений между другими группами тесно связанных подпрограмм. Используйте этот факт для организации вашего кода в модули. Это существенно ограничит кажущуюся сложность вашего кода.

Вышесказанное в целом верно для всего кодирования. Я не обсуждал использование параметров, локальных или статических глобальных переменных и т. Д. Причина в том, что для встроенного программирования пространство приложения часто накладывает экстремальные и очень существенные новые ограничения, и невозможно обсудить их все без обсуждения каждого встроенного приложения. И этого здесь не происходит, во всяком случае.

Этими ограничениями могут быть любые (и более) из них:

  • Серьезные ограничения стоимости, требующие чрезвычайно примитивных микроконтроллеров с минимальной оперативной памятью и почти без подсчета выводов ввода / вывода. Для них применяются совершенно новые наборы правил. Например, вам, возможно, придется писать в ассемблерном коде, потому что не так много места для кода. Возможно, вам придется использовать ТОЛЬКО статические переменные, потому что использование локальных переменных слишком дорого и занимает много времени. Возможно, вам придется избегать чрезмерного использования подпрограмм, потому что (например, некоторые детали PIC Microchip) есть только 4 аппаратных регистра, в которых можно хранить адреса возврата подпрограмм. Так что вам, возможно, придется резко «сплющить» свой код. И т.п.
  • Серьезные ограничения по мощности, требующие тщательно созданного кода для запуска и выключения большей части MCU, и наложение жестких ограничений на время выполнения кода при работе на полной скорости. Опять же, иногда это может потребовать некоторого ассемблера.
  • Серьезные временные требования. Например, бывают случаи, когда мне приходилось следить за тем, чтобы передача 0 с открытым стоком потребовала ТОЧНО того же числа циклов, что и передача 1. И что выборка этой же линии также должна была быть выполнена с точной относительной фазой к этому времени. Это означало, что C не может быть использован здесь. Единственный возможный способ сделать эту гарантию - тщательно составить код сборки. (И даже тогда, не всегда на всех проектах ALU.)

И так далее. (Код проводки для жизненно важных медицинских приборов тоже имеет свой собственный мир.)

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


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

Читаемый код может быть достаточно эффективным и соответствовать всем вышеупомянутым требованиям, которые я уже упоминал. Главное, что вы полностью понимаете, что каждая строка кода, которую вы пишете, производит на уровне сборки или машины, когда вы ее кодируете. C ++ накладывает серьезное бремя на программиста, потому что во многих ситуациях идентичные фрагменты кода C ++ фактически генерируют разные фрагменты машинного кода, которые имеют совершенно разные характеристики. Но C, как правило, в основном "то, что вы видите, то и получаете". Так что в этом отношении безопаснее.


РЕДАКТИРОВАТЬ за JasonS:

Я использую C с 1978 года, а C ++ примерно с 1987 года, и у меня большой опыт использования как для мейнфреймов, так и для миникомпьютеров и (в основном) встроенных приложений.

Джейсон приводит комментарий об использовании «inline» в качестве модификатора. (С моей точки зрения, это относительно «новая» возможность, потому что она просто не существовала, возможно, половину моей жизни или более с использованием C и C ++.) Использование встроенных функций может фактически делать такие вызовы (даже для одной строки код) довольно практично. И это гораздо лучше, где это возможно, чем использование макроса из-за типизации, которую может применить компилятор.

Но есть и ограничения. Во-первых, вы не можете полагаться на то, что компилятор «поймет подсказку». Может или не может. И есть веские причины, чтобы не понять намек. (Для очевидного примера, если адрес функции взят, для этого требуется создание экземпляра функции, а использование адреса для выполнения вызова ... потребует вызова. Тогда код не может быть встроен.) и другие причины. Компиляторы могут иметь широкий спектр критериев, по которым они судят, как обрабатывать подсказку. И как программист, это означает, что вы должныпотратьте некоторое время на изучение этого аспекта компилятора, иначе вы, скорее всего, примете решение, основываясь на ошибочных идеях. Таким образом, это увеличивает нагрузку как на автора кода, так и на любого читателя, а также на любого, кто планирует перенести код на другой компилятор.

Также компиляторы C и C ++ поддерживают раздельную компиляцию. Это означает, что они могут скомпилировать один фрагмент кода C или C ++ без компиляции любого другого связанного кода для проекта. Для того, чтобы встроить код, предполагая, что компилятор в противном случае может сделать это, он должен не только иметь объявление «в области видимости», но и также иметь определение. Обычно программисты работают, чтобы убедиться, что это так, если они используют «встроенный». Но ошибки легко закрасться.

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

Последнее замечание о том, что «inline» и определения находятся «в области видимости» для отдельного этапа компиляции. Возможно (не всегда надежно) выполнение работ на этапе связывания. Это может произойти в том и только в том случае, если компилятор C / C ++ внедряет в объектные файлы достаточно деталей, чтобы компоновщик мог выполнять «встроенные» запросы. Лично у меня не было системы компоновщика (вне Microsoft), которая бы поддерживала эту возможность. Но это может произойти. Опять же, следует ли полагаться на это или нет, будет зависеть от обстоятельств. Но я обычно предполагаю, что это не было переложено на компоновщик, если я не знаю иначе, основываясь на убедительных доказательствах. И если я на это полагаюсь, это будет задокументировано на видном месте.


C ++

Для тех, кто интересуется, вот пример того, почему я остаюсь достаточно осторожным в отношении C ++ при кодировании встроенных приложений, несмотря на его готовность сегодня. Я выбросить некоторые термины , которые я думаю , что все встроенные программистам C ++ нужно знать холод :

  • частичная специализация шаблона
  • виртуальные таблицы
  • виртуальный базовый объект
  • рамка активации
  • раскрутка активационной рамки
  • использование умных указателей в конструкторах и почему
  • оптимизация возвращаемого значения

Это всего лишь короткий список. Если вы еще не знаете все об этих терминах и почему я их перечислил (и многие другие я не перечислил здесь), то я бы посоветовал не использовать C ++ для встроенной работы, если это не вариант для проекта. ,

Давайте кратко рассмотрим семантику исключений в C ++, чтобы получить представление.

AВ

A

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

A

В

Компилятор C ++ видит первый вызов foo () и может просто позволить размотке обычного кадра активации произойти, если foo () выдает исключение. Другими словами, компилятор C ++ знает, что в этот момент не требуется никакого дополнительного кода для поддержки процесса раскрутки фрейма, участвующего в обработке исключений.

Но как только String s создан, компилятор C ++ знает, что он должен быть должным образом уничтожен, прежде чем можно будет разрешить раскрутку кадра, если позднее произойдет исключение. Таким образом, второй вызов foo () семантически отличается от первого. Если второй вызов функции foo () генерирует исключение (что он может или не может делать), компилятор должен разместить код, предназначенный для обработки уничтожения String, прежде чем позволить обычному размотке кадра произойти. Это отличается от кода, необходимого для первого вызова foo ().

(Можно добавить дополнительные декорации в C ++, чтобы помочь ограничить эту проблему. Но дело в том, что программисты, использующие C ++, просто должны гораздо лучше понимать последствия каждой строки кода, которую они пишут.)

В отличие от malloc C, новый C ++ использует исключения, чтобы сигнализировать, когда он не может выполнить необработанное выделение памяти. Так будет и «dynamic_cast». (См. 3-е изд. Страуструпа «Язык программирования C ++», стр. 384 и 385, для стандартных исключений в C ++.) Компиляторы могут разрешить отключение этого поведения. Но в целом вы будете подвергаться некоторым накладным расходам из-за правильно сформированных прологов и эпилогов обработки исключений в сгенерированном коде, даже когда исключения на самом деле не имеют места и даже когда скомпилированная функция фактически не имеет каких-либо блоков обработки исключений. (Страуструп публично посетовал на это.)

Без частичной специализации шаблонов (не все компиляторы C ++ поддерживают это) использование шаблонов может привести к катастрофе для встроенного программирования. Без этого расцвет кода будет серьезным риском, который может убить встроенный проект с небольшой памятью в одно мгновение.

Когда функция C ++ возвращает объект, временный компилятор без имени создается и уничтожается. Некоторые компиляторы C ++ могут предоставить эффективный код, если в операторе return используется конструктор объекта вместо локального объекта, что уменьшает потребность в строительстве и разрушении одним объектом. Но не каждый компилятор делает это, и многие программисты на C ++ даже не знают об этой «оптимизации возвращаемого значения».

Предоставление конструктора объекта с одним типом параметра может позволить компилятору C ++ найти путь преобразования между двумя типами совершенно неожиданным для программиста способом. Такое «умное» поведение не является частью C.

Предложение catch, задающее базовый тип, будет «разрезать» брошенный производный объект, потому что брошенный объект копируется с использованием «статического типа» предложения catch, а не «динамического типа» объекта. Весьма распространенный источник страданий от исключений (когда вы чувствуете, что можете даже позволить себе исключения во встроенном коде.)

Компиляторы C ++ могут автоматически генерировать для вас конструкторы, деструкторы, конструкторы копирования и операторы присваивания с непредвиденными результатами. Требуется время, чтобы получить средства с деталями этого.

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

Поскольку C ++ не вызывает деструктор частично сконструированных объектов, когда в конструкторе объектов возникает исключение, обработка исключений в конструкторах обычно требует «умных указателей», чтобы гарантировать, что сконструированные фрагменты в конструкторе будут должным образом уничтожены, если там произойдет исключение. , (См. Страуструп, стр. 367 и 368.) Это распространенная проблема при написании хороших классов на C ++, но, конечно же, ее избегают в C, так как C не имеет встроенной семантики построения и уничтожения. Написание правильного кода для обработки конструкции подобъекты внутри объекта означают написание кода, который должен справиться с этой уникальной семантической проблемой в C ++; Другими словами, «написание вокруг» C ++ семантического поведения.

C ++ может копировать объекты, переданные в параметры объекта. Например, в следующих фрагментах вызов "rA (x);" может заставить компилятор C ++ вызывать конструктор для параметра p, чтобы затем вызвать конструктор копирования для передачи объекта x в параметр p, затем другой конструктор для возвращаемого объекта (неназванного временного) функции rA, которая, конечно, скопировано из параметра p. Хуже того, если у класса А есть свои собственные объекты, которые нуждаются в строительстве, это может привести к катастрофическим телескопам. (Программист AC избежал бы большей части этого мусора, оптимизируя вручную, поскольку программисты на C не имеют такого удобного синтаксиса и должны выражать все детали по одному).

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Наконец, короткое примечание для программистов на Си. longjmp () не имеет переносимого поведения в C ++. (Некоторые программисты на C используют это как своего рода механизм «исключения».) Некоторые компиляторы C ++ на самом деле пытаются настроить вещи для очистки, когда берется longjmp, но это поведение не переносимо в C ++. Если компилятор очищает построенные объекты, он не переносим. Если компилятор не очищает их, то объекты не уничтожаются, если код покидает область действия созданных объектов в результате longjmp и поведение недопустимо. (Если использование longjmp в foo () не выходит из области видимости, поведение может быть нормальным.) Это не слишком часто используется программистами на C, но они должны знать об этих проблемах перед их использованием.


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

6
@Dorian - ваш комментарий может быть верным при определенных обстоятельствах для определенных компиляторов. Если функция внутри файла статическая, то у компилятора есть возможность сделать код встроенным. если она внешне видима, то, даже если она на самом деле никогда не вызывается, должен быть способ для вызова функции.
uɐɪ

1
@jonk - Еще один трюк, который вы не упомянули в хорошем ответе, - это написание простых макрофункций, которые выполняют инициализацию или настройку в виде расширенного встроенного кода. Это особенно полезно на очень маленьких процессорах, где глубина вызова ОЗУ / стека / функции ограничена.
uɐɪ

@ ʎəʞouɐɪ Да, я пропустил обсуждение макросов на C. Они устарели в C ++, но обсуждение этого вопроса может быть полезным. Я могу обратиться к нему, если смогу найти что-нибудь полезное, чтобы написать об этом
Джон

1
@jonk - Я полностью не согласен с твоим первым предложением. Пример, inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }который называют во многих местах, является идеальным кандидатом.
Джейсон С

8

1) Код для удобочитаемости и ремонтопригодности в первую очередь. Наиболее важным аспектом любой кодовой базы является то, что она хорошо структурирована. Красиво написанное программное обеспечение, как правило, имеет меньше ошибок. Вам может потребоваться внести изменения через пару недель / месяцев / лет, и это очень поможет, если ваш код будет приятно читать. Или, может быть, кто-то другой должен внести изменения.

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

3) Даже код в тесных циклах должен быть корректным в первую очередь. Если у вас возникли проблемы с производительностью, оптимизируйте, как только код верен.

4) Если вам нужно оптимизировать, вы должны измерить! Неважно, думаете ли вы, или кто-то говорит вам, что static inlineэто просто рекомендация для компилятора. Вы должны взглянуть на то, что делает компилятор. Вы также должны измерить, действительно ли встраивание улучшило производительность. Во встроенных системах вы также должны измерять размер кода, так как память кода обычно довольно ограничена. Это самое важное правило, которое отличает инженерию от догадок. Если вы не измеряли это, это не помогло. Инженерия измеряет. Наука это записывает;)


2
Единственная критика моего превосходного поста - это пункт 2). Это правда, что производительность кода инициализации не имеет значения - но во встроенной среде размер может иметь значение. (Но это не отменяет пункт 1; начинайте оптимизацию по размеру, когда вам нужно - и не раньше)
Мартин Боннер поддерживает Монику

2
Производительность кода инициализации может поначалу не иметь значения. Когда вы добавляете режим низкого энергопотребления и хотите быстро восстановиться, чтобы обработать событие пробуждения, тогда оно становится актуальным.
Беренди - протестуя

5

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

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

Также вы захотите иметь, например, код инициализации ADC в той же библиотеке, что и другие функции ADC, которых нет в главном файле c.

Многие компиляторы позволяют вам задавать разные уровни оптимизации для скорости или размера кода, поэтому, если у вас есть небольшая функция, вызываемая во многих местах, тогда функция будет «встроенной», скопированной туда вместо вызова.

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

Код как это:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

будет компилировать в:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

без использования какого-либо вызова.

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

Обновите, чтобы указать, что приведенные выше утверждения недействительны для специально урезанных компиляторов бесплатной версии, таких как бесплатная версия Microchip XCxx. Подобные вызовы функций - это золотая жила для Microchip, чтобы показать, насколько лучше платная версия, и если вы скомпилируете ее, вы найдете в ASM ровно столько же вызовов, сколько и в C-коде.

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

Это раздел электроники, а не общий раздел C C ++ или раздел программирования, речь идет о программировании на микроконтроллерах, где любой приличный компилятор будет выполнять вышеуказанную оптимизацию по умолчанию.

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


15
Становится ли код встроенным или нет - это специфическая проблема реализации поставщика компилятора; даже использование встроенного ключевого слова не гарантирует встроенный код. Это подсказка компилятору. Конечно, хорошие компиляторы будут встроенные функции, используемые только один раз, если они знают о них. Однако обычно это не происходит, если в области видимости есть «изменчивые» объекты.
Питер Смит

9
Этот ответ просто не соответствует действительности. Как говорит @PeterSmith, и в соответствии со спецификацией языка C, компилятор имеет возможность встроить код, но не может, и во многих случаях этого не сделает. В мире так много разных компиляторов для стольких целевых процессоров, что в этом ответе делается своего рода бланкетное утверждение, и предполагается, что все компиляторы поместят код в строку, если у них есть только опция, - ненадежно.
uɐɪ

2
@ ʎəʞouɐɪ Вы указываете на редкие случаи, когда это невозможно, и было бы плохой идеей сначала не вызывать функцию. Я никогда не видел компилятор, настолько глупый, чтобы действительно использовать вызов в простом примере, приведенном OP.
Дориан

6
В тех случаях, когда эти функции вызываются только один раз, оптимизация вызова функции практически не является проблемой. Нужно ли системе возвращать каждый тактовый цикл во время установки? Как и в случае с оптимизацией в любом месте - пишите читаемый код и оптимизируйте его, только если профилирование показывает, что это необходимо .
Балдрикк

5
@MSalters Меня не интересует, что здесь делает компилятор - больше о том, как программист подходит к этому. В результате разбивки инициализации, как видно из вопроса, или нет, или незначительное снижение производительности.
Балдрикк

2

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

Отдельный файл:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Выбор цели и компилятора для демонстрационных целей.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

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

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

удаляет их сейчас, когда они встроены.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Но реальность такова, когда вы берете на себя чип производителя или библиотеки BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

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

Почему это все равно делается? Некоторые из них - это набор правил, которые преподаватели будут или все еще учат, чтобы упростить классификацию кода. Функции должны помещаться на странице (назад, когда вы печатали свою работу на бумаге), не делайте этого, не делайте этого и т. Д. Многое из этого - создание библиотек с общими именами для разных целей. Если у вас есть десятки семейств микроконтроллеров, некоторые из которых имеют общие периферийные устройства, а некоторые нет, возможно, три или четыре разных варианта UART смешаны между семействами, разные GPIO, контроллеры SPI и т. Д. Вы можете иметь общую функцию gpio_init (), get_timer_count () и т. д. И повторно используйте эти абстракции для различных периферийных устройств.

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

Это в значительной степени основанный на мнении вопрос, и вышеизложенное показывает три основных пути, которыми это может пойти. Что касается того, какой путь ЛУЧШИЙ, то это строгое мнение. Делает ли всю работу в одной функции? Вопрос, основанный на мнении, некоторые люди склонны к производительности, некоторые определяют модульность и свою версию читабельности как ЛУЧШИЙ. Интересная проблема, которую многие называют «читабельностью», чрезвычайно болезненна; Чтобы «увидеть» код, вам нужно одновременно открыть 50–10 000 файлов и как-то попытаться линейно увидеть функции в порядке выполнения, чтобы увидеть, что происходит. Я считаю, что это противоположно удобочитаемости, но другие находят его читабельным, поскольку каждый элемент помещается в окне экрана / редактора и может использоваться целиком после того, как они запомнят вызываемые функции и / или имеют редактор, который может входить и выходить из каждая функция в проекте.

Это еще один важный фактор, когда вы видите различные решения. Текстовые редакторы, IDE и т. Д. Являются очень личными, и это выходит за рамки vi против Emacs. Эффективность программирования, количество строк в день / месяц увеличивается, если вам удобно и эффективно использовать инструмент, который вы используете. Возможности инструмента могут / будут намеренно или не склоняться к тому, как фанаты этого инструмента пишут код. И в результате, если один человек пишет эти библиотеки, проект в какой-то степени отражает эти привычки. Даже если это команда, привычки / предпочтения ведущего разработчика или босса могут быть навязаны остальной части команды.

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

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

Вы не собираетесь ударить домой бежать в первый раз в летучей мыши. Могут / должны пройти десятилетия, чтобы по-настоящему определить ваш стиль. В то же время со временем ваш стиль может измениться: наклониться в одну сторону, а затем - в другую.

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

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


4
В самом деле? Какую «цель» и «какой-то компилятор» вы используете, можно спросить?
Дориан

Для меня это больше похоже на 32/64 бит ARM8, может быть, от хулиганского PI, чем обычный микроконтроллер. Вы читали первое предложение в вопросе?
Дориан

Ну, компилятор не удаляет неиспользуемые глобальные функции, но компоновщик это делает. Если он настроен и используется правильно, они не будут отображаться в исполняемом файле.
Беренди - протестуя

Если кому-то интересно, какой компилятор может оптимизировать работу с разрывами файлов: компиляторы IAR поддерживают многофайловую компиляцию (именно так они это называют), что позволяет оптимизировать кросс-файлы. Если вы бросите все файлы c / cpp в него за один раз, вы получите исполняемый файл, который содержит одну функцию: main. Преимущества производительности могут быть весьма значительными.
Арсенал

3
@Arsenal Конечно, gcc поддерживает встраивание, даже если оно вызывается правильно в разных единицах компиляции. См. Gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html. и найдите параметр -flto.
Питер - Восстановить Монику

1

Очень общее правило - компилятор может оптимизировать лучше, чем вы. Конечно, есть исключения, если вы делаете очень интенсивные циклы, но в целом, если вы хотите хорошую оптимизацию для скорости или размера кода, выбирайте свой компилятор с умом.


К сожалению, это правда для большинства программистов сегодня.
Дориан

0

Это наверняка зависит от вашего собственного стиля кодирования. Одно общее правило, которое существует, состоит в том, что имена переменных, а также имена функций должны быть как можно более понятными и понятными. Чем больше вложенных вызовов или строк кода вы помещаете в функцию, тем сложнее становится определить четкую задачу для этой одной функции. В вашем примере у вас есть функция, initModule()которая инициализирует вещи и вызывает подпрограммы, которые затем устанавливают часы или устанавливают конфигурацию . Вы можете сказать это, просто прочитав название функции. Если вы поместите весь код из подпрограмм initModule()непосредственно в вашу программу, станет менее очевидно, что на самом деле делает функция. Но, как часто, это просто руководство.


Спасибо за ваш ответ. Я мог бы изменить стиль, если это необходимо для производительности, но вопрос здесь в том, влияет ли читабельность кода на производительность?
MaNyYaCk

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

@Humpawumpa - Если вы пишете для микроконтроллера только 256 или 64 байта оперативной памяти , то десяток слоев вызовов функций не является незначительным жертва, это просто не возможно
uɐɪ

Да, но это две крайности ... обычно у вас больше 256 байтов и вы используете меньше дюжины слоев - надеюсь.
po.pe

0

Если функция действительно делает только одну очень маленькую вещь, подумайте о ее создании static inline.

Добавьте его в файл заголовка вместо файла C и используйте слова static inline для его определения:

static inline void setCLK()
{
    //code to set the clock
}

Теперь, если функция даже немного длиннее, например, содержит более 3 строк, было бы неплохо избежать ее static inlineи добавить в файл .c. В конце концов, встроенные системы имеют ограниченную память, и вы не хотите слишком сильно увеличивать размер кода.

Кроме того, если вы определите функцию file1.cи используете ее file2.c, компилятор не будет автоматически вставлять ее. Однако, если вы определите это file1.hкакstatic inline функцию, скорее всего, ваш компилятор вставит его в строку.

Эти static inlineфункции чрезвычайно полезны в высокопроизводительном программировании. Я обнаружил, что они часто повышают производительность кода более чем в три раза.


«например, более 3 строк» ​​- количество строк не имеет к этому никакого отношения; вложение стоимости имеет все отношение к этому. Я мог бы написать 20-строчную функцию, которая идеально подходит для встраивания, и 3-строчную функцию, которая ужасна для встраивания (например, functionA (), которая вызывает functionB () 3 раза, functionB (), которая вызывает functionC () 3 раза, и пара других уровней).
Джейсон С

Кроме того, если вы определите функцию file1.cи используете ее file2.c, компилятор не будет автоматически вставлять ее. Ложные . Смотрите, например, -fltoв gcc или clang.
Беренди - протестуя

0

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

Например, если есть одноядерная система с подпрограммой обработки прерываний [запускается по таймеру или как-то еще]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

должна быть возможность написания функций для запуска фоновой операции записи или ожидания ее завершения:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

а затем вызвать такой код, используя:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

К сожалению, с включенной полной оптимизацией «умный» компилятор, такой как gcc или clang, решит, что первый набор записей никак не повлияет на наблюдаемую программу, и, таким образом, их можно будет оптимизировать. Качественные компиляторы, такие как icc, менее склонны делать это, если установка прерывания и ожидание завершения включает в себя как энергозависимые записи, так и энергозависимые чтения (как в данном случае), но платформа, на которую ориентируется, iccне так популярна для встроенных систем.

Стандарт преднамеренно игнорирует проблемы качества реализации, полагая, что существует несколько разумных способов, с помощью которых вышеуказанная конструкция может быть обработана:

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

  2. Качественная реализация может обрабатывать все обращения к volatileобъектам так, как будто они могут инициировать действия, которые будут обращаться к любому объекту, который виден внешнему миру.

  3. Простая, но достойная реализация, предназначенная для использования во встроенных системах, может обрабатывать все вызовы функций, не помеченных как «встроенные», как если бы они могли обращаться к любому объекту, который был выставлен внешнему миру, даже если он не обрабатывает, volatileкак описано в # 2.

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

В некоторых случаях, убедившись, что функции ввода-вывода находятся в отдельном модуле компиляции, и у компилятора не останется иного выбора, кроме как предположить, что они могут получить доступ к любому произвольному подмножеству объектов, которые были выставлены внешнему миру, может быть разумным минимумом - of of Evils способ написания кода, который будет надежно работать с gcc и clang. В таких случаях, однако, цель состоит не в том, чтобы избежать дополнительных затрат на ненужный вызов функции, а скорее в том, чтобы принять ненужные затраты в обмен на получение требуемой семантики.


«убедиться, что функции ввода-вывода находятся в отдельном модуле компиляции» ... не является верным способом предотвращения подобных проблем оптимизации. По крайней мере, LLVM и я полагаю, что во многих случаях GCC будет выполнять оптимизацию всей программы, поэтому может решить встроить ваши функции ввода-вывода, даже если они находятся в отдельном модуле компиляции.
Жюль

@Jules: не все реализации подходят для написания встроенного программного обеспечения. Отключение оптимизации всей программы может быть наименее дорогим способом заставить gcc или clang вести себя как качественная реализация, подходящая для этой цели.
суперкат

@Jules: более качественная реализация, предназначенная для встроенного или системного программирования, должна быть настраиваемой, чтобы иметь семантику, подходящую для этой цели, без необходимости полностью отключать оптимизацию всей программы (например, имея возможность обрабатывать volatileдоступы так, как будто они потенциально могут инициировать произвольный доступ к другим объектам), но по какой-то причине gcc и clang скорее будут рассматривать проблемы качества реализации как приглашение вести себя бесполезно.
суперкат

1
Даже реализации «высшего качества» не исправят ошибочный код. Если buffон не объявлен volatile, он не будет рассматриваться как переменная переменная, доступ к нему может быть переупорядочен или полностью оптимизирован, если, очевидно, не будет использован позже. Правило простое: пометьте все переменные, к которым можно обращаться за пределами обычного потока программы (как это видно из компилятора), как volatile. buffДоступ к содержимому осуществляется в обработчике прерываний? Да. Тогда так и должно быть volatile.
Беренди - протестуя

@berendi: компиляторы могут предложить гарантии, выходящие за рамки требований Стандарта, и качественные компиляторы сделают это. Качественная автономная реализация для использования встраиваемых систем позволит программистам синтезировать конструкции мьютекса, что, по сути, и делает код. Когда magic_write_countноль, хранилище принадлежит основной линии. Когда он ненулевой, он принадлежит обработчику прерываний. Для создания buffvolatile потребуется, чтобы каждая функция, где бы она ни volatileработала, использовала квалифицированные указатели, что значительно ухудшило бы оптимизацию, чем наличие компилятора ...
суперкат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.