¿Los compiladores como Javac detectan automáticamente funciones puras y las paralelizan?

12

Se sabe que las funciones puras facilitan la parelización. ¿Qué tiene la programación funcional que la hace inherentemente adaptada a la ejecución paralela?

¿Son los compiladores como Javac lo suficientemente inteligentes como para detectar cuándo un método es una función pura? Siempre se pueden implementar clases que implementan interfaces funcionales como Function , pero tienen efectos secundarios.

Naveen
fuente
77
La pregunta no es solo si el compilador puede saber si una función es pura, sino también si puede programar de manera inteligente la ejecución paralela de funciones puras. No es suficiente disparar un nuevo hilo para cada uno: esto es ineficiente. GHC (Haskell) se ocupa de esto utilizando la pereza y los "hilos verdes"; Sinceramente, me sorprendería si algún lenguaje impuro lo intentara, dada la dificultad adicional de asegurarse de que los hilos puros se programaran correctamente con respecto al hilo impuro principal.
Ryan Reich
@RyanReich, ¿hay alguna ganancia de rendimiento al usar la programación funcional en un lenguaje funcional impuro como Java? ¿Son las ganancias de la programación funcional puramente funcionales como la modularidad?
Naveen
@RyanReich GHC trata el problema haciendo que el programador haga anotaciones cuando quieren paralelismo. La pureza implica que estas anotaciones nunca cambian la semántica, solo el rendimiento. (También hay mecanismos de concurrencia que pueden dar lugar al paralelismo, pero este es un caldero diferente de peces).
Derek Elkins dejó SE
@Naveen Existen otras ventajas para las funciones puras con respecto a la optimización, además del paralelismo, como el código de reordenamiento de mayor libertad, la memorización y la eliminación de la subexpresión común. Podría estar equivocado, pero dudo que javac intente detectar la pureza, ya que probablemente sea bastante raro en el código idiomático y algo difícil para todos, excepto los casos más triviales. Por ejemplo, necesita saber que no habrá ningún NullPointerExceptions. Los beneficios de las optimizaciones basadas en esto también son probablemente bastante pequeños para las aplicaciones Java típicas.
Derek Elkins salió del SE
66
javac es el compilador de java, que toma el código fuente de java y genera archivos de clase de código de bytes de java. Está bastante limitado en cuanto a lo que puede (y se supone que debe hacer). No tiene la libertad o los mecanismos subyacentes necesarios para introducir paralelismo en el archivo de clase de código de byte.
Erik Eidt

Respuestas:

33

son compiladores como Javac lo suficientemente inteligentes como para detectar cuándo un método es una función pura.

No se trata de "lo suficientemente inteligente". Esto se llama análisis de pureza y es demostrablemente imposible en el caso general: es equivalente a resolver el problema de detención.

Ahora, por supuesto, los optimizadores hacen cosas demostrablemente imposibles todo el tiempo, "demostrablemente imposibles en el caso general" no significa que nunca funcione, solo significa que no puede funcionar en todos los casos. Entonces, de hecho, existen algoritmos para verificar si una función es pura o no, es solo que la mayoría de las veces el resultado será "No sé", lo que significa que, por razones de seguridad y corrección, debe asumir que esta función particular podría ser impura.

E incluso en los casos en que se hace el trabajo, los algoritmos son complejos y costosos.

Entonces, ese es el problema # 1: solo funciona para casos especiales .

Problema # 2: Bibliotecas . Para que una función sea pura, solo puede llamar funciones puras (y esas funciones solo pueden llamar funciones puras, y así sucesivamente). Javac obviamente solo sabe sobre Java, y solo sabe sobre el código que puede ver. Entonces, si su función llama a una función en otra unidad de compilación, no puede saber si es pura o no. Si llama a una función escrita en otro idioma, no puede saberlo. Si llama a una función en una biblioteca que aún podría no estar instalada, no puede saberlo. Y así.

Esto solo funciona cuando tiene un análisis de todo el programa, cuando todo el programa está escrito en el mismo idioma y todo se compila de una vez. No puedes usar ninguna biblioteca.

Problema # 3: programación . Una vez que haya descubierto qué partes son puras, todavía tiene que programarlas para separar los hilos. O no. Iniciar y detener subprocesos es muy costoso (especialmente en Java). Incluso si mantiene un grupo de subprocesos y no los inicia o detiene, el cambio de contexto de subprocesos también es costoso. Debe asegurarse de que el cálculo se ejecutará significativamente más tiempo que el tiempo que lleva programar y cambiar de contexto, de lo contrario perderá rendimiento, no lo ganará.

Como probablemente ya haya adivinado, averiguar cuánto tiempo llevará un cálculo es imposible en el caso general (ni siquiera podemos determinar si tomará una cantidad de tiempo finita, y mucho menos cuánto tiempo) y difícil y costoso incluso en El caso especial.

Aparte: Javac y optimizaciones . Tenga en cuenta que la mayoría de las implementaciones de javac en realidad no realizan muchas optimizaciones. La implementación de javac de Oracle, por ejemplo, se basa en el motor de ejecución subyacente para realizar optimizaciones . Esto lleva a otro conjunto de problemas: digamos, javac decidió que una función particular es pura y es lo suficientemente costosa, por lo que la compila para que se ejecute en un hilo diferente. Luego, aparece el optimizador de la plataforma (por ejemplo, el compilador HotSpot C2 JIT) y optimiza toda la función. Ahora, tienes un hilo vacío que no hace nada. O, imagine, nuevamente, javac decide programar una función en un hilo diferente, y el optimizador de plataforma podría optimizarlo completamente, excepto que no puede realizar la alineación a través de los límites del hilo, por lo que ahora se ejecuta innecesariamente una función que podría optimizarse completamente.

