So I've been trying to get my QuickCheck tests to run in parallel. I did take a 
look at Don's Parallel QuickCheck library 
<http://www.cse.unsw.edu.au/~dons/pqc.html>, but I didn't like how much code it 
had in it and I figured it'd be a good exercise to try to do myself.

After quite a lot of help from the good folk of #haskell, I eventually came up 
with this:
  module Pcheck (parTest, parCheck) where

  import Control.Monad (replicateM_, liftM)
  import Control.Concurrent.Chan (newChan, writeChan, getChanContents)
  import Control.Concurrent (forkIO)
  import Test.QuickCheck (quickCheck', Testable())

  -- | Takes a list of functions using parCheck, and returns True iff all return
  -- True. Evaluates them in parallel.
  parTest :: [IO Bool] -> IO Bool
  parTest = andTest . parList
      where andTest :: IO [Bool] -> IO Bool
            andTest = liftM and

  {- | Test in parallel. Forks off a QuickCheck 'n' times; QuickCheck tests 
using
   the proposition 't'. Returns True if all tests were passed, else
   False. Should be run with parallelizing options like with +RTS -N4 -RTS 
&etc. -}
  parCheck :: (Testable prop) => prop -> Int -> IO Bool
  parCheck t n = do chan <- newChan
                    replicateM_ n $ forkIO $ (writeChan chan) =<< (quickCheck' 
t)
                    liftM (and . take n) $ getChanContents chan

  -- | Takes a list of functions (presumably using parCheck) and evaluates all 
in parallel.
  parList :: [IO a] -> IO [a]
  parList fs = do chan <- newChan
                  mapM_ (\m -> forkIO $ m >>= writeChan chan) fs
                  liftM (take n) $ getChanContents chan
                      where n = length fs

I liked how simple the Channels library 
<http://haskell.org/ghc/docs/latest/html/libraries/base-3.0.0.0/Control-Concurrent-Chan.html>
 seemed to be; I could just pass the channel as an argument and have every 
forkIO'd test simply chuck its Boolean result into it when done - which seem'd 
much simpler than using MVars and the other techniques for returning stuff from 
forkIO threads.

And so it compiles, it runs tests correctly, and so on. But the problem is that 
it does so slowly. I have another module of equations about nuclear bombs 
called nuke.hs, which has a number of QuickCheck properties defined. Here's 
what happens when main is defined as 'parTest [the various tests..]':

 ./nuke +RTS -N7 -sstderr -RTS 40.57s user 46.55s system 116% cpu 1:14.61 total
 ./nuke +RTS -N6 -sstderr -RTS 40.72s user 47.66s system 117% cpu 1:15.50 total
 ./nuke +RTS -N5 -sstderr -RTS 42.33s user 49.08s system 116% cpu 1:18.67 total
 ./nuke +RTS -N4 -sstderr -RTS 43.71s user 48.41s system 117% cpu 1:18.48 total
 ./nuke +RTS -N3 -sstderr -RTS 41.51s user 48.25s system 114% cpu 1:18.10 total
 ./nuke +RTS -N2 -sstderr -RTS 42.28s user 47.18s system 115% cpu 1:17.39 total
 ./nuke +RTS -N1 -sstderr -RTS 27.87s user 18.40s system 99% cpu 46.498 total

(From <http://hpaste.org/3886#a6>; compiled as "=ghc -v --make -threaded -O2 
./nuke.hs".)

For some reason, running the parallel tests with a single thread is faster than 
running with 4 threads (I have a quad-core Intel processor)? I find this 
counter-intuitive to say the least. the par* functions are indeed operating in 
parallel, as evidenced by it using more than 100% CPU time, or, running on 
multiple cores, and all the tests are passed as True in both -N1 and -N[2-7] 
versions, so -N1 can't be bailing out early due to "and"'s laziness, and in 
general everything seems to be written correctly.

I am perplexed by this. Is Chan simply a very inefficient way of parallelizing 
things? Is it not as parallel as I think? Or am I missing something else 
entirely?

(Attached is the source of nuke.hs and pcheck.hs, as well as some data from 
-sstderr.)

--
gwern
.45 GIGN jya. wire ISI SADCC JPL embassy Recon World
-- module Nuke (main)
--   where
-- TODO: work in radiation deaths.

import Pcheck
import Test.QuickCheck
import Monad (liftM3)

{- For many equations and results, it is nonsensical to have negative results, but we don't want
to use solely natural numbers because then we lose precision. So we define a PosReal type which tries
to define the subset of real numbers which are 0 or positive; this way the type system checks for negative
results instead of every other function having conditionals checking for negative input or output. -}
newtype PosReal = MakePosReal Float deriving (Show, Eq, Ord)

-- Basic numerical operations on positive reals
instance Num PosReal where
    fromInteger = toPosReal . fromInteger
    x + y = MakePosReal (fromPosReal x + fromPosReal y)
    x - y = toPosReal ((fromPosReal x) - (fromPosReal y))
    x * y = MakePosReal (fromPosReal x * fromPosReal y)
    abs x | x >= 0 = x
          | otherwise = x * (-1)
    signum x | x >= 0 = 1
             | otherwise = (-1)

-- Define division on PosReals
instance Fractional PosReal where
    x / y = toPosReal ((fromPosReal x) / (fromPosReal y))
    fromRational x = MakePosReal (fromRational x)

-- Positive reals are truncated at 0
toPosReal :: Float -> PosReal
toPosReal x
    | x < 0     = MakePosReal 0
    | otherwise = MakePosReal x
fromPosReal :: PosReal -> Float
fromPosReal (MakePosReal i) = i

-- Define an instance to allow QuickCheck operations
instance Arbitrary PosReal where
    arbitrary = liftM3 fraction arbitrary arbitrary arbitrary
        where fraction :: Integer -> Integer -> Integer -> PosReal
              fraction a b c = fromInteger a + (fromInteger b / (abs (fromInteger c) + 1))
--    coarbitrary x = coarbitrary (x, x)

type KiloPascal = PosReal
type PSI = PosReal
type Meters = PosReal
type W = PosReal -- Note w is always in kilotons.
type Joule = PosReal
type Time = PosReal

-- Characterizes the output of a nuclear weapon
data Output = Output {
      heat :: PosReal,
      blast :: PosReal,
      nuclear_radiation :: PosReal } deriving (Show, Eq)

main :: IO ()
main = do bool <- parTest [kpaTest, psiTest, energyTest, thermalMaxTest, timeMaxTest]
          if bool then print "Success" else print "Failure"

-- Units
kpaTest :: IO Bool
kpaTest = parCheck (\s -> kpa s >= 0) 1000
kpa :: KiloPascal -> PSI
kpa a
    | (a > 0) = 0.145 * a
    | otherwise = 0

psiTest :: IO Bool
psiTest = parCheck (\s -> psi s >= s) 100
psi :: PSI -> KiloPascal
psi = (6.895 *)

meter :: PosReal -> PosReal
meter = (3.281 *)

foot :: PosReal -> PosReal
foot = (0.3048 *)

-- Find ideal height for an air burst.
height :: W -> Float
--height w = 60 * (1 / (w * w * w)) -- w^3
height w = 60 * ((fromPosReal w)**(1/3))

{- Calculate total energy of a given kilotonnage. Answer in joules.
   This works because critical mass gives a lower kilotonnage bound for fission bombs. -}
energyTest :: IO Bool
energyTest = parCheck (\s -> energy s == 0 || energy s >= 2.0929999e12) 100
energy :: W -> Joule
energy s
    | s >= 0.5 = 4.186 * joule * s
    | otherwise = 0
         where joule = 1000000000000 :: PosReal -- 10^12

regularNuke :: W -> Output
regularNuke w = Output { heat=(0.35 * energy w), -- 35% heat
                         blast=(0.50 * energy w), -- 50% kinetic energy
                         nuclear_radiation=(0.15 * energy w) -- 15% non-thermal radiation
                       }

{- A radiation weapon trades off a decreased thermal and blast energy against a considerably
   increased radiation yield. It is defined in terms of regularNuke because it just modifies it. -}
radiationNuke :: W -> Output
radiationNuke w = Output {
                    -- regularNuke w
                    heat=(0.35 * energy (0.50 * w)),
                    blast=(0.50 * energy (0.50 * w)),
                    nuclear_radiation=(0.15 * energy (10.0 * w)) } -- 10x non-thermal radiation!

{- Answer in joules /m^2. r = distance from impact/radius; t = correction factor (1.5^2 for snow & high
   clouds, 1.5 for singly either). This is how many joules of heat per square inch at a given distance for
   given kilotonnage (weather included). -}
totalPointImpact :: W -> Meters -> PosReal -> PosReal -> Joule
totalPointImpact w r clouds snow
    | r /= 0 = (0.35 * (weatherMultiplier snow clouds) * (energy w)) / (4.0 * reducedPrecisionPi * r)
    | otherwise = energy w -- If 0, then at ground zero and no diminution b/c of distance
    where
      weatherMultiplier :: PosReal -> PosReal -> PosReal
      weatherMultiplier a b
          | a*b > 0 = (a * b)
          | a + b > 0 = (a `max` b)
          | otherwise = 1
      reducedPrecisionPi = 3.141592653589793 :: PosReal

-- Results in kPA
peakOverPressureUnderWater :: W -> Meters -> Joule
peakOverPressureUnderWater w r = 1.07e7 * (1 / (w / 0.37)) * (1 / (r / (-1.18))) -- = 1.07e7 * w^(0.37) * r^(-1.18)

-- thermal deaths - pmax = kJ/m^2
--thermalMax = 270 -- incapacitation
thermalMaxTest :: IO Bool
thermalMaxTest = parCheck (\s -> (thermalMax s s s s) >= 0) 100
thermalMax :: Meters -> Joule -> Joule -> Joule -> Joule
thermalMax w r clouds snow = (0.38 * (totalPointImpact w r clouds snow)) / timeMax w

timeMaxTest :: IO Bool
timeMaxTest = parCheck (\s -> timeMax s >= 0) 100
timeMax :: W -> Time
timeMax w = 0.0417 * (1 / (w / 0.44))

-- TODO: add a test
pressureKilledPerson :: Meters -> Joule -> Joule -> Joule -> Bool
pressureKilledPerson w r clouds snow
                     | thermalMax w r clouds snow >= 270 = True
                     | otherwise = False

--radiationDeaths = dose-rate * exposure-time
-- radiationKilled = undefined
-- centigray is 1/10 a gray
--gray joule kg = joule / kg
--450 centigray = 50% fatalities
-- We'll assume the following scales linearly
-- at 10 r, 3billion / 1million major defects 1stgen (1/3000th), /10million (1/300th) cumulative total
--          3billion / 10million (1/300th) early mortalities 1stgen, /200million (1/15th) cumulative total

{- weapon vital statistics
--
icbms
--
minuteman II
# 450
11 300
1 W56

minuteman III
# 200
13 000
3 W62

minuteman III (MK12A)
# 300
13 000
3 W78

mx
50
11 000
10 W87
--
slbm
--
poseidon
224
4 600
10 w68

trident I
384
7 400
8 w76
--
bombers
--
B-1B
97
9 800
22

FB-111A
59
4 700
6

B-52G/H
193
16 000
b052h/g model: 20 SRAM or
b-52h: 20 ALCM
b-52g:  12 ALCM and 6 bombs

f-4 C/D/E
2 250
1060-2400
3

F-15 A/C
5 W25 or genies

F-16 A/B/C/D
5 B43 or B57

F-111 A/D/E/F
3 b43|b57|b61|b83
--
Missiles
--
pershing II
111
1 790
1 W85

GLCM
250
2 500
1 W84

Pershing IA
72
740
1 W50

lance
100
125
1 W70

nike hercules
27
160
1 W31

tomahawk
200
2 500
1 W80-0
--
bombs
--
W31
75
1-20kt

W56
450
1.2MT

W50
100
60-400

W62
600
170kt

W70
1 282
1-100kt

W78
900
335kt

W87
500
300kt

W68
2 240
40kt

w76
3 072
100kt

w80-0
200
5-150kt

w85
125
.3-80

w84
325
.2-150kt

b28
b28RE
b43
b57
b61
b83 "genie"

"b-52G/H 20 SRAM"
B-52G
B-52H

-}
{-
A list of possible scenarios.

US first strike
USSR first strike
NATO / Warsaw Pact
Far East strategy
US USSR escalation

Middle East war
USSR - China attack
India Pakistan war
Mediterranean war
Hong Kong variant

SEATO decapitating
Cuban provocation
Inadvertent
Atlantic heavy
Cuban paramilitary

Nicaraguan preemptive
Pacific territorial
Burmese theatre-wide
Turkish decoy
NATO first strike

Argentina escalation
Iceland maximum
Arabian theatre-wide
U.S. subversion
Australian maneuver

Iranian diversion
...? limited
Sudan surprise
NATO territorial
Zaire alliance

Iceland incident
English escalation
Zaire sudden
Egypt paramilitary
Middle East heavy

Mexican takeover
Chad alert
Saudi maneuver
African territorial
Ethiopian escalation

Canadian ...?
Turkish heavy
NATO incursion
U.S. defense
Cambodian heavy

Pact medium
Arctic minimal
Mexican domestic
Taiwan theatre-wide
Pacific maneuver

Portugal revolution
Albanian decoy
Palestinian local
Moroccan minimal
Hungarian diversion

Czech option
French alliance
Arabian clandestine
Gabon rebellion
Northern maximum

Syrian surprise
...?sh paramilitary
SEATO takeover
Hawaiian escalation
Iranian maneuver

NATO containment
Swiss incident
Cuban minimal
Chad alert
Iceland escalation

Vietnamese retaliation
Syrian provocation
Libyan local
Gabon takeover
Romanian war

Middle East offensive
Denmark massive
Chile confrontation
S.African subversion
USSR alert

Nicaraguan thrust
Greenland domestic
Iceland heavy
Kenya option
Pacific defense

Uganda maximum
Thai subversion
Romanian strike
Pakistan sovereignty
Afghan misdirection

Thai variation
Northern territorial
Polish paramilitary
S.African offensive
Panama misdirection

Scandinavian domestic
Jordan preemptive
English thrust
Burmese maneuver
Spain counter

Arabian offensive
Chad interdiction
Taiwan misdirection
Bangladesh theatre-wide
Ethiopian local

Italian takeover
Vietnamese incident
English preemptive
Denmark alternate
Thai confrontation

Taiwan surprise
Brazilian strike
Venezuela sudden
Malaysian alert
Israel discretionary

Libyan action
Palestinian tactical
NATO alternate
Cyprus maneuver
Egypt misdirection

Bangladesh thrust
Kenya defense
Bangladesh containment
Vietnamese strike
Albanian containment

Gabon surprise
Iraq sovereignty
Vietnamese sudden
Lebanon interdiction
Taiwan domestic

Algerian sovereignty
Arabian strike
Atlantic sudden
Mongolian thrust
Polish decoy

Alaskan discretionary
Canadian thrust
Arabian light
S.African domestic
Tunisian incident

Malaysian maneuver
Jamaica decoy
Malaysian minimal
Russian sovereignty
Chad option

Bangladesh war
Burmese containment
Asian theatre-wide
Bulgarian clandestine
Greenland incursion

Egypt surgical
Czech heavy
Taiwan confrontation
Greenland maximum
Uganda offensive

Caspian defense
-}
module Pcheck (parTest, parCheck) where

import Control.Monad (replicateM_, liftM)
import Control.Concurrent.Chan (newChan, writeChan, getChanContents)
import Control.Concurrent (forkIO)
import Test.QuickCheck (quickCheck', Testable())

-- | Takes a list of functions using parCheck, and returns True iff all return
-- True. Evaluates them in parallel.
parTest :: [IO Bool] -> IO Bool
parTest = andTest . parList
    where andTest :: IO [Bool] -> IO Bool
          andTest = liftM and

{- | Test in parallel. Forks off a QuickCheck 'n' times; QuickCheck tests using
 the proposition 't'. Returns True if all tests were passed, else
 False. Should be run with parallelizing options like with +RTS -N4 -RTS &etc. -}
parCheck :: (Testable prop) => prop -> Int -> IO Bool
parCheck t n = do chan <- newChan
                  replicateM_ n $ forkIO $ (writeChan chan) =<< (quickCheck' t)
                  liftM (and . take n) $ getChanContents chan

-- | Takes a list of functions (presumably using parCheck) and evaluates all in parallel.
parList :: [IO a] -> IO [a]
parList fs = do chan <- newChan
                mapM_ (\m -> forkIO $ m >>= writeChan chan) fs
                liftM (take n) $ getChanContents chan
                    where n = length fs
4,536,511,400 bytes allocated in the heap 2,318,559,160 bytes copied during GC 
(scavenged) 69,225,392 bytes copied during GC (not scavenged) 8,597,504 bytes 
maximum residency (213 sample(s)) 8436 collections in generation 0 ( 9.07s) 213 
collections in generation 1 ( 1.98s) 19 Mb total memory in use Task 0 (worker) 
: MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s elapsed) Task 1 
(worker) : MUT time: 2.78s ( 35.21s elapsed) GC time: 0.03s ( 0.05s elapsed) 
Task 2 (worker) : MUT time: 31.59s ( 35.21s elapsed) GC time: 11.02s ( 11.52s 
elapsed) Task 3 (worker) : MUT time: 0.00s ( 35.21s elapsed) GC time: 0.00s ( 
0.00s elapsed) Task 4 (worker) : MUT time: 0.12s ( 35.21s elapsed) GC time: 
0.00s ( 0.00s elapsed) INIT time 0.00s ( 0.00s elapsed) MUT time 14.91s ( 
35.21s elapsed) GC time 11.05s ( 11.57s elapsed) EXIT time 0.00s ( 0.00s 
elapsed) Total time 25.96s ( 46.79s elapsed) %GC time 42.6% (24.7% elapsed) 
Alloc rate 304,259,651 bytes per MUT second Productivity 57.4% of total user, 
31.9% of total elapsed ./nuke +RTS -N1 -sstderr -RTS 25.96s user 19.73s system 
97% cpu 46.790 total
4,537,111,528 bytes allocated in the heap 966,337,104 bytes copied during GC 
(scavenged) 9,500,416 bytes copied during GC (not scavenged) 7,516,160 bytes 
maximum residency (53 sample(s)) 4286 collections in generation 0 ( 6.88s) 53 
collections in generation 1 ( 0.55s) 19 Mb total memory in use Task 0 (worker) 
: MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s elapsed) Task 1 
(worker) : MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s elapsed) 
Task 2 (worker) : MUT time: 0.07s ( 70.90s elapsed) GC time: 0.00s ( 0.01s 
elapsed) Task 3 (worker) : MUT time: 0.05s ( 70.91s elapsed) GC time: 0.00s ( 
0.00s elapsed) Task 4 (worker) : MUT time: 40.76s ( 70.91s elapsed) GC time: 
3.94s ( 3.89s elapsed) Task 5 (worker) : MUT time: 41.12s ( 70.91s elapsed) GC 
time: 3.49s ( 3.40s elapsed) Task 6 (worker) : MUT time: 0.07s ( 70.91s 
elapsed) GC time: 0.00s ( 0.00s elapsed) Task 7 (worker) : MUT time: 0.00s ( 
70.91s elapsed) GC time: 0.00s ( 0.00s elapsed) INIT time 0.00s ( 0.00s 
elapsed) MUT time 35.82s ( 70.91s elapsed) GC time 7.43s ( 7.30s elapsed) EXIT 
time 0.00s ( 0.00s elapsed) Total time 43.25s ( 78.21s elapsed) %GC time 17.2% 
(9.3% elapsed) Alloc rate 126,664,196 bytes per MUT second Productivity 82.8% 
of total user, 45.8% of total elapsed ./nuke +RTS -N2 -sstderr -RTS 43.25s user 
47.14s system 115% cpu 1:18.22 total
4,536,868,632 bytes allocated in the heap 723,357,976 bytes copied during GC 
(scavenged) 8,012,832 bytes copied during GC (not scavenged) 7,335,936 bytes 
maximum residency (44 sample(s)) 2774 collections in generation 0 ( 5.32s) 44 
collections in generation 1 ( 0.49s) 20 Mb total memory in use Task 0 (worker) 
: MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s elapsed) Task 1 
(worker) : MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s elapsed) 
Task 2 (worker) : MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 0.00s 
elapsed) Task 3 (worker) : MUT time: 0.00s ( 0.00s elapsed) GC time: 0.00s ( 
0.00s elapsed) Task 4 (worker) : MUT time: 0.07s ( 73.31s elapsed) GC time: 
0.01s ( 0.00s elapsed) Task 5 (worker) : MUT time: 0.01s ( 73.31s elapsed) GC 
time: 0.00s ( 0.00s elapsed) Task 6 (worker) : MUT time: 0.07s ( 73.31s 
elapsed) GC time: 0.01s ( 0.01s elapsed) Task 7 (worker) : MUT time: 0.11s ( 
73.31s elapsed) GC time: 0.02s ( 0.01s elapsed) Task 8 (worker) : MUT time: 
28.12s ( 73.31s elapsed) GC time: 4.80s ( 4.61s elapsed) Task 9 (worker) : MUT 
time: 11.31s ( 73.31s elapsed) GC time: 0.01s ( 0.00s elapsed) Task 10 (worker) 
: MUT time: 0.05s ( 73.31s elapsed) GC time: 0.00s ( 0.00s elapsed) Task 11 
(worker) : MUT time: 0.00s ( 73.31s elapsed) GC time: 0.00s ( 0.00s elapsed) 
Task 12 (worker) : MUT time: 20.37s ( 73.31s elapsed) GC time: 0.05s ( 0.04s 
elapsed) Task 13 (worker) : MUT time: 27.27s ( 73.31s elapsed) GC time: 0.91s ( 
0.98s elapsed) INIT time 0.00s ( 0.00s elapsed) MUT time 37.41s ( 73.31s 
elapsed) GC time 5.81s ( 5.66s elapsed) EXIT time 0.00s ( 0.00s elapsed) Total 
time 43.22s ( 78.98s elapsed) %GC time 13.4% (7.2% elapsed)

Attachment: pgpcrqGbeNGuM.pgp
Description: PGP signature

_______________________________________________
Haskell-Cafe mailing list
Haskell-Cafe@haskell.org
http://www.haskell.org/mailman/listinfo/haskell-cafe

Reply via email to