haskell-notes

2M

0M

0.0

0.1

0.1

0.2

0.2

0.2

seconds

Рис. 10.10: Профиль кучи для утечки памяти

В картинку не поместились имена функций мы можем увеличить строку флагом L. Теперь все имена

поместились (рис. 10.11).

$ ./leak 6 +RTS -K30m -hc -L45

(500000,500000)

$ hp2ps -e80mm -c leak.hp

С помощью флага hd посмотрим на объекты, которые застряли в куче (рис. 10.12):

$ ./leak 6 +RTS -K30m -hd -L45

(500000,500000)

$ hp2ps -e80mm -c leak.hp

Теперь куча разбита по типу объектов (замыканий) (рис. 10.12). BLACKHOLE это специальный объект, ко-

торый заменяет THUNK во время его вычисления. I# – это скрытый конструктор Int. sat_sUa и sat_sUd – это

имена застрявших отложенных вычислений. Если бы наша программа была очень большой на этом месте мы

бы запустили профилирование по функциям с флагом p и из файла leak. prof узнали бы в каких функциях

программа тратит больше всего ресурсов. После этого мы бы пошли смотреть исходный код подозрительных

функций и после внесённых изменений снова посмотрели бы на графики кучи.

Если подумать, что мы делаем? Мы создаём отложенное вычисление, которое обещает построить большой

список, вытягиваем из списка по одному элементу и, если элемент оказывается чётным, прибавляем к одному

элементу пары, а если не чётным, то к другому. Проблема в том, что внутри пары происходит накопление

отложенных вычислений, необходимо сразу вычислять значения перед запаковыванием их в пару. Изменим

код:

