Що ефективніше: повернути значення порівняно з передачею за посиланням?


77

На даний момент я вивчаю, як писати ефективний код на C ++, і з приводу викликів функцій мені приходить в голову питання. Порівняння цієї функції псевдокоду:

not-void function-name () {
    do-something
    return value;
}
int main () {
    ...
    arg = function-name();
    ...
}

з цією ідентичною інакше функцією псевдокоду:

void function-name (not-void& arg) {
    do-something
    arg = value;
}
int main () {
    ...
    function-name(arg);
    ...
}

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

Редагувати : Для контексту це питання обмежується різницями, що не залежать від апаратної платформи, і здебільшого програмним забезпеченням. Чи є якісь незалежні від машини різниці в продуктивності?

Редагувати : я не бачу, як це дублікат. Іншим питанням є порівняння передачі за посиланням (попередній код) до передачі за значенням (нижче):

not-void function-name (not-void arg)

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


12
Чому б вам просто не спробувати? Імовірно, це залежить від вашої платформи та компілятора. Зробіть це мільйон разів і профілюйте. Крім того, загалом, пишіть код, як він є найбільш зрозумілим, і турбуйтеся про оптимізацію, лише якщо вам потрібно збільшити продуктивність.
xaxxon

1
Спробуйте обидві версії пару мільйонів разів, синхронізуючи дзвінки. Робіть це як без, так і з включеною оптимізацією. Беручи до уваги оптимізацію поверненої вартості та копіювання, я сумніваюся, що ви знайдете якісь великі відмінності в будь-якому випадку.
Якийсь програміст, чувак,


3
@Pedro: Завдяки копіюванню elision та переміщенню семантики існує багато випадків, коли передача / повернення за значенням насправді є більш ефективними.
MikeMB

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

Відповіді:


28

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

  1. Якщо вам потрібно повернути простий або базовий об'єкт, продуктивність буде подібною в обох випадках.

  2. Якщо об’єкт настільки великий і складний, повернення потребує копії, і це може бути повільніше, ніж мати його як посиланий параметр, але, на мою думку, це затратить менше пам'яті.

Ти все одно повинен думати, що компілятори роблять багато оптимізацій, які роблять обидва вистави дуже схожими. Див. Copy Elision .


2
А як щодо копіювання елізії?
juanchopanza

8
Насправді, на x86 - і ігноруючи оптимізацію компілятора - обидва створили б однаковий код збірки, оскільки повертаються значення, більші за 1 або 2? регістри передаються через область пам'яті, яка виділяється абонентом і передається абоненту через неявний параметр покажчика.
MikeMB

10

Ну, треба розуміти, що складання - це непроста справа. Є багато розглядів, коли компілятор компілює ваш код.

Не можна просто відповісти на це запитання, оскільки стандарт С ++ не забезпечує стандартний ABI (абстрактний двійковий інтерфейс), тому кожному компілятору дозволено компілювати код, що йому подобається, і ви можете отримувати різні результати в кожній компіляції.

Наприклад, на деяких проектах C ++ компілюється до керованого розширення Microsoft CLR (C ++ / CX). оскільки все, що вже є посиланням на об'єкт у купі, я думаю, різниці немає.

Відповідь не простіша для некерованих компіляцій. декілька запитань приходять мені на думку, коли я думаю про "Чи буде XXX працювати швидше, ніж РРР?", наприклад:

  • Ви заперечуєте глухий конструктив?
  • Чи підтримує ваш компілятор оптимізацію поверненого значення?
  • Чи підтримує ваш об’єкт семантику лише для копіювання, або як копіювати, так і переміщувати?
  • Об'єкт упакований суперечливо (наприклад std::array), або він має вказівник на щось у купі? (наприклад std::vector)?

Якщо я наведу конкретний приклад, я здогадуюсь, що для MSVC ++ та GCC повернення std::vectorза значенням буде таким, як передача його за посиланням, через оптимізацію значення r, і буде трохи (на кілька наносекунд) швидше, ніж повернення вектора переїздом. наприклад, це може повністю відрізнятися від Clang.

врешті-решт, профілювання тут є єдиною вірною відповіддю.


8

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

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

Погляньте на std::getlineприклад, який бере std::stringпосилання. Ця функція призначена для використання як умова циклу і продовжує заповнювати a std::stringдо досягнення EOF. Використання одного і того ж std::stringдозволяє std::stringповторно використовувати простір для зберігання в кожній ітерації циклу, різко зменшуючи кількість виділень пам'яті, які потрібно виконати.


5

Деякі відповіді торкалися цього, але я хотів би наголосити у світлі редагування

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

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

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

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

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


4

Ми не можемо бути на 100% загальними, оскільки різні платформи мають різні ABI, але я думаю, що ми можемо зробити деякі досить загальні твердження, які застосовуватимуться до більшості реалізацій, з тим застереженням, що ці речі в основному застосовуються до функцій, які не вбудовані.

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

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

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

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


2

Ця функція псевдокоду:

not-void function-name () {
    do-something
    return value;
}

було б краще використовувати, коли повернене значення не вимагає подальших змін до нього. Переданий параметр змінюється лише в function-name. Більше посилань на нього немає.


інакше ідентична функція псевдокоду:

void function-name (not-void& arg) {
    do-something
    arg = value;
}

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

void another-function-name (not-void& arg) {
    do-something
    arg = value;
}

1

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

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

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


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

На додаток до того, що сказав Вермільйон, компілятор не буде оптимізувати повернену копію у переміщення: повернення визначається з точки зору переміщення (копія є резервною). Однак це може повністю відхилити цей крок.
MikeMB
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.