Передавать по значению быстрее, чем передавать по ссылке
Я сделал простую программу на С++ для сравнения производительности между двумя подходами - перейдите по значению и перейдите по ссылке. Фактически переходите по значению, выполненному лучше, чем по ссылке.
Вывод должен заключаться в том, что при передаче по значению требуется меньше тактовых циклов (инструкций)
Я был бы очень рад, если бы кто-то мог подробно объяснить , почему передавать по значению требуется меньше тактовых циклов.
#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;
}
Ответы
Ответ 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 бит, перейдите по значению, это безопаснее.