{-# Language BangPatterns #-}

module Main where

import System.Environment(getArgs)

Статистика выполнения программы | 169

leak 6 +RTS -K30m -hc -L45

2,489,935 bytes x seconds

Fri Jun 1 23:11 2012

bytes

14M

12M

(103)tick/sum2.iter/sum2/main/Main.CAF

10M

8M

(102)main.xs/main/Main.CAF

6M

4M

(101)sum2.iter/sum2/main/Main.CAF

2M

0M

0.0

0.0

0.0

0.1

0.1

0.1

0.1

0.1

0.2

0.2

0.2

0.2

seconds

Рис. 10.11: Профиль кучи для утечки памяти

leak 6 +RTS -K30m -hd -L45

3,016,901 bytes x seconds

Fri Jun 1 23:14 2012

bytes

14M

BLACKHOLE

12M

10M

I#

8M

6M

4M

2M

0M

0.0

0.1

0.1

0.2

0.2

0.2

seconds

Рис. 10.12: Профиль кучи для утечки памяти

main = print . sum2 . xs . read =<< fmap head getArgs

where xs n = [1 .. 10 ^ n]

sum2 :: [Int] -> (Int, Int)

sum2 = iter (0, 0)

where iter c

[]

= c

iter c

(x:xs) = iter (tick x c) xs

tick :: Int -> (Int, Int) -> (Int, Int)

tick x (! c0, ! c1) | even x

= (c0, c1 + 1)

| otherwise = (c0 + 1, c1)

Мы сделали функцию tick строгой. Теперь посмотрим на профиль:

$ ghc —make leak2.hs -rtsopts -prof -auto-all

$ ./leak2 6 +RTS -K30m -hc

(500000,500000)

170 | Глава 10: Реализация Haskell в GHC

$ hp2ps -e80mm -c leak2.hp

Не получилось (рис. 10.13). Как же так. Посмотрим на расход памяти отдельных функций. tick стала

строгой, но этого не достаточно, потому что в первом аргументе iter накапливаются вызовы tick. Сделаем

iter строгой по первому аргументу:

leak2 6 +RTS -K30m -hc

1,854,625 bytes x seconds

Fri Jun 1 21:38 2012

bytes

12M

10M

(102)main.xs/main/Main.CAF

8M

6M

(101)sum2.iter/sum2/main/M…

4M

2M

0M

0.0

0.0

0.0

0.1

0.1

0.1

0.1

0.1

0.2

0.2

0.2

seconds

Рис. 10.13: Опять двойка

sum2 :: [Int] -> (Int, Int)

sum2 = iter (0, 0)

where iter ! c

[]

= c

iter ! c

(x:xs) = iter (tick x c) xs

Теперь снова посмотрим на профиль:

$ ghc —make leak2.hs -rtsopts -prof -auto-all

$ ./leak2 6 +RTS -K30m -hc

(500000,500000)

$ hp2ps -e80mm -c leak2.hp

Мы видим (рис. 10.14), что память резко подскакивает и остаётся постоянной. Но теперь показатели

измеряются не в мегабайтах, а в килобайтах. Мы справились. Остальные флаги hX позволяют наблюдать за

разными специфическими объектами в куче. Мы можем узнать сколько памяти приходится на разные модули

(hm), сколько памяти приходится на разные конструкторы (hd), на разные типы замыканий (hy).

Поиск источников внезапной остановки

case-выражения и декомпозиция в аргументах функции могут стать источником очень неприятных оши-

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

*** Exception: Prelude. head: empty list

или

*** Exception: Maybe. fromJust: Nothing

И совсем не понятно откуда эта ошибка. В каком модуле сидит эта функция. Может мы её импортировали

из чужой библиотеки или написали сами. Как раз для таких случаев в GHC предусмотрен специальный флаг

xc.

Посмотрим на выполнение такой программы:

Статистика выполнения программы | 171

leak2 6 +RTS -hc

5,944 bytes x seconds

Fri Jun 1 21:51 2012

bytes

30k

(51)PINNED

25k

20k

(72)GHC.IO.Encoding.CAF

15k

(59)GHC.IO.Handle.FD.CAF

10k

(58)GHC.Conc.Signal.CAF

5k

0k

0.0

0.0

0.0

0.1

0.1

0.1

0.1

0.1

0.2

0.2

0.2

seconds

Рис. 10.14: Профиль кучи без утечки памяти

module Main where

addEvens :: Int -> Int -> Int

addEvens a b

| even a && even b = a + b

q = zipWith addEvens [0, 2, 4, 6, 7, 8, 10] (repeat 0)

main = print q

Для того, чтобы воспользоваться флагом xc необходимо скомпилировать программу с возможностью про-

филирования:

$ ghc —make break.hs -rtsopts -prof

$ ./break +RTS -xc

*** Exception (reporting due to +RTS -xc): (THUNK_2_0), stack trace:

Main.CAF

break: break.hs:(4,1)-(5,30): Non-exhaustive patterns in function addEvens

Так мы узнали в каком месте кода проявился злосчастный вызов, это строки (4,1)(5,30). Что соот-

ветствует определению функции addEvens. Не очень полезная информация. Мы и так бы это узнали. Нам

бы хотелось узнать тот путь, по которому шла программа к этому вызову. Проблема в том, что все вызовы

слились в один CAF для модуля. Так разделим их:

$ ghc —make break.hs -rtsopts -prof -caf-all -auto-all

$ ./break +RTS -xc

*** Exception (reporting due to +RTS -xc): (THUNK_2_0), stack trace:

Main.addEvens,

called from Main.q,

called from Main.CAF:q

—> evaluated by: Main.main,

called from :Main.CAF:main

break: break.hs:(4,1)-(5,30): Non-exhaustive patterns in function addEvens

Теперь мы видим путь к этому вызову, мы пришли в него из знчения q, которое было вызвано из main.

10.7 Оптимизация программ

В этом разделе мы поговорим о том этапе компиляции, на котором происходят преобразования Core ->

Core. Мы называли этот этап упрощением программы.

172 | Глава 10: Реализация Haskell в GHC

Флаги оптимизации

Мы можем задавать степень оптимизации программы специальными флагами. Самые простые флаги на-

чинаются с большой буквы O. Естесственно, чем больше мы оптимизируем, тем дольше компилируется код.

Поэтому не стоит увлекаться оптимизацией на начальном этапе проектирования. Посмотрим какие возмож-

ности у нас есть:

• без -O – минимум оптимизаций, код компилируется как можно быстрее.

-O0 – выключить оптимизацию полностью

-O – умеренная оптимизация.

O2 – активная оптимизация, код компилируется дольше, но пока O2 не сильно выигрывает у O по про-

дуктивности.

Для оптимизации мы компилируем программу с заданным флагом, например попробуйте скомпилиро-

вать самый первый пример с флагом O:

ghc —make sum.hs -O

и утечка памяти исчезнет.

Посмотреть описание конкретных шагов оптимизации можно в документации к GHC. Например при вклю-

чённой оптимизации GHC применяет анализ строгости. В ходе него GHC может исправить простые утечки

памяти за нас. Стоит отметить оптимизацию fexcessprecision, он может существенно ускорить програм-

мы, в которых много вычислений с Double. Но при этом вычисления могут потерять в точности, округление

становится непредсказуемым.

Прагма INLINE

Читай продолжение на следующей странице
Добавить комментарии