2016-07-13 10 views
12

ich diese Grundtypen in meinem Code haben:Haskell Leistung von Beispiel

newtype Foo (m :: Factored) = Foo Int64 deriving (NFData) 

foo :: forall m . (Fact m) => Foo m -> Foo m 

class T t where t :: (Fact m) => t m -> t m 

instance T Foo where t = foo 

newtype Bar t (m :: Factored) = Bar (t m) deriving (NFData) 

bar :: (Fact m, T t) => Bar t m -> Bar t m 
bar (Bar v) = Bar $ t v 

(ignorieren Fact und Factored für den Moment). Ich benchmarkiere den Code auf verschiedenen Ebenen und vergleiche die Leistung von foo, t und bar. In den Benchmarks und bar gilt nur t über eine newtype. So sollte ihre Laufzeit im Wesentlichen identisch sein, aber criterion berichtet, dass foo dauert 9,2ns, t dauert etwa doppelt so hoch bei 17,45ns, und bar dauert eine satte 268,1ns.

Ich habe mit dem Hinzufügen INLINABLE und sogar einem SPECIALIZE Pragma experimentiert, aber sie helfen nicht. Ich möchte glauben, dass GHC einige magische Syntax/Optimierung/etc hat, die einheitlich angewandt werden kann, um diese Art von Leistungsproblemen zu lösen. Zum Beispiel habe ich Fälle gesehen, in denen das Schreiben von Code im Point-Free-Stil dramatische Auswirkungen auf die Leistung hat.

Der vollständige Code kann here gefunden werden. Ich verspreche, es ist nicht einschüchternd. Die Module sind:

  • Foo: definiert Foo, foo und T
  • Bar: definiert Bar und bar
  • FooBench: definiert als Maßstab für foo
  • TBench: eine Benchmark für t
  • definiert
  • BarBench: definiert einen Benchmark für bar
  • Haupt: läuft die drei Benchmarks
  • einkalkuliert: Die meisten der Module definiert Fact und Factoredsingletons

verwenden, sind winzig; Ich habe die drei Benchmarks in separaten Dateien definiert, damit ich ihren Kern untersuchen konnte. Ich habe den Kern für die drei Module *Bench erzeugt und so gut wie möglich ausgerichtet. Sie sind nur ~ 250 Zeilen und die ersten ~ 200 Zeilen sind identisch, bis zur Umbenennung. Das Problem ist, dass ich nicht weiß, was ich von den letzten 50 Zeilen machen soll. Das (Kern) diff für FooBench vs TBench ist here, ist der Unterschied für TBench vs BarBenchhere, und der Unterschied für FooBench vs BarBench ist here.

Nur einige der Fragen, die ich habe:

  1. Auf einem hohen Niveau, was ist der wesentliche Unterschied zwischen den Core-Dateien? Ich suche nach etwas wie "Hier können Sie sehen, dass GHC den Anruf zu x nicht inlining ist." Nach was soll ich suchen?

  2. Was kann getan werden, um die drei Benchmarks in etwa 9,2ns laufen zu lassen? GHC-Optimierungen? INLINE/INLINABLE Pragmas? SPECIALIZE Pragmas, die ich verpasst habe?(Sie dürfen sich nicht auf F128::Factored spezialisieren; in meiner realen Bibliothek kann dieser Wert zur Laufzeit verdinglicht werden.) Einschränken/Verzögern des Inlining auf eine bestimmte Phase?

Obwohl ich für eine tatsächliche Lösung, die Benchmarks alle schnell zu machen, dann ist es möglich, dass Tricks, die für dieses Beispiel nicht funktioniert zu meiner eigentlichen Bibliothek skaliert. Daher suche ich auch nach der "high-level" Erklärung, warum eine bestimmte Technik funktionieren sollte.

+0

Dies kann hilfreich sein: http://stackoverflow.com/questions/6121146/reading-ghc-core –

+0

Mein _guess_ ist, dass der Unterschied, den Sie beobachten, ist die Kosten für die Weitergabe der 'Fact' und' T' Wörterbücher um –

+0

@BenjaminHodgson Ich würde wirklich gerne Spezialisierung den ganzen Weg hinunter den Stapel bekommen, so dass es keine Wörterbücher beteiligt sind, sobald Sie auf der obersten Ebene monomorphisieren. Irgendeine Idee, wie man das macht? – crockeea

Antwort

6

Zuerst wird bei bar suchen:

bar :: (Fact m, T t) => Bar t m -> Bar t m 
bar (Bar v) = Bar $ t v 

können wir dies schreiben, ohne das Argument, um mit coerce:

bar :: (Fact m, T t) => Bar t m -> Bar t m 
bar = (coerce :: (t m -> t m) -> Bar t m -> Bar t m) t 

diese (wie wir hoffen würden) bekommt bar die gleiche wie t Durchführung . (In der Tat ist der Kern für TBench und BarBench genau das gleiche, ohne Typ-Signaturen).

Ich bin nicht ganz sicher, warum, aber mit INLINE statt INLINEABLE macht t und bar die gleiche wie foo auszuführen. Ich bin kein Experte, aber normalerweise ist es besser, INLINE für kleine Funktionen zu verwenden, die Sie sicher inline möchten.

Das sagte ich denke, einige dieser Probleme sind, wie Kriterium Benchmarks zu stoppen ghc Betrug. Wenn Sie zum Beispiel bench "Bar" $ nf (GHC.Magic.inline bar) x auf Ihren ursprünglichen Code schreiben, wird bar genauso ausgeführt wie foo. Ich vermute, ein "echtes" Programm wäre nicht so heikel.

+1

Wie verletzt 'bar' das Modell [newtypes are free] (https://ghc.haskell.org/trac/ghc/wiki/NewtypeWrappers)? Ich habe meine GADTs genau so in newtypes geändert, dass ich ausschließen konnte, dass Datenkonstruktoren das Problem waren. Da ich in meinem echten Code keine neuen Typen habe, kann ich 'coerce' nicht verwenden. – crockeea

+0

Wie für 'INLINE' /' INLINABLE' scheinen beide in meinem echten Code nicht zu helfen. Es ist sehr schwierig, ein Beispiel zu geben, das klein genug ist, um eine Eingabe zu erhalten, aber groß genug, dass die Lösungen immer noch gelten. Ähnlich mit 'GHC.Magic.inline' (was großartig klingt!), Habe ich es nur geschafft, mein Programm zu verlangsamen. Ich schätze deine Bemühungen sehr, und ich werde weiterhin mit den Ideen herumspielen. – crockeea

+0

Ich bin auch interessiert, wenn Sie sich den Kern für eine Diagnose angesehen haben, oder wenn Sie es nur als eine Bestätigung der Lösung angesehen haben. Ich möchte wirklich nur verstehen, wie Sie das Problem angegangen sind. – crockeea

Verwandte Themen