【发布时间】:2017-10-25 13:04:28
【问题描述】:
我有一个 Haskell 程序,它用 Metropolis 模拟 Ising 模型 算法。主要操作是一个模板操作,它取 next 的总和 2D 中的邻居,然后将其与中心元素相乘。然后 元素可能已更新。
在 C++ 中,我获得了不错的性能,我使用一维数组,然后线性化
使用简单的索引算法访问它。在过去的几个月里,我学习了 Haskell 来拓宽我的视野,并尝试在那里实现 Ising 模型。数据结构只是Bool的列表:
type Spin = Bool
type Lattice = [Spin]
那我有一些固定的范围:
extent = 30
还有一个get 函数,用于检索特定晶格位置,包括周期性边界条件:
-- Wrap a coordinate for periodic boundary conditions.
wrap :: Int -> Int
wrap = flip mod $ extent
-- Converts an unbounded (x,y) index into a linearized index with periodic
-- boundary conditions.
index :: Int -> Int -> Int
index x y = wrap x + wrap y * extent
-- Retrieve a single element from the lattice, automatically performing
-- periodic boundary conditions.
get :: Lattice -> Int -> Int -> Spin
get l x y = l !! index x y
我在 C++ 中使用了同样的东西,它工作得很好,虽然我知道
std::vector 保证我快速随机访问。
在分析时,我发现get 函数占用了大量资源
计算时间:
COST CENTRE MODULE SRC no. entries %time %alloc %time %alloc
get Main ising.hs:36:1-26 153 899100 8.3 0.4 9.2 1.9
index Main ising.hs:31:1-36 154 899100 0.5 1.2 0.9 1.5
wrap Main ising.hs:26:1-24 155 0 0.4 0.4 0.4 0.4
neighborSum Main ising.hs:(40,1)-(43,56) 133 899100 4.9 16.6 46.6 25.3
spin Main ising.hs:(21,1)-(22,17) 135 3596400 0.5 0.4 0.5 0.4
neighborSum.neighbors Main ising.hs:43:9-56 134 899100 0.9 0.7 0.9 0.7
neighborSum.retriever Main ising.hs:42:9-40 136 899100 0.4 0.0 40.2 7.6
neighborSum.retriever.\ Main ising.hs:42:32-40 137 3596400 0.2 0.0 39.8 7.6
get Main ising.hs:36:1-26 138 3596400 33.7 1.4 39.6 7.6
index Main ising.hs:31:1-36 139 3596400 3.1 4.7 5.9 6.1
wrap Main ising.hs:26:1-24 141 0 2.7 1.4 2.7 1.4
我读到 Haskell 列表只有在将元素推入/弹出最前面时才有效,因此只有将其用作堆栈时才能提供性能。
当我“更新”晶格时,我使用splitAt 然后++ 返回一个新列表,其中一个元素已更改。
我可以做一些相对简单的事情来提高随机访问性能吗?
完整代码在这里:
-- Copyright © 2017 Martin Ueding <dev@martin-ueding.de>
-- Ising model with the Metropolis algorithm. Random choice of lattice site for
-- a spin flip.
import qualified Data.Text
import System.Random
type Spin = Bool
type Lattice = [Spin]
-- Lattice extent is fixed to a square.
extent = 30
volume = extent * extent
temperature :: Double
temperature = 0.0
-- Converts a `Spin` into `+1` or `-1`.
spin :: Spin -> Int
spin True = 1
spin False = (-1)
-- Wrap a coordinate for periodic boundary conditions.
wrap :: Int -> Int
wrap = flip mod $ extent
-- Converts an unbounded (x,y) index into a linearized index with periodic
-- boundary conditions.
index :: Int -> Int -> Int
index x y = wrap x + wrap y * extent
-- Retrieve a single element from the lattice, automatically performing
-- periodic boundary conditions.
get :: Lattice -> Int -> Int -> Spin
get l x y = l !! index x y
-- Computes the sum of neighboring spings.
neighborSum :: Lattice -> Int -> Int -> Int
neighborSum l x y = sum $ map spin $ map retriever neighbors
where
retriever = \(x, y) -> get l x y
neighbors = [(x+1,y), (x-1,y), (x,y+1), (x,y-1)]
-- Computes the energy difference at a certain lattice site if it would be
-- flipped.
energy :: Lattice -> Int -> Int -> Int
energy l x y = 2 * neighborSum l x y * (spin (get l x y))
-- Converts a full lattice into a textual representation.
latticeToString l = unlines lines
where
spinToChar :: Spin -> String
spinToChar True = "#"
spinToChar False = "."
line :: String
line = concat $ map spinToChar l
lines :: [String]
lines = map Data.Text.unpack $ Data.Text.chunksOf extent $ Data.Text.pack line
-- Populates a lattice given a random seed.
initLattice :: Int -> (Lattice,StdGen)
initLattice s = (l,rng)
where
rng = mkStdGen s
allRandom :: Lattice
allRandom = randoms rng
l = take volume allRandom
-- Performs a single Metropolis update at the given lattice site.
update (l,rng) x y
| doUpdate = (l',rng')
| otherwise = (l,rng')
where
shift = energy l x y
r :: Double
(r,rng') = random rng
doUpdate :: Bool
doUpdate = (shift < 0) || (exp (- fromIntegral shift / temperature) > r)
i = index x y
(a,b) = splitAt i l
l' = a ++ [not $ head b] ++ tail b
-- A full sweep through the lattice.
doSweep (l,rng) = doSweep' (l,rng) (extent * extent)
-- Implementation that does the needed number of sweeps at a random lattice
-- site.
doSweep' (l,rng) 0 = (l,rng)
doSweep' (l,rng) i = doSweep' (update (l,rng'') x y) (i - 1)
where
x :: Int
(x,rng') = random rng
y :: Int
(y,rng'') = random rng'
-- Creates an IO action that prints the lattice to the screen.
printLattice :: (Lattice,StdGen) -> IO ()
printLattice (l,rng) = do
putStrLn ""
putStr $ latticeToString l
dummy :: (Lattice,StdGen) -> IO ()
dummy (l,rng) = do
putStr "."
-- Creates a random lattice and performs five sweeps.
main = do
let lrngs = iterate doSweep $ initLattice 2
mapM_ dummy $ take 1000 lrngs
【问题讨论】:
-
Haskell 中的列表是链表,所以如果你想在 O(1) 中随机访问,你可能需要使用数组。
-
@WillemVanOnsem:哦,我不知道!看看Data.Array,它似乎很完美,它甚至允许增量更新。我会更新我的程序,然后我会报告它的进展情况。
-
@MartinUeding
vector也很方便。见stackoverflow.com/questions/9611904/… -
使用
Data.Array而不是[],程序现在需要 10 秒而不是 17 秒。数组索引现在花费的总时间也少了很多。在 C++ 实现中,我的同事受随机数生成器的约束,尽管他使用 Ranlux 而我可能是线性全等的?它仍然比 C++ 实现慢 10 倍以上,这正常吗? -
@MartinUeding 你在没有
-prof的情况下测量了-O2中的十秒吗?还是-prof?请记住,分析会使您的应用程序变慢(通常是 2-3 倍)。我在您的列表版本 (-O2) 中得到 5 秒,如果启用分析,则为 12 秒。话虽如此,random号码生成器很慢。
标签: performance haskell