c++ - retornan - ¿Regla de oro para cuando pasar por valor es más rápido que pasar por referencia de referencia?
parametros por referencia c++ (7)
Supongamos que tengo una función que toma un argumento de tipo T
No lo muta, así que tengo la opción de pasarlo por const referencia const T&
o por valor T
:
void foo(T t){ ... }
void foo(const T& t){ ... }
¿Existe una regla general de cuán grande debe llegar a ser T
antes de pasar por referencia de referencia que se vuelve más barato que pasar por valor? Por ejemplo, supongamos que sé que sizeof(T) == 24
. ¿Debo usar referencia o valor const?
Supongo que el constructor de copias de T
es trivial. De lo contrario, la respuesta a la pregunta depende de la complejidad del constructor de copia, por supuesto.
Ya he buscado preguntas similares y me encontré con esta:
plantilla pase por valor o const referencia o ...?
Sin embargo, la respuesta aceptada ( https://stackoverflow.com/a/4876937/1408611 ) no establece ningún detalle, simplemente indica:
Si espera que T sea siempre un tipo numérico o un tipo que sea muy barato de copiar, entonces puede tomar el argumento por valor.
Por lo tanto, no resuelve mi pregunta sino que la reformula: ¿Qué tan pequeño debe ser un tipo para ser "muy barato de copiar"?
Creo que elegiría pasar de valor siempre que sea posible (es decir, cuando la semántica dicte que no necesito el objeto real para trabajar). Confiaría en que el compilador realice los movimientos apropiados y la elisión de copia.
Después de que mi código sea semánticamente correcto, lo perfilaría para ver si estoy haciendo copias innecesarias; Yo los modificaría en consecuencia.
Creo que este enfoque me ayudaría a centrarme en la parte más importante de mi software: la corrección. Y no me pondría en el camino del compilador --- interferir; inhibir --- para realizar optimizaciones (sé que no puedo superarlo).
Habiendo dicho eso, las referencias nominales se implementan como punteros. Por lo tanto, en un vacío, sin considerar la semántica, las elipsis de copia, la semántica del movimiento y cosas así, sería más "eficiente" pasar por el puntero / referencia cualquier cosa cuyo tamaño sea más grande que el puntero.
La regla más apropiada en mi opinión es pasar por referencia cuando:
sizeof(T) >= sizeof(T*)
La idea detrás de esto es que cuando toma por referencia, en el peor de los casos su compilador puede implementar esto usando un puntero.
Esto, por supuesto, no tiene en cuenta la complejidad de su constructor de copia y la semántica de movimiento y todo el infierno que se puede crear en torno a su ciclo de vida del objeto.
Además, si no te importan las micro optimizaciones, puedes pasar todo por referencia, en la mayoría de los punteros de las máquinas son 4 u 8 bytes, muy pocos tipos son más pequeños e incluso en ese caso perderías algunos (menos de 8) la operación de copiado de bytes y algunas indirecciones que en el mundo moderno probablemente no serán tu cuello de botella :)
Los chicos aquí están en lo cierto, la mayoría de las veces no importa cuando el tamaño de la estructura / clase es pequeño y no hay un constructor de copia elegante. Sin embargo, eso no es excusa para ser ignorante. Esto es lo que sucede en algunos ABI modernos como x64. Puede ver que en esa plataforma, un buen umbral para su regla empírica es pasar por el valor donde el tipo es un tipo POD y tamaño de () <= 16, ya que pasará en dos registros. Las reglas generales son buenas, te evitan perder el tiempo y una capacidad mental limitada en las pequeñas decisiones como esta que no mueven la aguja.
Sin embargo, a veces será importante. Cuando hayas determinado con un generador de perfiles que tienes uno de esos casos (¡y NO antes, a menos que sea súper obvio!), Entonces necesitas comprender los detalles de bajo nivel, no escuchar trivialidades reconfortantes acerca de cómo no importa, qué SO está lleno de. Algunas cosas para tener en mente:
- Pasar por valor le dice a un compilador que el objeto no cambia. Existen tipos de códigos malvados que involucran hilos o globales en los que la cosa señalada por la referencia constante se cambia en otro lugar. Aunque seguramente no escribes código malvado, el compilador puede tener dificultades para probarlo y, como resultado, tiene que generar un código muy conservador.
- Si hay demasiados argumentos para pasar por el registro, entonces no importa qué tamaño tenga, el objeto pasará a la pila.
- Un objeto que es pequeño y POD hoy puede crecer y obtener un constructor de copia costoso mañana. La persona que agrega esas cosas probablemente no verificó si se transmitía por referencia o por valor, por lo que el código que solía funcionar de manera excelente puede resurgir repentinamente. Pasar por referencia constante es una opción más segura si trabaja en un equipo sin pruebas integrales de regresión de rendimiento. Serás mordido por esto tarde o temprano.
Para una "T" abstracta en un "C ++" abstracto, la regla de oro sería usar la forma que mejor refleje la intención, que para un argumento que no se modifica es casi siempre "pasar de valor". Además, los compiladores concretos del mundo real esperan una descripción tan abstracta y pasarán su T de la manera más eficiente, independientemente de cómo lo haga en la fuente.
O, para hablar sobre compilación y composición naive, "muy barato de copiar" es "cualquier cosa que pueda cargar en un solo registro". No es más barato que eso realmente.
Pasar un valor en lugar de una referencia constante tiene la ventaja de que el compilador sabe que el valor no va a cambiar. "const int & x" no significa que el valor no puede cambiar; solo significa que su código no puede cambiarlo usando el identificador x (sin un molde que el compilador notaría). Toma este horrible pero perfectamente legal ejemplo:
static int someValue;
void g (int i)
{
--someValue;
}
void f (const int& x)
{
for (int i = 0; i < x; ++i)
g (i);
}
int main (void)
{
someValue = 100;
f (someValue);
return 0;
}
Dentro de la función f, x no es realmente constante! ¡Cambia cada vez que se llama a g (i), por lo que el ciclo solo va de 0 a 49! Y dado que el compilador generalmente no sabe si usted escribió un código horrible como este, debe suponer que x podría cambiar cuando se llame a g. Como resultado, puede esperar que el código sea más lento que si hubiera usado "int x".
Lo mismo es obviamente cierto para muchos objetos que podrían pasarse por referencia. Por ejemplo, si pasa un objeto por const &, y el objeto tiene un miembro que es int o unsigned int, entonces cualquier asignación que use un char *, int * o unsigned int * puede cambiar ese miembro, a menos que el compilador demuestre lo contrario . Pasado por el valor, la prueba es mucho más fácil para el compilador.
Si tiene motivos para sospechar que hay una ganancia de rendimiento que vale la pena, recórtela con las reglas generales y mida . El objetivo del asesoramiento que cita es que no copia grandes cantidades de datos sin ningún motivo, pero no pone en peligro las optimizaciones haciendo que todo sea una referencia. Si algo está en el límite entre "claramente barato de copiar" y "claramente caro de copiar", entonces puede permitirse cualquier opción. Si debe quitarle la decisión, arroje una moneda.
Un tipo es barato de copiar si no tiene un constructor de copia funky y su tamaño es pequeño. No hay un número difícil para "pequeño" que sea óptimo, ni siquiera por plataforma, ya que depende en gran medida del código de llamada y de la función en sí. Solo ve por tu instinto. Una, dos, tres palabras son pequeñas. Diez, quién sabe. Una matriz de 4x4 no es pequeña.
Si vas a utilizar una "regla empírica" para el valor por by vs. by-const-reference, haz esto:
- elige UN enfoque y úsalo en todas partes
- acordar cuál de todos tus compañeros de trabajo
- solo después, en la fase de "ajuste manual", comienza a cambiar las cosas
- y luego, solo cámbialos si ves una mejora mensurable