Лінь
Це не "оптимізація компілятора", але це щось гарантоване специфікацією мови, тому ви завжди можете розраховувати на те, що це станеться. По суті це означає, що робота не виконується, поки ви не зробите щось із результатом. (Якщо ви не зробите одну з кількох речей, щоб навмисно вимкнути лінь.)
Це, очевидно, сама по собі тема, і ТАК вже багато питань і відповідей на неї.
За моїм обмеженим досвідом, занадто лінивий або занадто суворий код має значно більші штрафи за продуктивність (у часі та просторі), ніж будь-який інший матеріал, про який я говорю ...
Аналіз строгості
Лінь - це уникнути роботи, якщо це не потрібно. Якщо компілятор може визначити, що певний результат буде "завжди" потрібен, він не буде турбуватися зберігати обчислення і виконувати його пізніше; він буде виконувати це безпосередньо, тому що це більш ефективно. Це так званий "аналіз строгості".
Очевидно, що компілятор не завжди може виявити, коли щось можна зробити суворим. Іноді потрібно давати компілятору невеликі підказки. (Мені невідомий будь-який простий спосіб визначити, чи зробив аналіз суворості те, що, на вашу думку, має, окрім того, як пробиратися через основні результати).
Вкладиш
Якщо ви викликаєте функцію, і компілятор може сказати, яку функцію ви викликаєте, вона може спробувати "вбудувати" цю функцію - тобто замінити виклик функції на копію самої функції. Накладні витрати функціонального виклику, як правило, досить невеликі, але вбудовування часто дає можливість здійснити інші оптимізації, які б не сталися інакше, тому вбудовування може бути великим виграшем.
Функції вкладені лише в тому випадку, якщо вони "достатньо малі" (або якщо ви додасте прагму, яка спеціально просить вбудувати). Також функції можна вказувати лише тоді, коли компілятор може сказати, яку функцію ви викликаєте. Є два основні способи, через які компілятор не міг сказати:
Якщо функція, яку ви телефонуєте, передається з іншого місця. Наприклад, коли filter
компілюється функція, ви не можете вбудувати присудок фільтра, оскільки це аргумент, що надається користувачем.
Якщо функція, яку ви викликаєте, це метод класу, і компілятор не знає, для якого типу йдеться. Наприклад, коли sum
компілюється функція, компілятор не може вбудувати +
функцію, оскільки sum
працює з декількома типами чисел, кожен з яких має іншу +
функцію.
В останньому випадку ви можете використовувати {-# SPECIALIZE #-}
прагму для створення версій функції, які жорстко закодовані для певного типу. Наприклад, {-# SPECIALIZE sum :: [Int] -> Int #-}
складе версію з sum
жорстким кодом для Int
типу, тобто значення, яке +
може бути окреслено в цій версії.
Зауважте, що наша нова спеціальна sum
функція буде викликана лише тоді, коли компілятор може сказати, що ми працюємо Int
. Інакше sum
називається оригінальний, поліморфний . Знову ж таки, фактичний накладний виклик функції досить невеликий. Це додаткові оптимізації, які вкладення може дати, які вигідні.
Поширене усунення субдекспресії
Якщо певний блок коду обчислює одне і те ж значення вдвічі, компілятор може замінити його на один екземпляр одного і того ж обчислення. Наприклад, якщо ви робите
(sum xs + 1) / (sum xs + 2)
тоді компілятор може оптимізувати це до
let s = sum xs in (s+1)/(s+2)
Ви можете очікувати, що компілятор завжди це зробить. Однак, мабуть, у деяких ситуаціях це може призвести до погіршення продуктивності, а не до кращого, тому GHC не завжди робить це. Чесно кажучи, я не дуже розумію деталей цього. Але суть полягає в тому, що якщо ця трансформація важлива для вас, це не важко зробити це вручну. (А якщо це не важливо, чому ви переживаєте про це?)
Виразні вирази
Розглянемо наступне:
foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo ( []) = "end"
Перші три рівняння перевіряють, чи список не порожній (серед іншого). Але тричі перевіряти те саме, що марно. На щастя, компілятору дуже легко оптимізувати це в декілька вкладених виразів регістру. У цьому випадку щось подібне
foo xs =
case xs of
y:ys ->
case y of
0 -> "zero"
1 -> "one"
_ -> foo ys
[] -> "end"
Це досить менш інтуїтивно, але більш ефективно. Оскільки компілятор може легко здійснити це перетворення, вам не доведеться турбуватися про це. Просто запишіть відповідність шаблону найбільш інтуїтивно зрозумілим способом; компілятор дуже добре переупорядковує та переставляє це, щоб зробити це якомога швидше.
Злиття
Стандартна ідіома Haskell для обробки списку полягає в з’єднанні функцій, які беруть один список і створюють новий список. Канонічний приклад буття
map g . map f
На жаль, хоча лінь гарантує пропускання непотрібної роботи, всі розподіли та розстановки для проміжного списку сап-роботи. "Злиття" або "вирубка лісів" - це коли компілятор намагається усунути ці проміжні кроки.
Проблема в тому, що більшість цих функцій є рекурсивними. Без рекурсії було б елементарною вправою складати всі функції в один великий код коду, запустити спрощувач над ним і створити дійсно оптимальний код без проміжних списків. Але через рекурсію це не вийде.
Ви можете використовувати {-# RULE #-}
прагми, щоб виправити щось із цього. Наприклад,
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
Тепер, кожного разу, коли GHC бачить map
застосований до map
нього, він стискає його в один прохід над списком, виключаючи проміжний список.
Проблема полягає в тому, що це працює лише за map
цим map
. Існує багато інших можливостей - map
слідуючи за ними filter
, filter
слідуючи і map
т. Д. Замість того, щоб вручну кодувати рішення для кожного з них, було винайдено так зване "потокове синтез". Це більш складна хитрість, яку я тут не опишу.
Довгий і короткий: Це все спеціальні прийоми оптимізації, написані програмістом . Сам GHC нічого не знає про синтез; це все в бібліотеках списків та інших бібліотеках контейнерів. Тож, які оптимізації відбудуться, залежить від того, як записані ваші бібліотеки контейнерів (або, що більш реально, які бібліотеки ви вирішили використовувати).
Наприклад, якщо ви працюєте з масивами Haskell '98, не чекайте будь-якого злиття. Але я розумію, що vector
бібліотека має широкі можливості синтезу. Це все про бібліотеки; компілятор просто надає RULES
прагму. (До речі, це надзвичайно потужно. Як автор бібліотеки, ви можете використовувати його для перезапис коду клієнта!)
Мета:
Я погоджуюсь з тим, що люди говорять "код по-перше, профіль другий, оптимізуйте третє".
Я також погоджуюся з людьми, які говорять: "корисно мати ментальну модель, скільки коштує дане дизайнерське рішення".
Баланс у всьому і всьому тому ...