Das ProblemHaskell Leistung, wenn Klassen und Instanzen mit
ich in Haskell mehrwertig Ausgabefunktionen simuliert werden soll. Die Haskell-Code erzeugt (nicht handschriftlich) - dies ist eine wichtige Information, siehe unten:
Dies kann easly durch Rückgabe eines Tupels von Funktion erfolgt natürlich sein, wie
f x y = (x+y, x-y)
Aber wenn eine solche Funktion ich muss wissen, welche Art von Tupel es zurückgibt:
...
(out_f_1, out_f_2) = f a b
(out_g_1, out_g_2, out_g_3) = g out_f_1
...
Und so weiter ... Aber während Code zu erzeugen, ich weiß nicht, was die Art der ouput ist von f können sagen, so jetzt ich Verwenden Sie das Data.List.Select
Paket und simulieren Sie das obige mit:
import Data.List.Select
...
out_f = f a b
out_g = g (sel1 outf)
...
Das Problem ist die Leistung - auf meinem Testprogramm, die Version, die Data.List.Select
verwendet, ist zweimal langsamer als die Version, die von Hand geschrieben.
Dies ist sehr offensichtlich Situation, weil Data.List.Select
geschrieben mit classes
und instances
, so verwendet es eine Art von Laufzeit-Wörterbuch (Wenn ich nicht falsch liege). (http://hackage.haskell.org/packages/archive/tuple/0.2.0.1/doc/html/src/Data-Tuple-Select.html#sel1)
Die Frage
Ich möchte Sie fragen, ob es möglich ist, irgendwie die Version zu kompilieren (die Data.List.Select
verwendet) so schnell zu sein wie die von Hand gefertigt ein?
Ich denke es sollte einen Wechsel zum Compiler geben, der ihm sagt, dass er die Klassen und Interfaces für jede Verwendung "instanziieren" soll (so etwas wie Vorlagen aus C++).
Benchmarks
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."
kompilieren mit 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."
kompilieren mit ghc -O3 Test2.hs
Ergebnisse
time ./Test1 10000000 = 5.54 s
time ./Test2 10000000 = 10.06 s
Die beiden Benchmarks führen das gleiche für mich. In beiden Fällen erzeugen sie tailrekursive Schleifen, die mit ungetasteten ganzen Zahlen arbeiten. Eine Möglichkeit für den wahrgenommenen Unterschied in der Leistung besteht darin, dass Ihr zweiter Benchmark stärker von der zusätzlichen Zeigerumlenkung betroffen ist, da alles in "OneTuple" verpackt wird, da GHC in diesem Fall leicht die Typklassenwörterbücher verlassen könnte. – sabauma
@sabauma - ok, ich habe es! Meine Tests wurden nicht mit dem '-O3'-Flag kompiliert (weil ich kompiliert habe ohne' -force-rekompilieren '), also hat GHC solche Optimierungen nicht gemacht. Können Sie mir bitte sagen, warum wir 'specialize pragma' verwenden sollten, wenn der Compiler solche Ausdrücke so optimieren kann? –
Dies ist ein ziemlich einfaches Beispiel für den Compiler zu optimieren. GHC kann in diesem Fall die Typclass-Lookups unterstützen, da es in der Lage ist, die polymorphen Aufrufe zu inlinern und zu bestimmen, welche Typklasseninstanz zur Kompilierzeit verwendet werden soll. Sie können (oder wollen) nicht immer eine Inline-Large-Funktion verwenden, in diesem Fall ist es für GHC immer noch vorteilhaft, eine spezielle Version der Funktion zu erzeugen. – sabauma