Функция Break Haskell переходит в подзадачи без дополнительных обходов списка

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

mean xs = realToFrac (sum xs) / realToFrac (length xs)
cov xs ys = mean (zipWith (*) xs ys) - mean xs * mean ys
covmat vectors = [cov xs ys | ys <- vectors, xs <- vectors]

который работал для небольшого ввода, но вы можете видеть, насколько неэффективен даже "средний". Он сохраняет все xs в памяти при выполнении суммы, потому что они будут использоваться по длине. Thats fixable, так:

mean xs = realToFrac thisSum / realToFrac thisLength
    where (thisSum, thisLength) = foldl' (\(s,l) y-> (s+y,l+1)) (0,0) xs

но затем я столкнулся с той же проблемой с "cov" . Когда я переписал "cov" в этом стиле, он не закончил использовать мою "среднюю" функцию. И у меня все еще есть один уровень, чтобы подняться, когда я напишу функцию "covmat", которая станет чрезвычайно сложной.

Итак, у меня есть две цели, которые, похоже, находятся в конфликте:

  • Перемещайте каждый список один раз, не сохраняя его в памяти

  • Разбить "covmat" на более простые, значимые функции, в частности "cov" и "mean"

Я не вижу никакого способа объединить эти два цели с тем, что я знаю о Haskell. Но концептуально это кажется простым: все эти функции должны "слушать" значения тех же немногих списков, что и они. Есть ли способ в Haskell организовать его так? Если для этого требуется другая структура данных или дополнительная библиотека, я открыт для этого.

Ответ 1

Итак, я кое-что копал, и я придумал следующее.

EDIT: Gist для людей, которые предпочли бы это для форматирования SO.

Во-первых, несколько реализаций среднего

module Means where

import Data.List
import Control.Applicative

mean :: (Fractional a1, Real a, Foldable t) => t a -> a1
mean xs = realToFrac (sum xs) / realToFrac (length xs)

mean' :: (Foldable f, Fractional a) => f a -> a
mean' = liftA2 (/) sum (fromIntegral . length)

