Чи передача аргументів як посилань на const передчасна оптимізація?


20

"Передчасна оптимізація - корінь усього зла"

Я думаю, що з цим ми можемо домовитись усіх. І я дуже намагаюся уникати цього.

Але останнім часом мене цікавить практика передачі параметрів через const Reference замість Value . Мене вчили / дізналися, що нетривіальні аргументи функцій (тобто більшість непримітивних типів) бажано передавати через посилання const - досить багато книг, які я прочитав, рекомендують це як "найкращу практику".

Але я не можу не здивуватися: сучасні компілятори та нові мовні функції можуть творити чудеса, тому знання, які я засвоїв, можуть бути дуже застарілими, і я ніколи не переймався профілем, якщо є якісь відмінності в продуктивності між

void fooByValue(SomeDataStruct data);   

і

void fooByReference(const SomeDataStruct& data);

Чи є практика, яку я засвоїв - передача посилань на const (за замовчуванням для нетривіальних типів) - передчасна оптимізація?


1
Дивіться також: F.call в Основних керівних принципах C ++ для обговорення різних стратегій передачі параметрів.
амон


1
@DocBrown Прийнята відповідь на це питання стосується принципу найменшого здивування , який може застосовуватися і тут (тобто використання посилань на const є галузевим стандартом тощо) тощо. З цього приводу я не погоджуюся з тим, що питання є дублікатом: питання, на яке ви посилаєтесь, запитує, чи погана практика (як правило) покладатися на оптимізацію компілятора. Це питання задає зворотне: чи передача посилань на const (передчасна) оптимізація?
CharonX

@CharonX: якщо тут можна покластися на оптимізацію компілятора, відповідь на ваше запитання чітко "так, ручна оптимізація не потрібна, вона передчасна". Якщо на нього не можна покластися (можливо, тому, що ви не знаєте заздалегідь, які компілятори коли-небудь будуть використовуватися для коду), відповідь "для великих об'єктів це, мабуть, не передчасно". Тож навіть якщо ці два питання не є буквально рівними, вони IMHO здаються досить схожими, щоб з'єднати їх разом як дублікати.
Док Браун

1
@DocBrown: Отже, перш ніж ви зможете оголосити це дурпом, вкажіть, де у запитанні сказано, що компілятору буде дозволено і зможуть "оптимізувати" це.
Дедуплікатор

Відповіді:


49

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

Використання "const &" замість передачі об'єкта за значенням - це добре зрозуміла оптимізація, з добре зрозумілим впливом на час виконання, практично без зусиль і без поганих наслідків для читабельності та ремонтопридатності. Насправді це покращує і те, і інше, тому що це говорить мені, що виклик не змінює переданий об'єкт. Тому додавання "const &" право, коли ви пишете код, НЕ ПЕРЕДМОВА.


2
Я погоджуюся з частиною вашої відповіді "практично без зусиль". Але передчасна оптимізація - це перш за все оптимізація, перш ніж їх помітний, вимірюваний вплив на продуктивність. І я не думаю, що більшість програмістів на C ++ (до яких я входить і я) здійснюють будь-які вимірювання перед використанням const&, тому я вважаю, що питання досить розумне.
Док Браун

1
Ви вимірюєте, перш ніж оптимізувати, щоб знати, чи варті будь-які компроміси. Завдяки const & total зусиллям набирається сім символів, і це має інші переваги. Якщо ви не збираєтесь змінювати змінну, що передається, це має перевагу, навіть якщо не відбувається покращення швидкості.
gnasher729

3
Я не експерт з C, тому питання:. const& fooкаже, що функція не змінює foo, тому абонент в безпеці. Але скопійоване значення говорить про те, що жоден інший потік не може змінити foo, тому виклик буде безпечним. Правильно? Отже, у багатопотоковому додатку відповідь залежить від правильності, а не оптимізації.
user949300

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

1
@ user949300: Мало функцій дозволяють змінювати аргументи одночасно або використовуються зворотні виклики, і вони прямо говорять про це.
Дедуплікатор

16

TL; DR: Проходити через посилання const все ще є хорошою ідеєю в C ++, все враховано. Не передчасна оптимізація.

TL; DR2: Більшість пристосувань не мають сенсу до тих пір, поки це не відбудеться.


Мета

Ця відповідь просто намагається трохи розширити пов'язаний елемент у Основних керівних принципах C ++ (вперше згаданий у коментарі Амона).

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


Застосовуваність

Ця відповідь стосується лише функціональних викликів (нерозбірних вкладених областей на одній нитці).

(Бічна примітка.) Коли прохідні речі можуть вийти за межі сфери (тобто мати тривалість життя, яка потенційно перевищує зовнішню область), стає важливішим задовольнити потребу програми в управлінні життєдіяльністю об'єкта раніше, ніж будь-що інше. Зазвичай для цього потрібне використання посилань, які також можуть довічно керувати, наприклад, смарт-покажчики. Альтернативою може бути використання менеджера. Зауважимо, що лямбда - це свого роду знімна область; лямбда-знімки поводяться так, ніби мають об’єктну область. Тому будьте обережні з захопленням лямбда. Також будьте уважні до того, як передається сама лямбда - копія або посилання.


Коли пройти за значенням

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