Entonces, hacer algo como esto solo tiene sentido si tiene un único compilador que realiza la mayoría de las optimizaciones de una sola vez, para que el compilador conozca y pueda explotar todas las diferentes optimizaciones en diferentes niveles y sus interacciones entre sí.

Tenga en cuenta que, por ejemplo, el compilador JIT HotSpot C2 realidad hace realizar alguna auto-vectorización, que es también una forma de auto-paralelización.

Jörg W Mittag
fuente
Bueno, dependiendo de su definición de "función pura", puede permitirse el uso de funciones impuras en la implementación.
Deduplicador
@Deduplicator Bueno, dependiendo de su definición de definition, el uso de un dispar definitionde purityprobablemente sea oscuro
gato
1
Su problema n. ° 2 se invalida principalmente por el hecho de que prácticamente todas las optimizaciones se ejecutan mediante el JIT (obviamente lo sabe, pero ignórelo). Del mismo modo, el problema n. ° 3 queda parcialmente invalidado ya que JIT depende en gran medida de las estadísticas recopiladas por el intérprete. Estoy especialmente en desacuerdo con "No puedes usar ninguna biblioteca" ya que hay desoptimización para el rescate. Estoy de acuerdo en que la complejidad añadida sería un problema.
maaartinus
2
@maaartinus: Además, solo el final de mi respuesta es específico de javac. Específicamente hacer mención, por ejemplo, que "Esto sólo funciona, cuando se tiene el análisis de todo el programa, cuando todo el programa está escrito en el mismo idioma, y todo se compila a la vez de una sola vez." Esto es obviamente cierto para C2: solo trata con un lenguaje (código de byte JVM), y tiene acceso a todo el programa a la vez.
Jörg W Mittag
1
@ JörgWMittag Sé que el OP pregunta sobre javac, pero apuesto a que están asumiendo que javac es el responsable de las optimizaciones. Y que apenas saben que hay C2. No digo que tu respuesta sea mala. Es solo que dejar que javac haga cualquier optimización (excepto por trivialidades como el uso StringBuilder) no tiene sentido, así que lo descartaría y simplemente asumiría que el OP escribe javac pero significa Hotspot. Su problema # 2 es una buena razón para no optimizar nada en javac.
maaartinus
5

La respuesta votada no pudo notar una cosa. La comunicación síncrona entre hilos es extremadamente costosa. Si la función es capaz de ejecutarse a una velocidad de muchos millones de llamadas por segundo, en realidad le duele más paralelizarla en lugar de dejarla como está.

Desafortunadamente, la forma más rápida de comunicación síncrona entre subprocesos, utilizando bucles ocupados con variables atómicas, es ineficiente energéticamente. Si tiene que recurrir al uso de variables de condición para ahorrar energía, el rendimiento de su comunicación entre subprocesos se ve afectado.

Por lo tanto, el compilador no solo necesita determinar si una función es pura, sino que también debe estimar el tiempo de ejecución de la función para ver si la paralelización es una ganancia neta. Además, necesitaría elegir entre bucles ocupados utilizando variables atómicas o variables de condición. Y necesitaría crear hilos a sus espaldas.

Si crea los hilos dinámicamente, es aún más lento que usar variables de condición. Entonces, el compilador necesitaría configurar una cierta cantidad de hilos que ya se están ejecutando.

Entonces, la respuesta a su pregunta es no , los compiladores no son lo suficientemente "inteligentes" como para paralelizar automáticamente las funciones puras, especialmente en el mundo de Java. ¡Son inteligentes al no paralelizarlos automáticamente!

juhist
fuente
55
Son inteligentes al no paralelizarlos automáticamente! " : Esto va demasiado lejos. Si bien es cierto que la paralelización en todos los puntos posibles solo por su propio bien generalmente sería ineficiente, un compilador inteligente identificaría una estrategia práctica de paralelización. Creo que la mayoría de la gente entiende esto, así que cuando hablamos de auto-paralelización, nos referimos a la paralelización auto-práctica.
Nat
@Nat: ridículamente demasiado difícil. Esto requeriría identificar funciones puras en la escala de tiempo de ejecución de cientos de milisegundos, y esperar que el compilador tenga alguna idea del tiempo de ejecución de los bucles que no tienen constantes en sus iteraciones (y los casos que desea no) es una tontería.
Joshua
Estoy de acuerdo: el comentario de @ Nat implica que la paralelización no necesariamente significa múltiples hilos, lo cual es cierto. El JIT podría, por ejemplo, en línea múltiples llamadas a una función pura e intercalar sus instrucciones de CPU en ciertos casos. Por ejemplo, si ambas llamadas a métodos obtienen una constante, se puede recuperar una vez y mantener en un registro de CPU para que utilicen ambas instancias del método. Las CPU modernas son bestias con numerosos registros de uso general e instrucciones especializadas que pueden ser muy útiles al optimizar el código.
1
@Joshua: mucho más fácil para un compilador JIT. El compilador JIT también puede descubrir que una función puede no ser pura, pero hasta ahora ninguna llamada ha invocado un comportamiento impuro.
gnasher729
Estoy de acuerdo con @Joshua. Tengo un algoritmo difícil de paralelizar en el trabajo. He tratado de paralelizarlo manualmente, incluso haciendo algunas aproximaciones simplificadoras (y, por lo tanto, modificando el algoritmo), y siempre he fallado miserablemente. Incluso un programa que dice si es posible paralelizar algo es extremadamente difícil, aunque sería mucho más simple que hacerlo en paralelo. Recuerde que estamos hablando de lenguajes de programación completos de Turing.
juhist