¿Cuánto afectan las llamadas de función al rendimiento?

13

Extraer la funcionalidad en métodos o funciones es imprescindible para la modularidad, legibilidad e interoperabilidad del código, especialmente en OOP.

Pero esto significa que se realizarán más llamadas a funciones.

¿Cómo la división de nuestro código en métodos o funciones realmente impacta el rendimiento en los idiomas modernos * ?

* Los más populares: C, Java, C ++, C #, Python, JavaScript, Ruby ...

dabadaba
fuente
1
Creo que cada implementación de lenguaje que valga la pena ha estado en línea durante varias décadas. IOW, la sobrecarga es precisamente 0.
Jörg W Mittag
1
"a menudo se realizarán más llamadas a funciones" a menudo no es cierto, ya que muchas de esas llamadas tendrán su sobrecarga optimizada por los diversos compiladores / intérpretes que procesan su código e incorporan cosas. Si su idioma no tiene este tipo de optimizaciones, podría no considerarlo moderno.
Ixrec
2
¿Cómo afectará el rendimiento? O lo hará más rápido o más lento, o no lo cambiará, dependiendo del idioma específico que use y cuál es la estructura del código real y posiblemente de qué versión del compilador está usando y tal vez incluso qué plataforma usa ' sigue corriendo. Cada respuesta que obtenga será una variación de esta incertidumbre, con más palabras y más evidencia de apoyo.
GrandOpener
1
El impacto, si lo hay, es tan pequeño que usted, una persona, nunca lo notará. Hay otras cosas mucho más importantes de las que preocuparse. Al igual que las pestañas deben ser 5 o 7 espacios.
MetaFight

Respuestas:

21

Tal vez. El compilador podría decidir "oye, esta función solo se llama unas pocas veces, y se supone que debo optimizar la velocidad, así que simplemente alinearé esta función". Esencialmente, el compilador reemplazará la llamada a la función con el cuerpo de la función. Por ejemplo, el código fuente se vería así.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

El compilador decide en línea DoSomethingElse, y el código se convierte en

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Cuando las funciones no están en línea, sí, hay un golpe de rendimiento para realizar una llamada a la función. Sin embargo, es un golpe tan minúsculo que solo el código de rendimiento extremadamente alto se preocupará por las llamadas a funciones. Y en ese tipo de proyectos, el código generalmente se escribe en ensamblador.

Las llamadas a funciones (dependiendo de la plataforma) generalmente implican unos 10 segundos de instrucciones, y eso incluye guardar / restaurar la pila. Algunas llamadas a funciones consisten en instrucciones de salto y retorno.

Pero hay otras cosas que podrían afectar el rendimiento de las llamadas de función. Es posible que la función que se llama no se cargue en la memoria caché del procesador, lo que provoca una pérdida de memoria caché y obliga al controlador de memoria a tomar la función de la RAM principal. Esto puede causar un gran éxito para el rendimiento.

En pocas palabras: las llamadas a funciones pueden o no afectar el rendimiento. La única forma de saberlo es perfilando su código. No intente adivinar dónde están los puntos de código lentos, porque el compilador y el hardware tienen algunos trucos increíbles bajo la manga. Perfile el código para obtener la ubicación de los puntos lentos.

CHendrix
fuente
1
He visto con compiladores modernos (gcc, clang) en situaciones en las que realmente me importaba que crearan un código bastante malo para bucles dentro de una función grande . Extraer el bucle en una función estática no ayudó debido a la alineación. Extraer el bucle en una función externa creada en algunos casos mejoras de velocidad significativas (medibles en puntos de referencia).
gnasher729
1
Me resguardaría de esto y diría que OP debería tener cuidado con la optimización prematura
Patrick
1
@Patrick Bingo. Si va a optimizar, use un generador de perfiles para ver dónde están las secciones lentas. No lo adivines. Por lo general, puede tener una idea de dónde podrían estar las secciones lentas, pero confirme con un generador de perfiles.
CHendrix
@ gnasher729 Para resolver ese problema en particular, uno necesitará más que un generador de perfiles: también tendrá que aprender a leer el código de la máquina desmontada. Si bien existe una optimización prematura, no existe el aprendizaje prematuro (al menos en el desarrollo de software).
rwong
Es posible que tenga este problema si está llamando a una función un millón de veces, pero es más probable que tenga otros problemas que están teniendo un impacto significativamente mayor.
Michael Shaw
5

