I had a funny such experience lately in a job interview. I told the interviewer his misconception could be falsified with ~10 LOC summing a list with 2 threads.
I had a funny such experience lately in a job interview. I told the interviewer his misconception could be falsified with ~10 LOC summing a list with 2 threads.
If any operations were GIL-bound (which is extremely common, even if not intentional, by reliance on bytecode instructions dealing with CPython API under the hood, like attribute lookups or iteration special methods), then execution is probably interleaved serially and constrained by the interpreter’s GIL allocation, and slower than just summing serially.
I’ve seen a lot of people who are cocksure they know some contrarian “actually you can use threads” trivia about Python and just naively use the threading module or naively use the newish ThreadPoolExecutor stuff not realizing that no, in fact, it’s not somehow magically always GIL-avoiding to do so.
By having multiple concurrent IO requests from multiple threads? Can probably log detailed timestamps to see if IO requests were happening in parallel.
# main.py
import random
from concurrent.futures import ThreadPoolExecutor as Pool
items = [random.random() for _ in range(10 ** 7)]
def run(items, n):
step = len(items) // n
with Pool(max_workers=n) as ex:
res = [ex.submit(sum, items[i*step : (i+1)*step]) for i in range(n)]
return sum(r.result() for r in res)
if __name__ == '__main__':
import timeit
import sys
n = sys.argv[1] if len(sys.argv) > 1 else 1
time = timeit.timeit('run(items, %s)' % n, 'from __main__ import run, items', number=10)
print("%s\t%.3f" % (n, time / 10))
$ for x in `seq 1 16` ; do python3 -m main $x ; done
1 0.172
2 0.170
3 0.166
4 0.155
5 0.149
6 0.142
7 0.144
8 0.140
9 0.136
10 0.135
11 0.135
12 0.137
13 0.135
14 0.136
15 0.136
16 0.136I think it does that pretty well but, well, that's just like my opinion.
Ideally, pure computation like the one in your example should get a speed up linear to number of threads. 2 threads -> 2x faster. 3 threads -> 3x faster, up to a number of cores.
If that's not happening then you have excessive locking.
In Python excessive locking is caused by GIL - Global Interpreter Lock. As the name implies, there's a single, global lock and execution in a single threads effectively blocks all other threads because it holds GIL captive.
What this particular benchmarks is showing is that GIL is as bad as everyone says: instead of getting 16x speedup, you get speedups that are almost within margin of error for such a coarse measure.
Your example I think is demonstrating the opposite of what you want to show. Those figures are atrocious.
I'm not even sure what the opposite of that is.
The way I read your post, it seemed like your interviewer told you that threads in python are not effective for parallel computation because of the GIL, and your example proves exactly that. The performance of your threads are absolutely horrible, if you were to do that in C++/Java/Go you would likely see a speedup in the order of min(16, cores). Your example proves that your threads are exactly serialized during the computation, which I assume was the point of your interviewer (but please clarify my assumption).
Perhaps your interviewer was instead proposing that threads in Python work like a charm?
1. There is a common misconception that any threaded python code is slower than sequential. This is trivially false for IO-bound, and possibly, potentially, in some cases false for CPU-bound. Said interviewer had that very misconception.
2. More importantly - you and another parent are right. The code does not demonstrate what I thought it does. This is proven by removing the ThreadPool and leaving the code intact:
import random
import math
from concurrent.futures import ThreadPoolExecutor as Pool
items = [random.random() for _ in range(math.factorial(11))]
def run(items, n):
step = len(items) // n
res = [sum(items[i*step : (i+1)*step]) for i in range(n)]
return sum(res)
if __name__ == '__main__':
import timeit
import sys
n = sys.argv[1] if len(sys.argv) > 1 else 1
time = timeit.timeit('run(items, %s)' % n, 'from __main__ import run, items', number=10)
print("%s\t%.3f" % (n, time / 10))
$ for x in `seq 1 11`; do python3 -m main $x ; done
1 0.799
2 0.749
3 0.671
4 0.715
5 0.730
6 0.704
7 0.649
8 0.631
9 0.689
10 0.613
11 0.616
This is a result I'd have to silently contemplate before making any further comments.(2) You're not measuring what you think you are measuring.
Take out the Pool altogether:
def run(items, n):
step = len(items) // n
res = [sum(items[i*step : (i+1)*step]) for i in range(n)]
return sum(r for r in res)
and the results are similar: 1 0.170
2 0.170
3 0.147
4 0.145
5 0.142
6 0.142
7 0.138
8 0.137
9 0.135
10 0.138
I have some guesses why it improves, but I'm not certain. It's definitely nothing to do with multithreading.EDIT: As an aside, you can speed this up 20x by using Numpy:
items = numpy.array([random.random() for _ in range(10 ** 7)])
def run(items, n):
return numpy.sum(items)
1 0.007
Supporting the original point that running code in low-level languages produces amazing speedups vs fancy parallelism methods.I’m pretty sure the time “savings” you are seeing here come from somewhere else. At a first glance, your copying the huge list while submitting it to the thread pool, this has overhead. Maybe lots of smaller copies are faster on your machine.
I imagine Python works roughly the same.
Parallelism is possible, but only for computations in native code.
FWIW Node.js is essentially the same situation.
Otherwise yes, Node or Ruby play by the same global lock rules.
Launching multiple processes without mutable shared memory is always an option.
Been a while since I've used Python, but as far as I remember the GIL only affects Python objects. So if you use Numpy for operations, you can avoid the GIL.
And, more importantly for us, while numpy is doing an array operation, python also releases the GIL. Thus if you tell one thread to do:
print "%s %s %s %s and %s" %( ("spam",) *3 + ("eggs",) + ("spam",) )
A = B + C
print A
During the print operations and the % formatting operation, no other thread can execute. But during the A = B + C, another thread can run - and if you've written your code in a numpy style, much of the calculation will be done in a few array operations like A = B + C. Thus you can actually get a speedup from using multiple threads.Correct.
Threads cannot help Python-heavy code; they can help I/O-heavy or native-heavy code (assuming native code releases the GIL).
Even without multithreading, numpy programming can give amazing speedups. It's the reason why data science Python isn't dead in the water.
Spawn a thread and handle disk or socket IO requests there? You can get fancy and use a thread pool so maybe 15 lines of code.
Not sure why you used the word “supposedly”. Threads have been in the standard library for a very long time.
Yes and still run the risk of running into GIL problems. You claim to have shown 10 lines to an interviewer that proved he was wrong about the GIL still being an issue and I've yet to see an example of it not being one when using pure python. Yes there are certain cases where you don't hit the GIL, no that doesn't mean it isn't still an issue when dealing with threads.
Otherwise another trick is to identify a call which uses extensions or releases the GIL once it goes into C.
I had multiple of those experiences. Even then people wouldn’t believe me so we had to look at Python C code and read about threads and IO operations in C and such. The other alternative is to write a deliberately slow socket server that takes say 5 seconds to respond then then write a threaded client to issue requests and seeing how they complete in parallel.