Неожиданное поведение кеширования с использованием полиморфных записей в Haskell - PullRequest
0 голосов
/ 21 января 2019

Я столкнулся с неожиданным поведением, используя полиморфные записи в Haskell, где некоторые значения не кэшируются, когда я ожидаю, что они будут кэшированы.

Вот минимальный пример:

{-# LANGUAGE RankNTypes #-}
import Debug.Trace

-- Prints out two "hello"s
data Translation = Trans { m :: forall a . Floating a => a }

g :: Floating a => a -> a
g x = x + 1

f :: Floating a => a -> a
f x = trace "hello" $ x - 2.0

-- Only one "hello"
-- data Translation = Trans { m :: Float }
--
-- f :: Float -> Float
-- f x = trace "hello" $ x - 2.0

main :: IO ()
main = do
    let trans = Trans { m = f 1.5 }
    putStrLn $ show $ m trans
    putStrLn $ show $ m trans

В этом примере я подумал, что если значение f 1.5 было вычислено и сохранено в поле m, при следующем обращении к нему оно не будет вычислено снова. Тем не менее, он, кажется, пересчитывается при каждом доступе к полю записи, как показывает тот факт, что «привет» печатается дважды.

С другой стороны, если мы удаляем полиморфизм из поля, значение кэшируется, как и ожидалось, и «привет» печатается только один раз.

Я подозреваю, что это связано с взаимодействием классов типов (рассматриваемых как записи), предотвращающих запоминание. Однако я не совсем понимаю, почему.

Я понял, что компиляция с -O2 устраняет проблему, однако, это происходит в гораздо большей системе, где компиляция с -O2, кажется, не имеет никакого эффекта, поэтому я хотел бы понять основную причину проблема, поэтому я могу исправить проблемы с производительностью в большей системе.

Ответы [ 2 ]

0 голосов
/ 22 января 2019
{-# LANGUAGE RankNTypes #-}

import Debug.Trace

--Does not get cached
data Translation = Trans { m :: forall a. Floating a => a }

f :: Floating a => a -> a
f x = trace "f" $ x - 2.0

Поскольку a - это переменная жесткого типа, связанная с типом, ожидаемым контекстом forall a. Floating a => a вам также придется кэшировать контекст

--Does get cached
data Translation' = Trans' { m' :: Float }

f' :: Float -> Float
f' x = trace "f'" $ x - 2.0

Поскольку это значение типа Float, оно может быть вычислено один раз и затем кэшировано.

main :: IO ()
main = do
    let
        trans = Trans { m = f 1.5 }
        trans' = Trans' { m' = f' 1.5}

    putStrLn $ show $ (m trans :: Double)
    putStrLn $ show $ (m trans :: Float)
    -- ^ you can evaluate it with 2 different contexts

    putStrLn $ show $ (m' trans' :: Float)
    putStrLn $ show $ (m' trans' :: Float)
    -- ^ context fixed

Обратите внимание, что первый не кэшируется, включена ли оптимизация компилятора.

Когда они оба Float и вы включаете оптимизацию, проблема исчезла.

Если вы компилируете большую систему с оптимизацией, и она неэффективна по некоторой метрике, я подозреваю, что проблема кроется где-то еще.

0 голосов
/ 22 января 2019

Держи мое пиво.

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ConstraintKinds #-}
import Debug.Trace

data Dict c where Dict :: c => Dict c

-- An isomorphism between explicit dictionary-passing style (Dict c -> a)
-- and typeclass constraints (c => a) exists:
from :: (c => a) -> (Dict c -> a)
from v Dict = v

to :: (Dict c -> a) -> (c => a)
to f = f Dict

data Translation = Trans { m :: forall a . Floating a => a }

f1, f2 :: Dict (Floating a) -> a -> a
f1 = trace "hello" $ \Dict x -> x - 2.0
f2 = \Dict -> trace "hello" $ \x -> x - 2.0

main = do
    let trans1 = Trans { m = to (flip f1 1.5) }
        trans2 = Trans { m = to (flip f2 1.5) }
    putStrLn "trans1"
    print (m trans1)
    print (m trans1)
    putStrLn "trans2"
    print (m trans2)
    print (m trans2)

Уделите секунду, чтобы предсказать, что это выдаст, прежде чем запустить его.Затем спросите вашего GHC, согласна ли она с вашим предположением.

Очистить как грязь?

Основное различие, которое вам нужно провести здесь, здесь, в этом значительно упрощенном примере:

> g = trace "a" $ \() -> trace "b" ()
> g ()
a
b
()
> g ()
b
()

Существует отдельное понятие кэширования функции и кэширования ее выходных данных .Последнее просто никогда не выполняется в GHC (хотя см. Обсуждение того, что происходит с вашей оптимизированной версией ниже).Первое может показаться глупым, но на самом деле это не так глупо, как вы думаете;Вы можете представить себе написание функции, которая, скажем, id, если гипотеза Коллатца верна, и not в противном случае.В такой ситуации имеет смысл только один раз проверить гипотезу Коллатца, а затем кэшировать, будем ли мы вести себя как id или not навсегда.

Как только вы поймете этот основной факт, следующийВы должны поверить, что в GHC ограничения класса типов скомпилированы в функции.(Аргументами функции являются словари классов типов, рассказывающие о том, как ведут себя все методы классов типов.) Сам GHC управляет созданием и передачей этих словарей за вас, и в большинстве случаев он довольно прозрачен для пользователя.

НоРезультатом этой стратегии компиляции является: полиморфный , но тип с ограничением по типу - это функция , даже если в ней нет стрелок функций .То есть

f 1.5 :: Floating a => a

выглядит как обычное старое значение;но на самом деле это функция , которая принимает словарь Floating a и выдает значение типа a.Таким образом, любые вычисления, которые входят в вычисление значения a, переделывают заново каждый раз, когда эта функция применяется (читай: используется для определенного мономорфного типа), потому что, в конце концов, выбранное точное значение критически зависит от того, какметоды класса типов ведут себя.

Это оставляет только вопрос о том, почему оптимизации изменили ситуацию в вашей ситуации.Там, я полагаю, то, что произошло, называется «специализацией», в которой компилятор попытается заметить, когда полиморфные вещи используются со статически известным мономорфным типом, и сделать для этого обязательную привязку.Это выглядит примерно так:

-- starting point
main = do
    let trans = \dict -> trace "hello" $ minus dict (fromRational dict (3%2)) (fromRational dict (2%1))
    print (trans dictForDouble)
    print (trans dictForDouble)

-- specialization
main = do
    let trans = \dict -> trace "hello" $ minus dict (fromRational dict (3%2)) (fromRational dict (2%1))
    let transForDouble = trans dictForDouble
    print transForDouble
    print transForDouble

-- inlining
main = do
    let transForDouble = trace "hello" $ minus dictForDouble (fromRational dict (3%2)) (fromRational dictForDouble (2%1))
    print transForDouble
    print transForDouble

В этом последнем случае функция исчезла;это «как если бы» GHC кэшировал вывод trans при применении к словарю dictForDouble.(Если вы скомпилируете с оптимизацией и -ddump-simpl, вы увидите, что это идет еще дальше, делая постоянное распространение, чтобы превратить материал minus ... в D# -0.5##. Вот так!)

...