On Sun, Jan 23, 2011 at 8:56 PM, Michael Gardner <gardne...@gmail.com> wrote:
> Suppose I have a sequence of tasks I'd like to parallelize using pmap. The 
> amount of CPU time these tasks require varies greatly; in particular, many of 
> them will require virtually no work. Can I rely on pmap to divide the work 
> efficiently even if there is some pattern to the distribution of easy tasks 
> (e.g. clumped at the beginning, or every nth)? Assume it's not possible to 
> identify the easy tasks beforehand.

Here's a little data:
user=> (def printer (agent nil))
#'user/printer
user=> (defn xyz [n] (send printer (fn [_] println
(Thread/currentThread))) (* n n))
#'user/xyz
user=> (xyz 3)
#<Thread Thread[Thread-2,5,main]>
9
user=> (pmap xyz (range 20))
#<Thread Thread[pool-2-thread-25,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-25,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-21,5,main]>
#<Thread Thread[pool-2-thread-21,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-29,5,main]>
#<Thread Thread[pool-2-thread-29,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-22,5,main]>
#<Thread Thread[pool-2-thread-30,5,main]>
#<Thread Thread[pool-2-thread-28,5,main]>
#<Thread Thread[pool-2-thread-26,5,main]>
#<Thread Thread[pool-2-thread-24,5,main]>
(0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361)
user=>

The fiddling with an agent is to prevent individual println outputs
from getting intermingled; instead the lines are perhaps slightly out
of order but at least are not mangled into unreadability.

As you can see, there are at least seven threads called somewhat
randomly; not a simple alternating pattern of 2 as you might naively
expect on a dual-core machine. It seems to prefer thread 22 for some
reason.

I don't know exactly what is going on under the hood but it looks like
it either involves some degree of randomness, or it tries to
load-balance (and something else was sometimes preempting threads -- I
don't know what, when I ran this nothing on my system was doing
anything CPU-intensive with a priority equal to or higher than the
REPL process, but NetBeans and the REPL were both being a bit
sluggish; some sort of Windows weirdness). Either way it's likely that
a mix of easy and hard jobs, even with a pattern, won't screw things
up too much.

That can be further tested:

user=> (defn heavy [f amt] (fn [x] (Thread/sleep amt) (f x)))
#'user/heavy
user=> (defn s [x] (* x x))
#'user/x
user=> (def l (heavy s 1000))
#'user/y
user=> (defn run [fseq n] (time (doall (pmap #(%1 %2) fseq (range n)))) nil)
#'user/run
user=> (run (cycle [s l]) 100)
"Elapsed time: 17141.64872 msecs"
nil
user=> (run (cycle [s s l l]) 100)
"Elapsed time: 17020.59136 msecs"
nil
user=> (run (cycle [s s l s l l]) 100)
"Elapsed time: 17007.0366 msecs"
nil
user=> (defn randomize [s] (let [n (count s)] (repeatedly #(nth s
(rand-int n)))))
#'user/randomize
user=> (run (randomize [s l]) 100)
"Elapsed time: 18015.8562 msecs"
nil

As you can see, the timings are fairly consistent when a 50/50 mix of
short (<1msec) and long (>1000msec) tasks get parallelized using pmap,
with several possible patterns and also with no pattern to their
distribution.

This was again on a dual-core machine; it's interesting that the
numbers are around 17-18s instead of 25s (100 jobs, averaging 500ms
each, split between two cores). The use of more than two threads in
the thread pool is presumably the cause of this; Thread/sleep lets
another of the threads make progress. It didn't collapse down to just
1s so it's also not the case that pmap is spawning as many threads as
will maximize throughput (fifty running the slow jobs plus two running
the fast ones on both cores while the slow ones sleep). Since the jobs
would cumulatively take 50s if run serially, the speedup factor from
pmap here is just under 3.

One more data point:

user=> (run (repeat l) 50)
"Elapsed time: 10172.37124 msecs"
nil

That's fifty long jobs without any short ones. The speedup factor here
is 5 rather than 3. This shows that it will allow five, but no more,
jobs to be running concurrently, and that it doesn't load balance. If
it did, adding the short jobs wouldn't slow it down more than a few
ms; in fact doing so almost doubles the time, so it seems the jobs are
preallocated in some manner and the short jobs take up half the
available threads in the pool. (This is odd given that it seemed
before to be using at least seven threads, not just five.)

Of course, real jobs are likely to be CPU-intensive instead of
yielding like Thread/sleep.

user=> (def foo (atom nil))
#'user/foo
user=> (defn l2 [x] (doseq [a (range 7500000)] (reset! foo (* a x))) (* x x))
#'user/l2

The number 7500000 causes l2 to take about 900ms on my machine.

user=> (run (cycle [s l2]) 100)
"Elapsed time: 37024.37508 msecs"
nil

Oddly, quite a bit less than 50s. Perhaps reset! spends some time
yielding the CPU for some reason?

user=> (run (cycle [s s l2 l2]) 100)
"Elapsed time: 46963.28936 msecs"
nil
user=> (run (cycle [s s l2 s l2 l2]) 100)
"Elapsed time: 36551.62528 msecs"
nil
user=> (run (randomize [s l2]) 100)
"Elapsed time: 51713.50044 msecs"
nil
user=> (run (repeat l2) 50)
"Elapsed time: 43197.77312 msecs"
nil

These numbers suggest that the CPU load will generally be divvied up
equally among your cores, if the jobs don't yield the CPU (sleep,
block on I/O, block on monitors). If they do yield the CPU here and
there, it looks like pmap may perform slightly better, rather than
slightly worse, if the jobs that do so form a pattern.

Long story short: Don't worry about it. :)

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to