【问题标题】:Ever increasing CPU consumption with Haskell and stream-fusion使用 Haskell 和流融合不断增加 CPU 消耗
【发布时间】:2014-04-15 09:08:36
【问题描述】:

这是一个简短的 Haskell 程序,它可以生成 440 Hz 的声音。它使用 pulseaudio 作为音频后端。

import GHC.Float
import Control.Arrow
import Sound.Pulse.Simple
import qualified Data.List.Stream as S
import Data.List

type Time = Double
type Frequency = Double
type Sample = Double
type CV = Double

chunksize = 441 * 2
sampleRate :: (Fractional a) => a
sampleRate = 44100

integral :: [Double] -> [Double]
integral = scanl1  (\acc x -> acc + x / sampleRate)

chunks :: Int -> [a] -> [[a]]
chunks n = S.takeWhile (not . S.null) . S.unfoldr (Just . S.splitAt n)

pulseaudioOutput :: [Sample] -> IO ()
pulseaudioOutput sx = do

    pa <- simpleNew Nothing "Synths" Play Nothing "Synths PCM output"
         (SampleSpec (F32 LittleEndian) 44100 1) Nothing Nothing

    mapM_ (simpleWrite pa . S.map double2Float) $ chunks 1000 sx

    simpleDrain pa
    simpleFree pa

oscSine :: Frequency -> [CV] ->  [Sample]
oscSine f = S.map sin <<< integral <<< S.map ((2 * pi * f *) . (2**))

music ::[Sample]
music = oscSine 440 (S.repeat 0)

main = do
    pulseaudioOutput music

如果我编译并运行它,我会看到 CPU 消耗不断增长。

如果我在“chunks”的定义中将“S.splitAt”改为“splitAt”,一切都很好。

谁能猜出为什么会这样?

谢谢。

更新

在以下代码中,所有三个版本的块都可以产生上述行为:

import GHC.Float
import Control.Arrow
import Sound.Pulse.Simple
import Data.List.Stream

import Prelude hiding ( unfoldr
                      , map
                      , null
                      , scanl1
                      , takeWhile
                      , repeat
                      , splitAt
                      , drop
                      , take
                      )

type Time = Double
type Frequency = Double
type Sample = Double
type CV = Double

chunksize = 441 * 2
sampleRate :: (Fractional a) => a
sampleRate = 44100

integral :: [Double] -> [Double]
integral = scanl1  (\acc x -> acc + x / sampleRate)

chunks :: Int -> [a] -> [[a]]
--chunks n = takeWhile (not . null) . unfoldr (Just . splitAt n)
--chunks n xs = take n xs : chunks n (drop n xs)
chunks n xs = h : chunks n t
    where
        (h, t) = splitAt n xs

pulseaudioOutput :: [Sample] -> IO ()
pulseaudioOutput sx = do

    pa <- simpleNew Nothing "Synths" Play Nothing "Synths PCM output"
         (SampleSpec (F32 LittleEndian) 44100 1) Nothing Nothing

    mapM_ (simpleWrite pa . map double2Float) $ chunks 1000 sx

    simpleDrain pa
    simpleFree pa

oscSine :: Frequency -> [CV] ->  [Sample]
oscSine f = map sin <<< integral <<< map ((2 * pi * f *) . (2**))

music ::[Sample]
music = oscSine 440 (repeat 0)

main = do
    pulseaudioOutput music

我清理了代码以避免混合普通的旧列表和流融合列表。内存/cpu 泄漏仍然存在。要查看代码是否适用于旧列表,只需删除“Data.List”之后的 Prelude 导入和“.Stream”即可。

【问题讨论】:

  • 基本上,列表无论如何都不适合音频样本。它有点工作,但永远不要指望性能足以应付任何不平凡的事情。 (虽然the trivial stuff can already be quite exciting...)
  • 确实,但是当我到达那座桥时,我想越过它。如果我使用普通的旧列表,则性能实际上就其在硬件级别上的实际表现而言相当不错:) 在实际设置中,单个振荡器的 1-2% CPU 是可以的,因为昂贵的合成器程序可能会吃掉一两个一个简单的合成器补丁的核心。 (是的,它们写得不好。无论如何。)我实际上可以只使用列表来运行 128 阶 FIR(好吧,也许是系数的序列),所以只要这些东西是实验性的,性能对我来说还可以。感谢您的链接,非常好! :)
  • 真的,128 阶 FIR?所以你直接在列表上运行 FFT,比如user3407776 does?我的意思是,很酷,GHC 可以如此快速地制作列表,但我可以理解为什么!如果使用紧密的数组块,您将获得更好的性能,而且 Haskell 的类型系统非常能够将它们抽象出来,因此它看起来和使用列表一样好。
  • 正如我所说的,我会越过那座桥。这与性能无关,这只是一个实验。这个问题特别是关于我的代码或流融合库中可能存在的错误。目前我对其他方面不感兴趣。

标签: haskell memory-leaks cpu-usage stream-fusion


【解决方案1】:

由融合规则 (http://hackage.haskell.org/package/stream-fusion-0.1.2.5/docs/Data-Stream.html#g:12) 替换的流上的 splitAt 具有以下签名:

splitAt :: Int -> Stream a -> ([a], [a])

从这里我们可以看到,由于它生成列表而不是流,因此阻碍了进一步的融合。我认为,正确的做法是生成一个splitAt 来生成流,或者更好地直接在流上编写一个chunks 函数,并使用列表版本中的适当融合规则。

这是我认为应该不错的流上的splitAt。您当然需要将它与列表中 splitAt 中的适当重写规则配对,如果这些重写规则变得棘手,也许可以直接编写 chunks 函数,尽管这样做似乎也有点棘手:

splitAt :: Int -> Stream a -> (Stream a, Stream a)
splitAt n0 (Stream next s0)
  | n0 < 0    = (nilStream, (Stream next s0))
  | otherwise = loop_splitAt n0 s0
  where
    nilStream = Stream (const Done) s0
    loop_splitAt  0 !s = (nilStream, (Stream next s))
    loop_splitAt !n !s = case next s of
      Done            -> (nilStream, nilStream)
      Skip    s'      -> loop_splitAt n s'
      Yield x s'      -> (cons x xs', xs'')
        where
          (xs', xs'') = loop_splitAt (n-1) s'

【讨论】:

  • 谢谢你,即使过了这么长时间你的解释让我有所启发。我可能会给我的这个项目第二次机会;)
  • @netom,您可能需要在此处添加一些内联注释。基于vector 对类似事情所做的事情,我认为可能{-# INLINE [1] splitAt #-}{-# INLINE [0] loop_splitAt #-}。但是任何时候你在处理这种融合的东西时,你应该确保分析、分析、分析并花一些时间在核心上卑躬屈膝。我推荐-ddump-simpl -dsuppress-all -dno-suppress-type-signatures。如果您以前从未见过 Core,它可能看起来有点吓人,但它确实与 Haskell 非常相似。
猜你喜欢
  • 2010-09-06
  • 2021-03-10
  • 1970-01-01
  • 2020-12-08
  • 2019-06-16
  • 1970-01-01
  • 1970-01-01
  • 2017-09-05
  • 2022-01-12
相关资源
最近更新 更多