2012-02-17 15 views
15

Tengo un pequeño marco de prueba. Ejecuta un ciclo que hace lo siguiente:Acelerar runhaskell

  1. Genera un pequeño archivo fuente Haskell.

  2. Ejecute esto con runhaskell. El programa genera varios archivos de disco.

  3. Procese los archivos de disco recién generados.

Esto ocurre varias docenas de veces. Resulta que runhaskell ocupa la gran mayoría del tiempo de ejecución del programa.

Por un lado, el hecho de que runhaskell logra cargar un archivo del disco, convertirlo en token, analizarlo, hacer un análisis de dependencia, cargar 20KB más de texto del disco, ensamblar y analizar todo esto, realizar inferencia de tipo completo, verificar tipos, desugar a Core, enlazar contra código de máquina compilado, y ejecutarlo en un intérprete, todo dentro de 2 segundos de tiempo de pared, en realidad es bastante impresionante cuando lo piensas. Por otro lado, todavía quiero que sea más rápido. ;-)

La compilación del probador (el programa que ejecuta el ciclo anterior) produjo una pequeña diferencia de rendimiento. La compilación de los 20 KB del código de biblioteca con el que se vinculan los scripts produjo una mejora bastante más notable. Pero aún tarda aproximadamente 1 segundo por invocación de runhaskell.

Los archivos Haskell generados tienen poco más de 1 KB cada uno, pero solo una parte del archivo realmente cambia. Quizás compilar el archivo y usar el interruptor -e de GHC sería más rápido?

Alternativamente, ¿tal vez sea la sobrecarga de crear y destruir repetidamente muchos procesos del sistema operativo, lo que está ralentizando esto? Cada invocación de runhaskell presuntamente hace que el sistema operativo explore la ruta de búsqueda del sistema, localice el archivo binario necesario, lo cargue en la memoria (seguramente esto ya está en la memoria caché de disco), lo conecte con cualquier DLL y lo encienda. ¿Hay alguna forma en que pueda (fácilmente) mantener en ejecución una instancia de GHC, en lugar de tener que crear y destruir constantemente el proceso del sistema operativo?

En última instancia, supongo que siempre existe la API de GHC. Pero, como yo lo entiendo, eso es terriblemente difícil de usar, altamente indocumentado y propenso a cambios radicales en cada lanzamiento de punto menor de GHC. La tarea que intento realizar es muy simple, así que no quiero hacer las cosas más complejas de lo necesario.

Sugerencias?

Actualización: Cambio a GHC -e (es decir, ahora todo se compila excepto la frase siendo ejecutado) no hizo ninguna diferencia de rendimiento medible. Parece bastante claro en este punto que todo es sobrecarga del sistema operativo. Me pregunto si podría quizás crear una tubería del probador a GHCi y así hacer uso de un solo proceso del sistema operativo ...

+0

Todo su flujo de trabajo no se ve exactamente orientado al rendimiento, ¿o sí? ¿Por qué tienes que crear el código Haskell? – leftaroundabout

+3

¡Obviamente necesita un daemon de GHC! : p (algunas personas que conozco solían bromear sobre la creación de un daemon grep para evitar la sobrecarga de llamar continuamente a grep durante el arranque, etc.) – ivanm

+1

+1 para un intento de optimización justificado y bien ejecutado. – delnan

Respuesta

9

Muy bien, tengo una solución: He creado un único proceso GHCi y conectado su stdin a un tubo, de modo que pueda enviar expresiones para evaluar interactivamente.

Varias refactorizaciones de programas bastante grandes más tarde, y todo el conjunto de pruebas ahora toma aproximadamente 8 segundos para ejecutarse, en lugar de 48 segundos. Eso hará por mí! :-D

(Para cualquier otra persona tratando de hacer esto: Por el amor de Dios, recuerde que debe pasar el parámetro -v0 a GHCi, o que obtendrá un GHCi bienvenida bandera Extrañamente, si se ejecuta de forma interactiva GHCi! , incluso con -v0 todavía aparece el símbolo del sistema, pero cuando está conectado a una tubería de la línea de comandos se desvanece; estoy suponiendo que esto es una característica de diseño útil en lugar de un accidente al azar)


Por supuesto, el medio. La razón por la que voy por esta extraña ruta es que quiero capturar stdout y stderr en un archivo. Usando RunHaskell, eso es bastante fácil; solo pase las opciones apropiadas al crear el proceso secundario. Pero ahora todos de los casos de prueba están siendo ejecutados por un solo proceso del sistema operativo, por lo que no hay una forma obvia de redirigir stdin y stdout.

