Pass by value быстрее, чем pass by reference

Я сделал простую программу на c++ для сравнения производительности между двумя подходами-pass by value и pass by reference. Фактически pass by value выполняется лучше, чем pass by reference.

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

Я был бы очень рад, если бы кто-то мог объяснить подробно почему pass по значению требуют меньше тактовых циклов.

#include <iostream>
#include <stdlib.h>
#include <time.h>

using namespace std;

void function(int *ptr);
void function2(int val);

int main() {

   int nmbr = 5;

   clock_t start, stop;
   start = clock();
   for (long i = 0; i < 1000000000; i++) {
       function(&nmbr);
       //function2(nmbr);
   }
   stop = clock();

   cout << "time: " << stop - start;

   return 0;
}

/**
* pass by reference
*/
void function(int *ptr) {
    *ptr *= 5;
}

/**
* pass by value
*/
void function2(int val) {
   val *= 5;
}

8 ответов


хороший способ узнать, почему есть какие-либо различия, чтобы проверить разборку. Вот результаты, которые я получил на своей машине с Visual Studio 2012.

с флагами оптимизации обе функции генерируют один и тот же код:

009D1270 57                   push        edi  
009D1271 FF 15 D4 30 9D 00    call        dword ptr ds:[9D30D4h]  
009D1277 8B F8                mov         edi,eax  
009D1279 FF 15 D4 30 9D 00    call        dword ptr ds:[9D30D4h]  
009D127F 8B 0D 48 30 9D 00    mov         ecx,dword ptr ds:[9D3048h]  
009D1285 2B C7                sub         eax,edi  
009D1287 50                   push        eax  
009D1288 E8 A3 04 00 00       call        std::operator<<<std::char_traits<char> > (09D1730h)  
009D128D 8B C8                mov         ecx,eax  
009D128F FF 15 2C 30 9D 00    call        dword ptr ds:[9D302Ch]  
009D1295 33 C0                xor         eax,eax  
009D1297 5F                   pop         edi  
009D1298 C3                   ret  

это в принципе эквивалентно:

int main ()
{
    clock_t start, stop ;
    start = clock () ;
    stop = clock () ;
    cout << "time: " << stop - start ;
    return 0 ;
}

без флагов оптимизации вы, вероятно, получите разные результаты.


накладные расходы с прохождением по ссылке:

  • каждый доступ требует разыменования, т. е. есть еще одна память read

накладные расходы с прохождением по значению:

  • значение необходимо скопировать в стек или в регистры

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


к некоторым рассуждениям: В большинстве популярных машин целое число равно 32 битам, а указатель-32 или 64 битам

таким образом, вы должны передать столько информации.

чтобы умножить целое число, надо:

умножить его.

чтобы умножить целое число на указатель, вы должны:

почтение указателю. Умножить.

надеюсь, это достаточно понятно :)


теперь к более конкретным вещи:

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

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

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

передать простые объекты по значению и более сложные по ссылке (или указателю) (но опять же, что такое сложный? Что просто? Он меняется со временем, как аппаратное обеспечение следует)

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


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

Pass by value-это как войти в функцию, и когда кто-то просит значение int foo, вы просто даете его им.

Pass by reference входит в функцию с адресом значения int foo. Теперь, когда кому-то нужна ценность foo, они должны пойти и посмотреть ее. Все жалуются о том, что нужно разыграть Фу все это долбаное время. Я был в этой функции в течение 2 миллисекунд, и я, должно быть, посмотрел foo тысячу раз! Почему вы просто не дали мне стоимость в первую очередь? Почему вы не прошли мимо ценности?

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


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

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

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

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

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

хотя эта последняя точка нетривиальна, хорошее эмпирическое правило-делать то, что делает Java: передавать фундаментальные типы по значению и сложные типы (const) ссылка.


передача по значению часто очень быстрая для небольших типов, так как большинство из них меньше указателя на современных системах (64bit). Также могут быть определенные оптимизации, выполняемые при передаче по значению.

Как правило, передайте встроенные типы по значению.


в этом случае компилятор, вероятно, понял, что результат умножения не используется в случае pass-by-value и полностью оптимизировал его. Не видя разобранного кода, невозможно быть уверенным.


довольно часто выполнение 32-битных инструкций по манипулированию памятью медленнее на родной 64-битной платформе, потому что процессор должен запускать 64-битные инструкции независимо. Если это сделано правильно компилятором, 32-битные инструкции "спариваются" в кэше инструкций, но если 32-битное чтение выполняется с 64-битной инструкцией, 4 дополнительных байта копируются как заполнение, а затем отбрасываются. Короче говоря, значение меньше размера указателя не обязательно означает, что оно быстрее. Это зависит от ситуация и на компиляторе, и абсолютно не должна приниматься во внимание для производительности, за исключением составных типов, где значение определенно больше указателя на величину 1, или в случаях, когда вам нужна абсолютная лучшая производительность для одной конкретной платформы без учета переносимости. Выбор между передачей по ссылке или по значению должен зависеть только от того, хотите ли вы, чтобы вызываемая процедура могла изменять передаваемый объект. Если это только Читайте для типа меньше 128 бит, проходите по значению, это безопаснее.