A simple program which calculates square of numbers and stores the results:
import time
from joblib import Parallel, delayed
import multiprocessing
array1 = [ 0 for i in range(100000) ]
def myfun(i):
return i**2
#### Simple loop ####
start_time = time.time()
for i in range(100000):
array1[i]=i**2
print( "Time for simple loop --- %s seconds ---" % ( time.time()
- start_time
)
)
#### Parallelized loop ####
start_time = time.time()
results = Parallel( n_jobs = -1,
verbose = 0,
backend = "threading"
)(
map( delayed( myfun ),
range( 100000 )
)
)
print( "Time for parallelized method --- %s seconds ---" % ( time.time()
- start_time
)
)
#### Output ####
# >>> ( executing file "Test_vr20.py" )
# Time for simple loop --- 0.015599966049194336 seconds ---
# Time for parallelized method --- 7.763299942016602 seconds ---
Could it be the difference in array handling for the two options? My actual program would have something more complicated but this is the kind of calculation that I need to parallelize, as simply as possible, but not with such results.
System Model: HP ProBook 640 G2, Windows 7,
IDLE for Python System Type: x64-based PC Processor:
Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz,
2401 MHz,
2 Core(s),
4 Logical Processor(s)
From the documentation of
threading
:The problem is that in the this case, you don't know that. Python itself will only allow one thread to run at once (the python interpreter locks the GIL every time it executes a python operation).
threading
is only going to be useful ifmyfun()
spends most of its time in a compiled Python extension, and that extension releases the GIL.The
Parallel
code is so embarrassingly slow because you are doing a huge amount of work to create multiple threads - and then you only execute one thread at a time anyway.If you use the
multiprocessing
backend, then you have to copy the input data into each of four or eight processes (one per core), do the processing in each processes, and then copy the output data back. The copying is going to be slow, but if the processing is a little bit more complex than just calculating a square, it might be worth it. Measure and see.Why?
Because trying to use tools in cases,
where tools principally cannot and DO NOT adjust the costs of entry:
I love python.
I pray educators better explain the costs of tools, otherwise we get lost in these wish-to-get
[PARALLEL]
-schedules.A few facts:
No.0: With a lot of simplification, python intentionally uses GIL to
[SERIAL]
-ise access to variables and thus avoiding any potential collision from[CONCURRENT]
modifications - paying these add-on costs of GIL-stepped dancing in extra timeNo.1:
[PARALLEL]
-code execution is way harder than a "just"-[CONCURRENT]
( read more )No.2:
[SERIAL]
-process has to pay extra costs, if trying to split work onto[CONCURRENT]
-workers No.3: If a process does inter-worker communication, immense extra costs per data exchange are paid No.4: If hardware has few resources for[CONCURRENT]
processes, results get way worse furtherTo have some smell of what can be done in standard python 2.7.13:
Efficiency is in better using silicon, not in bulldozing syntax-constructors into territories, where they are legal, but their performance has adverse effects on the experiment-under-test end-to-end speed:
You pay about
8 ~ 11 [ms]
just to iteratively assemble an emptyarray1
( the
Stopwatch().stop()
method yields[us]
from.start()
)while, the memory-efficient, vectorisable, GIL-free approach can do the same about +230x ~ +450x faster:
So, using the proper tools just starts the story of performance:
While a naive
[SERIAL]
-iterative implementation works, you pay immense costs for opting to do so~ 70 [ms]
for a 100000-D vector:Using a more suitable / appropriate tool costs just ~ 0.2 [ms]
i.e. ++350x FASTER
and with another glitch, a.k.a. an inplace modus-operandi:
Yields ~ +514x SPEEDUP, just from using appropriate tool
The art of performance is not in following marketing-sounding claims
about parallellizing-( at-any-cost ),
but in using know-how based methods, that pay least costs for biggest speedups achievable.
For "small"-problems, typical costs of distributing "thin"-work-packages are indeed hard to get covered by any potentially achievable speedups, so "problem-size" actually limits one's choice of methods, that could reach positive gain ( speedups of 0.9 or even << 1.0 are so often reported here, on StackOverflow, that you need not feel lost or alone in this sort of surprise ).
Epilogue
Processor number counts.
Core number counts.
But cache-sizes + NUMA-irregularities count more than that.
Smart, vectorised, HPC-cured, GIL-free libraries matter
(
numpy
et al - thanks a lot Travis OLIPHANT & al ... Great Salute to his team ... )As an overhead-strict Amdahl Law (re-)-formulation explains, why even many-
N
-CPU parallelised code execution may ( and indeed often does ) suffer from speedups << 1.0Overhead-strict formulation of the Amdahl's Law speedup
S
includes the very costs of the paid[PAR]-
Setup +[PAR]-
Terminate Overheads, explicitly:( an interactive animated tool for 2D visualising effects of these performance constraints is cited here )