¿Por qué los compiladores no integran todo? [cerrado]

13

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?

Aviv Cohn
fuente
18
IDK sobre ti, pero no tengo cientos de GB de memoria simplemente por ahí.
Ampt
2
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.
Doval
55
Eres demasiado genérico, ¿"compiladores" significa "todos los compiladores" y "todo" significa realmente "todo"? Entonces, la respuesta es simple, hay situaciones en las que simplemente no se puede alinear. La recursión viene a la mente.
Otávio Décio
17
La localidad de caché es mucho más importante que la pequeña sobrecarga de llamadas de función.
SK-logic
3
¿La mejora del rendimiento realmente importa en estos días con cientos de GFLOPS de potencia de procesamiento?
mouviciel

Respuestas:

22

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.

Un programador
fuente
3
algunas llamadas recursivas pueden ser inline (llamadas de cola), pero todos pueden transformarse en iteración si se añade opcionalmente una pila explícita
monstruo de trinquete
@ratchetfreak, también puedes transformar alguna llamada recursiva no tail en tail one. Pero eso es para mí en el ámbito de la "difícil" (especialmente cuando tiene funciones co-recursivas o tiene que determinar dinámicamente dónde saltar para simular el retorno), pero eso no es imposible (simplemente establece un marco de continuación y considerando ese presente se hace más fácil).
Programador
11

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.

Simon Bergot
fuente
3
En línea a través de un despacho virtual es muy difícil, pero no imposible. Algunos compiladores de C ++ pueden hacerlo bajo ciertas circunstancias.
bstamour
2
... así como algunos compiladores JIT (desvirtualización).
Frank
@bstamour Cualquier compilador medio decente de cualquier idioma con las optimizaciones apropiadas enviará estáticamente, es decir, devirtualise, una llamada a un método virtual declarado en un objeto cuyo tipo dinámico se puede conocer en tiempo de compilación. Esto puede facilitar la alineación si la fase de desvirtualización ocurre antes de la (u otra) fase de alineación. Pero esto es trivial. ¿Había algo más a lo que te refieres? No veo cómo se puede lograr ninguna "Realización a través de un despacho virtual" real. Para línea, hay que saber el tipo estático - es decir devirtualise - por lo que la existencia de medios Inlining no es ningún despacho virtuales
underscore_d
9

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 factcon solo una impresión de fact(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 CALLinstrucció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.

Basile Starynkevitch
fuente
1
Para el registro, LLVM / Clang (y varios otros compiladores) también admite la optimización del tiempo de enlace .
Usted
Yo sé eso; LTO existió en el siglo anterior (IIRC, al menos en algún compilador propietario de MIPS).
Basile Starynkevitch
7

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 ...

Tom Tanner
fuente
esto parece simplemente repetir los puntos hechos y explicados en una respuesta anterior que fue publicada hace una hora
mosquito
1
¿Qué cachés? L1? L2? L3? ¿Cual es mas importante?
Peter Mortensen
1

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.

m3th0dman
fuente
1

Uno, hay ejemplos simples en los que la alineación de todo funcionará muy mal. Considere este simple código C:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

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.

gnasher729
fuente