data Pair = Pair {-# UNPACK #-}!Int {-# UNPACK #-}!Double 

mean'' :: [Double] -> Double
mean'' xs = s / fromIntegral n
  where
    Pair n s = foldl' k (Pair 0 0) xs
    k (Pair n s) x = Pair (n+1) (s+x)

Последний использует явный строгий конструктор пар. IIRC, (,) ленив, поэтому это должно дать нам лучшие характеристики производительности.

module Covariance where

import Means

covariance :: (Fractional a, Real a1) => [a1] -> [a1] -> a
covariance xs ys = mean (zipWith (*) xs ys) - mean xs * mean ys

covariance' :: Fractional a => [a] -> [a] -> a
covariance' xs ys = mean' (zipWith (*) xs ys) - mean' xs * mean' ys

covariance'' :: [Double] -> [Double] -> Double
covariance'' xs ys = mean'' (zipWith (*) xs ys) - mean'' xs * mean'' ys

covariance''' :: [Double] -> [Double] -> Double
covariance''' xs ys =
    let mx = mean'' xs
        my = mean'' ys
    in
    sum (zipWith (\x y -> (x - mx) * (y - my)) xs ys) / fromIntegral (length xs)

Я попробовал несколько версий вашего cov, используя каждую из различных средних опций, затем одну более уродливую версию "производительности".

Я собрал простой Main с некоторыми жестко запрограммированными списками для тестирования.

module Main where

import Means
import Covariance

v1 = [1000000..2000000]

v2 = [2000000..3000000]

main :: IO ()
main = do
  -- let cov = covariance v1 v2
  -- let cov = covariance' v1 v2
  -- let cov = covariance'' v1 v2
  let cov = covariance''' v1 v2
  print cov

Компиляция с -rtsopts и работающая с +RTS -s, я получил следующую информацию о размещении.

covariance:

8.33335e10
     531,816,984 bytes allocated in the heap
     890,566,720 bytes copied during GC
     148,609,912 bytes maximum residency (11 sample(s))
      15,649,528 bytes maximum slop
             374 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       981 colls,     0 par    0.385s   0.389s     0.0004s    0.0012s
  Gen  1        11 colls,     0 par    0.445s   0.584s     0.0531s    0.2084s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.194s  (  0.168s elapsed)
  GC      time    0.830s  (  0.973s elapsed)
  EXIT    time    0.001s  (  0.029s elapsed)
module Main where
  Total   time    1.027s  (  1.172s elapsed)

  %GC     time      80.9%  (83.0% elapsed)

  Alloc rate    2,741,140,975 bytes per MUT second

  Productivity  19.1% of total user, 16.8% of total elapsed

covariance':

8.333350000320508e10
     723,822,456 bytes allocated in the heap
     891,626,240 bytes copied during GC
     185,629,664 bytes maximum residency (11 sample(s))
       3,693,296 bytes maximum slop
             435 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      1372 colls,     0 par    0.388s   0.392s     0.0003s    0.0010s
  Gen  1        11 colls,     0 par    0.388s   0.551s     0.0501s    0.1961s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.227s  (  0.202s elapsed)
  GC      time    0.777s  (  0.943s elapsed)
  EXIT    time    0.001s  (  0.029s elapsed)
  Total   time    1.006s  (  1.176s elapsed)

  %GC     time      77.2%  (80.2% elapsed)

  Alloc rate    3,195,430,190 bytes per MUT second

  Productivity  22.8% of total user, 19.6% of total elapsed

covariance'':

8.333350000320508e10
     456,108,392 bytes allocated in the heap
     394,432,096 bytes copied during GC
      79,295,648 bytes maximum residency (15 sample(s))
      21,161,776 bytes maximum slop
             201 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       861 colls,     0 par    0.085s   0.089s     0.0001s    0.0005s
  Gen  1        15 colls,     0 par    0.196s   0.274s     0.0182s    0.0681s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.124s  (  0.106s elapsed)
  GC      time    0.282s  (  0.362s elapsed)
  EXIT    time    0.001s  (  0.021s elapsed)
  Total   time    0.408s  (  0.491s elapsed)

  %GC     time      69.1%  (73.7% elapsed)

  Alloc rate    3,681,440,521 bytes per MUT second

  Productivity  30.9% of total user, 25.9% of total elapsed

covariance''':

8.333349999886264e10
     336,108,336 bytes allocated in the heap
     202,943,312 bytes copied during GC
      47,493,864 bytes maximum residency (10 sample(s))
      13,578,520 bytes maximum slop
             115 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       633 colls,     0 par    0.053s   0.055s     0.0001s    0.0002s
  Gen  1        10 colls,     0 par    0.089s   0.131s     0.0131s    0.0472s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.095s  (  0.086s elapsed)
  GC      time    0.142s  (  0.186s elapsed)
  EXIT    time    0.001s  (  0.011s elapsed)
  Total   time    0.240s  (  0.286s elapsed)

  %GC     time      59.2%  (65.1% elapsed)

  Alloc rate    3,522,631,228 bytes per MUT second

  Productivity  40.8% of total user, 34.1% of total elapsed

Как вы можете видеть, большая часть выделения зависит от того, с чем мы идем. Мы получаем наибольшее повышение, используя mean'' со строгим конструктором пары, даже с наивной реализацией zipWith.

Я работаю над подключением реализаций с помощью weigh, поэтому у меня может быть несколько данных в бит.

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

EDIT: weigh результаты

Case                                  Allocated    GCs
naive mean                          723,716,168  1,382
applicative mean                    723,714,736  1,382
optimized mean, naive zipWith       456,000,688    875
optimized mean, hand-tuned zipWith  336,000,672    642

Второй EDIT:

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

import qualified Control.Foldl as L

mean''' :: [Double] -> Double
mean''' = L.fold (liftA2 (/) L.sum L.genericLength)

covariance'''' :: [Double] -> [Double] -> Double
covariance'''' xs ys = mean''' (zipWith (*) xs ys) - mean''' xs * mean''' ys

covariance''''' :: [Double] -> [Double] -> Double
covariance''''' xs ys = let mx = mean''' xs
                            my = mean''' ys
                        in
                        mean''' (zipWith (\x y -> (x - mx) * (y - my)) xs ys)

Результаты распределения:

covariance'''':

8.333350000320508e10
     336,108,272 bytes allocated in the heap
     222,635,752 bytes copied during GC
      61,198,528 bytes maximum residency (10 sample(s))
      13,627,544 bytes maximum slop
             140 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       633 colls,     0 par    0.052s   0.054s     0.0001s    0.0003s
  Gen  1        10 colls,     0 par    0.105s   0.155s     0.0155s    0.0592s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.110s  (  0.099s elapsed)
  GC      time    0.156s  (  0.209s elapsed)
  EXIT    time    0.001s  (  0.014s elapsed)
  Total   time    0.269s  (  0.323s elapsed)

  %GC     time      58.1%  (64.5% elapsed)

  Alloc rate    3,054,641,122 bytes per MUT second

  Productivity  41.8% of total user, 34.9% of total elapsed

covariance''''':

8.333349999886264e10
     336,108,232 bytes allocated in the heap
     202,942,400 bytes copied during GC
      47,493,816 bytes maximum residency (10 sample(s))
      13,578,568 bytes maximum slop
             115 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       633 colls,     0 par    0.057s   0.059s     0.0001s    0.0003s
  Gen  1        10 colls,     0 par    0.086s   0.126s     0.0126s    0.0426s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.096s  (  0.087s elapsed)
  GC      time    0.143s  (  0.184s elapsed)
  EXIT    time    0.001s  (  0.011s elapsed)
  Total   time    0.241s  (  0.285s elapsed)

  %GC     time      59.2%  (64.7% elapsed)

  Alloc rate    3,504,449,342 bytes per MUT second

  Productivity  40.8% of total user, 34.5% of total elapsed

И weigh результаты:

foldl mean                          336,000,568    642
foldl mean, tuned zipWith           336,000,568    642

В целом, похоже, что реализация foldl - ваш лучший выбор. Это предельно ясно, что он делает, и тянет некоторые действительно причудливые трюки, чтобы эффективно управлять потоками, выполняя или превосходя результат нашей ручной настройки. Вы могли бы, вероятно, извлечь дополнительный сок из всего этого, используя другую структуру данных, но это довольно хорошая производительность для скромного списка.: D

Третье редактирование:

Я раньше не использовал Edward folds, поэтому я могу делать что-то очень глупое, но я также пробовал реализацию с помощью этих.

import qualified Data.Fold as F

sumL :: Num a => F.L a a
sumL = F.L id (+) 0

lengthL :: Num b => F.L a b
lengthL = F.L id (const . (+1)) 0

mean'''' :: [Double] -> Double
mean'''' = flip F.run (liftA2 (/) sumL lengthL)

covariance'''''' :: [Double] -> [Double] -> Double
covariance'''''' xs ys = mean'''' (zipWith (*) xs ys) - mean'''' xs * mean'''' ys

covariance''''''' :: [Double] -> [Double] -> Double
covariance''''''' xs ys = let mx = mean'''' xs
                              my = mean'''' ys
                        in
                        mean'''' (zipWith (\x y -> (x - mx) * (y - my)) xs ys)

Результаты распределения:

covariance'''''':

8.333350000320508e10
     456,108,488 bytes allocated in the heap
     394,432,096 bytes copied during GC
      79,295,648 bytes maximum residency (15 sample(s))
      21,161,776 bytes maximum slop
             201 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       861 colls,     0 par    0.089s   0.092s     0.0001s    0.0003s
  Gen  1        15 colls,     0 par    0.198s   0.276s     0.0184s    0.0720s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.135s  (  0.119s elapsed)
  GC      time    0.287s  (  0.367s elapsed)
  EXIT    time    0.001s  (  0.019s elapsed)
  Total   time    0.425s  (  0.506s elapsed)

  %GC     time      67.6%  (72.5% elapsed)

  Alloc rate    3,388,218,993 bytes per MUT second

  Productivity  32.3% of total user, 27.1% of total elapsed

covariance''''''':

8.333349999886264e10
     456,108,552 bytes allocated in the heap
     291,275,200 bytes copied during GC
      62,670,040 bytes maximum residency (11 sample(s))
      15,029,432 bytes maximum slop
             172 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       862 colls,     0 par    0.068s   0.070s     0.0001s    0.0003s
  Gen  1        11 colls,     0 par    0.149s   0.210s     0.0191s    0.0570s

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.118s  (  0.104s elapsed)
  GC      time    0.217s  (  0.280s elapsed)
  EXIT    time    0.001s  (  0.016s elapsed)
  Total   time    0.337s  (  0.403s elapsed)

  %GC     time      64.3%  (69.6% elapsed)

  Alloc rate    3,870,870,585 bytes per MUT second

  Productivity  35.7% of total user, 29.9% of total elapsed

И weigh результаты:

folds mean                          456,000,784    875
folds mean, tuned zipWith           456,000,888    871

Еще один EDIT: я также попробовал параметр folds, используя L', а не L, но результаты были одинаковыми.