22

Hasta hoy, siempre he pensado que los compiladores decentes convierten automáticamente struct pass-by-value en pass-by-reference si la estructura es lo suficientemente grande para que esta última sea más rápida. Según mi leal saber y entender, esto parece una optimización sin complicaciones. Sin embargo, para satisfacer mi curiosidad sobre si esto realmente sucede, creé un caso de prueba simple tanto en C++ como en D y miré la salida de GCC y Digital Mars D. Ambos insistieron en pasar estructuras de 32 bytes por valor cuando todo el La función en cuestión hizo fue sumar los miembros y devolver los valores, sin modificación de la estructura pasada. La versión de C++ está debajo.¿Por qué no pasar estructura por referencia una optimización común?

#include "iostream.h" 

struct S { 
    int i, j, k, l, m, n, o, p; 
}; 

int foo(S s) { 
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p; 
} 

int main() { 
    S s; 
    int bar = foo(s); 
    cout << bar; 
} 

Mi pregunta es, ¿por qué diablos no sería algo como esto ser optimizado por el compilador para pasar por referencia en lugar de realmente empujar todos esos int s en la pila?

Nota: Se utilizan los siguientes modificadores del compilador: GCC -O2 (-O3 inline foo().), DMD -O -inline-release.

Editar: Obviamente, en el caso general, la semántica de pass-by-value vs. pass-by-reference no será la misma, como si están involucrados constructores de copia o la estructura original está modificada en el callee . Sin embargo, en muchos escenarios del mundo real, la semántica será idéntica en términos de comportamiento observable. Estos son los casos sobre los que estoy preguntando.

Respuesta

22

No se olvide que en C/C++ del compilador necesita ser capaz de compilar una llamada a una función basada únicamente en la declaración de la función.

Teniendo en cuenta que las personas que llaman podrían estar utilizando únicamente la información, no hay manera para que un compilador para compilar la función de tomar ventaja de la optimización que estás hablando. La persona que llama no puede saber que la función no modificará nada y por lo tanto no puede pasar por ref. Como algunas personas que llaman pueden pasar por alto debido a la falta de información detallada, la función debe compilarse asumiendo el valor de paso por paso y todos deben pasar por el valor.

Tenga en cuenta que incluso si marcó el parámetro como 'const', el compilador aún no puede realizar la optimización, porque la función podría estar mintiendo y descartando la constness (esto está permitido y bien definido siempre que el objeto que se pasa en realidad no es const).

Creo que para las funciones estáticas (o aquellas en un espacio de nombre anónimo), el compilador posiblemente podría hacer la optimización de la que está hablando, ya que la función no tiene enlaces externos. Siempre que la dirección de la función no se pase a alguna otra rutina o se almacene en un puntero, no se podrá llamar desde otro código. En este caso, el compilador podría tener un conocimiento completo de todas las personas que llaman, por lo que supongo que podría hacer la optimización.

No estoy seguro de si alguno de tareas (en realidad, me sorprendería si alguno lo hacen, ya que probablemente no podría aplicarse muy a menudo).

Por supuesto, como programador (cuando usa C++) puede forzar al compilador a realizar esta optimización usando los parámetros const& siempre que sea posible. Sé que estás preguntando por qué el compilador no puede hacerlo automáticamente, pero supongo que es la mejor opción.

+0

Al realizar optimización de tiempo de enlace, a.k.a. generación de código de tiempo de enlace o compilación de programa completo, el compilador no necesita compilar la llamada solo en función de la declaración. Tiene una visión completa de lo que está pasando. Para compilar aplicaciones integradas que son sensibles al tamaño y velocidad, la generación de código de tiempo de enlace es la única manera de hacerlo. –

10

Una respuesta es que el compilador debería detectar que el método llamado no modifica el contenido de la estructura de ninguna manera. Si lo hiciera, entonces el efecto de pasar por referencia diferiría del de pasar por el valor.

+2

podrían adoptar la semántica copia en escritura para este tipo de estructuras. –

+0

Sí, supongo que podría hacer que el ABI para su idioma declare que las estructuras se pasan por referencia, pero se convierten en copias si el destinatario intenta modificarlas. Sin embargo, suena un poco desordenado: necesitas emplear todo el complicado análisis de alias del peor caso para determinar que están garantizados para ser intactos. Es más fácil simplemente tener reglas explícitas en el lenguaje: si no quiere pasar una estructura para ralentizarla, haga una referencia o un puntero a ella. – Edmund

11

El problema es que le está pidiendo al compilador que tome una decisión sobre la intención del código de usuario. Tal vez quiero que mi estructura súper grande pase por valor para que pueda hacer algo en el constructor de copias. Créanme, alguien por ahí tiene algo que necesitan ser llamados válidamente en un constructor de copia para ese escenario. Cambiar a a por ref omitirá el constructor de copia.

Teniendo esto como una decisión generada por el compilador sería una mala idea. La razón es que hace que sea imposible razonar sobre el flujo de su código. No puede mirar una llamada y saber exactamente qué hará. Tienes que a) conocer el código yb) adivinar la optimización del compilador.

