VizTracer
VizTracer
- это инструмент для профилирования Python-программы.
Установка:
$ pip install viztracer
Запуск:
$ viztracer test.py
По окончании выполнения программы формируется отчет в формате JSON.
Визуализация:
$ vizviewer result.json
Примеры
Числа Фибоначчи. Рекурсия
def fibonacci(n: int) -> int:
if n < 3:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
if __name__ == '__main__':
print(fibonacci(10))
На FlameGraph
'е можно увидеть, сколько времени выполнялась функция - чем она длиннее на графике, тем дольше.
Для n = 10
глубина рекурсии составила 9.
threading
Посмотрим, как потоки себя ведут при решении CPU-bound и IO-bound задач.
IO-bound
import random
import time
from threading import Thread
def worker():
time.sleep(10 * random.random())
if __name__ == '__main__':
threads = []
for _ in range(8):
threads.append(Thread(target=worker))
threads[-1].start()
for thread in threads:
thread.join()
CPU-bound
import random
from threading import Thread
result = 0
def worker():
global result
for _ in range(100_000):
result += random.random()
if __name__ == '__main__':
threads = []
for _ in range(8):
threads.append(Thread(target=worker))
threads[-1].start()
for thread in threads:
thread.join()
print(result)
194685.43393841566
Как можно заметить, worker
'ы исполняются параллельно, но участки кода, связанные с доступом к переменной, работают последовательно благодаря GIL.
multiprocessing
Посчитаем сумму квадратов целых чисел от 1 до 1,000,000 с помощью потоков и с помощью процессов.
Пул процессов
from multiprocessing import Pool, cpu_count
def sum_of_squares(from_: int, to: int) -> int:
result = 0
for i in range(from_, to):
result += i ** 2
return result
def count_sum_of_squares(n: int, chunks: int = cpu_count()) -> int:
step: int = n // chunks
offset: int = 1
ranges = [(step * i + offset, step * (i + 1) + offset)
for i in range(chunks)]
with Pool(chunks) as pool:
results = pool.starmap(sum_of_squares, ranges)
return sum(results)
if __name__ == '__main__':
import time
start = time.perf_counter()
print(count_sum_of_squares(10_000_000))
print(time.perf_counter() - start)
333333383333335000000
0.5275702479998472
Пул потоков
from multiprocessing import cpu_count
from multiprocessing.pool import ThreadPool
def sum_of_squares(from_: int, to: int) -> int:
result = 0
for i in range(from_, to):
result += i ** 2
return result
def count_sum_of_squares(n: int, chunks: int = cpu_count()) -> int:
step: int = n // chunks
offset: int = 1
ranges = [(step * i + offset, step * (i + 1) + offset)
for i in range(chunks)]
with ThreadPool(chunks) as pool:
results = pool.starmap(sum_of_squares, ranges)
return sum(results)
if __name__ == '__main__':
import time
start = time.perf_counter()
print(count_sum_of_squares(10_000_000))
print(time.perf_counter() - start)
333333383333335000000
2.2580976360000022
При получении такого результата я пришел в замешательство - всегда думал, что потоки работают последовательно при решении CPU-bound задач, а по графику можно видеть, что несколько sum_of_squares
работают одновременно.
Оказалось, что реализация потоков в multiprocessing
несколько иная, чем в threading
- в первом они могут работать сразу на нескольких процессорах:
$ python3 test.py &
[1] 21592
$ ps -p 21592 -T -o pid,tid,psr,pcpu
PID TID PSR %CPU
21592 21592 2 0.1
21592 21593 7 13.7
21592 21594 4 9.1
21592 21595 0 19.5
21592 21596 1 11.8
21592 21597 5 13.7
21592 21598 5 19.3
21592 21599 1 8.4
21592 21600 2 7.1
21592 21601 1 0.0
21592 21602 0 0.0
21592 21603 1 0.0