This is an issue that came to light when testing the patches on #910 (closed). You can see some of the numbers there. Basically, recent GHC HEAD builds, running a large number of threads blocked on MVars will result in burning a lot of system time.
The attached file provides a mediocre reproducer. With it, you can see that building with HEAD as of Aug 31st and running with -RTS -N32 will result in around 200ms system time, whereas GHC 7.6.3 spends about 30ms in system time. This shows the disparity, but the result is not that egregious.
A more noticeable example is on ticket #910 (closed), where when running on 31 threads, there is an 8 minutes of system time for 17 minutes of user time, and yet at one thread that system time drops to under two seconds!
1 thread: real 1m20.028s user 1m17.921s sys 0m1.768s 31 threads: real 1m27.445s user 17m0.314s sys 8m0.175s
It needs to be determined whether this system time is a result of the parallel compilation patches on #910 (closed), or whether it is a new problem with the runtime system, and in particular with the parallel IO manager. I am inclined to believe that compiling in parallel involves extra IO (repeatedly reading interface files?), but not eight minutes of it!!
I also see the 200ms system time for excessive_system.hs when I run it with -N32 (and compiled with recent HEAD). Below is the first few entries after running perf record; it looks like it may be GC related. I don't see any IO manager related entries in the list.
I haven't tested this (don't have a 32 core machine,) but if the gcWorkerThread is causing problems, what does it look like when you turn off the multicore collector and load balancer? i.e. run with:
Even with MVars removed and each thread simply waiting (see attached program) it takes 200ms system time with -N32. Here is a perf recording with call stack info; here is most of the first entry:
and I get 200ms in kernel when run with -N32. I'm not sure how significant this 200ms time is - I guess it is time spent initializing or cleaning up capabilities (possibly including IO manager resources).
Right, the 200ms isn't really bad. Can anyone reproduce the 8 *minutes* of system time I was seeing on the "ghc-parmake-gsoc" branch? I'd like to figure out whether that problem has anything to do with the patches on #910 (closed), or is unrelated.
Was there a switch from some other kind of lock to a spin lock at some point?
200ms spent doing unspecified "initialization" should definitely be investigated, that's a lot. I can't see anything useful in that call stack, it looks like it might be related to signals or epoll() but it's hard to tell anything else.
One reason for extra user/system time that could account for your results in #910 (closed) is that if there isn't enough parallelism during GC then you'll have 30 threads spinning looking for work, and doing the occasional sched_yield(). Spinning accounts for the user time, the sched_yield() accounts for the system time. Still, it does look like a lot, so it would be good to look at some !ThreadScope traces.
That doesn't account for the 200ms in the empty program though, so there could be something else going on here.
I'm not sure about the 8 minutes problem, but the 200ms system time for the empty program does indeed seem to be related to the parallel IO manager changes. I just modified the IO manager to start only one epoll instance and I get about 50 ms system time, whereas with HEAD I get about 200ms still. I will upload the patch to make the IO manager use just one poller in case anyone else wants to try it.
200ms spent doing unspecified "initialization" should definitely be investigated, that's a lot. I can't see anything useful in that call stack, it looks like it might be related to signals or epoll() but it's hard to tell anything else.
543,096 bytes allocated in the heap 64 bytes copied during GC 412,256 bytes maximum residency (1 sample(s)) 279,968 bytes maximum slop 18 MB total memory in use (0 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 0 colls, 0 par 0.00s 0.00s 0.0000s 0.0000s Gen 1 1 colls, 0 par 0.00s 0.00s 0.0009s 0.0009s Parallel GC work balance: -nan% (serial 0%, perfect 100%) TASKS: 65 (1 bound, 64 peak workers (64 total), using -N32) SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) INIT time 0.04s ( 0.03s elapsed) MUT time 0.00s ( -0.00s elapsed) GC time 0.00s ( 0.00s elapsed) EXIT time 0.01s ( 0.00s elapsed) Total time 0.05s ( 0.04s elapsed) Alloc rate 0 bytes per MUT second Productivity 17.4% of total user, 24.7% of total elapsedgc_alloc_block_sync: 0whitehole_spin: 0gen[0].sync: 0gen[1].sync: 0
I did some more experiments, and I now think it has to do with making "safe" foreign calls from all of the 32 threads. I modified the IO manager so that all of the polling threads (and their epoll backends) are created. Only one of the them is used (on cap 0) is used to register files. I then modified the poll loops so that only the one on cap 0 is running the normal polling loop and the others simply return right away. With this setup, the performance is good (about 40-50 ms for -N32). However, if I have all poll loops (except the one normal poll loop for cap 0) simply make a "safe" call to usleep(10) then the performance goes back to about 200ms. I'll post a patch here so that others can see.
So it looks to be related to the cost of doing safe foreign calls. With the parallel IO manager each of the N pollers will make a safe foreign call when there is no work to do. I'm not sure that this will have much of an impact on most programs, but for the empty program it represents a large part of the work.
I'm also not sure this is related to the 8 minute system time mentioned in the original report. rnewton, can you provide a recipe for how to recreate this?
Is the 200 ms startup time for 32 cores an issue? At 16 cores it is about 50-60ms and at 8 cores about 10-20ms.
One possible resolution (besides doing nothing or making safe foreign calls cheaper) would be to provide an RTS option to use a specific number of IO managers, or at least to differentiate between using 1 or using N.
The first safe call on each Capability will cause the creation of a new OS thread, which probably accounts for some of the cost. From the strace profile it looks like there is some contention too. It might be useful to do some mutex profiling to see what lock is being contended for - perhaps it is all_tasks_mutex.
One interesting thing I just noticed is that taskset makes a big difference. excessive_system_2_HEAD is the empty program compiled with HEAD and excessive_system_2_ONEACTIVELOOP is the empty program compiled with only one IO manager loop being started. We see that for excessive_system_2_HEAD if we use taskset to restrict the threads to run on CPUs 0-9 (all on one NUMA node) we get a big difference:
$ time ./excessive_system_2_HEAD +RTS -N32 real 0m0.046suser 0m0.020ssys 0m0.188s
$ time taskset -c 0-9 ./excessive_system_2_HEAD +RTS -N32 real 0m0.025suser 0m0.000ssys 0m0.056s
excessive_system_2_ONEACTIVELOOP also performs better with taskset but the difference is much smaller. Without taskset I see system tim from 30ms-60ms and with taskset it is about 10-20ms.
So maybe the explanation is that with 31 extra safe foreign calls (from calls to epoll_wait) we get about 31 extra OS threads and if we are unlucky and the OS schedules these on different NUMA nodes we will get cache issues slowing down. And finally a spin lock burns longer accounting for more system time?
Here is a run of excessive_system_2_HEAD using taskset to restrict to first NUMA node or no restrictions with perf stat:
Do you have an example of an application which is particularly badly affected by this issue? It would be quite helpful if you could provide a testcase if so.
I tried reproducing this with cf5eec3e on a 2-socket, 24-core machine I have access to. I was unable to see anything like the system time numbers like reported in ticket:8224#comment:76935,
A few tweaks happened in #9221 since latest GHC-8.0.1 release. It has a few more knobs to tune like
-qn<value> (number of GC threads), -n<value> (nursery chunks), -qb0 (work-stealing nursery scan mode).
In previous post I used ghc-7.10.1 by mistake. With 8.0.1 (stack ghc -- -O2 -rtsopts -threaded x) single core performance is same (around 0.18s), but with +RTS -N I get better results:
real 0m0.843suser 0m22.149ssys 0m2.256s
Even better with -A200M:
real 0m0.793suser 0m1.200ssys 0m0.692s
$ head -c10MB /dev/zero | ./x +RTS -s -N > /dev/null 406,012,416 bytes allocated in the heap 30,457,408 bytes copied during GC 674,776 bytes maximum residency (2 sample(s)) 665,248 bytes maximum slop 27 MB total memory in use (0 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 775 colls, 775 par 16.945s 0.429s 0.0006s 0.0302s Gen 1 2 colls, 1 par 0.052s 0.002s 0.0012s 0.0013s Parallel GC work balance: 0.14% (serial 0%, perfect 100%) TASKS: 98 (1 bound, 97 peak workers (97 total), using -N48) SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) INIT time 2.660s ( 0.091s elapsed) MUT time 11.065s ( 0.744s elapsed) GC time 16.997s ( 0.432s elapsed) EXIT time 0.028s ( 0.003s elapsed) Total time 30.750s ( 1.270s elapsed) Alloc rate 36,694,519 bytes per MUT second Productivity 36.1% of total user, 873.3% of total elapsedgc_alloc_block_sync: 38774whitehole_spin: 0gen[0].sync: 408gen[1].sync: 130
I attached outputs of lstopo (I don't have -no-graphics variant there) and numactl -H.
Hi, as a developer on 48-core machine, I upvote the issue. I can reproduce it with many programs, e.g. this trivial one:
It is far from clear to me that we should expect the program you cited to run well with -N. It allocates heavily and -N will use parallel GC on a heavily imbalanced heap. Looking at the eventlog confirms that nearly all of the cores are simply waiting for GC. As expected disabling parallel GC with +RTS -qg drastically reduces system time due to reduced synchronization overhead.
As expected disabling parallel GC with +RTS -qg drastically reduces system time due to reduced synchronization overhead.
Wow! Just tried +RTS -N -qg and brings performance back in the sane world! Now it's something like
real 0m0.269suser 0m0.232ssys 0m0.104s
That is, 30-40% slower than without -N.
A real-life example of a program where I'd want a long single thread computation with -N turned on is a faster analog of fgrep -f patterns.txt input1 ... inputN:
Read all lines from patterns.txt into a set.
In parallel for each input file, filter (\str -> member str set) . lines