Який найкращий підхід при написанні функцій для вбудованого програмного забезпечення, щоб досягти кращої продуктивності? [зачинено]


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

Тоді надходять інші функції, які використовують цей 1 рядовий код, що містить функцію для інших цілей. Наприклад:

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

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

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

Тож якщо я записую безпосередньо функціональний модуль InitModule, який встановлює годинник, додає потрібну конфігурацію та робить щось інше без виклику функцій. Це поганий підхід при написанні вбудованого програмного забезпечення?


EDIT 2:

  1. Схоже, багато людей зрозуміли це питання так, ніби я намагаюся оптимізувати програму. Ні, я не маю цього робити . Я дозволяю компілятору це робити, тому що це буде завжди (я не сподіваюсь!) Краще за мене.

  2. Я звинувачую в тому, що я обрав приклад, який представляє якийсь код ініціалізації . Питання не має наміру щодо функціональних викликів, зроблених для ініціалізації. Моє запитання полягає в тому, чи має розрив певної задачі на невеликі функції багаторядкових ( так що в рядку не виникає сумнівів ), що працює всередині нескінченного циклу, має якусь перевагу перед написанням тривалої функції без будь-якої вкладеної функції?

Будь ласка, врахуйте читаність, визначену у відповіді @Jonk .


28
Ви дуже наївні (не маючи на увазі образи), якщо вірите, що будь-який розумний компілятор сліпо перетворить код, як записаний у бінарні файли, як написано. Більшість сучасних компіляторів досить добре визначають, коли рутина краще вбудована, і навіть коли розташування регістру проти оперативної пам’яті слід використовувати для утримання змінної. Дотримуйтесь двох правил оптимізації: 1) не оптимізуйте. 2) зробити оптимізують НЕ ще . Зробіть свій код читабельним та легкозабезпеченим, а потім лише після профілювання робочої системи прагніть оптимізувати.
akohlsmith

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

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

1
@Mawg: Оперативне слово є передчасним . (Як пояснюється в наступному абзаці цього документу. Буквально наступне речення: "Все ж ми не повинні передавати наші можливості на той критичний 3%.") Не оптимізуйте, поки вам не потрібно - ви не знайдете повільного біт, поки у вас є що профайлювати, але також не займайтеся песимізацією, наприклад, використовуючи очевидно неправильні інструменти для роботи.
cHao

1
@Mawg Я не знаю, чому я отримав відповіді / відгуки, пов'язані з оптимізацією, оскільки я ніколи не згадував це слово і маю намір це зробити. Питання набагато більше про те, як записати функції у вбудоване програмування для досягнення кращої продуктивності.
MaNyYaCk

Відповіді:


28

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

Я використовую правило: Напишіть свій код якомога легше для читання і починайте оптимізувати лише тоді, коли помітите, що ваш компілятор не працює належним чином.

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

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


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

11

Немає жодної переваги, яку я можу придумати (але див. Примітку до JasonS внизу), загортаючи один рядок коду як функцію чи підпрограму. За винятком того, що ви можете назвати функцію чимось "читабельним". Але ви можете так само добре прокоментувати рядок. А оскільки загортання рядка коду у функцію коштує пам'яті коду, місця у стеку та часу виконання, мені здається, що це здебільшого контрпродуктивне. У навчальній ситуації? Це може мати певний сенс. Але це залежить від класу учнів, їх попередньої підготовки, навчальної програми та викладача. Переважно, я думаю, що це не дуже гарна ідея. Але це моя думка.

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

