Я работаю над 8-ядерным процессором с 8G RAM и Linux Redhat 7, и я использую IDE Pycharm.
Я попытался использовать модуль потоковой передачи python, чтобы воспользоваться преимуществами многоядерной обработки, но у меня получился гораздо более медленный код. Я выпустил GIL через Numba и убедился, что мои потоки выполняют достаточно сложные вычисления, поэтому проблема не в том, что обсуждается в примере. Как сделать numba @jit использовать все ядра процессора (распараллелить numba @jit)
Вот многопоточный код:
l=200
@nb.jit('void(f8[:],f8,i4,f8[:])',nopython=True,nogil=True)
def force(r,ri,i,F):
sum=0
for j in range(12):
if (j != i):
fij=-4 * (12*1**12/(r[j]-ri)**13-6*1**6/(r[j]-ri)**7)
sum=sum+fij
F[i+12]=sum
def ODEfunction(r, t):
f = np.zeros(2 * 12)
lbound=-4* (12*1**12/(-0.5*l-r[0])**13-6*1**6/(-0.5*l-r[0])**7)
rbound=-4* (12*1**12/(0.5*l-r[12-1])**13-6*1**6/(0.5*l-r[12-1])**7)
f[0:12]=r[12:2*12]
thlist=[threading.Thread(target=force, args=(r,r[i],i,f)) for i in range(12)]
for thread in thlist:
thread.start()
for thread in thlist:
thread.join()
f[12]=f[12]+lbound
f[2*12-1]=f[2*12-1]+rbound
return f
И это последовательная версия:
l=200
@nb.autojit()
def ODEfunction(r, t):
f = np.zeros(2 * 12)
lbound=-4* (12*1**12/(-0.5*l-r[0])**13-6*1**6/(-0.5*l-r[0])**7)
rbound=-4* (12*1**12/(0.5*l-r[12-1])**13-6*1**6/(0.5*l-r[12-1])**7)
f[0:12]=r[12:2*12]
for i in range(12):
fi = 0.0
for j in range(12):
if (j!=i):
fij = -4 * (12*1**12/(r[j]-r[i])**13-6*1**6/(r[j]-r[i])**7)
fi = fi + fij
f[i+12]=fi
f[12]=f[12]+lbound
f[2*12-1]=f[2*12-1]+rbound
return f
Я также думал приложить изображение системного монитора во время многопоточного и последовательного кода:
System Motinor во время запуска многопоточного кода
Система Motinor во время запуска последовательного кода
Кто-нибудь знает, что может быть причиной этой неэффективности в потоковом коде?
Вы должны знать, что вызов функции в Python довольно дорогостоящий (например, для вызова функции на C), тем более вызов функции numba-jitted: необходимо проверить, что параметры правильные (то есть, что они действительно массивы float-numpy, которые вы передаете, - мы увидим, что это может быть фактором 5 медленнее, чем обычный Python -call).
Позвольте проверить накладные расходы вашей jitted функции, по сравнению с работой, происходящей в функции:
import numba as nb
import numpy as np
@nb.jit('void(f8[:],f8,i4,f8[:])',nopython=True,nogil=True)
def force(r,ri,i,F):
sum=0
for j in range(12):
if (j != i):
fij=-4 * (12*1**12/(r[j]-ri)**13-6*1**6/(r[j]-ri)**7)
sum=sum+fij
F[i+12]=sum
@nb.jit('void(f8[:],f8,i4,f8[:])',nopython=True,nogil=True)
def nothing(r,ri,i,F):
pass
def no_jit(r,ri,i,F):
pass
F=np.zeros(24)
r=np.zeros(12)
И сейчас:
>>>%timeit force(r,1.0,0,F)
706 ns ± 8.96 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>>%timeit nothing(r,1.0,0,F)
645 ns ± 5.36 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>>%timeit no_jit(r,1.0,0,F) #to measure overhead of numba
120 ns ± 6.56 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Таким образом, накладные расходы в основном составляют 90%, которые у вас нет в однопоточном режиме, потому что функция "встроена". Это неудивительно: ваш цикл for имеет только 12 итераций - это просто недостаточно, например, в примере, когда вы связали внутренний цикл с 10^10
итерациями!
Кроме того, есть некоторые накладные расходы, связанные с распределением работы между потоками, мои кишки говорят, что это даже больше, чем накладные расходы из jitted -call, но, конечно, нужно профилировать программу. Эти шансы довольно сложно победить даже с 8 ядрами!
Сейчас самый большой блокировщик дорог, вероятно, является большим накладным капиталом force
-call по сравнению с временем, проведенным в самой функции. Вышеприведенный анализ является довольно поверхностным, поэтому я не гарантирую, что других существенных проблем нет, но для того, чтобы иметь большие куски работы для force
это был бы шаг в правильном направлении.