web-dev-qa-db-fra.com

Transitivité de l'auto-spécialisation dans GHC

De les docs pour GHC 7.6:

[Souvent] vous n'avez même pas besoin du pragma SPECIALIZE. Lors de la compilation d'un module M, l'optimiseur de GHC (avec -O) considère automatiquement chaque fonction surchargée de niveau supérieur déclarée dans M et le spécialise pour les différents types auxquels il est appelé en M. L'optimiseur considère également chaque fonction surchargée INLINABLE importée. et le spécialise pour les différents types auxquels il est appelé en M.

et

De plus, étant donné un pragma SPECIALIZE pour une fonction f, GHC créera automatiquement des spécialisations pour toutes les fonctions surchargées de classe-type appelées par f, si elles se trouvent dans le même module que le pragma SPECIALIZE ou si elles sont INLINABLE. et ainsi de suite, de manière transitoire.

Donc, GHC devrait se spécialiser automatiquement certains/la plupart/tous (?) fonctions marquées INLINABLE sans un pragma, et si j'utilise un pragma explicite , la spécialisation est transitive. Ma question est la suivante: la auto - spécialisation est-elle transitive?

Plus précisément, voici un petit exemple:

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 inlines `plus` if I remove the bangs or the Baz constructor
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 spécialise l'appel à plus, mais ne ne spécialise pas (+) dans l'instance QuxNum qui tue les performances.

Cependant, un pragma explicite

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

résulte en une spécialisation transitive comme l'indique la documentation. (+) est donc spécialisé et le code est 30 fois plus rapide (les deux sont compilés avec -O2). Est-ce comportement attendu? Devrais-je m'attendre à ce que (+) soit spécialisé de manière transitoire avec un pragma explicite?


UPDATE

La documentation de 7.8.2 n'a pas changé et le comportement est le même. Cette question est donc toujours pertinente.

390
crockeea

Réponses courtes:

Les points clés de la question, tels que je les comprends, sont les suivants:

  • "l'auto-spécialisation est-elle transitive?"
  • Devrais-je m'attendre uniquement à (+) être spécialisé de manière transitoire avec un pragma explicite?
  • (apparemment destiné) Est-ce un bug de GHC? Est-ce incompatible avec la documentation?

Pour autant que je sache, les réponses sont non, la plupart du temps oui, mais il existe d'autres moyens, et non.

L'inclusion de code et la spécialisation d'applications de type constituent un compromis entre la vitesse (temps d'exécution) et la taille du code. Le niveau par défaut accélère sans faire gonfler le code. Le choix d'un niveau plus exhaustif est laissé à la discrétion du programmeur via SPECIALISE pragma.

Explication:

L'optimiseur considère également chaque fonction surchargée INLINABLE importée et la spécialise pour les différents types auxquels il est appelé dans M.

Supposons que f soit une fonction dont le type comporte une variable de type a contrainte par une classe de types C a. GHC par défaut spécialise f par rapport à une application de type (en remplaçant a par t) si f est appelé avec cette application de type dans le code source de (a) fonctionner dans le même module, ou (b) si f est marqué INLINABLE, alors tout autre module qui importef de B. Ainsi, l'auto-spécialisation n'est pas transitive, elle ne touche que INLINABLE fonctions importées et appelées dans le code source de A .

Dans votre exemple, si vous réécrivez l'instance de Num comme suit:

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

quxAdd (Qux x) (Qux y) = Qux $ U.zipWith (+) x y
  • quxAdd n'est pas spécifiquement importé par Main. Main importe le dictionnaire d'instances de Num (Qux Int), et ce dictionnaire contient quxAdd dans l'enregistrement de (+). Cependant, bien que le dictionnaire soit importé, le contenu utilisé dans le dictionnaire ne l’est pas.
  • plus n'appelle pas quxAdd, il utilise la fonction stockée pour l'enregistrement (+) dans le dictionnaire d'instances de Num t. Ce dictionnaire est défini sur le site d’appel (dans Main) par le compilateur.
3