palm_mute ([info]palm_mute) wrote,
@ 2007-07-06 12:59:00
Previous Entry  Add to memories!  Tell a Friend!  Next Entry
Entry tags:continuations, haskell, ocaml

К вопросу об эффективности композиции
Хотелось бы написать о чем-то возвышенном - например, о delimited continuations и инверсии парсеров, а еще раскритиковать трансформеры монад по ходу дела. Но, к сожалению, я пока мало в этом разбираюсь, потому напишу об обычных списках и функциях.

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

type PartialList a = [a]-> [a]


Подход известный, используется в стандартной библиотеке (см. тип ShowS и связанные с ним функции) и не ограничивается списками - Conal Elliot недавно писал об общем случае.

Конкатенация частичных списков - это просто композиция функций:

emptyPartialList = id

addElem :: a -> PartialList a
addElem x = (x:)

toList :: PartialList a -> [a]
toList p = p []

-- test = [1,2,3]
test = toList $ emptyPartialList . addElem 1 . addElem 2 . addElem 3 


Возникает вопрос о границах применимости данного подхода. Если формировать во внутреннем цикле длинные частичные списки - скажем, 20000 элементов - они будут представлены композицией 20000 крошечных функций. Насколько такое представление (не-)эффективно? Я измерил и был удивлен - значительно эффективнее, чем можно было ожидать. Приведенная ниже реализация функции inits разрывает библиотечную реализацию в тряпки:
myInits xs = loop xs emptyPartialList
    where loop [] prefix = [toList prefix]
          loop (x:xs) prefix = (toList prefix) : loop xs (prefix . addElem x)


Код теста:
test :: ([Int]->[[Int]]) -> Int -> Int
test initsf n = sum $ map sum $ initsf $ take n $ cycle [1,-1]

measure expr = do
  t0 <- getClockTime
  r  <- return $! expr
  t1 <- getClockTime
  return (r, diffClockTimes t1 t0)

tests initsf = do
  putStrLn "n\t\tresult\t\ttime"
  putStrLn "============================================"
  mapM_ printTiming [2000, 5000, 10000, 15000, 20000, 25000, 30000]
    where printTiming n = do
            (r, td) <- measure (test initsf n)
            printf "%d\t\t%d\t\t%s\n" n r (timeDiffToString td)

main = do
  putStrLn "Data.List.inits:"
  tests inits
  putStrLn "myInits"
  tests myInits


Результаты:
Data.List.inits:
n		result		time
============================================
2000		1000		1 sec
5000		2500		1 sec
10000		5000		6 secs
15000		7500		32 secs
20000		10000		74 secs
25000		12500		129 secs
30000		15000		196 secs

myInits
n		result		time
============================================
2000		1000		
5000		2500		
10000		5000		1 sec
15000		7500		2 secs
20000		10000		4 secs
25000		12500		6 secs
30000		15000		9 secs



Для полноты картины проверил, насколько хорошо Окамл справляется с десятками тысяч замыканий - оказалось, значительно хуже, чем Haskell:
n               result          time
============================================
2000            1000            0.046000 sec(s)
5000            2500            0.500000 sec(s)
10000           5000            3.125000 sec(s)
15000           7500            8.781000 sec(s)
20000           10000           18.641000 sec(s)
25000           12500           32.905000 sec(s)
30000           15000           52.906000 sec(s)


Полный код теста на Окамле:
type 'a partial_list = 'a list -> 'a list

let empty_partial_list = fun x -> x

let append prefix x = fun xs -> prefix (x::xs)

let to_list p = p []

let iter_inits f xs = 
  let rec loop lst prefix = match lst with
    | [] -> f (to_list prefix)
    | x::xs -> begin f (to_list prefix); loop xs (append prefix x) end
  in
    loop xs empty_partial_list

let rec cycle = 1::-1::cycle

let sum = List.fold_left (+) 0

let rec take n lst = match (n, lst) with
  | (0, _)     -> []
  | (_, [])    -> []
  | (n, x::xs) -> x :: take (n-1) xs

