Haskell 在REPA数组中绘制矩形的最快方法是什么?
通常,在位图中绘制矩形的一个好方法是在两个边界维度之间循环并设置单个像素。例如,在伪代码上:Haskell 在REPA数组中绘制矩形的最快方法是什么?,haskell,repa,Haskell,Repa,通常,在位图中绘制矩形的一个好方法是在两个边界维度之间循环并设置单个像素。例如,在伪代码上: drawRect(array, color, x, X, y, Y): for x from x til X: for y from y til Y: array[x,y] = color Haskell的REPA机制与制作新数组的普通REPA机制有什么相同之处?在将数组复制到外部内存一次时,制作新的延迟数组的速度最快。使用REPA的实际性能将取决于您对
drawRect(array, color, x, X, y, Y):
for x from x til X:
for y from y til Y:
array[x,y] = color
Haskell的REPA机制与制作新数组的普通REPA机制有什么相同之处?在将数组复制到外部内存一次时,制作新的延迟数组的速度最快。使用REPA的实际性能将取决于您对阵列所做的操作 让我们定义一种计算类型,它只取决于数组中的位置和该位置的当前值
{-# LANGUAGE ScopedTypeVariables #-}
import Data.Array.Repa hiding ((++))
import Data.Array.Repa.Repr.ForeignPtr
import Data.Word
import Control.Monad
import Data.Time.Clock
import System.Mem
type Ghost sh a b = sh -> a -> b
我们可以定义任何形状的填充
fill :: Shape sh => sh -> sh -> a -> Ghost sh a a
fill from to color = go
where
{-# INLINE go #-}
go sh a =
if inShapeRange from to sh
then color
else a
我们将用三种不同的方法来定义一个新的数组——延迟数组、结构化遍历和非结构化遍历
最简单的延迟是函数的延迟
ghostD :: (Shape sh, Source r a) => Ghost sh a b -> Array r sh a -> Array D sh b
ghostD g a = fromFunction (extent a) go
where
{-# INLINE go #-}
go sh = g sh (a ! sh)
结构化遍历可以利用了解底层数组表示的结构。不幸的是,在结构化遍历中,我们获得位置信息的唯一方法是使用szipWith
对已经包含位置信息的数组进行压缩
ghostS :: (Shape sh, Structured r1 a b, Source r1 a) => Ghost sh a b -> Array r1 sh a -> Array (TR r1) sh b
ghostS g a = szipWith ($) ghost a
where
ghost = fromFunction (extent a) g
非结构化遍历非常类似于由fromFunction
构建的延迟数组;它还返回一个数组D
ghostT :: (Shape sh, Source r a) => Ghost sh a b -> Array r sh a -> Array D sh b
ghostT g a = traverse a id go
where
{-# INLINE go #-}
go lookup sh = g sh (lookup sh)
通过一些非常简单的基准测试,我们可以运行这些测试,看看它们有多快。我们在测量时间之前执行垃圾收集,以获得可靠的计时结果。我们将有两个基准。对于每个机制,。我们将运行一个步骤,将结果写入内存10次。然后我们将组成101个相同的步骤,将结果写入内存一次
bench :: Int -> String -> IO a -> IO ()
bench n name action = do
performGC
start <- getCurrentTime
replicateM_ n action
performGC
end <- getCurrentTime
putStrLn $ name ++ " " ++ (show (diffUTCTime end start / fromIntegral n))
iterN :: Int -> (a -> a) -> (a -> a)
iterN 0 f = id
iterN n f = f . iterN (n-1) f
main = do
(img :: Array F DIM2 Word32) <- computeP (fromFunction (Z :. 1024 :. 1024 ) (const minBound))
let (Z :. x :. y ) = extent img
drawBox = fill (Z :. 20 :. 20 ) (Z :. x - 20 :. y - 20 ) maxBound
bench 10 "Delayed 10x1" ((computeP $ ghostD drawBox img) :: IO (Array F DIM2 Word32))
bench 10 "Unstructured 10x1" ((computeP $ ghostT drawBox img) :: IO (Array F DIM2 Word32))
bench 10 "Structured 10x1" ((computeP $ ghostS drawBox img) :: IO (Array F DIM2 Word32))
bench 1 "Delayed 1x101" ((computeP $ (iterN 100 (ghostD drawBox)) . ghostD drawBox $ img) :: IO (Array F DIM2 Word32))
bench 1 "Unstructured 1x101" ((computeP $ (iterN 100 (ghostT drawBox)) . ghostT drawBox $ img) :: IO (Array F DIM2 Word32))
bench 1 "Structured 1x101" ((computeP $ (iterN 100 (ghostS drawBox)) . ghostS drawBox $ img) :: IO (Array F DIM2 Word32))
结果似乎并不取决于基准测试的运行顺序
Structured 10x1 0.03276s
Unstructured 10x1 0.02652s
Delayed 10x1 0.01716s
Structured 1x101 0.2184s
Unstructured 1x101 0.1092s
Delayed 1x101 0.0624s
这些结果表明,您可以进行一些完整的数组计算,并且仍然可以通过内存访问来控制性能以写入结果
通过绘制场景来渲染场景的库通常具有与REPA非常不同的结构,REPA主要用于并行处理所有数据。绘制和渲染库通常使用称为的场景元素的图形或树,该图形或树允许它们快速剔除不会在图像或图像的一部分中绘制的元素。如果您可以快速剔除不影响特定像素的所有内容,则无需改变结果即可获得良好的性能。如何?对于2D情况,它类似于drawRect x y color=fromFunction(Z:.y:.x)(const color)
。除非您想实际更新数组(可能是个坏主意),否则请使用\color->map(const color)
。问题是我不想检查1024x1024图像的每个像素,以便在pos(600600)上画一个小圆圈。为什么必须是REPA?我相信如果你真的担心迭代每个像素,你将不得不使用相当低级别的操作。简单的方法是创建小图像,附加填充有零的数组以获得正确的位置,然后将它们相加。不那么简单的方法是使用loadP/S
写入“大”图像,然后使用loadRangeP/S
写入小“图像”。您的伪代码似乎假设了变异,但像REPA这样的高级库的要点是不向用户公开底层变异。如果你给出你想要解决的实际问题,也许会有所帮助。
Structured 10x1 0.03276s
Unstructured 10x1 0.02652s
Delayed 10x1 0.01716s
Structured 1x101 0.2184s
Unstructured 1x101 0.1092s
Delayed 1x101 0.0624s