4

Es cierto que los compiladores en algunos idiomas podrían hacer esto si tienen acceso a la función que se está llamando y si pueden suponer que la función llamada no cambiará. Esto a veces se denomina optimización global y parece probable que algunos compiladores C o C++ de hecho optimicen casos como este, más probablemente al insertar el código para una función tan trivial.

3

Hay muchas razones para pasar por alto, y hacer que el compilador optimice su intención puede romper su código.

Ejemplo, si la función llamada modifica la estructura de alguna manera. Si pretendía que los resultados se transmitieran a la persona que llama, entonces podría pasar un puntero/referencia o devolverlo usted mismo.

Lo que le pide al compilador que haga es cambiar el comportamiento de su código, que se consideraría un error del compilador.

Si desea realizar la optimización y pasar por referencia, modifique las definiciones de función/método existentes de alguien para aceptar referencias; no es tan difícil de hacer. Puede que se sorprenda de la rotura que causa sin darse cuenta.

2

Cambiar de por valor a por referencia cambiará la firma de la función. Si la función no es estática, esto causaría errores de enlace para otras unidades de compilación que no conocen la optimización que usted hizo.
De hecho, la única forma de hacer tal optimización es mediante algún tipo de fase de optimización global posterior al enlace. Estos son notoriamente difíciles de hacer, sin embargo, algunos compiladores los hacen en cierta medida.

1

Bueno, la respuesta trivial es que la ubicación de la estructura en la memoria es diferente, y por lo tanto los datos que está pasando es diferente. La respuesta más compleja, creo, está enhebrando.

su compilador tendría que detectar a) que foo no modifica la estructura; b) que foo no hace ningún cálculo en la ubicación física de los elementos de la estructura; Y c) que la persona que llama, u otro hilo generado por la persona que llama, no modifica la estructura antes de que foo termine de ejecutarse.

En su ejemplo, es concebible que el compilador podría hacer estas cosas - pero la memoria almacenada es intrascendente y probablemente no vale la pena tomar la conjetura. ¿Qué sucede si ejecuta el mismo programa con una estructura que tiene dos millones de elementos?

2

Creo que esta es definitivamente una optimización que podría implementar (bajo algunas suposiciones, vea el último párrafo), pero no me queda claro que sería rentable. En lugar de insertar argumentos en la pila (o pasarlos a través de registros, dependiendo de la convención de llamadas), presionaría un puntero a través del cual leería los valores. Esta indirección adicional costaría ciclos. También requeriría que el argumento pasado esté en la memoria (para que pueda señalarlo) en lugar de en los registros. Solo sería beneficioso si los registros que se pasan tienen muchos campos y la función que recibe el registro solo lee algunos de ellos. Los ciclos adicionales desperdiciados por la indirección tendrían que compensar los ciclos no desperdiciados empujando los campos innecesarios.

Puede sorprenderse que la optimización inversa, argument promotion, se implemente realmente en LLVM. Esto convierte un argumento de referencia en un argumento de valor (o un agregado en escalares) para funciones internas con un pequeño número de campos que solo se leen. Esto es particularmente útil para los idiomas que pasan casi todo por referencia. Si sigue esto con dead argument elimination, tampoco tiene que pasar campos que no se tocan.

Cabe mencionar que las optimizaciones que cambian la forma de llamar a una función solo pueden funcionar cuando la función optimizada es interna al módulo que se está compilando (esto se consigue declarando una función static en C y con plantillas en C++). El optimizador debe reparar no solo la función sino también todos los puntos de llamada. Esto hace que dichas optimizaciones sean bastante limitadas en su alcance a menos que las haga en tiempo de enlace. Además, nunca se llamaría a la optimización cuando se trata de un constructor de copias (como han mencionado otros carteles) porque podría cambiar la semántica del programa, algo que un buen optimizador nunca debería hacer.

2

Pass-by-reference es solo azúcar sintáctica para pass-by-address/puntero. Por lo tanto, la función debe desreferencia implícitamente un puntero para leer el valor del parámetro. La desreferenciación del puntero puede ser más costosa (si está en un bucle) que la copia de estructura para copia por valor.

Más importante aún, como han mencionado otros, pass-by-reference tiene una semántica diferente a pass-by-value. const referencias do not significa que el valor al que se hace referencia no cambia. otras llamadas a funciones pueden cambiar el valor al que se hace referencia.

+0

IIRC el comentario final no es válido en D2.0. El compilador/es/libre de suponer que no hay cambios a través de referencias de referencias. – BCS

1

el compilador tendría que estar seguro de que la estructura que se pasa (como se nombran en el código de llamada) no se modifica en

double x; // using non structs, oh-well 

void Foo(double d) 
{ 
     x += d; // ok 
     x += d; // Oops 
} 

void main() 
{ 
    x = 1; 
    Foo(x); 
} 
Cuestiones relacionadas