Це може бути відсторонено від настільки ж обґрунтованого на думку думки, якби ви були більш детальними щодо ситуації та ретельно описали цілі, які ви поставили як основні. Чим краще ви визначите свої інструменти вимірювання, тим об'єктивнішими можуть бути відповіді.


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

  1. Будьте послідовні щодо свого підходу, щоб чергове читання вашого коду могло розвинути розуміння того, як ви підходите до процесу кодування. Бути непослідовним - це, мабуть, найгірший можливий злочин. Це не тільки ускладнює оточуючим, але й ускладнює повернення коду через рік.
  2. Наскільки це можливо, спробуйте впорядкувати речі так, щоб ініціалізація різних функціональних розділів могла здійснюватися без огляду на замовлення. У разі необхідності замовлення, якщо це пов'язано з тісним з'єднанням двох сильно пов’язаних підфункцій, то розгляньте одну ініціалізацію для обох, щоб її можна було переупорядкувати, не завдаючи шкоди. Якщо це неможливо, задокументуйте вимогу впорядкування ініціалізації.
  3. Якщо можливо, інкапсулюйте знання саме в одному місці. Константи не слід дублювати всюди в коді. Рівняння, які вирішуються для певної змінної, повинні існувати в одному і лише одному місці. І так далі. Якщо ви виявите, що ви копіюєте та вставляєте деякий набір ліній, які виконують певну необхідну поведінку в різних місцях, розгляньте спосіб захоплення цих знань в одному місці та використання його там, де потрібно. Наприклад, якщо у вас є структура дерева, до якої потрібно ходити певним чином, не вартокопіювати кодовий код у кожному місці, де потрібно прокрутити дерева. Замість цього захопіть метод вигулу дерева по одному місці і використовуйте його. Таким чином, якщо дерево змінюється і змінюється метод ходьби, у вас є лише одне місце, де потрібно хвилюватися, а весь інший код "просто працює правильно".
  4. Якщо ви розкладете всі свої програми на величезний плоский аркуш паперу зі стрілками, що з'єднують їх так, як їх називають інші підпрограми, ви побачите, що в будь-якій програмі будуть "кластери" підпрограм, у яких багато і багато стрілок між собою, але лише кілька стрілок поза групою. Таким чином, існуватимуть природні межі тісно зв'язаних процедур і слабко пов'язаних зв'язків між іншими групами тісно пов'язаних процедур. Скористайтеся цим фактом, щоб упорядкувати свій код у модулі. Це істотно обмежить уявну складність вашого коду.

Вище сказане справедливо в цілому щодо кодування. Я не обговорював використання параметрів, локальних чи статичних глобальних змінних тощо. Причина полягає в тому, що для вбудованого програмування прикладний простір часто ставить крайні та дуже значні нові обмеження, і неможливо обговорити їх усіх, не обговорюючи кожну вбудовану програму. І це все одно не відбувається.

Ці обмеження можуть бути будь-якими (і більше) з них:

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

І так далі. (Код підключення для життєво важливих медичних приладів також має цілий світ.)

Підсумок тут полягає в тому, що вбудоване кодування часто не є безкоштовним для всіх, де ви можете кодувати як на робочій станції. Часто існують важкі, конкурентні причини для широкого спектру дуже складних обмежень. І це може рішуче заперечувати проти більш традиційних та акціонерних відповідей.


Щодо читабельності, я вважаю, що код є читабельним, якщо він написаний послідовно, що я можу навчитися, читаючи його. І там, де немає навмисної спроби придушити код. Дійсно, не потрібно набагато більше.

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


EDIT за JasonS:

Я використовую C з 1978 року, а C ++ - приблизно з 1987 року, і я мав великий досвід використання як для основних комп'ютерів, мінікомп'ютерів, так і для (в основному) вбудованих додатків.

Джейсон демонструє коментар щодо використання "inline" в якості модифікатора. (На мій погляд, це відносно "нова" здатність, оскільки вона просто не існувала, можливо, половину мого життя або більше, використовуючи C і C ++.) Використання вбудованих функцій може насправді робити такі дзвінки (навіть для однієї лінії код) цілком практичний. І куди краще, де можливо, ніж використовувати макрос через типізацію, яку може застосувати компілятор.

