Неопровержимият модел не пропуска памет при рекурсия, но защо?

Функцията mapAndSum в кодовия блок докрай комбинира map и sum (няма значение, че в главната функция е приложено друго sum, то просто служи, за да направи изхода компактен). map се изчислява лениво, докато sum се изчислява с помощта на натрупващ се параметър. Идеята е, че резултатът от map може да бъде консумиран, без изобщо да има пълния списък в паметта, и (само) след това sum е достъпен "безплатно". Основната функция показва, че сме имали проблем с неопровержими модели при извикване на mapAndSum. Нека обясня този проблем.

Съгласно стандарта Haskell, примерът за неопровержим шаблон let (xs, s) = mapAndSum ... in print xs >> print s се превежда в

(\ v ->    print (case v of { (xs, s) -> xs })
        >> print (case v of { (xs, s) -> s }))
$ mapAndSum ...

И следователно и двете print извиквания носят препратка към цялата двойка, което води до запазване на целия списък в паметта.

Ние (колегата ми Toni Dietze и аз) решихме това с помощта на изрично изявление case (сравнете "лошо" срещу "добро2"). Между другото, намирането на това ни отне значително време..!

Това, което не разбираме, е две:

  • Защо mapAndSum работи на първо място? Той също така използва неопровержим модел, така че трябва също да пази целия списък в паметта, но очевидно не го прави. И преобразуването на let в case би накарало функцията да се държи напълно нелениво (до степен, че стекът препълва; не е предназначена игра на думи).

    Разгледахме „основния“ код, генериран от GHC, но доколкото можехме да го интерпретираме, той всъщност съдържаше същия превод на let като този по-горе. Така че тук няма представа и вместо това има повече объркване.

  • Защо "лошо?" работи, когато оптимизацията е изключена, но не и когато е включена?

Една забележка относно нашето действително приложение: искаме да постигнем поточна обработка (преобразуване на формат) на голям текстов файл, като същевременно натрупваме някаква стойност, която след това се записва в отделен файл. Както беше посочено, успяхме, но двата въпроса остават и отговорите може да подобрят разбирането ни за GHC за предстоящи задачи и проблеми.

Благодаря ти!

