Transitividade da Especialização Automática no GHC

392

Dos documentos para o GHC 7.6:

Em geral, você nem precisa do pragma SPECIALIZE. Ao compilar um módulo M, o otimizador do GHC (com -O) considera automaticamente cada função de sobrecarga de nível superior declarada em M e a especializa para os diferentes tipos em que é chamado em M. O otimizador também considera cada função de sobrecarga INLINABLE importada, e especializa-o para os diferentes tipos em que é chamado em M.

e

Além disso, dado um pragma SPECIALIZE para uma função f, o GHC criará automaticamente especializações para quaisquer funções sobrecarregadas de classe de tipo chamadas por f, se estiverem no mesmo módulo que o pragma SPECIALIZE ou se forem INLINABLE; e assim por diante, transitivamente.

Portanto, o GHC deve especializar automaticamente algumas / a maioria / todas as funções (?) Marcadas INLINABLE sem um pragma e, se eu usar um pragma explícito, a especialização é transitiva. Minha pergunta é: a auto-especialização é transitiva?

Especificamente, aqui está um pequeno exemplo:

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

O GHC especializa a chamada plus, mas não se especializa (+)na Qux Numinstância que mata o desempenho.

No entanto, um pragma explícito

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

resulta em especialização transitiva, conforme indicado pelos documentos, por isso (+)é especializado e o código é 30x mais rápido (ambos compilados -O2). Esse comportamento é esperado? Devo apenas esperar (+)ser especializado transitivamente com um pragma explícito?


ATUALIZAR

Os documentos para 7.8.2 não foram alterados e o comportamento é o mesmo, portanto, essa pergunta ainda é relevante.

crockeea
fonte
33
Não sei a resposta, mas parece que isso pode estar relacionado a: ghc.haskell.org/trac/ghc/ticket/5928 Provavelmente vale a pena abrir um novo ticket ou adicionar suas informações lá, se você acha que provavelmente está relacionado ao 5928
jberryman
6
@jberryman Parece haver duas diferenças entre esse ticket e minha pergunta: 1) No ticket, o equivalente a nãoplus foi marcado como INLINABLE e 2) simonpj indicou que havia algo de errado com o código do ticket, mas o núcleo de meu exemplo mostra que nenhuma das funções estava embutida (em particular, eu não conseguia me livrar do segundo construtor, caso contrário, coisas embutidas no GHC). Foo
22414 Crockeea
5
Ah ok. O que acontece quando você define plus (Bar v1) = \(Bar v2)-> Bar $ v1 + v2, para que o LHS seja totalmente aplicado no local da chamada? Ele é embutido e a especialização é ativada?
jberryman
3
@jberryman Engraçado você deve perguntar. Eu estive nessa estrada com esta pergunta que levou a este relatório do trac . Originalmente, eu tive a chamada para ser plustotalmente aplicada devido a esses links, mas, na verdade, recebi menos especialização: a chamada para plustambém não era especializada. Não tenho explicação para isso, mas pretendia deixar para outra pergunta, ou espero que isso seja resolvido em resposta a esta.
Crockeea
11
Em ghc.haskell.org/trac/ghc/wiki/ReportABug : "Em caso de dúvida, basta relatar seu bug." Você não deve se sentir mal, especialmente porque um número suficiente de haskellers realmente experientes aqui não sabem como responder à sua pergunta. Casos de teste como esse são provavelmente realmente valiosos para os desenvolvedores do GHC. De qualquer forma, boa sorte! Atualizado a pergunta se você apresentar um bilhete
jberryman

Respostas:

4

Respostas curtas:

Os pontos principais da pergunta, como eu os entendo, são os seguintes:

  • "a autoespecialização é transitiva?"
  • Devo apenas esperar que (+) seja especializado transitivamente com um pragma explícito?
  • (aparentemente destinado) Isso é um bug do GHC? É inconsistente com a documentação?

AFAIK, as respostas são Não, principalmente sim, mas existem outros meios e Não.

A inserção de código e a especialização de aplicativo de tipo são uma troca entre velocidade (tempo de execução) e tamanho do código. O nível padrão obtém alguma aceleração sem inchar o código. A escolha de um nível mais exaustivo fica a critério do programador via SPECIALISEpragma.

Explicação:

O otimizador também considera cada função sobrecarregada INLINABLE importada e a especializa para os diferentes tipos em que é chamada em M.

Suponha que fé uma função cujo tipo inclui uma variável de tipo arestrita por uma classe de tipo C a. GHC por padrão especializada fno que diz respeito a um tipo de aplicação (substituindo aa t) se fé chamado com que tipo de aplicação no código fonte de (a) qualquer função no mesmo módulo, ou (b) se ffor marcado INLINABLE, então qualquer outro módulo que importações f de B. Assim, a autoespecialização não é transitiva, apenas toca INLINABLEfunções importadas e solicitadas no código fonte de A.

No seu exemplo, se você reescrever a instância da Numseguinte maneira:

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

quxAdd (Qux x) (Qux y) = Qux $ U.zipWith (+) x y
  • quxAddnão é importado especificamente por Main. Mainimporta o dicionário de instância de Num (Qux Int)e esse dicionário contém quxAddno registro para (+). No entanto, embora o dicionário seja importado, o conteúdo usado no dicionário não é.
  • plusnão chama quxAdd, usa a função armazenada para o (+)registro no dicionário de instância de Num t. Este dicionário é definido no site da chamada (in Main) pelo compilador.
Diego E. Alonso-Blas
fonte