Але є і обмеження. Перший полягає в тому, що ви не можете покластися на компілятора, щоб "взяти підказку". Це може бути, а може і ні. І є вагомі причини не брати підказки. (Для очевидного прикладу, якщо адреса функції взята, для цього потрібна інстанція функції, а використання адреси для здійснення дзвінка вимагатиме виклику. Код не може бути накреслений тоді.) інші причини. Компілятори можуть мати найрізноманітніші критерії, за якими вони судять, як поводитися з підказкою. А як програміст, це означає, що ви повинніприділіть деякий час вивченню цього аспекту компілятора, інакше ви, ймовірно, будете приймати рішення, грунтуючись на хибних ідеях. Таким чином, це додає тягар як письменнику коду, так і будь-якому читачеві, а також тому, хто планує перенести код до іншого компілятора.

Також компілятори C і C ++ підтримують окрему компіляцію. Це означає, що вони можуть скласти один фрагмент коду С або С ++ без складання будь-якого іншого пов'язаного коду для проекту. Для вбудованого коду, якщо припустити, що компілятор може інакше зробити це, він не тільки повинен мати декларацію "в області", але і він повинен мати визначення. Зазвичай програмісти працюватимуть над тим, щоб це було в тому випадку, якщо вони використовують "inline". Але легко помилитися в помилках.

Взагалі, хоча я також використовую рядки там, де я вважаю це доречним, я схильний вважати, що не можу на нього покластися. Якщо продуктивність є суттєвою вимогою, і я думаю, що в ОП вже чітко написано, що було досягнуто значного посягання на ефективність, коли вони пішли на більш "функціональний" маршрут, то я, безумовно, вирішив би не покладатися на інлайнер як практику кодування і натомість слід дотримуватися дещо іншого, але цілком послідовного зразка написання коду.

Заключна примітка про те, що "вбудований" та визначення "в області" для окремого етапу компіляції. Можна (не завжди надійно) виконувати роботи на етапі зв’язки. Це може статися, якщо і лише тоді, коли компілятор C / C ++ поховає достатню кількість деталей у файлах об'єктів, щоб дозволити лінкеру діяти на запити "вбудованих". Я особисто не відчував системи зв’язків (за межами Microsoft), яка підтримує цю можливість. Але це може статися. Знову ж, на те, чи слід покладатися, залежатиме від обставин. Але, як правило, я вважаю, що це не переносилося на лінкер, якщо я не знаю інакше на основі хороших доказів. І якщо я на це покладаюся, це буде зафіксовано на чільному місці.


C ++

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

  • часткова спеціалізація шаблонів
  • vtables
  • віртуальний базовий об'єкт
  • кадр активації
  • кадр активації розмотати
  • використання розумних покажчиків у конструкторах та чому
  • оптимізація повернення

Це лише короткий список. Якщо ви ще не знаєте всього про ці терміни і чому я їх перерахував (і багато іншого я тут не перераховував), то я б радив не використовувати C ++ для вбудованих робіт, якщо це не варіант для проекту .

Давайте коротко розглянемо семантику винятків C ++, щоб отримати лише аромат.

AB

A

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

A

B

Компілятор C ++ бачить перший дзвінок до foo () і може просто дозволити, щоб відбувся звичайний кадр активації, якщо foo () кидає виняток. Іншими словами, компілятор C ++ знає, що в цей момент не потрібен додатковий код для підтримки процесу розмотування кадрів, що бере участь в обробці винятків.

Але після створення String s, компілятор C ++ знає, що він повинен бути належним чином знищений до того, як можна буде дозволити розмотування кадру, якщо пізніше станеться виняток. Отже, другий виклик foo () семантично відрізняється від першого. Якщо другий виклик foo () кидає виняток (який він може робити, а може і не робити), компілятор повинен розмістити код, призначений для обробки знищення String s, перш ніж дозволити звичайному кадру розкручуватися. Це відрізняється від коду, необхідного для першого дзвінка до foo ().

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