{-# LANGUAGE BangPatterns #-}

-- Tested with The Glorious Glasgow Haskell Compilation System, version 7.4.2.

module Main where


import Control.Arrow (first)
import Data.List (foldl')
import System.Environment (getArgs)


mapAndSum :: Num a => (a -> b) -> [a] -> ([b], a)
mapAndSum f = go 0
  where go !s (x : xs) = let (xs', s') = go (s + x) xs in (f x : xs', s')
                       -- ^ I have no idea why it works here. (TD)
        go !s []       = ([], s)


main :: IO ()
main = do
  let foo  = mapAndSum (^ (2 :: Integer)) [1 .. 1000000 :: Integer]
  let sum' = foldl' (+) 0
  args <- getArgs
  case args of
    ["bad" ]  -> let (xs, s) = foo in print (sum xs) >> print s
    ["bad?"]  -> print $ first sum' $ foo
              -- ^ Without ghc's optimizer, this version is as memory
              -- efficient as the “good” versions
              -- With optimization “bad?” is as bad as “bad”. Why? (TD)
    ["good1"] -> print $ first' sum' $ foo
                   where first' g (x, y) = (g x, y)
    ["good2"] -> case foo of
                    (xs, s) -> print (sum' xs) >> print s
    ["good3"] -> (\ (xs, s) -> print (sum' xs) >> print s) $ foo
    _ -> error "Sorry, I do not understand."

person buechse    schedule 24.07.2012    source източник
comment
Вашата mapAnsSum функция може да бъде подобрена чрез използване на mapAccumL от Data.Traversable.   -  person Laar    schedule 24.07.2012
comment
Предполагам, че имате предвид „оптимизатора на ghc“ в коментара, нали?   -  person Joachim Breitner    schedule 24.07.2012
comment
Да, съжалявам, имаме предвид оптимизатора на ghc.   -  person buechse    schedule 24.07.2012
comment
Може би пропускам нещо, но изглежда не използвате никакви неопровержими модели.   -  person dave4420    schedule 24.07.2012
comment
Този документ описва как изразите let се преобразуват в регистър с неопровержим модел: haskell .org/onlinereport/haskell2010/ Резюме: Обвързването на шаблони се съпоставя лениво; имплицитно ~ прави тези модели неопровержими   -  person buechse    schedule 24.07.2012


Отговори (1)


Нека първо отговоря защо mapAndSome изобщо може да работи добре: Това, което виждате, е (много вероятно) ефектът от оптимизация, описана от Philip Wadler в „Коригиране на някои течове на пространство с колектор за боклук". Кратко резюме: Ако събирачът на боклук види thunk на формата fst x и x вече е оценен на конструктора на кортежи, напр. (y,z), той ще замени fst x с y, евентуално освобождавайки z, ако не е споменат никъде другаде.

Във вашия код s', след като резултатът от go бъде оценен на кортеж и след един кръг на GCing, няма да запази препратка към кортежа, а ще бъде заменен от натрупания параметър.

Сега нека разгледаме другите модели, като изследваме ядрото. Обвързването foo е компилирано за:

foo_r2eT :: ([Type.Integer], Type.Integer)

foo_r2eT =
  case $wgo_r2eP mapAndSum1 lvl2_r2eS
  of _ { (# ww1_s2d7, ww2_s2d8 #) ->
  (ww1_s2d7, ww2_s2d8)
  }

Ето кода в случай "bad" (lvl18_r2fd е "лош"):

case eqString ds_dyA lvl18_r2fd of _ {
  False -> $wa_s2da new_s_a14o;
  True ->
    case ds1_dyB of _ {
      [] ->
        case Handle.Text.hPutStr2
               Handle.FD.stdout lvl17_r2fc True new_s_a14o
        of _ { (# new_s1_X15h, _ #) ->
        Handle.Text.hPutStr2
          Handle.FD.stdout lvl16_r2fb True new_s1_X15h
        };
      : ipv_sIs ipv1_sIt -> $wa_s2da new_s_a14o
    }

Виждаме, че се отпечатват две стойности на ниво модул, lvl17_r2fc и lvl16_r2fb, ето техния код:

lvl17_r2fc :: String
[GblId]
lvl17_r2fc =
  case foo_r2eT of _ { (xs_Xqp, s_Xq9) ->
  $w$cshowsPrec
    0
    (Data.List.sum_sum' xs_Xqp Data.List.genericDrop2)
    ([] @ Char)
  }

lvl16_r2fb :: String
[GblId]
lvl16_r2fb =
  case foo_r2eT of _ { (xs_apS, s_Xqp) ->
  $w$cshowsPrec 0 s_Xqp ([] @ Char)
  }

Защо са обвързани на ниво модул, а не вътре в израза? Това е ефект на мързеливо повдигане, друга оптимизация, която увеличава споделянето и следователно понякога има неблагоприятен ефект върху производителността на пространството. Вижте GHC билет 719 за друго появяване на този ефект.

И така, това, което се случва, е, че оценката на lvl17_r2fc кара foo да бъде оценена и левият запис се отпечатва лениво. За съжаление, lvl16_r2fb все още е активен и запазва пълния кортеж. И тъй като събирачът на отпадъци (изглежда) пропуска да види, че това е селектор, оптимизацията на Wadler не се включва.

За разлика от това, тук е кодът за "good1" известен още като lvl8_r2f1:

            case eqString ds_dyA lvl8_r2f1 of _ {
              False -> $wa2_s2dI w3_s2cF;
              True ->
                case ds1_dyB of _ {
                  [] ->
                    Handle.Text.hPutStr2
                      Handle.FD.stdout lvl7_r2f0 True w3_s2cF;
                  : ipv_sHg ipv1_sHh -> $wa2_s2dI w3_s2cF
                }
            } } in

където отпечатаната стойност е този низ:

lvl7_r2f0 :: String
[GblId]
lvl7_r2f0 =
  case foo_r2eT of _ { (x_af6, y_af7) ->
  show_tuple
    (:
       @ ShowS
       (let {
          w2_a2bY [Dmd=Just L] :: Type.Integer

          w2_a2bY = lgo_r2eU mapAndSum1 x_af6 } in
        \ (w3_a2bZ :: String) ->
          $w$cshowsPrec 0 w2_a2bY w3_a2bZ)
       (:
          @ ShowS
          (\ (w2_a2bZ :: String) ->
             $w$cshowsPrec 0 y_af7 w2_a2bZ)
          ([] @ ShowS)))
    ([] @ Char)
  }

Както можете да видите, кортежът се разглобява само веднъж и се използват и двете стойности. Така че нищо не се отнася за кортежа като цяло и може да се събира боклук. Подобно за "good2" и "good3".

Сега към "bad?": В неоптимизирания случай получаваме този код:

 case eqString ds_dyA (unpackCString# "bad?")
                 of _ {
                   False -> fail2_dyN realWorld#;
                   True ->
                     case ds1_dyB of _ {
                       [] ->
                         $
                           @ (Type.Integer, Type.Integer)
                           @ (IO ())
                           (System.IO.print
                              @ (Type.Integer, Type.Integer) $dShow_rzk)
                           ($
                              @ ([Type.Integer], Type.Integer)
                              @ (Type.Integer, Type.Integer)
                              (Control.Arrow.first
                                 @ (->)
                                 Control.Arrow.$fArrow(->)
                                 @ [Type.Integer]
                                 @ Type.Integer
                                 @ Type.Integer
                                 sum'_rzm)
                              foo_rzl);
                       : ipv_szd ipv1_sze -> fail2_dyN realWorld#
                     }
                 } } in

внедряването от first чрез *** използва опровержими шаблони, така че се генерира вид селектор, с който събирачът на боклук се справя добре.

В оптимизирания случай нещата са малко разпръснати, но така или иначе тук е съответният код (последната стойност е тази, която се отпечатва):

w_r2f2 :: Type.Integer

w_r2f2 =
  case foo_r2eT of _ { (x_aI1, y_aI2) ->
  lgo_r2eU mapAndSum1 x_aI1
  }

lvl9_r2f3 :: String -> String
[GblId, Arity=1]
lvl9_r2f3 =
  \ (w2_a2bZ :: String) ->
    $w$cshowsPrec 0 w_r2f2 w2_a2bZ

w1_r2f4 :: Type.Integer

w1_r2f4 = case foo_r2eT of _ { (x_aI6, y_aI7) -> y_aI7 }

lvl10_r2f5 :: String -> String
[GblId, Arity=1]
lvl10_r2f5 =
  \ (w2_a2bZ :: String) ->
    $w$cshowsPrec 0 w1_r2f4 w2_a2bZ

lvl11_r2f6 :: [ShowS]
[GblId]
lvl11_r2f6 =
  :
    @ ShowS lvl10_r2f5 ([] @ ShowS)

lvl12_r2f7 :: [ShowS]
[GblId]
lvl12_r2f7 = : @ ShowS lvl9_r2f3 lvl11_r2f6

lvl13_r2f8 :: ShowS
[GblId]
lvl13_r2f8 = show_tuple lvl12_r2f7

lvl14_r2f9 :: String
[GblId]
lvl14_r2f9 = lvl13_r2f8 ([] @ Char)

Използването на first е вградено. Виждаме две извиквания към case foo_r2eT, така че това е предразположено към изтичане на пространство, въпреки че w1_rf24 изглежда като селектор (така че бих очаквал времето за изпълнение да приложи оптимизацията на Wadler). Може би не работи добре за статични удари? Наистина коментар, ако е актуален, говори само за динамично разпределени прехвърляния на селектора. Така че, ако вашият foo не беше стойност на ниво модул (или по-скоро мързеливо вдигане в такъв), а по-скоро зависи от някакъв вход, w1_rf24 може да бъде динамично разпределен и следователно да отговаря на условията за специално третиране. Но тогава кодът така или иначе може да изглежда много различно.

person Joachim Breitner    schedule 24.07.2012
comment
Ако ви разбирам правилно, разглеждаме проблем с внедряването, а не проблем с езика? С други думи, не можах да намеря нищо за моя проблем в отчета на Haskell или основния изход, а с друга реализация на Haskell всички варианти, които имаме в основната функция, може да се държат като зле? С други думи, нашата критична за ефективността програма стои върху много тънък лед? - person buechse; 24.07.2012
comment
Е, докладът на Haskell не прави изявление за поведението в космоса, така че така или иначе сте изгубени там. И да, други реализации на Haskell може да се представят лошо във всеки от случаите. - person Joachim Breitner; 24.07.2012
comment
Добре, тогава има ли начин ледът да се удебели поне малко? Можем ли да направим идеята за поточно предаване, докато се натрупва, по-ясна? Опитахме нещо със ST монада и работи по принцип, но (ако си спомням правилно и изненадващо) имаше същото пространствено поведение като лошо... - person buechse; 24.07.2012
comment
Вярвам, че за това са неща като библиотеката pipe, но не съм експерт по това. - person Joachim Breitner; 24.07.2012
comment
Ситуацията е още по-лоша, тъй като компилаторът понякога не може да приложи оптимизацията, предложена от Wadler, например вижте тази тема haskell.org/pipermail/glasgow-haskell-users/2010-октомври/. Има случаи, при които първо се прилагат други оптимизации и компилаторът не може да разпознае, че програмата проектира към един компонент на конструктор. - person Jan Christiansen; 24.07.2012
comment
Уау, Йоахим, твоят отговор заслужава да бъде гласуван два пъти (за съжаление, не мога да направя това)! - person buechse; 24.07.2012
comment
Всичко добро, само че не е измислено от Фил Уодлър. Той е документиран от Фил, но е изобретен независимо от поне двама души, преди Фил да напише тази статия. - person augustss; 24.07.2012
comment
О, благодаря за корекцията. За щастие мога да редактирам академичната си наглост тук :-) - person Joachim Breitner; 24.07.2012
comment
Цялата тази работа е индикация за мен, че Хърб Сътър всъщност може да е прав, когато казва (или по-скоро цитира?), че C++ може да не е най-лесният от езиците, но писането на наистина ефективен код е най-лесно в C++ . Но не се притеснявайте, все още съм във влака на Haskell. - person buechse; 24.07.2012
comment
Имам предвид, че ако искате да направите програмата си Haskell наистина ефективна, трябва да разберете същите концепции като C програмиста плюс много повече. - person buechse; 24.07.2012
comment
Освен това ghc не използва ли вариант на това, което е описано в книгата на Jan Sparud Fixing Some Space Leaks without a Garbage Collector? citeseerx.ist.psu.edu/viewdoc/summary?doi= 10.1.1.46.5423 - person augustss; 24.07.2012
comment
@JoachimBreitner Случайно знам, че Фил не го е измислил, тъй като му казах за това. :) - person augustss; 24.07.2012
comment
Може би също така, но със сигурност съкращава селектора за преки пътища в колектора за събиране на боклук: hackage.haskell.org/trac/ghc/browser/rts/sm/Evac.c#L1031 - person Joachim Breitner; 24.07.2012
comment

Това е частичен отговор, той разглежда само как да създадете сървър, който ще слуша за клиентски WebSocket връзки.

Виждали ли сте класа WebSocketTransformer?

WebSocketTransformer

Все още не съм имал възможност да опитам това - но мисля, че е нещо подобно:

HttpServer.bind(...).then((server) {
     server.transform(new WebSocketTransformer()).listen((webSocket) => ... );
});

Вижте също тази дискусия в пощенския списък на Dart .

- person augustss; 24.07.2012