kodai
Ahora, sé que es porque no existe la sobrecarga de llamar a una función, pero ¿la sobrecarga de llamar a una función es realmente tan pesada (y vale la pena tenerla en línea)?
Por lo que puedo recordar, cuando se llama a una función, digamos f(x,y), x e y se colocan en la pila, y el puntero de la pila salta a un bloque vacío y comienza la ejecución. Sé que esto es un poco simplificado, pero ¿me estoy perdiendo algo? Unos pocos empujones y un salto para llamar a una función, ¿realmente hay tantos gastos generales?
Avísame si me estoy olvidando de algo, ¡gracias!
Hormiga
Aparte del hecho de que no hay llamada (y, por lo tanto, no hay gastos asociados, como la preparación de parámetros antes de la llamada y la limpieza después de la llamada), hay otra ventaja significativa de la alineación. Cuando el cuerpo de la función está en línea, su cuerpo se puede reinterpretar en el contexto específico de la persona que llama. Esto podría permitir inmediatamente que el compilador reduzca y optimice aún más el código.
Para un ejemplo simple, esta función
void foo(bool b) {
if (b) {
// something
}
else {
// something else
}
}
requerirá una bifurcación real si se llama como una función no en línea
foo(true);
...
foo(false);
Sin embargo, si las llamadas anteriores están en línea, el compilador podrá eliminar inmediatamente la bifurcación. Esencialmente, en el caso anterior, la inserción permite que el compilador interprete el argumento de la función como una constante de tiempo de compilación (si el parámetro es una constante de tiempo de compilación), algo que generalmente no es posible con las funciones que no están en línea.
Sin embargo, no se limita ni remotamente a eso. En general, las oportunidades de optimización que ofrece la inserción son mucho más amplias. Para otro ejemplo, cuando el cuerpo de la función se inserta en el específico En el contexto de la persona que llama, el compilador en general podrá propagar las relaciones conocidas relacionadas con el alias presentes en el código de llamada al código de la función en línea, lo que permitirá optimizar mejor el código de la función.
Nuevamente, los ejemplos posibles son numerosos, todos ellos derivados del hecho básico de que las llamadas en línea están inmersas en el específico el contexto de la persona que llama, lo que permite varias optimizaciones entre contextos, lo que no sería posible con llamadas no en línea. Con la inserción, básicamente obtiene muchas versiones individuales de su función original, cada versión se adapta y optimiza individualmente para cada contexto específico de la persona que llama. El precio de eso es, obviamente, el peligro potencial de que el código se hinche, pero si se usa correctamente, puede proporcionar beneficios de rendimiento notables.
-
Otra dulce optimización que le brinda la función en línea es la eficiencia de la memoria caché de instrucciones. Es mucho más probable que el código en línea ya esté en la memoria caché, mientras que el código invocado podría causar fácilmente una falla en la memoria caché.
– Detmar
25/10/2010 a las 19:10
-
@Detmar: Tal vez. Y tal vez no. Por lo que sé sobre cachés de instrucciones (muy poco, hay que admitirlo), por lo general es necesario medir para saber, y la mayoría de las veces el resultado parece divertido y extraño.
– sbi
25 de octubre de 2010 a las 19:24
-
@Detmar, @sbi: De acuerdo en que esto puede ser misterioso. El uso de líneas puede sacar el código activo de la dulce memoria caché de instrucciones L1, mientras que el uso de llamadas a funciones significa que cada función está en la memoria caché de forma independiente, utilizando menos espacio de memoria caché. Esta es la razón por la cual el código compilado en GCC con -Os (reducir tamaño) puede ser más rápido que O2 u O3.
– Zan Lince
25/10/2010 a las 20:45
-
¡¡¡Sí!!! El cuerpo de la función está en línea y … de repente, el compilador puede eliminar la mayor parte del código. Esta es la razón número uno por la cual la inserción (especialmente con la generación de código en tiempo de enlace) es una gran cosa.
– diente filoso
26 de octubre de 2010 a las 7:54
-
@meet: quiero decir que, por ejemplo, dentro de la función
void foo(int *a, int *b)
el compilador no puede hacer suposiciones sobre el alias:a
yb
puede apuntar al mismo objeto oa diferentes objetos. Cualquiera de las variantes ofrece oportunidades de optimización, pero el compilador no puede aprovechar estas oportunidades. Pero en un nivel superior (en el contexto de la persona que llama) esta información podría estar disponible. Por ejemplo, al alinearint x; foo(&x, &x);
llamada, el compilador puede optimizar inmediatamente paraa == b
condición. Del mismo modo paraint x, y; foo(&x, &y);
el compilador puede optimizar paraa != b
.– Ant
20 de junio de 2015 a las 15:51
Saludos y hth. – alf
“Algunos empujones y un salto para llamar a una función, ¿realmente hay tantos gastos generales?”
Depende de la función.
Si el cuerpo de la función es solo una instrucción de código de máquina, la sobrecarga de llamada y devolución puede ser de varios cientos %. Digamos, 6 veces, 500% de gastos generales. Entonces, si su programa consta de nada más que un montón de llamadas a esa función, sin intercalar, ha aumentado el tiempo de ejecución en un 500%.
Sin embargo, en la otra dirección, la inserción puede tener un efecto perjudicial, por ejemplo, porque el código que sin la inserción cabría en una página de memoria no lo hace.
Entonces, la respuesta siempre es cuando se trata de optimización, en primer lugar, MEDIR.
-
Además, una función realmente corta puede ser más pequeña que las instrucciones de configuración y desmontaje para una llamada de función, y la inserción en realidad puede hacer que el código sea más pequeño. Medida y perfil.
–David Thornley
25 de octubre de 2010 a las 16:57
Ponto Gagge
No hay actividad de llamadas ni de pila, lo que sin duda ahorra algunos ciclos de CPU. En las CPU modernas, la localidad del código también es importante: hacer una llamada puede vaciar el canalización de instrucciones y fuerce a la CPU a esperar a que se obtenga la memoria. Esto es muy importante en bucles estrechos, ya que la memoria principal es mucho más lenta que las CPU modernas.
Sin embargo, no se preocupe por incorporarlo si su código solo se llama unas pocas veces en su aplicación. ¡Preocúpate, mucho, si está siendo llamado millones de veces mientras el usuario espera respuestas!
sbi
El candidato clásico para inlinear es un descriptor de acceso, como std::vector<T>::size()
.
Con la inserción habilitada, esto es solo la obtención de una variable de la memoria, probablemente una sola instrucción en cualquier arquitectura. Los “pocos empujones y un salto” (más la devolución) son fácilmente varias veces como mucho.
Agregue a eso el hecho de que, cuanto más código sea visible a la vez para un optimizador, mejor podrá hacer su trabajo. Con muchas líneas, ve mucho código a la vez. Eso significa que podría ser capaz de mantener el valor en un registro de la CPU, y te ahorras por completo el costoso viaje a la memoria. Ahora podríamos considerar una diferencia de varios órdenes de magnitud.
Y luego está metaprogramación de plantillas. A veces, esto da como resultado llamar a muchas funciones pequeñas de forma recursiva, solo para obtener un valor único al final de la recursividad. (Piense en obtener el valor de la primera entrada de un tipo específico en una tupla con docenas de objetos). Con la inserción habilitada, el optimizador puede acceder directamente a ese valor (que, recuerde, podría estar en un registro), colapsando docenas de llamadas a funciones para acceder a un solo valor en un registro de la CPU. Esto puede convertir un terrible cerdo de rendimiento en un programa agradable y rápido.
Ocultar el estado como datos privados en objetos (encapsulación) tiene sus costos. Inlining fue parte de C++ desde el principio con el fin de minimizar estos costos de abstracción. En ese entonces, los compiladores eran significativamente peores en la detección de buenos candidatos para la inserción (y el rechazo de los malos) que en la actualidad, por lo que la inserción manual resultó en ganancias de velocidad considerables.
Hoy en día, los compiladores tienen fama de ser mucho más inteligentes que nosotros en línea. Los compiladores pueden incorporar funciones automáticamente o no incorporar funciones que los usuarios marcaron como inline
, aunque podrían. Algunos dicen que la inserción debe dejarse completamente en manos del compilador y que ni siquiera deberíamos molestarnos en marcar las funciones como inline
. Sin embargo, todavía tengo que ver un estudio completo que muestre si hacerlo manualmente todavía vale la pena o no. Entonces, por el momento, seguiré haciéndolo yo mismo y dejaré que el compilador lo anule si cree que puede hacerlo mejor.
dejar
int sum(const int &a,const int &b)
{
return a + b;
}
int a = sum(b,c);
es igual a
int a = b + c
Sin saltos, sin gastos generales
-
Mejor aún: “int a=sum(4,5);” puede convertirse en “int a=9;”. Además, leer y escribir variables a través de referencias suele ser más lento que leerlas y escribirlas directamente; en muchos casos, una función en línea se puede resolver para usar un acceso directo a variables más rápido (tenga en cuenta que en su escenario, si no está en línea, sería mejor pasar las variables por valor en lugar de por referencia, pero si la función lo hizo algo como “a+=b;” la referencia sería necesaria).
– Super gato
25 de octubre de 2010 a las 15:43
-
La declaración
reading and writing variables through references is generally slower than reading and writing them directly
es manera de general para ser verdad. También lo encuentro muy poco probable en la mayoría de las situaciones normales (vea qué fácil es generalizar en exceso).– Martín York
25 de octubre de 2010 a las 16:03
-
¿Y con qué frecuencia escribimos funciones como
sum()
? Creo que los accesores son un ejemplo mucho más relevante de lo que hace la inserción.– sbi
25/10/2010 a las 18:45
usp
Considere una función simple como:
int SimpleFunc (const int X, const int Y)
{
return (X + 3 * Y);
}
int main(int argc, char* argv[])
{
int Test = SimpleFunc(11, 12);
return 0;
}
Esto se convierte en el siguiente código (MSVC++ v6, depuración):
10: int SimpleFunc (const int X, const int Y)
11: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,40h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-40h]
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
12: return (X + 3 * Y);
00401038 mov eax,dword ptr [ebp+0Ch]
0040103B imul eax,eax,3
0040103E mov ecx,dword ptr [ebp+8]
00401041 add eax,ecx
13: }
00401043 pop edi
00401044 pop esi
00401045 pop ebx
00401046 mov esp,ebp
00401048 pop ebp
00401049 ret
Puede ver que solo hay 4 instrucciones para el cuerpo de la función, pero 15 instrucciones solo para la sobrecarga de la función sin incluir otras 3 para llamar a la función en sí. Si todas las instrucciones tomaron el mismo tiempo (no lo hacen), entonces el 80% de este código es una sobrecarga de funciones.
Para una función trivial como esta, existe una buena posibilidad de que el código general de la función tarde tanto en ejecutarse como el cuerpo de la función principal. Cuando tiene funciones triviales que se llaman en un cuerpo de bucle profundo millones o miles de millones de veces, la sobrecarga de la llamada a la función comienza a ser grande.
Como siempre, la clave es crear perfiles/medir para determinar si la integración de una función específica produce ganancias netas de rendimiento o no. Para funciones más “complejas” que no se denominan “a menudo”, la ganancia de la inserción puede ser inmensamente pequeña.
-
Mejor aún: “int a=sum(4,5);” puede convertirse en “int a=9;”. Además, leer y escribir variables a través de referencias suele ser más lento que leerlas y escribirlas directamente; en muchos casos, una función en línea se puede resolver para usar un acceso directo a variables más rápido (tenga en cuenta que en su escenario, si no está en línea, sería mejor pasar las variables por valor en lugar de por referencia, pero si la función lo hizo algo como “a+=b;” la referencia sería necesaria).
– Super gato
25 de octubre de 2010 a las 15:43
-
La declaración
reading and writing variables through references is generally slower than reading and writing them directly
es manera de general para ser verdad. También lo encuentro muy poco probable en la mayoría de las situaciones normales (vea qué fácil es generalizar en exceso).– Martín York
25 de octubre de 2010 a las 16:03
-
¿Y con qué frecuencia escribimos funciones como
sum()
? Creo que los accesores son un ejemplo mucho más relevante de lo que hace la inserción.– sbi
25/10/2010 a las 18:45
marca rescate
Hay múltiples razones para que la inserción sea más rápida, solo una de las cuales es obvia:
- Sin instrucciones de salto.
- una mejor localización, lo que resulta en una mejor utilización de la memoria caché.
- más oportunidades para que el optimizador del compilador realice optimizaciones, dejando valores en los registros, por ejemplo.
La utilización de la memoria caché también puede funcionar en su contra: si la inserción hace que el código sea más grande, hay más posibilidades de errores de memoria caché. Sin embargo, ese es un caso mucho menos probable.
Dependiendo de la implementación particular, no hay garantía absoluta de que exista una pila, por lo que los detalles de lo que sucede exactamente cuando se llama a una función pueden variar un poco entre plataformas. En cuanto a si los gastos generales son significativos… Depende de lo que estés haciendo.
– wroscrans
25 de octubre de 2010 a las 15:49
No siempre, los jinetes también están ahí. parashift.com/c++-faq-lite/inline-functions.html#faq-9.3
– Codificador tonto
25 de octubre de 2010 a las 15:51
No hay necesidad de preocuparse por esto. El compilador analiza el código y decide si una función debe estar en línea (ignora cualquier sugerencia que pueda dar). Como el compilador sabe mucho más acerca de cómo se llama la función, puede hacer un análisis informado de costo/beneficio y decidir caso por caso si vale la pena el esfuerzo de incorporar el código. Hay NO Se requiere interacción del usuario (ni debe intentar vencer al compilador).
– Martín York
25 de octubre de 2010 a las 15:57
@Martin York: No está preguntando si debe alinear una función, está preguntando por qué es importante alinear. Como tal, es una cosa perfectamente válida para tratar de entender.
– Chris Pitman
25 de octubre de 2010 a las 16:21
@ Michael Foukarakis: Nadie apagó
inlining the feature
. El compilador ya no usa elinline keyword
para determinar el clima para alinear el código. Por lo tanto, el compilador definitivamente lo está ignorando (a menos que lo obligue a hacerlo de otra manera).– Martín York
26 de octubre de 2010 a las 17:28