На відміну від C malloc, новий C ++ використовує винятки для сигналізації, коли він не може виконувати необмежену пам'ять. Так буде "динамічний_каст". (Див. 3-е видання Stroustrup, Мова програмування на C ++, стор. 384 та 385 для стандартних винятків у програмі C ++.) Компілятори можуть дозволити відключення такої поведінки. Але в цілому ви будете мати певні накладні витрати через правильно сформовані винятки, що обробляють прологи та епілоги в згенерованому коді, навіть коли винятки насправді не мають місце і навіть тоді, коли складена функція насправді не має блоків обробки виключень. (Stroustrup про це публічно поскаржився.)

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

Коли функція C ++ повертає об'єкт, неназваний компілятор створюється тимчасово і знищується. Деякі компілятори C ++ можуть надати ефективний код, якщо конструктор об'єктів використовується в операторі return, а не локальному об'єкті, зменшуючи потреби побудови та знищення одним об'єктом. Але не кожен компілятор робить це, і багато програмістів на C ++ навіть не знають про цю "оптимізацію повернутого значення".

Надання конструктору об’єктів одного типу параметрів може дозволити компілятору C ++ знаходити шлях перетворення між двома типами абсолютно несподіваними способами для програміста. Така "розумна" поведінка не є частиною C.

Застереження про вловлювання, що вказує базовий тип, "скине" викинутий похідний об'єкт, тому що викинутий об'єкт копіюється за допомогою "статичного типу" пункту лову, а не "динамічного типу об'єкта". Не рідкісне джерело нещасних випадків (коли ви відчуваєте, що можете навіть дозволити собі винятки зі свого вбудованого коду.)

Компілятори C ++ можуть автоматично генерувати для вас конструктори, деструктори, конструктори копій та оператори присвоєння із непередбачуваними результатами. Потрібен час, щоб отримати об'єкт з деталями цього.

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

Оскільки C ++ не викликає деструктор частково побудованих об'єктів, коли в конструкторі об'єктів відбувається виняток, обробка винятків у конструкторах, як правило, вимагає "розумних покажчиків", щоб гарантувати, що сконструйовані фрагменти в конструкторі належним чином знищені, якщо виняток виникає там . (Див. Stroustrup, стор. 367 і 368.) Це поширена проблема написання хороших класів на C ++, але, звичайно, цього уникати, оскільки C не має вбудованої семантики побудови та руйнування. Написання правильного коду для обробки конструкції підпредметів всередині об'єкта означає написання коду, який повинен впоратися з цією унікальною семантичною проблемою в C ++; Іншими словами "написання навколо" C ++ семантичної поведінки.

C ++ може копіювати об'єкти, передані параметрам об'єкта. Наприклад, у наступних фрагментах виклик "rA (x);" може змусити компілятор C ++ викликати конструктор для параметра p, щоб потім викликати конструктор копії для передачі об'єкта x до параметра p, а потім інший конструктор для об'єкта повернення (неназваного тимчасового) функції rA, що, звичайно, є скопійовано з параметра p. Гірше, якщо клас A має власні об'єкти, які потребують будівництва, це може катастрофічно телескопом. (Програміст змінного струму дозволить уникнути більшості цього сміття, оптимізуючи руку, оскільки програмісти C не мають такого зручного синтаксису і повинні викладати всі деталі по черзі.)

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

Нарешті, коротка примітка для програмістів C. longjmp () не має портативної поведінки в C ++. (Деякі програмісти на C використовують це як своєрідний механізм "винятку".) Деякі компілятори C ++ насправді намагаються налаштувати речі на очищення під час зйомки longjmp, але така поведінка не переноситься на C ++. Якщо компілятор очищає побудовані об'єкти, він не є портативним. Якщо компілятор не очищає їх, то об'єкти не руйнуються, якщо код покидає область побудованих об'єктів внаслідок longjmp і поведінка недійсна. (Якщо використання longjmp в foo () не залишає сферу застосування, то поведінка може бути нормальною.) Це не занадто часто використовується вбудованими програмістами C, але вони повинні знати про ці проблеми, перш ніж використовувати їх.


