Haskell 中可变的多变非积分类型列表

Mutable list of mutabale non-integral types in Haskell

提问人:blauerreimers 提问时间:4/29/2021 最后编辑:blauerreimers 更新时间:4/29/2021 访问量:156

问:

我正在尝试从二进制中解析一个巨大的复杂值的 3d 数据数组。稍后这应该成为矩阵 ()。由于我将研究这些矩阵,因此我仅限于矩阵库 - hmatrix 似乎很有前途。 数据布局不是我要求的格式,所以我必须在位置跳来跳去,其中 和 是 和 的元素。ln x m(i,j,k) -> (k,i,j)ijnmkl

我认为阅读此内容的唯一方法是使用可变对象,否则我最终会得到几 TB 的垃圾。我的想法是使用盒装互数组或互矩阵的向量(来自 Numeric.LinearAlgebra.Devel 的 STMatrix),所以我最终得到如下结果:

data MVector s (STMatrix s t)

但我不确定如何正确使用它们: 我可以使用 modify 修改 MVector 的一个元素:

modify :: PrimMonad m => MVector (PrimState m) a -> (a -> a) -> Int -> m () 

或使用 modifyM(奇怪:在堆栈 vector-0.12.3.0 中没有 modifyM......

modifyM :: PrimMonad m => MVector (PrimState m) a -> (a -> m a) -> Int -> m () 

因此,我可以使用对 runST 例程的函数调用来修改 SMatrix。我不确定,我是否应该将 ST 放在 IO 中 (?(a -> a)

尽管如此 - 我认为,这应该有效,但只有在我想修改整个矩阵时才有用,调用这个 -例程 - 时间会有点开销(也许它会被优化出来...... 因此,我最终将在编组数组时,通过指针修改内容并逐个矩阵读取所有内容 - 但这感觉不对,我想避免这种肮脏的伎俩。(a->a)n x m x l(i,j,k) -> (k,i,j)

你有没有办法做到这一点,但更多......干净? 泰

编辑:谢谢 K. A. Buhr。到目前为止,他的解决方案有效。现在,我只遇到了一些性能影响。如果我比较解决方案:

{-# LANGUAGE BangPatterns #-}
module Main where
import Data.List
import Numeric.LinearAlgebra
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Storable.Mutable as VSM

-- Create an l-length list of n x m hmatrix Matrices
toMatrices :: Int -> Int -> Int -> [C] -> [Matrix C]
toMatrices l n m dats = map (reshape m) $ VS.createT $ do
  mats <- V.replicateM l $ VSM.unsafeNew (m*n)
  sequence_ $ zipWith (\(i,j,k) x ->
      VSM.unsafeWrite (mats V.! k) (loc i j) x) idxs (dats ++ repeat 0)
  return $ V.toList mats

  where idxs = (,,) <$> [0..n-1] <*> [0..m-1] <*> [0..l-1]
        loc i j = i*m + j

test1 = toMatrices 1000 1000 100 (fromIntegral <$> [1..])

main = do
  let !a = test1
  print "done"

使用最简单的 C 代码:

#include <stdlib.h>
#include <stdio.h>
void main() 
{

    const int n = 1000;
    const int m = 1000;
    const int l = 100;

    double *src = malloc(n*m*l * sizeof(double));
    for (int i = 0; i < n*m*l; i++) {
        src[i] = (double)i;
    }

    double *dest = malloc(n*m*l * sizeof(double));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            for (int k = 0; k < l; k++) {
                dest[k*n*m+i*m+j] = src[i*m*l+j*l+k];
            }
        }
    }
    printf("done: %f\n", dest[n*m*l - 1]); // Need to access the array, otherwise it'll get lost by -O2
    free(src);
    free(dest);
}

使用 -O2 编译的两者都给出了以下性能猜测:

real    0m5,611s
user    0m14,845s
sys 0m2,759s

与。

real    0m0,441s
user    0m0,200s
sys 0m0,240s

这大约是每个内核性能的 2 个数量级。从分析中,我了解到

      VSM.unsafeWrite (mats V.! k) (loc i j) x

是昂贵的功能。 由于我将以类似分钟的时间间隔使用此过程,因此我希望将分析时间保持在与磁盘访问时间一样低的水平。我看看,如果我能加快速度

PS:这是针对某些测试的,如果我可以将通常的 DSP 从 C 类移动到 Haskell

编辑2:好的,这就是我在尝试和睦后得到的:

{-# LANGUAGE BangPatterns #-}

module Main where

import Data.List
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Storable.Mutable as VSM
import Numeric.LinearAlgebra

-- Create an l-length list of n x m hmatrix Matrices
toMatrices :: Int -> Int -> Int -> VS.Vector C -> V.Vector (Matrix C)
toMatrices l n m dats =
  V.map (reshape m) newMat
   where
    newMat = VS.createT $
      V.generateM l $ \k -> do
      curMat <- VSM.unsafeNew (m * n)
      VS.mapM_
        (\i ->
           VS.mapM_
             (\j -> VSM.unsafeWrite curMat (loc i j) (dats VS.! (oldLoc i j k)))
            idjs)
        idis
      return curMat
    loc i j = i * m + j
    oldLoc i j k = i * m * l + j * l + k
    !idis = VS.generate n (\a->a)
    !idjs = VS.generate m (\a->a)

test1 = toMatrices 100 1000 1000 arr
  where
    arr = VS.generate (1000 * 1000 * 100) fromIntegral :: VS.Vector C

main = do
  let !a = test1
  print "done"

它给出了一些关于以下内容的内容:

real    0m1,816s
user    0m1,636s
sys 0m1,120s

,所以比 C 代码慢 ~4 倍。我想我可以忍受这个。 我想,我正在用这段代码破坏向量的所有流功能。如果有任何建议让他们以可比的速度返回,我将不胜感激!

哈斯克尔 矩阵 可变 h矩阵

评论

1赞 luqui 4/29/2021
不明白为什么你认为你需要它们是可变的。你不信任垃圾收集器吗?通常垃圾可以增量收集,因此您不需要一次将整个结构保留在内存中。
1赞 amalloy 4/29/2021
你的描述没有向我尖叫你需要任何可变的东西。您是否考虑过将整个内容读取到单个 3 维数组中,并将其作为第一个索引?Data.Array.listArray 应该能够从对输入坐标和数据的简单列表推导中创建它。然后,如果你愿意,你可以映射数组的行(超行?),并在完成数据读取后将每个行转换为矩阵。k
0赞 amalloy 4/29/2021
@luqui 我认为这个问题并不完全是担心收集垃圾的成本,而是担心一个过程可能的 n^2 性质,该过程创建一个大的零矩阵,然后复制一个大矩阵,更改一个元素,依此类推每个元素。我不知道矩阵库是否做了足够的共享来不用担心,但如果这是一个问题,那么 GC 有多好并不重要。
0赞 blauerreimers 4/29/2021
@amalloy谢谢。为了澄清。是的,确实,我有点担心性能影响。
2赞 dfeuer 4/29/2021
你的问题很清楚。您能否具体描述一下数据格式的相关部分?您能否解释一下您正在尝试更完整地构建哪种结构,并可能提供一些伪代码?如果您真的需要突变才能从文件中读取一些矩阵,我会感到惊讶,但也许我遗漏了一些东西。

答:

1赞 K. A. Buhr 4/29/2021 #1

据我了解,您有一组 -major、-middling、-minor 顺序的“庞大”数据,并且您想将其加载到由其元素索引的矩阵中,这些矩阵具有 -indexed 行和 -indexed 列,对吧?因此,您需要一个类似这样的函数:ijkkij

import Numeric.LinearAlgebra

-- load into "l" matrices of size "n x m"
toMatrices :: Int -> Int -> Int -> [C] -> [Matrix C]
toMatrices l n m dats = ...

请注意,您已经编写了上面的矩阵,与 和 相关联。翻转 和 的角色会更常见,但我坚持使用您的符号,所以请密切关注这一点。n x minjmnm

如果整个数据列表可以舒适地放入内存中,则可以通过编写以下内容来不可变地执行此操作:[C]

import Data.List
import Data.List.Split
import Numeric.LinearAlgebra

toMatrices :: Int -> Int -> Int -> [C] -> [Matrix C]
toMatrices l n m = map (reshape m . fromList) . transpose . chunksOf l

这会将输入数据分解为 -大小的块,将它们转置为列表,并将每个列表转换为矩阵。如果有某种方法可以并行强制所有值,则可以通过一次遍历数据来完成此操作,而无需保留整个列表。不幸的是,单个值只能一个接一个地强制,并且需要保留整个列表,直到可以强制所有值为止。llMatrix CMatrix C

因此,如果“巨大”列表对于内存来说太大了,那么您需要将数据加载到(部分)可变结构中可能是对的。代码编写起来有些困难,但最终形式还不错。我相信以下方法会起作用:[C]

import Data.List
import Numeric.LinearAlgebra
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Storable.Mutable as VSM

-- Create an l-length list of n x m hmatrix Matrices
toMatrices :: Int -> Int -> Int -> [C] -> [Matrix C]
toMatrices l n m dats = map (reshape m) $ VS.createT $ do
  mats <- V.replicateM l $ VSM.unsafeNew (m*n)
  sequence_ $ zipWith (\(i,j,k) x ->
      VSM.unsafeWrite (mats V.! k) (loc i j) x) idxs (dats ++ repeat 0)
  return $ V.toList mats

  where idxs = (,,) <$> [0..n-1] <*> [0..m-1] <*> [0..l-1]
        loc i j = i*m + j

test1 = toMatrices 4 3 2 (fromIntegral <$> [1..24])
test2 = toMatrices 1000 1000 100 (fromIntegral <$> [1..])

main = do
  print $ test1
  print $ norm_Inf . foldl1' (+) $ test2

编译为 ,最大驻留约为 1.6Gigs,这与在内存中保存 100 个 100 万个 16 字节复数值的矩阵所需的预期内存相匹配,因此看起来是正确的。-O2

无论如何,由于使用了三种不同的向量变体,这个版本变得有些复杂。有 from ,它与不可变的可存储 from 相同;然后还有两种类型:不可变的盒装和可变的可存储。toMatricesVectorhmatrixVS.VectorvectorvectorV.VectorVSM.Vector

-block 创建一个 of s,并用一系列跨索引/值对执行的单子操作填充这些操作。您可以通过修改 的定义以匹配数据流的顺序,以任意顺序加载数据。-block 返回列表中的最后一个 s,辅助函数将它们全部冻结为 s(即 from ),并映射到向量以将它们转换为 -column 矩阵。doV.VectorVSM.VectoridxsdoVSM.VectorVS.createTVS.VectorVectorhmatrixreshapem

请注意,您必须注意,在实际应用程序中,从文件中读取的数据项列表不会由原始文本形式或分析的数字形式以外的代码保留。这应该不会太难,但你可能希望在将计算机锁定在真实数据集上之前,先对中等大小的测试输入进行测试。toMatrices

评论

0赞 dfeuer 4/29/2021
如果整个列表(超过一点点)太大而无法保存在内存中,那么部分矩阵集会不会太大了?
0赞 K. A. Buhr 4/29/2021
好吧,我正在考虑这样一种情况,即未装箱的矩阵可以舒适地放入内存中,但是输入装箱列表加上输出未装箱矩阵顶部的列表开销太大。
0赞 blauerreimers 4/29/2021
是的,这正是我的问题。我的具体数字在 2 - 8GiB 之间(也许我可以通过使用单精度将整个问题减少到 1-4GiB)。但是防止操作系统杀死内存不足是我最关心的问题:)