Diferencia en el rendimiento del código de aceleración compilado ejecutado desde ghci y shell


Problema

Hola, estoy usando accelerate library para crear una aplicación que permite al usuario llamar interactivamente a funciones que procesan imágenes, por eso estoy basándome y extendiendo ghci usando la api ghc.

El problema es que cuando se ejecuta el ejecutable compilado desde el shell los cálculos se realizan bajo 100ms (un poco menos de 80), mientras se ejecuta el mismo código compilado dentro de ghci toma más de 100ms (en promedio un poco más de 140) para terminar.

Recursos

Código de ejemplo + registros de ejecución: https://gist.github.com/zgredzik/15a437c87d3d8d03b8fc

Descripción

En primer lugar: las pruebas se ejecutaron después de que se compilara el núcleo CUDA (la compilación en sí agregó 2 segundos adicionales, pero ese no es el caso).

Cuando se ejecuta el ejecutable compilado desde el shell, los cálculos se realizan en menos de 10 ms. (shell first run y second shell run tienen diferentes argumentos pasado para asegurarse de que los datos no se almacenaron en caché en ningún lugar).

Al intentar ejecutar el mismo código desde ghci y juguetear con los datos de entrada, los cálculos toman más de 100ms. 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 enlace de nivel superior (packedFunction). Lo he escrito explícitamente para asegurarme de que es especializado (los mismos resultados que usar el pragma ESPECIALIZADO).

Sin embargo, los cálculos lo hacen tome menos de 10 ms si corro la función main en ghci (incluso cuando cambio los datos de entrada con :set args entre llamadas consecutivas).

Compilado el 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é está sucediendo esto?


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 me compilarlo y ejecutarlo toma 0,09 s para finalizar.

$ 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 Yo precompilar y ejecutar el intérprete toma 0,25 s

$ ghc -O2 Main.hs -c -dynamic
$ ghci Main
ghci> main
Array (Z) [1000001.0]
0.258224s
Author: Community, 2014-12-18

1 answers

Investigué accelerate y accelerate-cuda y puse algún código de depuración para medir un tiempo tanto bajo ghci como en una versión compilada y optimizada.

Los resultados están 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.

Código compilado run

$ 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: la evaluación de fromList (Z:.1000000) [1..1000000] :: Vector Double que toma 69 ms extra bajo ghci (106ms-37ms), y performGC llamada que toma 57 ms extra (58 ms-1 em). Estos dos se suman a la diferencia entre la ejecución bajo ghci y en una versión compilada.

Supongo, que en el programa compilado, RTS gestionar la memoria de una manera diferente que en ghci, por lo que la asignación y gc puede ser más rápido. También podemos probar solo esta parte evaluando 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.121653 s bajo ghci y 0.035162 s en una versión compilada
  • performGC toma 0.044876 s bajo ghci y 0.00031 s 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 que funcione más rápido bajo ghci?

 2
Author: remdezx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-22 09:14:47