Ответ 1
Хотя вы не можете "повторно открывать" экземпляры в Haskell, как вы могли, с классами на динамических языках, есть способы обеспечить, чтобы функции были агрессивно встроены, когда это возможно, путем передачи определенных флагов в GHC.
-fspecialise-aggressively
удаляет ограничения, по которым функции являются специализированными. Любая перегруженная функция будет специализироваться на этом флаге. Это может создать много дополнительного кода.
-fexpose-all-unfoldings
будет включать (оптимизированное) разворачивание всех функций в файлах интерфейса, чтобы они могли быть встроены и специализированы по всем модулям.Использование этих двух флагов в совокупности будет иметь почти такой же эффект, как и любое определение как
INLINABLE
кроме того, что разворачивание определенийINLINABLE
не оптимизировано.
Эти параметры позволят компилятору GHC встроить fmap
. Опция -fexpose-all-unfoldings
, в частности, позволяет компилятору подвергать внутренности Data.Functor
остальной части программы для встраивания целей (и, как представляется, она обеспечивает наибольшую производительность). Вот быстрый и неглубокий бенчмарк, который я бросил вместе:
functor.hs
содержит этот код:
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE Strict #-}
data Foo a = MakeFoo a a deriving (Functor)
one_fmap foo = fmap (+1) foo
main = sequence (fmap (\n -> return $ one_fmap $ MakeFoo n n) [1..10000000])
Составлено без аргументов:
$ time ./functor
real 0m4.036s
user 0m3.550s
sys 0m0.485s
Скомпилирован с -fexpose-all-unfoldings
:
$ time ./functor
real 0m3.662s
user 0m3.258s
sys 0m0.404s
Здесь файл .prof
из этой компиляции, чтобы показать, что вызов fmap
действительно вставляется:
Sun Oct 7 00:06 2018 Time and Allocation Profiling Report (Final)
functor +RTS -p -RTS
total time = 1.95 secs (1952 ticks @ 1000 us, 1 processor)
total alloc = 4,240,039,224 bytes (excludes profiling overheads)
COST CENTRE MODULE SRC %time %alloc
CAF Main <entire-module> 100.0 100.0
individual inherited
COST CENTRE MODULE SRC no. entries %time %alloc %time %alloc
MAIN MAIN <built-in> 44 0 0.0 0.0 100.0 100.0
CAF Main <entire-module> 87 0 100.0 100.0 100.0 100.0
CAF GHC.IO.Handle.FD <entire-module> 84 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding <entire-module> 77 0 0.0 0.0 0.0 0.0
CAF GHC.Conc.Signal <entire-module> 71 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding.Iconv <entire-module> 58 0 0.0 0.0 0.0 0.0
Скомпилирован с -fspecialise-aggressively
:
$ time ./functor
real 0m3.761s
user 0m3.300s
sys 0m0.460s
Скомпилирован с обоими флагами:
$ time ./functor
real 0m3.665s
user 0m3.213s
sys 0m0.452s
Эти небольшие тесты ни в коем случае не представляют, что производительность (или файл) понравится в реальном коде, но это определенно показывает, что вы можете заставить компилятор GHC встроить fmap
(и что он действительно может иметь незначительные эффекты на производительность),