4
Цей вид функцій, що використовуються лише один раз, ніколи не компілюється як виклик функції, код просто розміщується там без будь-якого виклику.
Доріан

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

1
@jonk - Ще одна хитрість, яку ви не згадали в хорошій відповіді, - це написання простих макро-функцій, які виконують ініціалізацію або конфігурацію як розширений вбудований код. Це особливо корисно для дуже малих процесорів, де глибина оперативної пам’яті / стека / функції обмежена.
uɐɪ

@ ʎəʞouɐɪ Так, я пропустив обговорення макросів у C. Ці застарілі в C ++, але обговорення цього питання може бути корисним. Я можу вирішити це, якщо зможу щось корисне написати про це.
jonk

1
@jonk - я повністю не згоден з вашим першим реченням. Приклад, inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }який називають у багатьох місцях - ідеальний кандидат.
Jason S

8

1) Спочатку код для читабельності та ремонтопридатності. Найважливішим аспектом будь-якої кодової бази є те, що вона добре структурована. Гарно написане програмне забезпечення, як правило, має менше помилок. Можливо, вам знадобиться внести зміни через пару тижнів / місяців / років, і це дуже допоможе, якщо ваш код приємно читати. А може хтось інший повинен змінити.

2) Продуктивність коду, який запускається один раз, не має великого значення. Турбота про стиль, а не про продуктивність

3) Навіть код у вузьких шлейфах повинен бути правильним насамперед. Якщо у вас виникли проблеми з продуктивністю, то оптимізуйте, як тільки код буде правильним.

4) Якщо вам потрібно оптимізувати, вам доведеться виміряти! Не має значення, чи думаєте ви, чи хтось скаже вам, що static inlineце лише рекомендація компілятору. Ви повинні подивитися, що робить компілятор. Ви також повинні виміряти, чи вкладка покращила продуктивність. У вбудованих системах вам також доведеться вимірювати розмір коду, оскільки пам'ять коду зазвичай досить обмежена. Це найважливіше правило, яке відрізняє техніку від здогадок. Якщо ви її не вимірювали, це не допомогло. Техніка - вимірювальна. Наука записує це;)


2
Єдина критика, яку я маю до вашого інакше чудового поста, - це пункт 2). Це правда, що виконання коду ініціалізації не має значення - але у вбудованому середовищі розмір може мати значення. (Але це не перекриває пункт 1; почніть оптимізувати розмір, коли вам потрібно - а не раніше)
Мартін Боннер підтримує Моніку

2
Виконання коду ініціалізації спочатку може бути неактуальним. Якщо ви додасте режим з низькою потужністю та хочете швидко відновитись, щоб обробити подію пробудження, це стає актуальним.
berendi - протестуючи

5

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

Після складання коду не буде декількох викликів, натомість читабельність значно покращиться.

Також ви хочете мати, наприклад, код введення 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
Незалежно від того, чи стає код вбудованим чи ні, це специфічна проблема реалізації постачальника компілятора; навіть використання ключового слова inline не гарантує вбудований код. Це натяк на компілятора. Звичайно хороші компілятори будуть вбудовувати функції, які використовуються лише один раз, якщо вони знають про них. Зазвичай це не буде робити, якщо є якісь "летючі" об'єкти за обсягом.
Пітер Сміт

9
Ця відповідь просто не відповідає дійсності. Як говорить @PeterSmith, і згідно зі специфікацією мови C, у компілятора є можливість вбудувати код, але може, але не може, і в багатьох випадках цього не зробить. У світі існує стільки різних компіляторів для стількох різних цільових процесорів, що введення у відповідь свого роду бланкетної заяви та припускаючи, що всі компілятори розмістять код в рядку, коли у них є лише можливість, щоб це не було прийнятним.
uɐɪ