У ситуаціях, коли виклику потрібне клонування об'єкта чи сукупності, передайте значення, в якому копія виклику відповідає потребі клонованого об'єкта.


Коли проходити посилання та ін.

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

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


Незвичайні парадигми

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


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

Sub-TL; DR Поширення посилання не повинно викликати код; проходження через const-посилання задовольняє цьому критерію. Однак всі інші мови без особливих зусиль задовольняють цей критерій.

(Новачкам програмістам на C ++ рекомендується повністю пропустити цей розділ.)

(Початок цього розділу частково натхненний відповіддю gnasher729. Однак дійшов інший висновок.)

C ++ дозволяє визначені користувачем конструктори копій та оператори призначення.

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

Навіть якщо програміст C ++ не визначає жодного, компілятор C ++ повинен генерувати такі методи на основі мовних принципів, а потім визначати, чи потрібно виконувати додатковий код, крім memcpy. Наприклад, a class/, structщо містить astd::vector члена, повинен мати конструктор копій та оператор присвоєння, який нетривіальний.

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

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

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

Щоб додати більше благ (або образи), C ++ вводить універсальні посилання (r-значення), щоб полегшити визначені користувачем оператори переміщення (конструктори переміщення та оператори присвоєння переміщення) з хорошою продуктивністю. Це корисно для надзвичайно актуального випадку використання (переміщення (перенесення) об'єктів з одного примірника в інший), зменшуючи потребу в копіюванні та глибокому клонуванні. Однак в інших мовах нелогічно говорити про таке переміщення предметів.


(Розділ поза темою) Розділ, присвячений статті "Хочеш швидкості? Перейди цінністю!" написано близько 2009 року.

Ця стаття була написана в 2009 році і пояснює виправдання дизайну для r-значення в C ++. Ця стаття представляє вагомий контргумент моєму висновку в попередньому розділі. Однак приклад коду статті та претензія на ефективність давно спростовані.

Sub-TL; DR Дизайн семантики значень r в C ++ дозволяє отримати напрочуд елегантну семантику на стороні користувача наSort функції користувача. Цього елегантного неможливо моделювати (імітувати) іншими мовами.

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

У програмі C ++ користувач може обрати одну з двох реалізацій: руйнівну з кращою продуктивністю або звичайну, яка не змінює вхід. (Шаблон опущено для стислості.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

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

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


Як оптимізація компілятора впливає на цю відповідь

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

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

Об'єкти, що перевищують значення реєстру машини, можуть бути скопійовані явними інструкціями щодо завантаження / зберігання пам'яті або викликом поважної memcpyфункції. На деяких платформах компілятор генерує інструкції SIMD для переміщення між двома місцями пам'яті, кожна інструкція переміщує десятки байтів (16 або 32).


Дискусія з питань багатослівності чи зорового скупчення

Програмісти на C ++ звикли до цього, тобто, поки програміст не ненавидить C ++, накладні витрати на написання чи читання const-посилань у вихідному коді не жахливі.

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

Ось що я уявляю (без доказів чи достовірних посилань) ...

  • Так, це впливає на продуктивність програмного забезпечення, написаного цією мовою.
  • Якщо компілятори можуть зрозуміти призначення коду, потенційно це може бути досить розумним для автоматизації цього
  • На жаль, у мовах, що сприяють мутаційності (на відміну від функціональної чистоти), компілятор класифікував би більшість речей як мутовані, тому автоматичне виведення constness відхиляло б більшість речей як non-const
  • Розумовий наклад залежить від людей; люди, які вважають, що це душевний розум, можуть відкинути C ++ як життєздатну мову програмування.

Це одна з ситуацій, коли я хотів би прийняти дві відповіді, замість того, щоб обрати лише одну ... зітхнути
CharonX

8

У статті ДональдКнута "StructuredProgrammingWithGoToStatements" він писав: "Програмісти витрачають величезну кількість часу на роздуми або переживаючи про швидкість некритичних частин своїх програм, і ці спроби ефективності насправді мають сильний негативний вплив при налагодженні та технічному обслуговуванні. . Ми повинні забути про малу ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь усього зла. Але ми не повинні пропускати свої можливості в цих критичних 3% ". - передчасна оптимізація

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


3
"якщо потрібно вибрати лише одну, виберіть чіткість." Друге слід віддавати перевагу замість цього, оскільки, можливо, ви будете змушені вибрати інше.
Дедуплікатор

@Deduplicator Дякую Однак у контексті ОП програміст має свободу вибору.
Лоуренс

Ваша відповідь звучить трохи більш загально, ніж це, хоча ...
Дедуплікатор

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

7

Проходження повз ([const] [rvalue] посилання) | (значення) має відповідати про наміри та обіцянки, зроблені інтерфейсом. Це не має нічого спільного з продуктивністю.

Правило великого пальця Річі:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state

3

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

У конкретному випадку переходу за посиланням сопзЬ, однак, я б сказав , що ідіоми настільки добре встановлена , що ситуація більш-менш навпаки: якщо ви не знаєте типу буде char/ short/ int/ long, люди очікують побачити , що пройшло по константностей посилання за замовчуванням, тому, мабуть, найкраще йти разом із цим, якщо у вас немає досить конкретної причини зробити інше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.