Como dicen otros, primero debe medir el rendimiento de su programa, y probablemente no encontrará ninguna diferencia en la práctica.
Aún así, desde un nivel conceptual, pensé que aclararía algunas cosas que se combinan en su pregunta. En primer lugar, preguntas:
¿Los costos de las llamadas a funciones siguen siendo importantes en los compiladores modernos?
Observe las palabras clave "función" y "compiladores". Su presupuesto es sutilmente diferente:
Recuerde que el costo de una llamada al método puede ser significativo, dependiendo del idioma.
Se trata de métodos , en el sentido orientado a objetos.
Si bien la "función" y el "método" a menudo se usan de manera intercambiable, existen diferencias en lo que respecta a su costo (que está preguntando) y cuando se trata de la compilación (que es el contexto que dio).
En particular, necesitamos saber acerca del despacho estático frente al despacho dinámico . Ignoraré las optimizaciones por el momento.
En un lenguaje como C, generalmente llamamos a funciones con despacho estático . Por ejemplo:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Cuando el compilador ve la llamada foo(y)
, sabe a qué función foo
se refiere ese nombre, por lo que el programa de salida puede saltar directamente a la foo
función, lo cual es bastante barato. Eso es lo que significa el envío estático .
La alternativa es el despacho dinámico , donde el compilador no sabe a qué función se llama. Como ejemplo, aquí hay un código Haskell (¡ya que el equivalente en C sería desordenado!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Aquí la bar
función está llamando a su argumento f
, que podría ser cualquier cosa. Por lo tanto, el compilador no puede simplemente compilar bar
a una instrucción de salto rápido, porque no sabe a dónde saltar. En cambio, el código que generamos bar
desreferenciará f
para averiguar a qué función está apuntando, y luego saltará a él. Eso es lo que significa el despacho dinámico .
Ambos ejemplos son para funciones . Usted mencionó métodos , que pueden considerarse como un estilo particular de función distribuida dinámicamente. Por ejemplo, aquí hay algo de Python:
class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
La y.foo()
llamada utiliza despacho dinámico, ya que busca el valor de la foo
propiedad en el y
objeto y llama a lo que encuentre; no sabe que y
tendrá clase A
, o que la A
clase contiene un foo
método, por lo que no podemos saltar directamente a ella.
OK, esa es la idea básica. Tenga en cuenta que el despacho estático es más rápido que el despacho dinámico, independientemente de si compilamos o interpretamos; en igualdad de condiciones. La desreferenciación conlleva un costo adicional en ambos sentidos.
Entonces, ¿cómo afecta esto a los compiladores modernos y optimizadores?
Lo primero a tener en cuenta es que el despacho estático se puede optimizar más: cuando sabemos a qué función estamos saltando, podemos hacer cosas como la alineación. Con el despacho dinámico, no sabemos que estamos saltando hasta el tiempo de ejecución, por lo que no hay mucha optimización que podamos hacer.
En segundo lugar, es posible en algunos idiomas inferir dónde terminarán algunos despachos dinámicos y, por lo tanto, optimizarlos en despacho estático. Esto nos permite realizar otras optimizaciones como la alineación, etc.
En el ejemplo anterior de Python, tal inferencia es bastante inútil, ya que Python permite que otro código anule las clases y propiedades, por lo que es difícil inferir mucho de lo que se mantendrá en todos los casos.
Si nuestro lenguaje nos permite imponer más restricciones, por ejemplo limitando y
a la clase A
usando una anotación, entonces podríamos usar esa información para inferir la función objetivo. En los idiomas con subclases (¡que es casi todos los idiomas con clases!) Eso en realidad no es suficiente, ya y
que en realidad puede tener una (sub) clase diferente, por lo que necesitaríamos información adicional como las final
anotaciones de Java para saber exactamente qué función se llamará.
Haskell no es un lenguaje OO, pero podemos inferir el valor de f
mediante la inserción bar
(que se envía estáticamente ) en main
, sustituyendo foo
por y
. Dado que el objetivo de foo
in main
es estáticamente conocido, la llamada se despacha estáticamente, y probablemente se alineará y optimizará por completo (dado que estas funciones son pequeñas, es más probable que el compilador las incorpore; aunque no podemos contar con eso en general )
Por lo tanto, el costo se reduce a:
- ¿El idioma despacha su llamada de forma estática o dinámica?
- Si es lo último, ¿permite el lenguaje que la implementación infiera el objetivo utilizando otra información (por ejemplo, tipos, clases, anotaciones, líneas, etc.)?
- ¿Qué tan agresivamente se puede optimizar el despacho estático (inferido o no)?
Si está utilizando un lenguaje "muy dinámico", con mucha distribución dinámica y pocas garantías disponibles para el compilador, entonces cada llamada tendrá un costo. Si está utilizando un lenguaje "muy estático", entonces un compilador maduro producirá un código muy rápido. Si está en el medio, puede depender de su estilo de codificación y de lo inteligente que sea la implementación.