let test n = 
  let ret = ref 0 in
  let process lst = ret := !ret + sum lst
  in (iter_inits process (take n cycle); !ret)

let measure f = 
  let t0 = Sys.time () in
  let r  = f () in
  let t1 = Sys.time () in
    (r, t1 -. t0)

let main () =
  let print_timing n = 
    let (r, td) = measure (fun () -> test n) in
      begin
        Printf.printf "%d\t\t%d\t\t%f sec(s)\n" n r td;
        flush stdout
      end
  in 
    begin
      print_endline "n\t\tresult\t\ttime";
      print_endline "============================================";
      List.iter print_timing [2000; 5000; 10000; 15000; 20000; 25000; 30000]
    end

let _ = main ()


На сегодня все. Надо поработать и почитать диссертацию Анджея Филински "Declarative Continuations and Categorical Duality" - глядишь, будет чего написать и по более интересным вопросам.


(Post a new comment)


[info]deni_ok
2007-07-06 11:02 am UTC (link)
Здорово!

А ключики оптимизации в GHC использовал?

(Reply to this)(Thread)


[info]palm_mute
2007-07-06 11:13 am UTC (link)
Ничего кроме -O2. Окамловский вариант компилировал ocamlopt'ом без дополнительных ключей.

(Reply to this)(Parent)


[info]lomeo
2007-07-06 11:45 am UTC (link)
Просто inits в Prelude неэффективный. Даже такие варианты как

inits = map reverse . scanl (flip (:)) []

или

inits = scanl (\xs x -> xs ++ [x])

должны быть эффективнее. Второй менее, конечно.

(Reply to this)(Thread)


[info]lomeo
2007-07-06 11:59 am UTC (link)
Поискал и нашёл вот что: http://www.haskell.org/pipermail/libraries/2006-April/005217.html
Оказывается мы тут сидим и выдумываем выдуманное :-/

Вот собственно начало треда: http://osdir.com/ml/lang.haskell.libraries/2006-04/msg00017.html

(Reply to this)(Parent)(Thread)


[info]palm_mute
2007-07-06 12:01 pm UTC (link)
Спасибо, почитаем

(Reply to this)(Parent)


[info]palm_mute
2007-07-07 09:02 pm UTC (link)
Почитал, интересно.
То, что это кто-нибудь выдумал, было вне всякого сомнения. Выдумывать уже выдуманное - полезное и интересное занятие. Приятно, в конце концов, независимо повторить изобретение самого тов. Manuel Chakravarty, которым пользуется сам Don Stewart :).

Забавно, что для теста они использовали ту же самую функцию - sum . map sum.

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

It's not really surprising: the nested composition built by helper is
essentially a list, which is traversed by ($ [])

Меня это и интересовало, только разницу в производительности Хаскеля и Окамла это не объясняет - видимо, нужно понимать кишки компиляторов.

(Reply to this)(Parent)(Thread)


[info]lomeo
2007-07-08 04:18 pm UTC (link)
На самом деле я не понимаю почему композиция эффективнее списка :-/
А то что inits неэффективный, просто уже натыкался.

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-08 04:46 pm UTC (link)
> На самом деле я не понимаю почему композиция эффективнее списка

Я тоже, но сталкиваюсь с этим не первый раз. Наверное какие-то общеизвестные простые редукции компилятору удобно делать, хотя кто его знает. В Харрисоне-Филде ничего не нарыл, но там в поседних главах сам чёрт ногу сломит.

(Reply to this)(Parent)


[info]some41
2007-08-13 07:14 am UTC (link)
похоже композиция эффективнее только в каких-то частных случаях. я попробовал список функций, которые последовательно выполняются, заменить на композицию функций - никакой разницы.

(Reply to this)(Parent)


[info]geniepro
2007-07-06 05:51 pm UTC (link)
> Как известно, добавлять элементы в конец списка в Haskell и других функциональных языках неэффективно.

