394 Stimmen

Transitivität der Auto-Spezialisierung in GHC

Von den Dokumenten für GHC 7.6:

Sie benötigen oft nicht einmal die SPECIALIZE-Pragma. Beim Kompilieren eines Moduls M berücksichtigt der Optimierer von GHC (mit -O) automatisch jede oberste überladene Funktion, die in M deklariert ist, und spezialisiert sie für die verschiedenen Typen, mit denen sie in M aufgerufen wird. Der Optimierer berücksichtigt auch jede importierte INLINABLE überladene Funktion und spezialisiert sie für die verschiedenen Typen, mit denen sie in M aufgerufen wird.

und

Darüber hinaus erstellt GHC bei einem SPECIALIZE-Pragma für eine Funktion f automatisch Spezialisierungen für alle durch f aufgerufenen typklassenüberladenen Funktionen, wenn sie sich im selben Modul wie das SPECIALIZE-Pragma befinden oder wenn sie INLINABLE sind; und so weiter, transitiv.

Daher sollte GHC automatisch einige/die meisten/alle(?) als INLINABLE markierte Funktionen ohne ein Pragma spezialisieren, und wenn ich ein explizites Pragma verwende, erfolgt die Spezialisierung transitiv. Meine Frage ist: ist die auto-Spezialisierung transitiv?

Genauer gesagt, hier ist ein kleines Beispiel:

Main.hs:

import Data.Vector.Unboxed as U
import Foo

main =
    let y = Bar $ Qux $ U.replicate 11221184 0 :: Foo (Qux Int)
        (Bar (Qux ans)) = iterate (plus y) y !! 100
    in putStr $ show $ foldl1' (*) ans

Foo.hs:

module Foo (Qux(..), Foo(..), plus) where

import Data.Vector.Unboxed as U

newtype Qux r = Qux (Vector r)
-- GHC inliniert `plus`, wenn ich die Ausrufezeichen oder den Baz-Konstruktor entferne
data Foo t = Bar !t
           | Baz !t

instance (Num r, Unbox r) => Num (Qux r) where
    {-# INLINABLE (+) #-}
    (Qux x) + (Qux y) = Qux $ U.zipWith (+) x y

{-# INLINABLE plus #-}
plus :: (Num t) => (Foo t) -> (Foo t) -> (Foo t)
plus (Bar v1) (Bar v2) = Bar $ v1 + v2

GHC spezialisiert den Aufruf von plus, spezialisiert jedoch nicht (+) in der Qux Num-Instanz, was die Leistung beeinträchtigt.

Ein explizites Pragma

{-# SPECIALIZE plus :: Foo (Qux Int) -> Foo (Qux Int) -> Foo (Qux Int) #-}

führt zu einer transitiven Spezialisierung wie in den Dokumenten angegeben, sodass (+) spezialisiert wird und der Code 30 mal schneller ist (beide mit -O2 kompiliert). Ist dieses Verhalten zu erwarten? Sollte ich nur erwarten, dass (+) transitiv mit einem expliziten Pragma spezialisiert wird?


AKTUALISIERUNG

Die Dokumente für 7.8.2 haben sich nicht geändert, und das Verhalten ist dasselbe, daher ist diese Frage weiterhin relevant.

4voto

Kurze Antworten:

Die Schlüsselpunkte der Frage, wie ich sie verstehe, sind folgende:

  • "Ist die automatische Spezialisierung transitiv?"
  • Sollte ich nur erwarten, dass (+) transitiv mit einem expliziten Pragma spezialisiert wird?
  • (offensichtlich beabsichtigt) Ist dies ein Fehler von GHC? Ist es inkonsistent mit der Dokumentation?

Nach meinem Kenntnisstand lauten die Antworten Nein, größtenteils ja, aber es gibt weitere Möglichkeiten, und Nein.

Code-Inline und Typanwendungs-Spezialisierung sind ein Kompromiss zwischen Geschwindigkeit (Ausführungszeit) und Codegröße. Das Standardniveau erhöht die Geschwindigkeit etwas, ohne den Code aufzublähen. Die Auswahl eines umfassenderen Niveaus obliegt dem Ermessen des Programmierers über das SPECIALISE-Pragma.

Erklärung:

Der Optimierer betrachtet auch jede importierte INLINABLE überladene Funktion und spezialisiert sie für die verschiedenen Typen, mit denen sie in M aufgerufen wird.

Angenommen, f ist eine Funktion, deren Typ eine Typvariable a enthält, die durch eine Typklasse C a eingeschränkt ist. GHC spezialisiert standardmäßig f bezüglich einer Typanwendung (Ersetzung von a durch t), wenn f in der Quellcode von (a) einer Funktion im gleichen Modul oder (b) falls f als INLINABLE markiert ist, dann in einem anderen Modul aufgerufen wird, das f aus B importiert. Daher ist die automatische Spezialisierung nicht transitiv, sie betrifft nur INLINABLE-Funktionen, die in der Quelldatei von A importiert und aufgerufen werden.

In Ihrem Beispiel, wenn Sie die Instanz von Num wie folgt umschreiben:

instance (Num r, Unbox r) => Num (Qux r) where
    (+) = quxAdd

quxAdd (Qux x) (Qux y) = Qux $ U.zipWith (+) x y
  • quxAdd wird nicht explizit von Main importiert. Main importiert den Instanz-Dictionary von Num (Qux Int) und dieses Dictionary enthält quxAdd im Record für (+). Jedoch, obwohl das Dictionary importiert wird, werden die enthaltenen Funktionen darin nicht verwendet.
  • plus ruft nicht quxAdd auf, es verwendet die Funktion, die im (+)-Record im Instanz-Dictionary von Num t gespeichert ist. Dieses Dictionary wird vom Compiler an der Aufrufstelle (in Main) festgelegt.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X