performance - La diferencia en el rendimiento del código de aceleración compilado se ejecutó desde ghci y shell
haskell profiling (1)
Problema
Hola, estoy usando la biblioteca de aceleración para crear una aplicación que permite al usuario llamar de forma interactiva a funciones que procesan imágenes, por eso estoy basando y ampliando ghci usando ghc api.
El problema es que al ejecutar el ejecutable compilado desde el shell, los cálculos se realizan en 100 ms (un poco menos de 80), mientras se ejecuta el mismo código compilado dentro de ghci toma más de 100 ms (un promedio más de 140) para terminar.
Recursos
código de muestra + registros de ejecución: https://gist.github.com/zgredzik/15a437c87d3d8d03b8fc
Descripción
En primer lugar: las pruebas se llevaron a cabo después de compilar el kernel CUDA (la compilación en sí misma agregó 2 segundos adicionales, pero ese no es el caso).
Al ejecutar el ejecutable compilado desde el shell, los cálculos se realizan en menos de 10 ms. (la shell first run
y la second shell run
tienen argumentos diferentes para garantizar que los datos no se almacenaron en caché en ningún lugar).
Al intentar ejecutar el mismo código desde ghci y manipular los datos de entrada, los cálculos toman más de 100 ms. Entiendo que el código interpretado es más lento que el compilado, pero estoy cargando el mismo código compilado dentro de la sesión ghci y llamando al mismo nivel superior de enlace (función packedFunction
). Lo he tipeado explícitamente para asegurarme de que esté especializado (los mismos resultados que con el pragma ESPECIALIZADO).
Sin embargo, los cálculos toman menos de 10 ms si ejecuto la función main
en ghci (incluso al cambiar los datos de entrada con :set args
entre llamadas consecutivas).
Compiló Main.hs
con ghc -o main Main.hs -O2 -dynamic -threaded
Me pregunto de dónde viene la sobrecarga. ¿Alguien tiene alguna sugerencia de por qué esto está sucediendo?
Una versión simplificada del ejemplo publicado por remdezx :
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Array.Accelerate as A
import Data.Array.Accelerate.CUDA as C
import Data.Time.Clock (diffUTCTime, getCurrentTime)
main :: IO ()
main = do
start <- getCurrentTime
print $ C.run $ A.maximum $ A.map (+1) $ A.use (fromList (Z:.1000000) [1..1000000] :: Vector Double)
end <- getCurrentTime
print $ diffUTCTime end start
Cuando lo compilo y lo ejecuto, toma 0,09 segundos para terminar.
$ ghc -O2 Main.hs -o main -threaded
[1 of 1] Compiling Main ( Main.hs, Main.o )
Linking main ...
$ ./main
Array (Z) [1000001.0]
0.092906s
Pero cuando lo precompilo y ejecuto en intérprete, toma 0,25s
$ ghc -O2 Main.hs -c -dynamic
$ ghci Main
ghci> main
Array (Z) [1000001.0]
0.258224s
Investigué accelerate
y accelerate-cuda
y puse un código de depuración para medir un tiempo tanto bajo ghci como en una versión compilada y optimizada.
Los resultados se muestran a continuación, puede ver el seguimiento de la pila y los tiempos de ejecución.
ghci run
$ ghc -O2 -dynamic -c -threaded Main.hs && ghci
GHCi, version 7.8.3: http://www.haskell.org/ghc/ :? for help
…
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Ok, modules loaded: Main.
Prelude Main> Loading package transformers-0.3.0.0 ... linking ... done.
…
Loading package array-0.5.0.0 ... linking ... done.
(...)
Loading package accelerate-cuda-0.15.0.0 ... linking ... done.
>>>>> run
>>>>> runAsyncIn.execute
>>>>> runAsyncIn.seq ctx
<<<<< runAsyncIn.seq ctx: 4.1609e-2 CPU 0.041493s TOTAL
>>>>> runAsyncIn.seq a
<<<<< runAsyncIn.seq a: 1.0e-6 CPU 0.000001s TOTAL
>>>>> runAsyncIn.seq acc
>>>>> convertAccWith True
<<<<< convertAccWith: 0.0 CPU 0.000017s TOTAL
<<<<< runAsyncIn.seq acc: 2.68e-4 CPU 0.000219s TOTAL
>>>>> evalCUDA
>>>>> push
<<<<< push: 0.0 CPU 0.000002s TOTAL
>>>>> evalStateT
>>>>> runAsyncIn.compileAcc
>>>>> compileOpenAcc
>>>>> compileOpenAcc.traveuseAcc.Alet
>>>>> compileOpenAcc.traveuseAcc.Use
>>>>> compileOpenAcc.traveuseAcc.use3
>>>>> compileOpenAcc.traveuseAcc.use1
<<<<< compileOpenAcc.traveuseAcc.use1: 0.0 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.use2
>>>>> compileOpenAcc.traveuseAcc.seq arr
<<<<< compileOpenAcc.traveuseAcc.seq arr: 0.105716 CPU 0.105501s TOTAL
>>>>> useArrayAsync
<<<<< useArrayAsync: 1.234e-3 CPU 0.001505s TOTAL
<<<<< compileOpenAcc.traveuseAcc.use2: 0.108012 CPU 0.108015s TOTAL
<<<<< compileOpenAcc.traveuseAcc.use3: 0.108539 CPU 0.108663s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Use: 0.109375 CPU 0.109005s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Fold1
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Fold1: 2.059e-3 CPU 0.002384s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Alet: 0.111434 CPU 0.112034s TOTAL
<<<<< compileOpenAcc: 0.11197 CPU 0.112615s TOTAL
<<<<< runAsyncIn.compileAcc: 0.11197 CPU 0.112833s TOTAL
>>>>> runAsyncIn.dumpStats
<<<<< runAsyncIn.dumpStats: 2.0e-6 CPU 0.000001s TOTAL
>>>>> runAsyncIn.executeAcc
>>>>> executeAcc
<<<<< executeAcc: 8.96e-4 CPU 0.00049s TOTAL
<<<<< runAsyncIn.executeAcc: 9.36e-4 CPU 0.0007s TOTAL
>>>>> runAsyncIn.collect
<<<<< runAsyncIn.collect: 0.0 CPU 0.000027s TOTAL
<<<<< evalStateT: 0.114156 CPU 0.115327s TOTAL
>>>>> pop
<<<<< pop: 0.0 CPU 0.000002s TOTAL
>>>>> performGC
<<<<< performGC: 5.7246e-2 CPU 0.057814s TOTAL
<<<<< evalCUDA: 0.17295 CPU 0.173943s TOTAL
<<<<< runAsyncIn.execute: 0.215475 CPU 0.216563s TOTAL
<<<<< run: 0.215523 CPU 0.216771s TOTAL
Array (Z) [1000001.0]
0.217148s
Prelude Main> Leaving GHCi.
compilación de código compilado
$ ghc -O2 -threaded Main.hs && ./Main
[1 of 1] Compiling Main ( Main.hs, Main.o )
Linking Main ...
>>>>> run
>>>>> runAsyncIn.execute
>>>>> runAsyncIn.seq ctx
<<<<< runAsyncIn.seq ctx: 4.0639e-2 CPU 0.041498s TOTAL
>>>>> runAsyncIn.seq a
<<<<< runAsyncIn.seq a: 1.0e-6 CPU 0.000001s TOTAL
>>>>> runAsyncIn.seq acc
>>>>> convertAccWith True
<<<<< convertAccWith: 1.2e-5 CPU 0.000005s TOTAL
<<<<< runAsyncIn.seq acc: 1.15e-4 CPU 0.000061s TOTAL
>>>>> evalCUDA
>>>>> push
<<<<< push: 2.0e-6 CPU 0.000002s TOTAL
>>>>> evalStateT
>>>>> runAsyncIn.compileAcc
>>>>> compileOpenAcc
>>>>> compileOpenAcc.traveuseAcc.Alet
>>>>> compileOpenAcc.traveuseAcc.Use
>>>>> compileOpenAcc.traveuseAcc.use3
>>>>> compileOpenAcc.traveuseAcc.use1
<<<<< compileOpenAcc.traveuseAcc.use1: 0.0 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.use2
>>>>> compileOpenAcc.traveuseAcc.seq arr
<<<<< compileOpenAcc.traveuseAcc.seq arr: 3.6651e-2 CPU 0.03712s TOTAL
>>>>> useArrayAsync
<<<<< useArrayAsync: 1.427e-3 CPU 0.001427s TOTAL
<<<<< compileOpenAcc.traveuseAcc.use2: 3.8776e-2 CPU 0.039152s TOTAL
<<<<< compileOpenAcc.traveuseAcc.use3: 3.8794e-2 CPU 0.039207s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Use: 3.8808e-2 CPU 0.03923s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Fold1
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 2.0e-6 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 2.0e-6 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0.000001s TOTAL
>>>>> compileOpenAcc.traveuseAcc.Avar
<<<<< compileOpenAcc.traveuseAcc.Avar: 0.0 CPU 0.000001s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Fold1: 1.342e-3 CPU 0.001284s TOTAL
<<<<< compileOpenAcc.traveuseAcc.Alet: 4.0197e-2 CPU 0.040578s TOTAL
<<<<< compileOpenAcc: 4.0248e-2 CPU 0.040895s TOTAL
<<<<< runAsyncIn.compileAcc: 4.0834e-2 CPU 0.04103s TOTAL
>>>>> runAsyncIn.dumpStats
<<<<< runAsyncIn.dumpStats: 0.0 CPU 0s TOTAL
>>>>> runAsyncIn.executeAcc
>>>>> executeAcc
<<<<< executeAcc: 2.87e-4 CPU 0.000403s TOTAL
<<<<< runAsyncIn.executeAcc: 2.87e-4 CPU 0.000488s TOTAL
>>>>> runAsyncIn.collect
<<<<< runAsyncIn.collect: 9.2e-5 CPU 0.000049s TOTAL
<<<<< evalStateT: 4.1213e-2 CPU 0.041739s TOTAL
>>>>> pop
<<<<< pop: 0.0 CPU 0.000002s TOTAL
>>>>> performGC
<<<<< performGC: 9.41e-4 CPU 0.000861s TOTAL
<<<<< evalCUDA: 4.3308e-2 CPU 0.042893s TOTAL
<<<<< runAsyncIn.execute: 8.5154e-2 CPU 0.084815s TOTAL
<<<<< run: 8.5372e-2 CPU 0.085035s TOTAL
Array (Z) [1000001.0]
0.085169s
Como podemos ver, hay dos problemas principales: evaluación de fromList (Z:.1000000) [1..1000000] :: Vector Double
que toma 69 ms extra bajo ghci (106ms - 37ms), y performGC
llamada performGC
que requiere 57 ms adicionales (58 ms - 1 ms). Estos dos resumen la diferencia entre ejecución bajo ghci y en una versión compilada.
Supongo que, en el programa compilado, RTS administra la memoria de una manera diferente que en ghci, por lo que la asignación y el gc pueden ser más rápidos. También podemos probar solo esta parte que evalúa el código siguiente (no requiere CUDA en absoluto):
import Data.Array.Accelerate.Array.Sugar
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import System.Mem (performGC)
main :: IO ()
main = do
measure $ seq (fromList (Z:.1000000) [1..1000000] :: Vector Double) $ return ()
measure $ performGC
measure action = do
start <- getCurrentTime
action
end <- getCurrentTime
print $ diffUTCTime end start
Resultados:
- el vector de evaluación toma 0.121653s bajo ghci y 0.035162s en una versión compilada
- performGC toma 0.044876s bajo ghci y 0.00031s en una versión compilada.
Esta podría ser otra pregunta, pero tal vez alguien sepa: ¿podemos sintonizar de alguna manera el recolector de basura para trabajar más rápido bajo ghci?