Кстати, а почему так?

(Reply to this)(Thread)


[info]palm_mute
2007-07-06 08:43 pm UTC (link)
Это же простые односвязные списки.

(Reply to this)(Parent)(Thread)


[info]geniepro
2007-07-07 11:09 am UTC (link)
Вопщем, имеется два варианта:

1) Программа знает адреса как первого, так и последнего элементов списков.
Тогда добавить элемент в конец списка займёт столько же времени, как и в начало.

2) Программа знает адрес только первого элемента списка.
Тогда для добавления элемента в конец придётся пробежаться по всему списку с самого начала...

Что заставляет трансляторы Хаскелла использовать второй вариант? Только лишь ленивость (бесконечные списки не имеют последнего элемента)? Или есть ещё какие-то веские причины?
А в других языках?

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-07 12:01 pm UTC (link)
> Что заставляет трансляторы Хаскелла использовать второй вариант?

Хвост гарантировано иммутабелен. Это обеспечивает массу оптимизаций. Конструирование или сопоставление с образцом (x:xs) совершенно бесплатны (в смысле - не зависят от длины списка).

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

(Reply to this)(Parent)


[info]palm_mute
2007-07-07 08:49 pm UTC (link)
Список мало отличается от пользовательскимх типов, кроме синтаксической поддержки компилятора, это просто
data List a = Nil | Cons a (List a)


Даже если допустить, что списки как-то особо обрабатываются компилятором, то знанием адреса последнего элемента не обойтись:
let xs = [1..10]
    xs' = xs ++ [11] -- что здесь произойдет с xs?


Здесь нужны какие-нибудь finger trees (см. Data.Sequence).
А списки - просты как валенок, и тем не менее, достаточно полезны.

(Reply to this)(Parent)(Thread)


[info]geniepro
2007-07-08 09:16 am UTC (link)
Ну хорошо, но почему в этом примере такой вариант :
let xs = [1..10]
    xs' = 11:xs

будет быстрее, чем
let xs = [1..10]
    xs' = xs ++ [11]

Ведь и в том, и в другом случаях идёт копирование списков...

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-08 09:55 am UTC (link)
> xs' = 11:xs

Какое тут копирование? Добавляется-то в голову

Hugs> 11:[1..10]
[11,1,2,3,4,5,6,7,8,9,10]

И в памяти как здесь (пример в конце)
http://www.rsdn.ru/forum/message/2576207.1.aspx

(Reply to this)(Parent)(Thread)


[info]geniepro
2007-07-08 03:40 pm UTC (link)
> Какое тут копирование? Добавляется-то в голову

Ну, эта... Императивный рецидив...

Я, конечно, знал о таком варианте, но как-то позабыл, вот...

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

> http://www.rsdn.ru/forum/message/2576207.1.aspx

С трудом осилил эту бредятину, затеянную лаперрусом...

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-08 03:57 pm UTC (link)
> С трудом осилил эту бредятину, затеянную лаперрусом...

Да ладно, человек вроде всё-таки больше разобраться хочет, чем похоливарничать...

(Reply to this)(Parent)


[info]lomeo
2007-07-08 04:21 pm UTC (link)
Аватарка - супер! Каски не хватает.

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-08 04:40 pm UTC (link)
> Каски не хватает.

Как догадался? До Кодорского ущелья - километров 30. Почти что во время заварушки.
Я - бесстрашный! :-)

Эта аватарка - не для повседневной носки. Парадно-выходная.

(Reply to this)(Parent)(Thread)


[info]lomeo
2007-07-08 11:19 pm UTC (link)
Показалось, что снято в пещере или гроте :)

(Reply to this)(Parent)(Thread)


[info]deni_ok
2007-07-09 04:54 am UTC (link)
В горах над Пцырсхой над Новым Афоном. Просто только что стемнело.

(Reply to this)(Parent)


Create an Account
Forgot your login?
Login w/ OpenID
English • Español • Deutsch • Русский…