391 votes

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 en premier lieu. Lors de la compilation d'un module M, l'optimiseur de GHC (avec -O) considère automatiquement chaque fonction surchargée de haut niveau déclarée dans M, et la spécialise pour les différents types auxquels elle est appelée dans M. L'optimiseur considère également chaque fonction surchargée importée INLINABLE, et la spécialise pour les différents types auxquels elle est appelée dans 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 en classe de type appelées par f, si elles sont dans le même module que le pragma SPECIALIZE, ou si elles sont INLINABLES ; et ainsi de suite, de manière transitive.

Ainsi, GHC devrait automatiquement spécialiser certains/plusieurs/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 : est-ce que le auto -spécialisation 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 est-ce que pas se spécialiser (+) dans le Qux Num instance, ce qui nuit aux performances.

Cependant, un pragma explicite

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

résulte en transitif comme l'indique la documentation, donc (+) est spécialisée et le code est 30x plus rapide (les deux compilés avec -O2 ). Est-ce un comportement attendu ? Dois-je m'attendre à ce que (+) pour être spécialisé de manière transitive avec un pragma explicite ?


UPDATE

Les documents relatifs à la version 7.8.2 n'ont pas changé et le comportement est le même, de sorte que cette question est toujours pertinente.

3voto

diesalbla Points 341

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 ?"
  • Dois-je m'attendre à ce que (+) soit spécialisé de manière transitive avec un pragma explicite ?
  • (apparemment prévu) Est-ce un bogue de GHC ? Est-il incompatible avec la documentation ?

AFAIK, les réponses sont Non, le plus souvent oui mais il existe d'autres moyens, et Non.

L'inlining du code et la spécialisation des applications de type sont un compromis entre la vitesse (temps d'exécution) et la taille du code. Le niveau par défaut permet d'obtenir une certaine vitesse sans alourdir le code. Le choix d'un niveau plus exhaustif est laissé à la discrétion du programmeur par le biais de SPECIALISE pragma.

Explication :

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

Supposons que f est une fonction dont le type inclut une variable de type a contraint par une classe de type C a . GHC par défaut se spécialise f en ce qui concerne une demande de type (en remplaçant a pour t ) si f est appelé avec cette application de type dans le code source de (a) toute fonction du même module, ou (b) si f est marqué INLINABLE alors tout autre module qui importations f de B . Ainsi, l'auto-spécialisation n'est pas transitive, elle touche seulement INLINABLE fonctions importées et appelées pour 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'instance de Num (Qux Int) et ce dictionnaire contient quxAdd dans le dossier pour (+) . Cependant, bien que le dictionnaire soit importé, les contenus utilisés dans le dictionnaire ne le sont pas.
  • plus n'appelle pas quxAdd il utilise la fonction stockée pour le (+) dans le dictionnaire d'instance de Num t . Ce dictionnaire est défini sur le site d'appel (en Main ) par le compilateur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X