La solución que se me ocurrió fue dirigir toda la salida de prueba a un solo archivo, y entre las pruebas, GHCi imprimió una cadena mágica que (¡espero!) No aparecerá en la salida de prueba. Luego salga de GHCi, aspire el archivo y busque las cadenas mágicas para poder cortar el archivo en trozos adecuados.

+0

¿Puede cambiar las funciones de prueba para que tomen los identificadores de salida y error en lugar de escribir directamente en stdout y stderr? – Alex

2

Si la mayoría de los archivos fuente permanecen sin cambios, posiblemente pueda usar el -fobject-code de GHC (posiblemente junto con el indicador -outputdir) para compilar algunos de los archivos de la biblioteca.

+0

Como dije, ya compilé los 20 KB del código de la biblioteca. Eso redujo el tiempo de ejecución de 2 segundos a 1 segundo. Pero me gustaría reducir esto aún más si hay una manera fácil de hacerlo. – MathematicalOrchid

+0

@MathematicalOrchid Oh, extrañé un poco lo siento: s – ivanm

0

Si llama a runhaskell toma tanto tiempo, ¿entonces quizás debería eliminarlo por completo?

Si realmente necesita trabajar con el cambio de código Haskell, puede intentar lo siguiente.

  1. Cree un conjunto de módulos con contenidos variables según sea necesario.
  2. Cada módulo debe exportar su función principal
  3. El módulo envoltorio adicional debe ejecutar el módulo correcto del conjunto en función de los argumentos de entrada. Cada vez que desee ejecutar una única prueba, usaría argumentos diferentes.
  4. Todo el programa se compila estáticamente

módulo Ejemplo:

module Tester where 

import Data.String.Interpolation -- package Interpolation 

submodule nameSuffix var1 var2 = [str| 
module Sub$nameSuffix$ where 

someFunction x = $var1$ * x 
anotherFunction v | v == $var2$ = v 
        | otherwise = error ("anotherFunction: argument is not " ++ $:var2$) 

|] 

modules = [ let suf = (show var1 ++ "_" ++ show var2) in (suf,submodule suf var1 var2) | var1 <- [1..10], var2 <- [1..10]] 

writeModules = mapM_ (\ (file,what) -> writeFile file what) modules 
+0

Eso no va a funcionar. Algunos de los programas de prueba pueden bloquearse; si todo fuera un programa gigante, eso dejaría de funcionar. Además, quiero capturar 'stdout' y' stderr' de cada prueba y registrarlos en el archivo. Si no fuera por eso, entonces sí, podría generar todo como un solo programa Haskell gigante. Eso sería mucho más fácil ... – MathematicalOrchid

+0

@MathematicalOrchid: Usted vuelve a ejecutar el programa para cada prueba, de modo que mientras todo se compile estará bien. Con respecto a la redirección: ¿qué está mal con './testRunner testNumber123 2> stderr.txt 1> stdout.txt'? – Tener

+0

¿Qué significa "bloqueo"? Debería poder integrar todas sus pruebas en un solo programa e invocarlas con un corredor de prueba de primer nivel que se ocupa de redirigir 'stdout' y' stderr' y recuperarse de bloqueos. – pat

0

Si las pruebas están bien aisladas entre sí, puede poner todo el código de prueba en un solo programa e invocar runhaskell una vez. Es posible que esto no funcione si se crean algunas pruebas basadas en los resultados de otros, o si algunas pruebas llaman al unsafeCrash.

que presumen su código generado es el siguiente

module Main where 
boilerplate code 
main = do_something_for_test_3 

Usted puede poner el código de todas las pruebas en un solo archivo. Cada generador de códigos de prueba es responsable de escribir do_something_for_test_N.

module Main where 
boilerplate code 

-- Run each test in its own directory 
withTestDir d m = do 
    cwd <- getCurrentDirectory 
    createDirectory d 
    setCurrentDirectory d 
    m 
    setCurrentDirectory cwd 

-- ["test1", "test2", ...] 
dirNames = map ("test"++) $ map show [1..] 
main = zipWithM withTestDir dirNames tests 

-- Put tests here 
tests = 
    [ do do_something_for_test_1 
    , do do_something_for_test_2 
    , ... 
    ] 

Ahora sólo incurren en la sobrecarga de una sola llamada a runhaskell.

3

Puede encontrar algún código útil en TBC.Tiene diferentes ambiciones, en particular, la repetición de prueba de chatarra y proyectos de prueba que pueden no compilarse por completo, pero podría ampliarse con una función de directorio de vigilancia. Las pruebas se ejecutan en GHCi pero se usan objetos construidos con éxito por cabal ("build build de Runghc").

Lo desarrollé para probar EDSL con hackers de tipo complicado, es decir, donde el levantamiento computacional pesado es realizado por otras bibliotecas.

Actualmente estoy actualizando a la última plataforma Haskell y recibo cualquier comentario o parche.