Коли в GHC Haskell запам'ятовування автоматично?


106

Я не можу зрозуміти, чому m1, мабуть, запам'ятовується, а m2 не в наступному:

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10000000 займає приблизно 1,5 секунди на перший дзвінок, і частина цього при наступних дзвінках (імовірно, це кешування списку), тоді як m2 10000000 завжди займає стільки ж часу (перебудову списку з кожним викликом). Будь-яка ідея, що відбувається? Чи є якісь правила щодо того, якщо і коли GHC запам'ятовує функцію? Дякую.

Відповіді:


112

GHC не запам'ятовує функції.

Однак він обчислює будь-який заданий вираз у коді не більше одного разу на час, коли вводиться його оточуюча лямбда-вираз або, максимум, коли-небудь коли-небудь, якщо він знаходиться на верхньому рівні. Визначення того, де знаходяться лямбда-вирази, може бути дещо складним, коли ви використовуєте синтаксичний цукор, як у вашому прикладі, тому давайте перетворимо їх у еквівалентний синтаксис зневоднення:

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n

(Примітка. Звіт Haskell 98 насправді описує лівий розділ оператора, (a %)як еквівалент \b -> (%) a b, але GHC приєднує його (%) a. Це технічно різні, тому що їх можна розрізнити seq. Я думаю, що я міг би подати квиток GHC Trac про це.)

Враховуючи це, ви можете бачити це в m1'виразіfilter odd [1..] не міститься в жодному лямбда-виразі, тому він буде обчислюватися лише один раз на запуск вашої програми, в той час як в m2', filter odd [1..]буде обчислюватися кожен раз, коли вводиться лямбда-вираз, тобто, на кожен дзвінок о m2'. Це пояснює різницю в термінах, які ви бачите.


Насправді, деякі версії GHC, з певними варіантами оптимізації, матимуть більше значень, ніж зазначено вище в описі. Це може бути проблематично в деяких ситуаціях. Наприклад, розглянемо функцію

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])

GHC може помітити, що yне залежить від цьогоx переписання функції

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

У цьому випадку нова версія набагато менш ефективна, оскільки їй доведеться зчитувати близько 1 Гб пам'яті, де yвона зберігається, тоді як оригінальна версія працюватиме в постійному просторі і вміщуватиметься в кеш-пам'яті процесора. Насправді в GHC 6.12.1 функція fмайже вдвічі швидша при компіляції без оптимізації, ніж при компіляції -O2.


1
Визначення вартості (фільтр непарний [1 ..]) вираз все одно близький до нуля - це лінивий список, зрештою, тому реальна вартість є в (x !! 10000000) додатку, коли список фактично оцінюється. Крім того, здається, що і m1 і m2 оцінюються лише один раз з -O2 та -O1 (на мій ghc 6.12.3) принаймні в рамках наступного тесту: (test = m1 10000000 seqm1 10000000). Існує різниця, хоча, коли не вказано прапор оптимізації. І обидва варіанти вашого "f" мають, до речі, максимальну резидентність 5356 байтів, незалежно від оптимізації (з меншим загальним розподілом, коли використовується -O2).
Едька

1
@ Ed'ka: Спробуйте цю тестову програму, з наведеним вище визначенням f: main = interact $ unlines . (show . map f . read) . lines; складати з або без -O2; то echo 1 | ./main. Якщо ви пишете тест на кшталт main = print (f 5), то yсміття можна збирати по мірі його використання, і різниці між цими двома fs немає.
Рейд Бартон

Е, це повинно бути map (show . f . read), звичайно. І тепер, коли я завантажив GHC 6.12.3, я бачу ті самі результати, що і в GHC 6.12.1. І так, ви маєте рацію щодо оригіналу m1і m2: версії GHC, які виконують подібний підйом із включеними оптимізаціями, перетворяться m2на m1.
Рейд Бартон

Так, зараз я бачу різницю (-O2, безумовно, повільніше). Дякую за цей приклад!
Ed'ka

29

m1 обчислюється лише один раз, оскільки це форма постійної програми, тоді як m2 не є CAF, і тому обчислюється для кожної оцінки.