2
@ ʎəʞouɐɪ Ви вказуєте на рідкісні випадки, коли це неможливо, і було б поганою ідеєю в першу чергу не викликати функцію. Я ніколи не бачив компілятора настільки німим, що дійсно використовував виклик у простому прикладі, поданому ОП.
Доріан

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 vs Emacs. Ефективність програмування, рядки на день / місяць зростають, якщо вам зручно та ефективно працювати з інструментом, який ви використовуєте. Особливості інструменту можуть / навмисно чи не нахилятися до того, як шанувальники цього інструменту пишуть код. І як результат, якщо одна людина пише ці бібліотеки, проект певною мірою відображає ці звички. Навіть якщо це команда, звички / вподобання провідного розробника чи начальника можуть бути вимушені на іншій частині команди.

Стандарти кодування, в яких поховано багато особистих уподобань, знову ж таки дуже релігійні vi проти Emacs, вкладки проти пробілів, як складені дужки тощо. І це грає в те, як бібліотеки певною мірою створені.

Як ви повинні писати своє? Хоча ви хочете, насправді неправильна відповідь, якщо вона функціонує. Код є поганим або ризикованим, але якщо він написаний таким чином, що ви можете підтримувати його за потребою, він відповідає вашим цілям дизайну, відмовтеся від читабельності та певної ремонтопридатності, якщо важлива продуктивність, або навпаки. Вам подобаються короткі імена змінних, щоб один рядок коду відповідав ширині вікна редактора? Або довгі занадто описові назви, щоб уникнути плутанини, але читабельність знижується, оскільки ви не можете отримати один рядок на сторінці; тепер вона візуально розбита, возиться з потоком.

Ви не збираєтеся вперше влучити в домашній біг. Це може / повинно зайняти десятиліття, щоб по-справжньому визначити свій стиль. У той же час з часом ваш стиль може змінюватися, нахиляючись в одну сторону на деякий час, потім спираючись на інший.

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

Також вам потрібно певною мірою розробити код для користувача. Якщо це ваш проект, ви є єдиним розробником; це все, що ти хочеш. Якщо ви намагаєтеся зробити бібліотеку віддати чи продати, ви, мабуть, хочете, щоб ваш код був схожий на всі інші бібліотеки, сотні до тисяч файлів із крихітними функціями, довгими іменами функцій та довгими назвами змінних. Незважаючи на проблеми з читанністю та ефективністю роботи, IMO ви знайдете більше людей, які зможуть використовувати цей код.


4
Дійсно? Про те, яку "якусь ціль" та "якийсь компілятор" ви використовуєте, я можу запитати?
Доріан

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

Ну, компілятор не видаляє невикористані глобальні функції, але лінкер робить. Якщо це правильно налаштовано та використовується, вони не відображатимуться у виконаному файлі.
berendi - протестуючи

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

3
@Arsenal Звичайно, gcc підтримує вбудовування, навіть через компіляційні одиниці, якщо їх правильно називати. Перегляньте сторінку gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html і знайдіть параметр -flto.
Пітер - Відновіть Моніку

1

Дуже загальне правило - компілятор може оптимізувати краще, ніж ви. Звичайно, є винятки, якщо ви робите дуже інтенсивні речі, але в цілому, якщо ви хочете гарної оптимізації для швидкості чи розміру коду, вибирайте компілятор з розумом.


На жаль, це правда для більшості програмістів сьогодні.
Доріан

0

Це точно залежить від вашого власного стилю кодування. Єдине загальне правило, яке існує, що імена змінних, а також імена функцій повинні бути максимально чіткими і максимально зрозумілими. Чим більше викликів або рядків коду ви вводите у функцію, тим складніше визначити чітке завдання для цієї функції. У вашому прикладі ви маєте функцію, initModule()яка ініціалізує матеріал і викликає підпрограми, які потім встановлюють годинник або встановлюють конфігурацію . Ви можете сказати це, просто прочитавши ім'я функції. Якщо ви помістите весь код з підпрограм у свою initModule()безпосередньо, то стає менш очевидно, що функція насправді виконує. Але, як часто, це просто настанова.


