Подтвердить что ты не робот

Передавать по значению быстрее, чем передавать по ссылке

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

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

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

#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;
}
4b9b3361

Ответ 1

Хороший способ узнать, почему существуют различия, - это проверить разборку. Вот результаты, которые я получил на своей машине с помощью 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 ;
}

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

(без оптимизации):

00114890 55                   push        ebp  
00114891 8B EC                mov         ebp,esp  
00114893 81 EC C0 00 00 00    sub         esp,0C0h  
00114899 53                   push        ebx  
0011489A 56                   push        esi  
0011489B 57                   push        edi  
0011489C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
001148A2 B9 30 00 00 00       mov         ecx,30h  
001148A7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
001148AC F3 AB                rep stos    dword ptr es:[edi]  
001148AE 8B 45 08             mov         eax,dword ptr [ptr]  
001148B1 8B 08                mov         ecx,dword ptr [eax]  
001148B3 6B C9 05             imul        ecx,ecx,5  
001148B6 8B 55 08             mov         edx,dword ptr [ptr]  
001148B9 89 0A                mov         dword ptr [edx],ecx  
001148BB 5F                   pop         edi  
001148BC 5E                   pop         esi  
001148BD 5B                   pop         ebx  
001148BE 8B E5                mov         esp,ebp  
001148C0 5D                   pop         ebp  
001148C1 C3                   ret 

function2 (без оптимизации)

00FF4850 55                   push        ebp  
00FF4851 8B EC                mov         ebp,esp  
00FF4853 81 EC C0 00 00 00    sub         esp,0C0h  
00FF4859 53                   push        ebx  
00FF485A 56                   push        esi  
00FF485B 57                   push        edi  
00FF485C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
00FF4862 B9 30 00 00 00       mov         ecx,30h  
00FF4867 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00FF486C F3 AB                rep stos    dword ptr es:[edi]  
00FF486E 8B 45 08             mov         eax,dword ptr [val]  
00FF4871 6B C0 05             imul        eax,eax,5  
00FF4874 89 45 08             mov         dword ptr [val],eax  
00FF4877 5F                   pop         edi  
00FF4878 5E                   pop         esi  
00FF4879 5B                   pop         ebx  
00FF487A 8B E5                mov         esp,ebp  
00FF487C 5D                   pop         ebp  
00FF487D C3                   ret  

Почему пропуск по значению быстрее (в случае без оптимизации)?

Ну, function() имеет две дополнительные операции mov. Посмотрим на первую дополнительную операцию mov:

001148AE 8B 45 08             mov         eax,dword ptr [ptr]  
001148B1 8B 08                mov         ecx,dword ptr [eax]  
001148B3 6B C9 05             imul        ecx,ecx,5

Здесь мы разыскиваем указатель. В function2 () мы уже имеем значение, поэтому мы избегаем этого шага. Сначала переместим адрес указателя в регистр eax. Затем мы переместим значение указателя в регистр ecx. Наконец, умножим значение на пять.

Посмотрите на вторую дополнительную операцию mov:

001148B3 6B C9 05             imul        ecx,ecx,5  
001148B6 8B 55 08             mov         edx,dword ptr [ptr]  
001148B9 89 0A                mov         dword ptr [edx],ecx 

Теперь мы движемся назад. Мы только что закончили умножение значения на 5, и нам нужно вернуть значение обратно в адрес памяти.

Поскольку function2 () не нужно иметь дело с ссылкой и разыменованием указателя, он пропускает эти две дополнительные операции mov.

Ответ 2

Накладные расходы с передачей по ссылке:

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

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

  • значение нужно скопировать в стек или в регистры

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

Ответ 3

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

Итак, вы должны передать эту информацию.

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

Умножьте его.

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

Отмечу указатель. Умножьте его.

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


Теперь для некоторых более конкретных вещей:

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

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

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

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

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

Ответ 4

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

Передача по значению аналогична переходу в функцию, и когда кто-то запрашивает значение int foo, вы просто передаете его им.

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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