performance haskell profiling ghci accelerate-haskell

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?