Почему операции Python в 30 раз медленнее после вызова time.sleep или subprocess.Popen?
Рассмотрим следующий цикл:
for i in range(20):
if i == 10:
subprocess.Popen(["echo"]) # command 1
t_start = time.time()
1+1 # command 2
t_stop = time.time()
print(t_stop - t_start)
Команда "команда 2" выполняется систематически дольше, когда "команда 1" выполняется перед ней. На следующем графике показано время выполнения 1+1
в зависимости от индекса i
цикла, усредненного по 100 циклам.
Выполнение 1+1
в 30 раз медленнее, когда ему предшествует subprocess.Popen
. subprocess.Popen
.
Это становится еще более странным. Можно подумать, что затрагивается только первая команда, выполняемая после subprocess.Popen()
, но это не так. Следующий цикл показывает, что затрагиваются все команды в текущей итерации цикла. Но последующие итерации цикла кажутся в основном нормальными.
var = 0
for i in range(20):
if i == 10:
# command 1
subprocess.Popen(['echo'])
# command 2a
t_start = time.time()
1 + 1
t_stop = time.time()
print(t_stop - t_start)
# command 2b
t_start = time.time()
print(1)
t_stop = time.time()
print(t_stop - t_start)
# command 2c
t_start = time.time()
var += 1
t_stop = time.time()
print(t_stop - t_start)
Вот график времени выполнения для этого цикла, в среднем более 100 запусков:
Больше замечаний:
- Мы получаем тот же эффект при замене
subprocess.Popen()
("command 1") на time.sleep()
или rawkits libraw C++ для инициализации привязок (libraw.bindings.LibRaw()
). Однако использование других библиотек с привязками C++, таких как libraw.py или OpenCVs cv2.warpAffine()
, не влияет на время выполнения. Открытие файлов тоже не происходит. - Эффект не вызван
time.time()
, потому что он виден с помощью timeit.timeit()
и даже измерением вручную, когда появляется результат print()
. - Это также происходит без цикла for.
- Это происходит даже тогда, когда между "командой 1" (
subprocess.Popen
) и "командой 2" выполняется много разных (возможно, CPU- и занимающих много памяти) операций. - С массивами Numpy замедление оказывается пропорциональным размеру массива. При относительно больших массивах (~ 60 M точек) простая операция
arr += 1
может занять до 300 мс!
Вопрос: Что может вызвать этот эффект и почему он влияет только на текущую итерацию цикла?
Я подозреваю, что это может быть связано с переключением контекста, но это, похоже, не объясняет, почему повлияла бы итерация всего цикла. Если переключение контекста действительно является причиной, почему некоторые команды запускают его, а другие нет?
Ответы
Ответ 1
я предполагаю, что это связано с тем, что код Python вытесняется из различных кэшей в системе ЦП/памяти
пакет perflib
можно использовать для получения более подробной статистики уровня ЦП о состоянии кэша, то есть о количестве попаданий/пропусков.
Я получаю в 5 раз LIBPERF_COUNT_HW_CACHE_MISSES
счетчика LIBPERF_COUNT_HW_CACHE_MISSES
после вызова LIBPERF_COUNT_HW_CACHE_MISSES
Popen()
:
from subprocess import Popen, DEVNULL
from perflib import PerfCounter
import numpy as np
arr = []
p = PerfCounter('LIBPERF_COUNT_HW_CACHE_MISSES')
for i in range(100):
ti = []
p.reset()
p.start()
ti.extend(p.getval() for _ in range(7))
Popen(['echo'], stdout=DEVNULL)
ti.extend(p.getval() for _ in range(7))
p.stop()
arr.append(ti)
np.diff(np.array(arr), axis=1).mean(axis=0).astype(int).tolist()
дает мне:
2605, 2185, 2127, 2099, 2407, 2120,
5481210,
16499, 10694, 10398, 10301, 10206, 10166
(строки разбиты в нестандартных местах для обозначения потока кода)