Haskell是否提供了立即评估IO monad的方法?

时间:2015-08-13 22:14:59

标签: haskell

我目前正在使用Haskell制作光线跟踪程序。由于我是Haskell的初学者,我不清楚IO monad的评估策略。

问题在于" IO a"的长列表的内存使用,这是" IO Vec"在我的代码中。

列表的每个元素由递归函数计算,该函数计算IO Vec,表示像素的颜色。因此,列表的长度等于width x height

另外,我为像素拍摄了多个样本。总的来说,计算像素值的函数radiance称为width x height x samples次。

首先,我只使用列表理解来实现此程序。代码就像,

main = do
    ...
    let ray = (compute ray for every pair of [0..w-1], [0..h-1]
    pixels <- sequence [ (sumOfRadiance scene ray samples) | ray <- rays]

根据我的理解,由于在将像素写入文件之前未使用像素,因此Haskell会在pixels内存储一些用于函数调用的数据,这是IO Vec的数组。最后,通过调用递归函数radiance来计算像素值,可以增加内存消耗。

如果我改变程序以使用unsafePerformIO逐个评估像素值可以防止这种奇怪的内存空间使用。

main = do
    ...
    let ray = (compute ray for every pair of [0..w-1], [0..h-1]
    let pixels = [ (unsafePerformIO (sumOfRadiance scene ray samples)) | ray <- rays]

我知道unsafePerformIO是一个糟糕的解决方案,所以我想知道Haskell是否提供了另一种立即评估IO monad内部的方法。以下是我的整个代码(对不起,它有点长......)

感谢您的帮助。

-- Small path tracing with Haskell
import System.Environment
import System.Random.Mersenne
import System.IO.Unsafe
import Control.Monad
import Codec.Picture
import Data.Time
import qualified Data.Word as W
import qualified Data.Vector.Storable as V

-- Parameters
eps :: Double
eps = 1.0e-4

inf :: Double
inf = 1.0e20

nc :: Double
nc  = 1.0

nt :: Double
nt  = 1.5

-- Vec
data Vec = Vec (Double, Double, Double) deriving (Show)
instance (Num Vec) where
    (Vec (x, y, z)) + (Vec (a, b, c)) = Vec (x + a, y + b, z + c)
    (Vec (x, y, z)) - (Vec (a, b, c)) = Vec (x - a, y - b, z - c)
    (Vec (x, y, z)) * (Vec (a, b, c)) = Vec (x * a, y * b, z * c)
    abs = undefined
    signum = undefined
    fromInteger x = Vec (dx, dx, dx) where dx = fromIntegral x

x :: Vec -> Double
x (Vec (x, _, _)) = x

y :: Vec -> Double
y (Vec (_, y, _)) = y

z :: Vec -> Double
z (Vec (_, _, z)) = z

mul :: Vec -> Double -> Vec
mul (Vec (x, y, z)) s = Vec (x * s, y * s, z * s)

dot :: Vec -> Vec -> Double
dot (Vec (x, y, z)) (Vec (a, b, c))  = x * a + y * b + z * c

norm :: Vec -> Vec
norm (Vec (x, y, z)) = Vec (x * invnrm, y * invnrm, z * invnrm)
    where invnrm = 1 / sqrt (x * x + y * y + z * z)

cross :: Vec -> Vec -> Vec
cross (Vec (x, y, z)) (Vec (a, b, c)) = Vec (y * c - b * z, z * a - c * x, x * b - a * y)

-- Ray
data Ray = Ray (Vec, Vec) deriving (Show)

org :: Ray -> Vec
org (Ray (org, _)) = org

dir :: Ray -> Vec
dir (Ray (_, dir)) = dir

-- Material
data Refl = Diff
          | Spec
          | Refr
          deriving Show

-- Sphere
data Sphere = Sphere (Double, Vec, Vec, Vec, Refl) deriving (Show)

rad :: Sphere -> Double
rad  (Sphere (rad, _, _, _, _   )) = rad

pos :: Sphere -> Vec
pos  (Sphere (_  , p, _, _, _   )) = p

emit :: Sphere -> Vec
emit (Sphere (_  , _, e, _, _   )) = e

col :: Sphere -> Vec
col  (Sphere (_  , _, _, c, _   )) = c

refl :: Sphere -> Refl
refl (Sphere (_  , _, _, _, refl)) = refl

intersect :: Sphere -> Ray -> Double
intersect sp ray =
    let op  = (pos sp) - (org ray)
        b   = op `dot` (dir ray)
        det = b * b - (op `dot` op) + ((rad sp) ** 2)
    in
        if det < 0.0
            then inf
            else
                let sqdet = sqrt det
                    t1    = b - sqdet
                    t2    = b + sqdet
                in ansCheck t1 t2
                      where ansCheck t1 t2
                                | t1 > eps  = t1
                                | t2 > eps  = t2
                                | otherwise = inf

-- Scene
type Scene = [Sphere]
sph :: Scene
sph = [ Sphere (1e5,  Vec ( 1e5+1,  40.8, 81.6),    Vec (0.0, 0.0, 0.0), Vec (0.75, 0.25, 0.25),  Diff)   -- Left
      , Sphere (1e5,  Vec (-1e5+99, 40.8, 81.6),    Vec (0.0, 0.0, 0.0), Vec (0.25, 0.25, 0.75),  Diff)   -- Right
      , Sphere (1e5,  Vec (50.0, 40.8,  1e5),       Vec (0.0, 0.0, 0.0), Vec (0.75, 0.75, 0.75),  Diff)   -- Back
      , Sphere (1e5,  Vec (50.0, 40.8, -1e5+170),   Vec (0.0, 0.0, 0.0), Vec (0.0, 0.0, 0.0),     Diff)   -- Front
      , Sphere (1e5,  Vec (50, 1e5, 81.6),          Vec (0.0, 0.0, 0.0), Vec (0.75, 0.75, 0.75),  Diff)   -- Bottom
      , Sphere (1e5,  Vec (50,-1e5+81.6,81.6),      Vec (0.0, 0.0, 0.0), Vec (0.75, 0.75, 0.75),  Diff)   -- Top
      , Sphere (16.5, Vec (27, 16.5, 47),           Vec (0.0, 0.0, 0.0), Vec (1,1,1) `mul` 0.999, Spec)   -- Mirror
      , Sphere (16.5, Vec (73, 16.5, 78),           Vec (0.0, 0.0, 0.0), Vec (1,1,1) `mul` 0.999, Refr)   -- Glass
      , Sphere (600,  Vec (50, 681.6 - 0.27, 81.6), Vec (12, 12, 12),    Vec (0, 0, 0),           Diff) ] -- Light

-- Utility functions
clamp :: Double -> Double
clamp = (max 0.0) . (min 1.0)

isectWithScene :: Scene -> Ray -> (Double, Int)
isectWithScene scene ray = foldr1 (min) $ zip [ intersect sph ray | sph <- scene ] [0..]

nextDouble :: IO Double
nextDouble = randomIO

lambert :: Vec -> Double -> Double -> (Vec, Double)
lambert n r1 r2 =
    let th  = 2.0 * pi * r1
        r2s = sqrt r2
        w = n
        u = norm $ (if (abs (x w)) > eps then Vec (0, 1, 0) else Vec (1, 0, 0)) `cross` w
        v = w `cross` u
        uu = u `mul` ((cos th) * r2s)
        vv = v `mul` ((sin th) * r2s)
        ww = w `mul` (sqrt (1.0 - r2))
        rdir = norm (uu + vv + ww)
    in (rdir, 1)

reflect :: Vec -> Vec -> (Vec, Double)
reflect v n =
    let rdir = v - (n `mul` (2.0 * n `dot` v))
    in (rdir, 1)

refract :: Vec -> Vec -> Vec -> Double -> (Vec, Double)
refract v n orn rr =
    let (rdir, _) = reflect v orn
        into = (n `dot` orn) > 0
        nnt  = if into then (nc / nt) else (nt / nc)
        ddn  = v `dot` orn
        cos2t = 1.0 - nnt * nnt * (1.0 - ddn * ddn)
    in
        if cos2t < 0.0
            then (rdir, 1.0)
            else
                let tdir = norm $ ((v `mul` nnt) -) $ n `mul` ((if into then 1 else -1) * (ddn * nnt + (sqrt cos2t)))
                    a = nt - nc
                    b = nt + nc
                    r0 = (a * a) / (b * b)
                    c = 1.0 - (if into then -ddn else (tdir `dot` n))
                    re = r0 + (1 - r0) * (c ** 5)
                    tr = 1.0 - re
                    pp = 0.25 + 0.5 * re
                in
                    if rr < pp
                         then (rdir, (pp / re))
                         else (tdir, ((1.0 - pp) / tr))

radiance :: Scene -> Ray -> Int -> IO Vec
radiance scene ray depth = do
    let (t, i) = (isectWithScene scene ray)
    if inf <= t
        then return (Vec (0, 0, 0))
        else do
            r0 <- nextDouble
            r1 <- nextDouble
            r2 <- nextDouble
            let obj = (scene !! i)
            let c = col obj
            let prob = (max (x c) (max (y c) (z c)))
            if depth >= 5 && r0 >= prob
                then return (emit obj)
                else do
                    let rlt = if depth < 5 then 1 else prob
                    let f = (col obj)
                    let d = (dir ray)
                    let x = (org ray) + (d `mul` t)
                    let n = norm $ x - (pos obj)
                    let orn = if (d `dot` n) < 0.0  then n else (-n)
                    let (ndir, pdf) = case (refl obj) of
                            Diff -> (lambert orn r1 r2)
                            Spec -> (reflect d orn)
                            Refr -> (refract d n orn r1)
                    nextRad <- (radiance scene (Ray (x, ndir)) (succ depth))
                    return $ ((emit obj) + ((f * nextRad) `mul` (1.0 / (rlt * pdf))))

toByte :: Double -> W.Word8
toByte x = truncate (((clamp x) ** (1.0 / 2.2)) * 255.0) :: W.Word8

accumulateRadiance :: Scene -> Ray -> Int -> Int -> IO Vec
accumulateRadiance scene ray d m = do
    let rays = take m $ repeat ray
    pixels <- sequence [radiance scene r 0 | r <- rays]
    return $ (foldr1 (+) pixels) `mul` (1 / fromIntegral m)

main :: IO ()
main = do
    args <- getArgs
    let argc = length args
    let w   = if argc >= 1 then (read (args !! 0)) else 400 :: Int
    let h   = if argc >= 2 then (read (args !! 1)) else 300 :: Int
    let spp = if argc >= 3 then (read (args !! 2)) else 4   :: Int

    startTime <- getCurrentTime

    putStrLn "-- Smallpt.hs --"
    putStrLn $ "  width = " ++ (show w)
    putStrLn $ " height = " ++ (show h)
    putStrLn $ "    spp = " ++ (show spp)

    let dw = fromIntegral w :: Double
    let dh = fromIntegral h :: Double

    let cam = Ray (Vec (50, 52, 295.6), (norm $ Vec (0, -0.042612, -1)));
    let cx  = Vec (dw * 0.5135 / dh, 0.0, 0.0)
    let cy  = (norm $ cx `cross` (dir cam)) `mul` 0.5135
    let dirs = [ norm $ (dir cam) + (cy `mul` (y / dh  - 0.5)) + (cx `mul` (x / dw - 0.5)) | y <- [dh-1,dh-2..0], x <- [0..dw-1] ]
    let rays = [ Ray ((org cam) + (d `mul` 140.0), (norm d)) | d <- dirs ]

    let pixels = [ (unsafePerformIO (accumulateRadiance sph r 0 spp)) | r <- rays ]

    let pixelData = map toByte $! pixels `seq` (foldr (\col lst -> [(x col), (y col), (z col)] ++ lst) [] pixels)
    let pixelBytes = V.fromList pixelData :: V.Vector W.Word8
    let img = Image { imageHeight = h, imageWidth = w, imageData = pixelBytes } :: Image PixelRGB8
    writePng "image.png" img

    endTime <- getCurrentTime
    print $ diffUTCTime endTime startTime

1 个答案:

答案 0 :(得分:10)

首先,我认为有一个错误。当你谈到从

开始
pixels <- sequence [ (sumOfRadiance scene ray samples) | ray <- rays]

pixels <- sequence [ (unsafePerformIO (sumOfRadiance scene ray samples)) | ray <- rays]

没有意义。这些类型不应该匹配 - sequence只有在组合一堆m a类型的东西时才有意义。

是正确的
let pixels = [ unsafePerformIO (sumOfRadiance scene ray samples) | ray <- rays ]

我会有点骑士认为这就是你所做的,而你在输入问题时却犯了一个错误。

如果是这种情况,那么您实际需要的是一种更懒惰地执行IO动作的方式,而不是立即执行。 sequence调用迫使所有操作立即运行,而unsafePerformIO版本只是创建一个未运行动作列表(实际上列表本身是懒惰生成的,所以它不会一次存在),并且在需要结果时单独运行操作。

您需要IO的原因似乎是生成随机数。随机性可能是一种痛苦 - 通常MonadRandom完成工作,但它仍然会在动作之间产生顺序依赖,并且可能仍然不够懒惰(我试试看 - 如果你使用你得到了可重复性 - 即使在尊重monad定律的重构之后,相同的种子也能得到相同的结果。

如果MonadRandom不起作用且你需要以更加按需的方式生成随机数,那么就可以制作自己的随机性monad,它与你的{{1解决方案,但以适当封装的方式。我会告诉你我认为是Haskell Way To Cheat的方式。首先,一个可爱的纯实现草图:

unsafePerformIO

(我认为我做对了。重点是每次绑定时你将种子分成两部分,然后将一部分传递到左边,另一部分传递到右边。)

现在这是一个完全纯粹的随机性实现......有几个捕获。 (1)没有非平凡的-- A seed tells you how to generate random numbers data Seed = ... splitSeed :: Seed -> (Seed, Seed) random :: Seed -> Double -- A Cloud is a probability distribution of a's, or an a which -- depends on a random seed. This monad is just as lazy as a -- pure computation. newtype Cloud a = Cloud { runCloud :: Seed -> a } deriving (Functor) instance Monad Cloud where return = Cloud . const m >>= f = Cloud $ \seed -> let (seed1, seed2) = splitSeed seed in runCloud (f (runCloud m seed1)) seed2 严格尊重monad法则;(2)即使我们允许法律被破坏,基于分裂的随机数生成器也可以很慢。但是如果我们放弃决定论,如果我们关心的是我们从分布中得到一个好的样本而不是完全相同的结果,那么我们就不需要严格遵守monad定律。在那一点上,我们作弊并假装有一个合适的splitSeed类型:

Seed

我们应该在模块中隐藏它以保持抽象屏障。不应暴露data Seed = Seed splitSeed Seed = (Seed, Seed) -- Always NOINLINE functions with unsafePerformIO to keep the -- optimizer from messing with you. {-# NOINLINE random #-} random Seed = unsafePerformIO randomIO Cloud因为它们允许我们违反纯度;仅暴露

runCloud

技术上并不需要runCloudIO :: Cloud a -> IO a runCloudIO = return . runCloud ,但传达这不是确定性的。然后,您可以在IO monad中构建您需要的任何值,并在主程序中运行一次。

如果它没有任何信息,您可能会问我们为什么会有Cloud类型。好吧,我认为Seed只是对纯度的点头,并没有实际做任何事情 - 你可以删除它 - 但我们需要splitSeed成为一个函数类型,以便隐式缓存懒惰并没有打破我们的语义。否则

Cloud

将始终返回具有两个相同组件的对,因为随机值实际上与let foo = random in liftM2 (,) foo foo 相关联。我不确定这些事情,因为此时我们正在与优化器进行战争,需要进行一些实验。

快乐作弊。 : - )