valores valor retornan referencia que por paso parametros funciones con c++ pass-by-reference pass-by-value

c++ - valor - paso por referencia en c



Pase por valor más rápido que pase por referencia (8)

Cuando pasa por valor, le está diciendo al compilador que haga una copia de la entidad que está pasando por valor.

Cuando pasa por referencia, le está diciendo al compilador que debe usar la memoria real a la que apunta la referencia. El compilador no sabe si está haciendo esto en un intento de optimización, o porque el valor al que se hace referencia podría estar cambiando en algún otro subproceso (por ejemplo). Tiene que usar ese área de memoria.

Pasar por referencia significa que el procesador tiene que acceder a ese bloque de memoria específico. Ese puede o no ser el proceso más eficiente, dependiendo del estado de los registros. Cuando pasa por referencia, se puede usar la memoria en la pila, lo que aumenta las posibilidades de acceder a la memoria caché (mucho más rápida).

Finalmente, según la arquitectura de su máquina y el tipo que está pasando, una referencia puede ser mayor que el valor que está copiando. Copiar un entero de 32 bits implica copiar menos que pasar una referencia en una máquina de 64 bits.

Así que pasar por referencia solo debe hacerse cuando necesite una referencia (para mutar el valor, o porque el valor puede estar mutado en otro lugar), o cuando copiar el objeto al que se hace referencia es más costoso que eliminar la referencia de la memoria necesaria.

Si bien este último punto no es trivial, una buena regla empírica es hacer lo que hace Java: pasar tipos fundamentales por valor y tipos complejos por (const) referencia.

Hice un programa simple en c ++ para comparar el rendimiento entre dos enfoques: pasar por valor y pasar por referencia. En realidad, el valor de paso se realiza mejor que pasar por referencia.

La conclusión debería ser que pasar por valor requiere menos ciclos de reloj (instrucciones)

Me alegraría muchísimo si alguien pudiera explicar en detalle por qué pasar de valor requiere menos ciclos de reloj.

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


En este caso, el compilador probablemente se dio cuenta de que el resultado de la multiplicación no se estaba utilizando en el caso del valor de paso y lo optimizó por completo. Sin ver el código desmontado, es imposible estar seguro.


Imagine que entra en una función y se supone que debe ingresar con un valor int. El código en la función quiere hacer cosas con ese valor int.

Pasar por valor es como entrar en la función y cuando alguien pregunta por el valor int foo, simplemente se lo da.

Pasar por referencia es ingresar a la función con la dirección del valor int foo. Ahora, cuando alguien necesita el valor de foo, tiene que ir y buscarlo. Todo el mundo se va a quejar de tener que quitarle la referencia a todos los malditos momentos. ¡He estado en esta función durante 2 milisegundos ahora y debo haber buscado miles de veces! ¿Por qué no me acabas de dar el valor en primer lugar? ¿Por qué no pasaste por valor?

Esta analogía me ayudó a ver por qué pasar por alto a menudo es la opción más rápida.


Muy a menudo la ejecución de instrucciones de manipulación de memoria de 32 bits es más lenta en una plataforma nativa de 64 bits, porque el procesador tiene que ejecutar instrucciones de 64 bits independientemente. Si el compilador lo hace correctamente, las instrucciones de 32 bits se "emparejan" en la memoria caché de instrucciones, pero si se ejecuta una lectura de 32 bits con una instrucción de 64 bits, se copian 4 bytes adicionales como relleno y luego se descartan. En resumen, el valor que es menor que el tamaño del puntero no significa necesariamente que sea más rápido. Depende de la situación y del compilador, y no debe tomarse en consideración para el rendimiento, excepto para los tipos compuestos donde el valor es definitivamente más grande que el puntero en una magnitud de 1, o en los casos donde se necesita el mejor rendimiento absoluto para una plataforma particular sin importar la portabilidad. La elección entre pasar por referencia o por valor debería depender únicamente de si desea o no que el procedimiento llamado pueda modificar el objeto pasado. Si solo es una lectura para un tipo de menos de 128 bits, pase por valor, es más seguro.


Para algunos razonamientos: en la mayoría de las máquinas populares, un entero es de 32 bits, y un puntero es de 32 o 64 bits

Entonces debes pasar tanta información.

Para multiplicar un número entero debes:

Multiplícalo.

Para multiplicar un número entero apuntado por un puntero debes:

Deferencia el puntero. Multiplícalo.

Espero que esté lo suficientemente claro :)

Ahora a algunas cosas más específicas:

Como se ha señalado, su función de valor por defecto no hace nada con el resultado, pero el by-puntero uno realmente guarda el resultado en la memoria. ¿Por qué eres tan injusto con el pobre puntero? :( (es una broma)

Es difícil decir qué tan válido es su punto de referencia, ya que los compiladores vienen con todo tipo de optimización. (Por supuesto, puedes controlar la libertad del compilador, pero no has proporcionado información sobre eso)

Y finalmente (y probablemente lo más importante) punteros, valores o referencias no tiene una velocidad asociada. Quién sabe, puede encontrar una máquina que sea más rápida con punteros y tome un tiempo difícil con valores, o al revés. Está bien, está bien, hay un patrón en el hardware y hacemos todas estas suposiciones, la más aceptada parece ser:

Pase objetos simples por valor y otros más complejos por referencia (o puntero) (pero, de nuevo, ¿qué es complejo? ¿Qué es simple? Cambia con el tiempo a medida que sigue el hardware)

Recientemente, siento que la opinión estándar se está volviendo: pasar de valor y confiar en el compilador. Y eso es genial. Los compiladores están respaldados por años de desarrollo de experiencia y usuarios enojados que exigen que siempre sea mejor.


Pasar por valor suele ser muy rápido para los tipos pequeños, ya que la mayoría de ellos son más pequeños que el puntero en los sistemas modernos (64 bits). También puede haber ciertas optimizaciones hechas cuando pasan por valor.

Como regla general, pase los tipos incorporados por valor.


Sobrecarga con pasar por referencia:

  • cada acceso necesita una desreferencia, es decir, hay una lectura de memoria más

Sobrecarga con pasar por valor:

  • el valor debe ser copiado en la pila o en los registros

Para objetos pequeños, como un entero, pasar por valor será más rápido. Para objetos más grandes (por ejemplo, una estructura grande), la copia crearía demasiada sobrecarga, por lo que pasar por referencia será más rápido.


Una buena forma de averiguar por qué existen diferencias es verificar el desmontaje. Estos son los resultados que obtuve en mi máquina con Visual Studio 2012.

Con indicadores de optimización, ambas funciones generan el mismo código:

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

Esto es básicamente equivalente a:

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

Sin indicadores de optimización, probablemente obtendrá diferentes resultados.

función (sin optimizaciones):

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 (sin optimizaciones)

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

¿Por qué es más rápido pasar el valor (en el caso sin optimización)?

Bueno, function() tiene dos operaciones mov adicionales. Echemos un vistazo a la primera operación de mov adicional:

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

Aquí estamos desreferenciando el puntero. En function2 () , ya tenemos el valor, por lo que evitamos este paso. Primero movemos la dirección del puntero hacia el registro eax. Luego movemos el valor del puntero al registro ecx. Finalmente, multiplicamos el valor por cinco.

Veamos la segunda operación de mov adicional:

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

Ahora estamos retrocediendo. Acabamos de terminar de multiplicar el valor por 5, y tenemos que volver a colocar el valor en la dirección de la memoria.

Debido a que function2 () no tiene que lidiar con la referenciación y desreferenciación de un puntero, se saltea estas dos operaciones mov adicionales.