Дивіться вікі GHC на CAF: http://www.haskell.org/haskellwiki/Constant_applicative_form


1
Пояснення "m1 обчислюється лише один раз, оскільки це форма постійної програми" для мене не має сенсу. Оскільки, імовірно, m1 і m2 є змінними верхнього рівня, я думаю, що ці функції обчислюються лише один раз, незалежно від того, вони є CAF чи ні. Різниця полягає в тому, чи [1 ..]обчислюється список лише один раз під час виконання програми, або він обчислюється один раз за застосування функції, але чи пов'язаний він з CAF?
Tsuyoshi Ito

1
З пов'язаної сторінки: "CAF ... може бути складений до фрагмента графіка, який буде спільним для всіх цілей використання, або до якогось спільного коду, який перезапише себе деяким графіком при першому оцінці". Оскільки m1це CAF, друге застосовується і filter odd [1..](не тільки [1..]!) Обчислюється лише один раз. GHC також може зауважити, що m2посилається filter odd [1..], та розмістити посилання на ту саму грудку, яку використовували m1, але це було б поганою ідеєю: це може призвести до великих витоків пам'яті в деяких ситуаціях.
Олексій Романов

@ Алекс: Дякую за виправлення щодо [1..]та filter odd [1..]. В іншому я все ще непереконаний. Якщо я не помиляюся, CAF актуальний тільки тоді , коли ми хочемо , щоб стверджувати , що компілятор може замінити filter odd [1..]в m2глобальних стуком (який може бути навіть той же перетворювач, який використовувався в m1). Але в ситуації запитувача компілятор не робив такої «оптимізації», і я не бачу його відповідності питанню.
Tsuyoshi Ito

2
Доречно , що він може замінити його в m1 , і це робить.
Олексій Романов

13

Існує вирішальна різниця між двома формами: обмеження мономорфізму поширюється на m1, але не на m2, оскільки m2 чітко наводить аргументи. Отже, тип m2 загальний, але m1 специфічний. Типи, яким вони призначаються, є:

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

Більшість компіляторів і інтерпретаторів Haskell (усі вони, які я насправді знаю) не запам'ятовують поліморфні структури, тому внутрішній список m2 відтворюється щоразу, коли його називають, де m1's немає.


1
Граючи з цими в GHCi, схоже, це також залежить від плаваючої трансформації (один з проходів оптимізації GHC, який не використовується в GHCi). І звичайно, коли компілюються ці прості функції, оптимізатор може змусити їх поводитися однаково в будь-якому випадку (згідно з деякими тестами критеріїв, які я все-таки виконував, з функціями в окремому модулі та позначеними прагмами NOINLINE). Імовірно, це тому, що генерація списку та індексація так чи інакше зливаються в супер тугий цикл.
мокус

1

Я не впевнений, тому що я зовсім новачок у Haskell сам, але, здається, що це друга функція, параметризована, а перша - ні. Характер функції полягає в тому, що його результат залежить від вхідного значення, а у функціональній парадигмі, особливо це залежить ТІЛЬКИ від введення. Очевидним підсумком є ​​те, що функція без параметрів повертає завжди те саме значення знову і знову, незалежно від того.

Очевидно, що в компіляторі GHC є оптимізуючий механізм, який використовує цей факт, щоб обчислити значення такої функції лише один раз за весь час виконання програми. Безумовно, це робиться це лінь, але все-таки. Я помітив це сам, коли написав таку функцію:

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0

Потім , щоб перевірити це, я увійшов в GHCI і написав: primes !! 1000. Минуло кілька секунд, але нарешті я отримав відповідь:7927 . Тоді я зателефонував primes !! 1001і отримав відповідь миттєво. Так само в одну мить я отримав результат take 1000 primes, тому що Haskell повинен був обчислити весь список тисяч елементів, щоб повернути 1001-й елемент раніше.

Таким чином, якщо ви можете записати свою функцію так, щоб вона не приймала жодних параметрів, ви, ймовірно, хочете її. ;)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.