Я пишу сценарий для задания, и из-за характера вычислений на моем ноутбуке он выполняется довольно медленно. Поэтому я попытался разделить основной цикл for на столько частей, сколько у меня есть ядер ЦП, но при работе каждое ядро используется только на 25%. По сути, мой вопрос таков: как я могу заставить Python использовать весь процессор?
Код выглядит следующим образом:
import numpy as np
import matplotlib.pyplot as plt
import os
import threading as thr
plt.close('all')
os.system("sudo renice -n -19 -p " + str(os.getpid()))
sinogram = np.random.rand((120, 240)) # for illustration purposes
N = np.shape(sinogram)[1] # width
M = np.shape(sinogram)[0]
threads = []
nthreads = 8
results = [None] * nthreads
for i in range(nthreads):
srange = range(int((i)*M/nthreads), int((i+1)*M/nthreads))
t = thr.Thread(target=backproject_threaded, args=(sinogram,srange,results,i))
print("started thread " + str(i) + " with srange " + str(srange))
t.start()
threads.append(t)
backarray = np.zeros([N,N])
for i in range(nthreads):
threads[i].join()
backarray += results[i]
plt.imshow((backarray), cmap='gray')
с backproject_threaded:
def backproject_threaded(sinogram, srange, results, index):
N = np.shape(sinogram)[1] # width
M = np.shape(sinogram)[0] # height (number of projections)
backarray = np.zeros([N,N])
for s in srange:
angle = np.pi*s/M # angle of axis
for y in range(N):
for x in range(N):
# project coordinate onto rotating axis
z = np.cos(angle) * (x-N/2) + np.sin(angle) * (y-N/2) # axis-coordinate
# select the closest coordinate in sinogram(s, :)
sn = max(0, min(int((z+N/2)), N-1))
# add this value to backarray value
backarray[N-y-1, x] += sinogram[s,sn]
results[index] = backarray
print("thread " + str(index) + " stopped")
return backarray
Вычисление дает ожидаемый результат, но вот что я вижу в htop: вид на htop, показывающий, что ядра находятся на уровне 20%
Как я могу сделать это быстрее?
Многопоточность должна быть очень простой, потому что потоки не пишут в один и тот же объект, а в противном случае читают только из синограммы.
Как видите, я попробовал установить приятность, но это не дало никакого эффекта. Еще я попробовал это os.environ["OPENBLAS_MAIN_FREE"] = "1", но это тоже не помогает.
Я надеялся, что с 8 потоками (мое количество ядер) он будет работать в восемь раз быстрее, но ядра не используются полностью.
Также я пытался закомментировать все утверждения print, но это тоже ничего не дало.
Для начала ваш код можно векторизовать. Векторизация не означает, что она работает в //. Но, во-первых, это гораздо больший источник повышения производительности, чем ваши 8 потоков (для некоторого кода вы можете получить x1000 с векторизацией. Когда вы не ожидаете большего, чем x8 с 8-поточным распараллеливанием). И, во-вторых, если он не векторизован, нет никаких шансов, что поток OPENBLAS не сможет помочь: как есть, вы просите numpy выполнять операции одну за другой. В то время как, если бы вы попросили его выполнить их пакетно, он мог бы решить сделать это параллельно.
Таким образом, это вовсе не ответ на вопрос «как использовать потоки для распараллеливания в Python» (короче говоря, ответ на этот вопрос: «Вы этого не делаете»). Либо используйте многопроцессорность, либо вы используете поток на другом языке, например использование ctypes для написания многопоточного кода C, вызываемого из Python). Но поскольку ваша реальная цель — ускорить код, первый шаг — не использовать потоки. Первый шаг — удалить эти двойные циклы Python for и векторизовать ваш код. Вы получите больше ожидаемого в 8 раз. И, возможно, numpy даже будет использовать для вас потоки
Обратите внимание, что Numpy обычно намного медленнее работает в скалярном режиме, чем Python. На самом деле Numpy предназначен не для этого, а для относительно больших массивов. Накладные расходы на вызовы функций Numpy огромны (например, 0,5–5 мкс) по сравнению со временем вычислений, необходимым для выполнения операции с небольшим массивом (даже дорогостоящие операции cos в настоящее время обычно требуют не более десятков ns на элемент, а в настоящее время значительно меньше). попрактикуйтесь на последних процессорах с хорошей серверной математической библиотекой x86-64, такой как SVML). По этой причине для эффективного использования вычислительной мощности ЦП массивы обычно должны состоять как минимум из сотен элементов.
Как мне его векторизовать, если мне также нужна информация об индексе?






Как правило, многопоточность не очень подходит для интенсивной работы процессора. Это лучше всего подходит для операций, связанных с вводом-выводом. Для высокой загрузки ЦП обычно используется многопроцессорность.
Я удалил ненужные части вашего кода, чтобы продемонстрировать шаблон, который можно использовать.
При этом не учитывается, подходит ли (или нет) numpy в этом случае использования:
import numpy as np
from concurrent.futures import ProcessPoolExecutor, as_completed
import os
import time
def backproject_process(sinogram: np.ndarray, srange: range) -> np.ndarray:
M, N = np.shape(sinogram)
backarray = np.zeros([N, N])
for s in srange:
angle = np.pi * s / M # angle of axis
for y in range(N):
for x in range(N):
# project coordinate onto rotating axis
z = np.cos(angle) * (x - N / 2) + np.sin(angle) * (
y - N / 2
) # axis-coordinate
# select the closest coordinate in sinogram(s, :)
sn = max(0, min(int((z + N / 2)), N - 1))
# add this value to backarray value
backarray[N - y - 1, x] += sinogram[s, sn]
print(f"Process {os.getpid()} completed")
return backarray
if __name__ == "__main__":
sinogram = np.random.rand(120, 240) # for illustration purposes
M, N = np.shape(sinogram)
nprocs = 8
start = time.time()
with ProcessPoolExecutor() as exe:
futures = []
for i in range(nprocs):
srange = range(int(i * M / nprocs), int((i + 1) * M / nprocs))
future = exe.submit(backproject_process, sinogram, srange)
futures.append(future)
backarray = np.zeros([N, N])
for f in as_completed(futures):
backarray += f.result()
end = time.time()
print(f"Duration = {end-start:.2f}s")
Выход:
Process 5606 completed
Process 5608 completed
Process 5604 completed
Process 5609 completed
Process 5605 completed
Process 5610 completed
Process 5603 completed
Process 5607 completed
Duration = 2.42s
Платформа:
macOS Sonoma 14.5
Apple Silicon M2 (effectively 8 CPUs)
Python 3.12.3
Вы можете уточнить, что ваше общее правило специфично для CPython.
Масштабирование работы, связанной с процессором, через потоки не будет работать в Python из-за GIL. Вы можете попробовать использовать альтернативные подходы, например отдельные процессы (вместо потоков)