We will try to run a few simulated processes to understand the performance difference between Single-threaded, Multi-threading and Multi-processing in Python.
We simulate the workload with two tasks
We will learn about GIL - Alternative Python interpreters - by counting to 255 million and downloading few webpages
from IPython.display import HTML
HTML('''
<script src='//code.jquery.com/jquery-3.3.1.min.js'></script>
<script>
code_show=false;
function code_toggle() {
if (code_show){
$('div.input').show();
$('div .jp-CodeCell .jp-Cell-inputWrapper').show();
} else {
$('div.input').hide();
$('div .jp-CodeCell .jp-Cell-inputWrapper').hide();
}
code_show = !code_show
}
$( document ).ready(code_toggle);
</script>
<form action="javascript:code_toggle()"><input type="submit" value="Code on/off"></form>''')
import datetime
import pandas as pd
import plotly.express as px
import local_nb_utils
# reloader
from importlib import reload
reload(local_nb_utils)
We have three types of work load
1. a purely IO bound work load.
2. a purely CPU bound work load.
3. a combination of IO and CPU work load randomly shuffled together.
io_work_load = get_io_work_load(load_size=load_size)
cpu_work_load = get_cpu_work_load(load_size=load_size)
io_and_cpu_work_load = io_work_load+cpu_work_load
random.shuffle(io_and_cpu_work_load)
runtimes = local_nb_utils.run_simulated_work_load()
process = []
tasks = []
for runtime in runtimes:
_process = {}
for key,value in runtime.items():
if key != 'results':
_process[key] = value
process.append(_process)
for result in runtime['results']:
result['work_load_label'] = runtime['work_load_label']
result['work_load_size'] = runtime['work_load_size']
result['process_duration'] = runtime['process_duration']
result['process_start_time'] = runtime['process_start_time']
tasks.append(result)
tasks_df = pd.DataFrame(tasks)
process_df = pd.DataFrame(process)
work_load_label_order = [
'io_work_load single thread',
'io_work_load 5 threads',
'io_work_load 5 process',
'cpu_work_load single thread',
'cpu_work_load 5 threads',
'cpu_work_load 5 process',
'io_and_cpu_work_load single thread',
'io_and_cpu_work_load 5 threads',
'io_and_cpu_work_load 5 process',
]
io_work_load single thread
and cpu_work_load single thread
to take roughly the same time to simulate a 50:50 CPU and IO work load.fig = px.bar(
process_df,
y='work_load_label',
x='process_duration',
text='process_duration',
category_orders={"work_load_label": work_load_label_order},
facet_col_wrap=3,
orientation='h',
)
fig.update_traces(texttemplate='%{text:.2s}', textposition='outside')
fig.show()
def fromtimestamp(timestamp):
return datetime.datetime.fromtimestamp(timestamp)
def set_microsecond_zero(dt):
return dt.replace(microsecond=0)
tasks_df['work_start_time_sec'] = tasks_df['work_start_time'].apply(fromtimestamp).apply(set_microsecond_zero)
def print_work_load_summary(work_load_label):
fig = px.bar(
process_df.query('work_load_label == @work_load_label'),
y='work_load_label',
x='process_duration',
text='process_duration',
category_orders={"work_load_label": work_load_label_order},
facet_col_wrap=3,
orientation='h',
height=175
)
fig.update_traces(texttemplate='%{text:.2s}', textposition='outside')
fig.show()
def print_work_load_details(work_load_label):
_df = tasks_df.query('work_load_label == @work_load_label')
fig = px.bar(
_df,
x='work_start_time_sec',
y='work_duration',
color='work_type',
facet_col="work_load_label",
)
fig.show()
We have captured duration for each work. we can use this to drilldown to see how each process is preforming the combined IO and CPU work load.
print_work_load_summary('io_and_cpu_work_load single thread')
print_work_load_details('io_and_cpu_work_load single thread')
print_work_load_summary('io_and_cpu_work_load 5 threads')
print_work_load_details('io_and_cpu_work_load 5 threads')
print_work_load_summary('io_and_cpu_work_load 5 process')
print_work_load_details('io_and_cpu_work_load 5 process')