Транзитивность авто-специализации в GHC

392

Из документов для GHC 7.6:

[Y] Вам часто даже не нужна прагма SPECIALIZE. При компиляции модуля M оптимизатор GHC (с -O) автоматически рассматривает каждую перегруженную функцию верхнего уровня, объявленную в M, и специализирует ее для различных типов, в которых она вызывается в M. Оптимизатор также учитывает каждую импортированную перегруженную функцию INLINABLE, и специализирует его для разных типов, в которых он называется в М.

а также

Более того, учитывая прагму SPECIALIZE для функции f, GHC автоматически создает специализации для любых функций, перегруженных классами типов, вызываемых f, если они находятся в том же модуле, что и прагма SPECIALIZE, или если они являются INLINABLE; и так далее, переходно.

Таким образом, GHC должен автоматически специализировать некоторые / большинство / все (?) Функции, помеченные INLINABLE без прагмы, и если я использую явную прагму, специализация транзитивна. Мой вопрос: является ли автоспециализация транзитивной?

В частности, вот небольшой пример:

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 специализируется на вызове plus, но не специализируется (+)на Qux Numэкземпляре, который убивает производительность.

Тем не менее, явная прагма

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

приводит к переходной специализации, как указывают документы, поэтому (+)является специализированным, и код работает в 30 раз быстрее (оба скомпилированы -O2). Это ожидаемое поведение? Стоит ли ожидать (+)специализированной транзитивности только с явной прагмой?


ОБНОВИТЬ

Документы по 7.8.2 не изменились, и поведение остается прежним, поэтому этот вопрос по-прежнему актуален.

crockeea
источник
33
Я не знаю ответа, но похоже, что он может быть связан с: ghc.haskell.org/trac/ghc/ticket/5928 Вероятно, стоит открыть новый тикет или добавить туда свою информацию, если вы считаете, что она, вероятно, связана с 5928
Jberryman
6
@jberryman Кажется, что есть два отличия между этим билетом и моим вопросом: 1) В билете эквивалент неplus был помечен как INLINABLE, и 2) simonpj указал, что с кодом билета происходит некоторое встраивание, но ядро ​​из мой пример показывает, что ни одна из функций не была встроенной (в частности, я не смог избавиться от второго конструктора, в противном случае встроенный материал GHC). Foo
crockeea
5
Ах хорошо. Что происходит, когда вы определяете plus (Bar v1) = \(Bar v2)-> Bar $ v1 + v2, так что LHS полностью применяется на сайте вызова? Становится ли это встроенным, а затем начинается специализация?
Jberryman
3
@jberryman Забавно, что ты должен спросить. Я пошел по этому пути с этим вопросом, который привел к этому отчету . plusИз-за этих ссылок у меня изначально был призыв к полному применению, но на самом деле я получил меньшую специализацию: вызов также plusне был специализированным. У меня нет объяснения этому, но я собирался оставить его для другого вопроса или надеюсь, что он будет решен в ответе на этот вопрос.
crockeea
11
От ghc.haskell.org/trac/ghc/wiki/ReportABug : «Если сомневаетесь, просто сообщите о своей ошибке». Вы не должны чувствовать себя плохо, тем более, что достаточное количество действительно опытных хакелеров здесь не знают, как ответить на ваш вопрос. Подобные тесты, вероятно, действительно полезны для разработчиков GHC. В любом случае, удачи! Обновлен вопрос, если вы
подаете заявку

Ответы:

4

Короткие ответы:

Ключевыми моментами вопроса, насколько я понимаю, являются следующие:

  • "Является ли авто-специализация переходной?"
  • Стоит ли ожидать, что (+) будет специализироваться транзитивно с явной прагмой?
  • (очевидно, предназначен) Это ошибка GHC? Это несовместимо с документацией?

AFAIK, ответы нет, в основном да, но есть и другие средства, и нет.

Внедрение кода и специализация приложения типа - это компромисс между скоростью (временем выполнения) и размером кода. Уровень по умолчанию получает некоторое ускорение без раздувания кода. Выбор более исчерпывающего уровня оставлен на усмотрение программиста через SPECIALISEпрагму.

Объяснение:

Оптимизатор также рассматривает каждую импортированную перегруженную функцию INLINABLE и специализирует ее для различных типов, в которых она вызывается в M.

Предположим, fчто это функция, тип которой включает переменную типа, aограниченную классом типа C a. GHC по умолчанию специализируется fпо отношению к типу приложения (заменяющего aдля t) , если fвызывается с этим типом приложения в исходном коде (а) любой функции в том же самом модуле, или (б) , если fотмечен INLINABLE, то любой другой модуль , который импортирует f от B. Таким образом, авто-специализация не является транзитивной, он затрагивает только INLINABLEфункции импортированных и призвали в исходном коде из A.

В вашем примере, если вы переписываете экземпляр Numследующим образом:

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

quxAdd (Qux x) (Qux y) = Qux $ U.zipWith (+) x y
  • quxAddспециально не импортируется Main. Mainимпортирует словарь экземпляров Num (Qux Int), и этот словарь содержится quxAddв записи для (+). Однако, несмотря на то, что словарь импортирован, содержимое, используемое в словаре, нет.
  • plusне вызывает quxAdd, он использует функцию, сохраненную для (+)записи в словаре экземпляра Num t. Этот словарь устанавливается на сайте вызова (в Main) компилятором.
Диего Э. Алонсо-Блас
источник