Дякую за вашу відповідь Я можу змінити стиль, якщо це потрібно для продуктивності, але питання тут полягає в тому, чи читаність коду впливає на продуктивність?
MaNyYaCk

Виклик функції призведе до виклику або команди jmp, але, на мій погляд, це незначна жертва ресурсів. Якщо ви використовуєте шаблони дизайну, ви іноді закінчуєте з десяток шарів викликів функцій, перш ніж дійти до фактичного відірваного коду.
по.пе

@Humpawumpa - Якщо ви пишете для мікроконтролера з лише 256 або 64 байтами оперативної пам’яті, то з десяток шарів викликів функцій не є незначною жертвою, це просто неможливо
u

Так, але це дві крайності ... зазвичай у вас більше 256 байтів і ви використовуєте менше десятка шарів - сподіваємось.
по.пе

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 рядки" - кількість рядків не має нічого спільного; inlining вартість має все спільне з цим. Я можу написати 20-рядкову функцію, яка ідеально підходить для вбудовування, і 3-рядкову функцію, яка жахлива для вбудовування (наприклад, функцію A (), яка викликає функціюB () 3 рази, functionB (), яка викликає functionC () 3 рази, і пару інших рівнів).
Jason S

Крім того, якщо визначити функцію в file1.cі використовувати її file2.c, компілятор не буде автоматично вбудовувати її. Неправдиві . Дивіться, наприклад, -fltoу gcc або clang.
berendi - протестуючи

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, фактично вимагають, щоб будь-який код, який бажає використовувати цей шаблон, повинен бути скомпільований з відключеною багатьма оптимізаціями.

У деяких випадках переконання, що функції вводу / виводу знаходяться в окремому блоці компіляції, і компілятор не матиме іншого вибору, окрім як припустити, що вони можуть отримати доступ до будь-якого довільного підмножини об'єктів, які потрапили в навколишній світ, може бути розумним мінімальним ефектом - зловмисного способу написання коду, який надійно працюватиме з gcc та clang. Однак у таких випадках мета не уникнути зайвих витрат непотрібного функціонального виклику, а скоріше прийняти необхідні зайві витрати в обмін на отримання необхідної семантики.


"переконавшись, що функції вводу / виводу знаходяться в окремому блоці компіляції" ... не є надійним способом запобігання таких проблем оптимізації. Принаймні, LLVM, і я вважаю, що GCC буде виконувати оптимізацію програми в багатьох випадках, тому може вирішити вбудувати свої функції вводу-виводу, навіть якщо вони знаходяться в окремому підрозділі компіляції.
Жуль

@Jules: Не всі реалізації підходять для написання вбудованого програмного забезпечення. Відключення оптимізації цілої програми може бути найменш дорогим способом змусити gcc або clang вести себе як якісну реалізацію, відповідну для цієї мети.
supercat

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

1
Навіть "найякісніші" реалізації не виправлять помилковий код. Якщо buffце не задекларовано volatile, воно не буде розглядатися як мінлива зміна, доступ до нього може бути переупорядкований або повністю оптимізований, якщо, мабуть, не буде використаний пізніше. Правило просте: позначте всі змінні, до яких можна отримати доступ за межами звичайного програмного потоку (як це бачить компілятор) volatile. Чи доступ до вмісту buffдоступу в оброблювач перерв? Так. Тоді так і має бути volatile.
berendi - протестуючи

@berendi: Компілятори можуть надавати гарантії, ніж те, що вимагає Стандарт і якісні компілятори. Якісна автономна реалізація для використання вбудованих систем дозволить програмістам синтезувати мютексні конструкції, що по суті є тим, що робить код. Коли magic_write_countдорівнює нулю, сховище належить магістралі. Коли це не нуль, він належить обробнику переривань. Створення buffлетючого зажадає , що кожна функція в будь-якому місці , яке працює на ньому використовувати volatile-qualified покажчиків, які будуть погіршувати оптимізації набагато більше , ніж мати компілятор ...
Supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.