Esta es una cuestión de implementación del compilador o del tiempo de ejecución (y sus opciones) y no se puede decir con certeza.

Dentro de C y C ++, algunos compiladores alinearán las llamadas en función de la configuración de optimización; esto se puede ver trivialmente al examinar el ensamblaje generado cuando se miran herramientas como https://gcc.godbolt.org/

Otros lenguajes, como Java, tienen esto como parte del tiempo de ejecución. Esto es parte del JIT y se detalla en esta pregunta SO . En particular, mire las opciones de JVM para HotSpot

-XX:InlineSmallCode=n Incluya un método compilado previamente solo si el tamaño del código nativo generado es menor que este. El valor predeterminado varía con la plataforma en la que se ejecuta JVM.
-XX:MaxInlineSize=35 Tamaño máximo de código de bytes de un método para ser en línea.
-XX:FreqInlineSize=n Tamaño máximo de código de bytes de un método ejecutado con frecuencia para ser en línea. El valor predeterminado varía con la plataforma en la que se ejecuta JVM.

Entonces sí, el compilador HotSpot JIT incorporará métodos que cumplan ciertos criterios.

El impacto de esto es difícil de determinar ya que cada JVM (o compilador) puede hacer las cosas de manera diferente y tratar de responder con el trazo amplio de un lenguaje es casi seguro que está mal. El impacto solo se puede determinar de manera adecuada perfilando el código en el entorno de ejecución apropiado y examinando el resultado compilado.

Esto puede verse como un enfoque equivocado con CPython no alineado, pero Jython (Python ejecutándose en la JVM) tiene algunas llamadas en línea. Del mismo modo, MRI Ruby no se alinea mientras que JRuby sí, y ruby2c, que es un transpilador para ruby ​​en C ... que luego podría estar en línea o no, dependiendo de las opciones del compilador C que se compiló.

Los idiomas no están en línea. Las implementaciones pueden .

usuario227864
fuente
5

Estás buscando rendimiento en el lugar equivocado. El problema con las llamadas a funciones no es que cuesten mucho. Hay otro problema Las llamadas a funciones podrían ser absolutamente gratuitas, y aún tendría este otro problema.

Es que una función es como una tarjeta de crédito. Como puede usarlo fácilmente, tiende a usarlo más de lo que debería. Supongamos que lo llamas 20% más de lo que necesitas. Entonces, el software grande típico contiene varias capas, cada función de llamada en la capa a continuación, por lo que el factor de 1.2 puede ser compuesto por el número de capas. (Por ejemplo, si hay cinco capas y cada capa tiene un factor de desaceleración de 1.2, el factor de desaceleración compuesto es 1.2 ^ 5 o 2.5.) Esta es solo una forma de pensarlo.

Esto no significa que deba evitar las llamadas a funciones. Lo que significa es que, cuando el código está en funcionamiento, debe saber cómo encontrar y eliminar el desperdicio. Hay muchos consejos excelentes sobre cómo hacer esto en los sitios de stackexchange. Esto le da una de mis contribuciones.

AGREGADO: Pequeño ejemplo. Una vez trabajé en un equipo en software de fábrica que rastreaba una serie de órdenes de trabajo o "trabajos". Había una función JobDone(idJob)que podría decir si se realizó un trabajo. Se realizó un trabajo cuando se realizaron todas sus subtareas, y cada una de ellas se realizó cuando se realizaron todas sus suboperaciones. Todas estas cosas fueron rastreadas en una base de datos relacional. Una sola llamada a otra función podría extraer toda esa información, JobDonellamada esa otra función, ver si el trabajo estaba hecho y tirar el resto. Entonces la gente podría escribir fácilmente código como este:

