Algunas veces los compiladores llaman a funciones en línea. Eso significa que mueven el código de la función llamada a la función de llamada. Esto hace las cosas un poco más rápidas porque no hay necesidad de empujar y hacer estallar cosas dentro y fuera de la pila de llamadas.
Entonces mi pregunta es, ¿por qué los compiladores no integran todo? Supongo que haría que el ejecutable sea notablemente más rápido.
La única razón por la que puedo pensar es un ejecutable significativamente más grande, pero ¿realmente importa en estos días con cientos de GB de memoria? ¿No vale la pena el rendimiento mejorado?
¿Hay alguna otra razón por la cual los compiladores no solo incorporan todas las llamadas a funciones?
optimization
compiler
Aviv Cohn
fuente
fuente
Isn't the improved performance worth it?
Para un método que ejecutará un bucle 100 veces y reducirá algunos números serios, la sobrecarga de mover 2 o 3 argumentos a los registros de la CPU no es nada.Respuestas:
Primero, tenga en cuenta que uno de los principales efectos de inline es que permite realizar más optimizaciones en el sitio de la llamada.
Para su pregunta: hay cosas que son difíciles o incluso imposibles de alinear:
bibliotecas vinculadas dinámicamente
funciones determinadas dinámicamente (despacho dinámico, llamado a través de punteros de función)
funciones recursivas (lata de recursión de cola)
funciones para las que no tiene el código (pero la optimización del tiempo de enlace permite esto para algunos de ellos)
Entonces, la alineación no solo tiene efectos beneficiosos:
El ejecutable más grande significa más espacio en el disco y mayor tiempo de carga
un ejecutable más grande significa un aumento de la presión de la caché (tenga en cuenta que incluir funciones lo suficientemente pequeñas como los captadores simples puede disminuir el tamaño del ejecutable y la presión de la caché)
Y finalmente, para las funciones que requieren un tiempo no trivial de ejecución, la ganancia simplemente no vale la pena.
fuente
Una limitación importante es el polimorfismo de tiempo de ejecución. Si ocurre un despacho dinámico cuando escribe
foo.bar()
, es imposible alinear la llamada al método. Esto explica por qué los compiladores no lo integran todo.Las llamadas recursivas tampoco pueden integrarse fácilmente.
La integración de módulos cruzados también es difícil de realizar por razones técnicas (la recompilación incremental sería imposible por ejemplo)
Sin embargo, los compiladores hacen muchas cosas en línea.
fuente
Primero, no siempre puede estar en línea, por ejemplo, las funciones recursivas pueden no estar siempre en línea (pero un programa que contiene una definición recursiva
fact
con solo una impresión defact(8)
podría estar en línea).Entonces, la alineación no siempre es beneficiosa. Si el compilador se alinea tanto que el código de resultado es lo suficientemente grande como para que sus partes activas no encajen en, por ejemplo, la memoria caché de instrucciones L1, podría ser mucho más lento que la versión no en línea (que fácilmente encajaría en la memoria caché L1) ... Además, los procesadores recientes son muy rápidos en la ejecución de una
CALL
instrucción de máquina (al menos en una ubicación conocida, es decir, una llamada directa, no una llamada a través del puntero).Finalmente, la alineación completa requiere un análisis completo del programa. Esto podría no ser posible (o es demasiado costoso). Con C o C ++ compilado por GCC (y también con Clang / LLVM ) necesita habilitar la optimización del tiempo de enlace (compilando y enlazando con, por ejemplo
g++ -flto -O2
) y eso lleva bastante tiempo de compilación.fuente
Por sorprendente que parezca, incluir todo no necesariamente reduce el tiempo de ejecución. El mayor tamaño de su código puede dificultar que la CPU mantenga todo su código en su caché a la vez. Una pérdida de caché en su código se vuelve más probable y una pérdida de caché es costosa. Esto empeora mucho si sus funciones potencialmente en línea son grandes.
De vez en cuando, he tenido mejoras notables en el rendimiento al tomar grandes porciones de código marcado como 'en línea' de los archivos de encabezado, ponerlos en el código fuente, por lo que el código está solo en un lugar en lugar de en cada sitio de llamada. Luego, el caché de la CPU se usa mejor y también obtienes un mejor tiempo de compilación ...
fuente
Incluir todo no significaría solo un mayor consumo de memoria de disco, sino también un mayor consumo de memoria interna, que no es tan abundante. Recuerde que el código también se basa en la memoria en el segmento de código; si se llama a una función desde 10000 lugares (digamos los de bibliotecas estándar en un proyecto bastante grande), entonces el código para esa función ocupa 10000 veces más memoria interna.
Otra razón podría ser los compiladores JIT; Si todo está en línea, entonces no hay puntos calientes para compilar dinámicamente.
fuente
Uno, hay ejemplos simples en los que la alineación de todo funcionará muy mal. Considere este simple código C:
Adivina lo que te hará todo en línea.
Luego, asume que la alineación hará que las cosas sean más rápidas. Ese es el caso a veces, pero no siempre. Una razón es que el código que se ajusta a la caché de instrucciones se ejecuta mucho más rápido. Si llamo a una función desde 10 lugares, siempre ejecutaré el código que está en el caché de instrucciones. Si está en línea, las copias están por todas partes y se ejecutan mucho más lentamente.
Hay otros problemas: la alineación produce grandes funciones. Las funciones enormes son mucho más difíciles de optimizar. Obtuve ganancias considerables en el código crítico de rendimiento al ocultar las funciones en un archivo separado para evitar que el compilador las incluya. Como resultado, el código generado para estas funciones era mucho mejor cuando estaban ocultas.
Por cierto. No tengo "cientos de GB de memoria". Mi computadora de trabajo ni siquiera tiene "cientos de GB de espacio en el disco duro". Y si mi aplicación tuviera "cientos de GB de memoria", tomaría 20 minutos cargar la aplicación en la memoria.
fuente