il problemaprestazioni Haskell quando si usano le classi e le istanze
voglio simulare in Haskell un multivalore funzioni output. Il codice Haskell è generato (non scritto a mano) - si tratta di informazioni importanti, vedere sotto:
Questo può essere, naturalmente, facilmente fatto restituendo una tupla dalla funzione, come
f x y = (x+y, x-y)
Ma quando si utilizza tale funzione devo sapere che tipo di tupla restituisce:
...
(out_f_1, out_f_2) = f a b
(out_g_1, out_g_2, out_g_3) = g out_f_1
...
E così via ... Ma durante la generazione del codice, non so qual è il tipo di output di f consente di dire, così adesso ho' m utilizzando il pacchetto Data.List.Select
e simulare quanto sopra con:
import Data.List.Select
...
out_f = f a b
out_g = g (sel1 outf)
...
Il problema è la prestazione - sul mio programma di test, la versione, che usa Data.List.Select
è due volte più lento rispetto alla versione scritta a mano.
Questa è la situazione molto evidente, perché Data.List.Select
è scritto utilizzando classes
e instances
, in modo che utilizza una sorta di dizionario runtime (se non sbaglio). (http://hackage.haskell.org/packages/archive/tuple/0.2.0.1/doc/html/src/Data-Tuple-Select.html#sel1)
La questione
voglio chiedervi se è possibile compilare in qualche modo la versione (che utilizza Data.List.Select
) di essere veloce come quello realizzato manualmente?
Penso che ci dovrebbe essere un passaggio al compilatore, che gli dirà di "istanziare" le classi e le interfacce per ogni utilizzo (qualcosa come i modelli di C++).
benchmark
Test1.hs:
import qualified Data.Vector as V
import System.Environment
b :: Int -> Int
b x = x + 5
c x = b x + 1
d x = b x - 1
a x = c x + d x
main = do
putStrLn "Starting..."
args <- getArgs
let iternum = read (head args) :: Int in do
putStrLn $ show $ V.foldl' (+) 0 $ V.map (\i -> a (iternum-i))
$ V.enumFromTo 1 iternum
putStrLn "Done."
compilare con ghc -O3 Test1.hs
Test2.hs:
import qualified Data.Vector as V
import Data.Tuple.Select
import Data.Tuple.OneTuple
import System.Environment
b x = OneTuple $ x + 5
c x = OneTuple $ (sel1 $ b x) + 1
d x = OneTuple $ (sel1 $ b x) - 1
a x = OneTuple $ (sel1 $ c x) + (sel1 $ d x)
main = do
putStrLn "Starting..."
args <- getArgs
let iternum = read (head args) :: Int in do
putStrLn $ show $ V.foldl' (+) 0 $ V.map (\i -> sel1 $ a (iternum-i))
$ V.enumFromTo 1 iternum
putStrLn "Done."
compilare con ghc -O3 Test2.hs
Risultati
time ./Test1 10000000 = 5.54 s
time ./Test2 10000000 = 10.06 s
I due benchmark hanno lo stesso per me. In entrambi i casi, producono loop ricorsivi tail operanti su interi non archiviati. Una possibilità per la differenza percepita nelle prestazioni è che il secondo benchmark è più influenzato dal puntatore indiretto aggiuntivo dovuto al wrapping di tutto in 'OneTuple', dal momento che GHC potrebbe facilmente elidere i dizionari typeclass in questo caso. – sabauma
@sabauma - ok, ce l'ho! I miei test non sono stati compilati usando il flag '-O3' (perché stavo compilando senza' -force-recompile') così GHC non ha fatto tali ottimizzazioni. Potresti dirmi perché dovremmo usare in qualsiasi momento 'specialize pragma' se il compilatore è in grado di ottimizzare tali espressioni come questa? –
questo è un esempio abbastanza facile da ottimizzare per il compilatore. In questo caso, GHC può elidere le ricerche di typeclass in quanto è in grado di allineare le chiamate polimorfiche e determinare quale istanza di classe tipo utilizzare in fase di compilazione. Non è possibile (o non si vuole) eseguire sempre una funzione di grandi dimensioni, nel qual caso è ancora vantaggioso che GHC produca una versione specializzata della funzione. – sabauma