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

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

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.
}

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

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

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

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

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


ИЗМЕНИТЬ 2:

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

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

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

12 голосов | спросил MaNyYaCk 16 J000000Monday18 2018, 08:53:52

9 ответов


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 () и т. д. И повторно используйте эти абстракции для разных периферийных устройств.

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

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

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

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

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

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

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

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

ответил old_timer 17 J000000Tuesday18 2018, 06:06:43
27

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

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

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

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

ответил Lanting 16 J000000Monday18 2018, 12:10:03
11

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

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

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


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

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

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

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

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

И так далее. (Проводной код для жизненно важного медицинского инструментария также имеет целый мир.)

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


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

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


ИЗМЕНИТЬ на JasonS:

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

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

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

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

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

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


C ++

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

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

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

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

Компилятор C ++ должен генерировать правильный код для единицы компиляции \ $ A \ $, когда он абсолютно не знает, какая обработка исключений может потребоваться в отдельной единице компиляции \ $ B \ $, скомпилированной отдельно и в другое время.

Возьмите эту последовательность кода, найденную как часть некоторой функции в некоторых единица компиляции \ $ A \ $:

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

Для целей обсуждения блок компиляции \ $ A \ $ не использует в своем источнике 'try..catch' где-либо . Он также не использует «бросок». Фактически, предположим, что он не использует какой-либо источник, который не может быть скомпилирован компилятором C, за исключением того факта, что он использует поддержку библиотеки C ++ и может обрабатывать такие объекты, как String. Этот код может быть даже исходным кодовым файлом C, который был слегка изменен, чтобы воспользоваться некоторыми функциями C ++, такими как класс String.

Кроме того, предположим, что foo () является внешней процедурой, расположенной в модуле компиляции \ $ B \ $, и что компилятор имеет для нее объявление, но не знает его определение.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ответил jonk 16 J000000Monday18 2018, 09:45:32
8

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

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

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

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

ответил Crazor 16 J000000Monday18 2018, 16:39:03
4

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

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

Также вы захотите иметь, например, код инициализации 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 ++ или раздел программирования, вопрос о программировании микроконтроллеров, где любой достойный компилятор по умолчанию выполняет указанную оптимизацию.

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

ответил Dorian 16 J000000Monday18 2018, 09:44:28
1

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

ответил Dirk Bruere 17 J000000Tuesday18 2018, 11:14:52
0

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

ответил Humpawumpa 16 J000000Monday18 2018, 09:10:11
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 чрезвычайно полезны в высокопроизводительном программировании. Я обнаружил, что они часто увеличивают производительность кода в три раза.

ответил juhist 16 J000000Monday18 2018, 15:25:48
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. Реализации качества , предназначенные исключительно для таких полей, как high-end number crunching , могут разумно ожидать, что код, написанный для таких полей, не будет содержать конструкции, подобные приведенным выше.

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

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

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

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

ответил supercat 16 J000000Monday18 2018, 19:26:15

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132