while(!JobDone(idJob)){
    ...
}

o

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

¿Ve el punto? La función era tan "poderosa" y fácil de llamar que se llamaba demasiado. Entonces, el problema de rendimiento no eran las instrucciones que entraban y salían de la función. Era que tenía que haber una forma más directa de saber si se hacían trabajos. Nuevamente, este código podría haberse incrustado en miles de líneas de código inocente. Intentar arreglarlo con anticipación es lo que todos intentan hacer, pero es como tratar de lanzar dardos en una habitación oscura. En cambio, lo que necesita es ponerlo en funcionamiento y luego dejar que el "código lento" le diga qué es, simplemente tomándose el tiempo. Para eso uso pausas aleatorias .

Mike Dunlavey
fuente
1

Creo que realmente depende del idioma y de la función. Si bien los compiladores c y c ++ pueden incorporar muchas funciones, este no es el caso de Python o Java.

Si bien no conozco los detalles específicos de Java (excepto que todos los métodos son virtuales, pero le sugiero que verifique mejor la documentación), en Python estoy seguro de que no hay en línea, no hay optimización de recursión de cola y las llamadas a funciones son bastante caras.

Las funciones de Python son básicamente objetos ejecutables (y de hecho también puede definir el método call () para hacer que una instancia de objeto sea una función). Esto significa que hay bastante sobrecarga al llamarlos ...

PERO

cuando define variables dentro de funciones, el intérprete usa LOADFAST en lugar de la instrucción LOAD normal en el código de bytes, haciendo que su código sea más rápido ...

Otra cosa es que cuando define un objeto invocable, son posibles patrones como la memorización y pueden acelerar mucho su cálculo (a costa de usar más memoria). Básicamente siempre es una compensación. El costo de las llamadas de función también depende de los parámetros, porque determinan la cantidad de material que realmente tiene que copiar en la pila (por lo tanto, en c / c ++ es una práctica común pasar parámetros grandes como estructuras por punteros / referencia en lugar de por valor).

Creo que su pregunta es en la práctica demasiado amplia para ser respondida completamente en stackexchange.

Lo que le sugiero que haga es comenzar con un idioma y estudiar la documentación avanzada para comprender cómo las llamadas de función son implementadas por ese lenguaje específico.

Te sorprenderá la cantidad de cosas que aprenderás en este proceso.

Si tiene un problema específico, realice mediciones / perfiles y decida el clima, es mejor crear una función o copiar / pegar el código equivalente.

Si hace una pregunta más específica, creo que sería más fácil obtener una respuesta más específica.

ingframina
fuente
Citando: "Creo que su pregunta es en la práctica demasiado amplia para ser respondida completamente en stackexchange". ¿Cómo puedo reducirlo entonces? Me encantaría ver algunos datos reales que representan el impacto de la llamada a la función en el rendimiento. No me importa qué idioma, solo tengo curiosidad de ver una explicación más detallada, respaldada con datos si es posible, como dije.
dabadaba
El punto es que depende del idioma. En C y C ++, si se inline la función, el impacto es 0. Si no inline, que depende de sus parámetros, si está en la caché o no, etc ...
ingframin
1

Medí la sobrecarga de las llamadas directas y virtuales a la función C ++ en el Xenon PowerPC hace algún tiempo .

Las funciones en cuestión tenían un único parámetro y un único retorno, por lo que el paso de parámetros se produjo en los registros.

En pocas palabras, la sobrecarga de una llamada de función directa (no virtual) fue de aproximadamente 5,5 nanosegundos, o 18 ciclos de reloj, en comparación con una llamada de función en línea. La sobrecarga de una llamada de función virtual fue de 13,2 nanosegundos, o 42 ciclos de reloj, en comparación con en línea.

Es probable que estos tiempos sean diferentes en diferentes familias de procesadores. Mi código de prueba está aquí ; puedes ejecutar el mismo experimento en tu hardware. Use un temporizador de alta precisión como rdtsc para su implementación de CFastTimer; system time () no es lo suficientemente preciso.

Crashworks
fuente