- PVSM.RU - https://www.pvsm.ru -
Наверное всем, кто хоть раз интересовался Python, известно про GIL — его одновременно и сильное и слабое место.
Не мешая однопоточным скриптам работать, он ставит изрядные палки в колеса при многопоточной работе на CPU-bound задачах (когда потоки выполняются, а не висят попеременно в ожидании I/O и т.п.).
Подробности хорошо описаны в переводе двухгодичной давности [1]. Побороть GIL в официальной сборке Python для настоящего распараллеливания потоков мы не можем, но можно пойти другим путем — запретить системе перебрасывать потоки Python между ядрами. В общем пост из серии, «если не нужно, но очень хочется» :)
Если вы знаете про processor/cpu affinity, пользовались ctypes и pywin32, то ничего нового не будет.
Возьмем простой код (почти как в статье-переводе):
cnt = 100000000
trying = 2
def count():
n = cnt
while n>0:
n-=1
def test1():
count()
count()
def test2():
t1 = Thread(target=count)
t1.start()
t2 = Thread(target=count)
t2.start()
t1.join(); t2.join()
seq1 = timeit.timeit( 'test1()', 'from __main__ import test1', number=trying )/trying
print seq1
par1 = timeit.timeit( 'test2()', 'from __main__ import test2', number=trying )/trying
print par1
Запустим на python 2.6.5 (ubuntu 10.04 x64, i5 750):
10.41 13.25
И на python 2.7.2 (win7 x64, i5 750):
19.25 27.41
Сразу отбросим, что win-версия явно медленнее. В обоих случаях видно значительное замедление параллельного варианта.
GIL в любом случае не позволит многопоточному варианту выполняться быстрее, чем линейный. Однако, если реализация некоего функционала упрощается при введении поточности в код, то стоит хотя бы попытаться по возможности сократить это отставание.
При работе многопоточного приложения ОС может произвольно «перебрасывать» разные потоки между ядрами. И когда два (и более) потока одного python-процесса одновременно пытаются захватывать GIL, начинаются тормоза. Переброс выполняется и для однопоточной программы, но там он не сказывается на скорости.
Соответственно, чтобы потоки захватывали GIL поочередно, можно ограничить python-процесс одним ядром. А поможет нам в этом CPU Affinity Mask, позволяющая в формате битовых флагов указывать на каких ядрах/процессорах разрешено выполняться программе.
На разных ОС данная операция выполняется разными средствами, но сейчас рассмотрим Ubuntu Linux и WinXP+. Также изучалась FreeBSD 8.2 на Intel Xeon, но это останется за пределами статьи.
Прежде чем выбирать ядра, нужно определиться сколько их у нас в распоряжении. Тут стоит плясать от возможностей платформы: multiprocessing.cpu_count() в python 2.6+, os.sysconf('SC_NPROCESSORS_ONLN') по POSIX и т.д. Пример определения можно посмотреть тут [2].
Непосредственно для работы с processor affinity были выбраны:
Чтобы достучаться до libc воспользуемся модулем ctypes [7]. Для загрузки нужной библиотеки воспользуемся ctypes.CDLL:
libc = ctypes.CDLL( 'libc.so.6' )
libc.sched_setaffinity # наша функция
Все бы было хорошо, но есть два момента:
Наши функции:
int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
pid_t — это int, cpu_set_t — структура из одного поля размером в 1024 бита (т.е. возможно работать с 1024 ядрами/процессорами).
Воспользуемся cpusetsize, чтобы работать не сразу со всеми ядрами и считать, что cpu_set_t — это unsigned long. В общем случае следует воспользоваться ctypes.Arrays [9], но это выходит за рамки темы статьи.
Также стоит заметить, что mask передается как указатель, т.е. ctypes.POINTER(<тип самого значения>).
После проведения соответствия типов C и ctypes [10] получаем:
__setaffinity = _libc.sched_setaffinity
__setaffinity.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
__getaffinity = _libc.sched_getaffinity
__getaffinity.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
После указания argtypes за типами передаваемых значений следит ctypes. Чтобы модуль не ругался, а делал свою работу, корректно укажем значения при вызове:
def get_affinity(pid=0):
mask = ctypes.c_ulong(0) # инициализируем переменную
c_ulong_size = ctypes.sizeof(ctypes.c_ulong) # данные только по первым 32/64 ядрам
if __getaffinity(pid, c_ulong_size, mask) < 0:
raise OSError
return mask.value # преобразование ctypes.c_ulong => python int
def set_affinity(pid=0, mask=1):
mask = ctypes.c_ulong(mask)
c_ulong_size = ctypes.sizeof(ctypes.c_ulong)
if __setaffinity(pid, c_ulong_size, mask) < 0:
raise OSError
return
Как видно, ctypes сам неявно разобрался с указателем. Также стоит заметить, что вызов с pid=0 выполняется над текущим процессом.
В документации к нужным нам функциям указано:
Minimum supported client - Windows XP Minimum supported server - Windows Server 2003 DLL - Kernel32.dll
Теперь мы знаем, когда это будет работать и какую библиотеку нужно грузить.
Делаем по аналогии с Linux версией. Берем заголовки:
BOOL WINAPI SetProcessAffinityMask(
__in HANDLE hProcess,
__in DWORD_PTR dwProcessAffinityMask
);
BOOL WINAPI GetProcessAffinityMask(
__in HANDLE hProcess,
__out PDWORD_PTR lpProcessAffinityMask,
__out PDWORD_PTR lpSystemAffinityMask
);
В качестве HANDLE нас вполне устроит ctypes.c_uint, а вот с типами out параметров нужно быть аккуратными:
DWORD_PTR — это все тот же ctypes.c_uint, а PDWORD_PTR — это уже ctypes.POINTER(ctypes.c_uint).
Итого получаем:
__setaffinity = ctypes.windll.kernel32.SetProcessAffinityMask
__setaffinity.argtypes = [ctypes.c_uint, ctypes.c_uint]
__getaffinity = ctypes.windll.kernel32.GetProcessAffinityMask
__getaffinity.argtypes = [ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint)]
И кажется, что вот сделаем там и все заработает:
def get_affinity(pid=0):
mask_proc = ctypes.c_uint(0)
mask_sys = ctypes.c_uint(0)
if not __getaffinity(pid, mask_proc, mask_sys):
raise ValueError
return mask_proc.value
def set_affinity(pid=0, mask=1):
mask_proc = ctypes.c_uint(mask)
res = __setaffinity(pid, mask_proc)
if not res:
raise OSError
return
Но увы. Функции принимают не pid, а HANDLE процесса. Его еще нужно получить. Для этого воспользуемся функцией OpenProcess [11] ну и «парной» к ней CloseHandle [12]:
PROCESS_SET_INFORMATION = 512
PROCESS_QUERY_INFORMATION = 1024
__close_handle = ctypes.windll.kernel32.CloseHandle
def __open_process(pid, ro=True):
if not pid:
pid = os.getpid()
access = PROCESS_QUERY_INFORMATION
if not ro:
access |= PROCESS_SET_INFORMATION
hProc = ctypes.windll.kernel32.OpenProcess(access, 0, pid)
if not hProc:
raise OSError
return hProc
Если не вдаваться в подробности, то мы просто получаем HANDLE нужного нам процесса с доступом на чтение параметров, а при ro=False и на их изменение. Об этом написано в документации по SetProcessAffinityMask и GetProcessAffinityMask:
SetProcessAffinityMask: hProcess [in] A handle to the process whose affinity mask is to be set. This handle must have the PROCESS_SET_INFORMATION access right. GetProcessAffinityMask: hProcess [in] A handle to the process whose affinity mask is desired. Windows Server 2003 and Windows XP: The handle must have the PROCESS_QUERY_INFORMATION access right.
Так что никакого метода Монте-Карло :)
Переписываем наши get_affinity и set_affinity c учетом изменений:
def get_affinity(pid=0):
hProc = __open_process(pid)
mask_proc = ctypes.c_uint(0)
mask_sys = ctypes.c_uint(0)
if not __getaffinity(hProc, mask_proc, mask_sys):
raise ValueError
__close_handle(hProc)
return mask_proc.value
def set_affinity(pid=0, mask=1):
hProc = __open_process(pid, ro=False)
mask_proc = ctypes.c_uint(mask)
res = __setaffinity(hProc, mask_proc)
__close_handle(hProc)
if not res:
raise OSError
return
Чтобы немного сократить объем кода для Win-реализации можно поставить модуль pywin32 [13]. Он избавит нас от необходимости задавать константы и разбираться с библиотеками и параметрами вызова. Наш код выше мог бы выглядеть как-то так:
import win32process, win32con, win32api, win32security
import os
def __open_process(pid, ro=True):
if not pid:
pid = os.getpid()
access = win32con.PROCESS_QUERY_INFORMATION
if not ro:
access |= win32con.PROCESS_SET_INFORMATION
hProc = win32api.OpenProcess(access, 0, pid)
if not hProc:
raise OSError
return hProc
def get_affinity(pid=0):
hProc = __open_process(pid)
mask, mask_sys = win32process.GetProcessAffinityMask(hProc)
win32api.CloseHandle(hProc)
return mask
def set_affinity(pid=0, mask=1):
try:
hProc = __open_process(pid, ro=False)
mask_old, mask_sys_old = win32process.GetProcessAffinityMask(hProc)
res = win32process.SetProcessAffinityMask(hProc, mask)
win32api.CloseHandle(hProc)
if res:
raise OSError
except win32process.error as e:
raise ValueError, e
return mask_old
Кратко, понятно, но это сторонний модуль.
Если собрать это все воедино и добавить к нашим первоначальным тестам еще один:
def test3():
cpuinfo.affinity.set_affinity(0,1) # меняем в своем процессе (pid=0) affinity на первое ядро.
test2()
par2 = timeit.timeit( 'test3()', 'from __main__ import test3', number=trying )/trying
print par2
то результаты будут следующими:
Linux: test1 : 10.41 | 102.89 test2 : 13.25 | 135.29 test3 : 10.45 | 104.51 Windows: test1 : 19.25 | 191.97 test2 : 27.41 | 269.78 test3 : 19.52 | 196.17
Цифры во второй колонке — теже тесты, но с cnt в 10 раз большим.
Мы получили два потока выполнения практически без потери в скорости работы по сравнению с однопоточным вариантом.
Affinity задается битовой маской на обоих ОС. На 4х ядерной машине get_affinity выдает значение 15 (1+2+4+8).
Пример и весь код для статьи выложил на github [14].
Принимаю любые предложения и претензии.
Также интересуют результаты на процессоре с поддержкой HT и на других версиях Linux.
Всех с первым апреля! Этот код действительно работает :)
Автор: AterCattus
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/4627
Ссылки в тексте:
[1] переводе двухгодичной давности: http://habrahabr.ru/post/84629/
[2] тут: https://github.com/AterCattus/pycpuinfo/blob/master/cpuinfo/info.py
[3] sched_setaffinity: http://linux.die.net/man/2/sched_setaffinity
[4] sched_getaffinity: http://linux.die.net/man/2/sched_getaffinity
[5] SetProcessAffinityMask: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686223(v=vs.85).aspx
[6] GetProcessAffinityMask: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683213(v=vs.85).aspx
[7] ctypes: http://docs.python.org/library/ctypes.html
[8] argtypes: http://docs.python.org/library/ctypes.html#specifying-the-required-argument-types-function-prototypes
[9] ctypes.Arrays: http://docs.python.org/library/ctypes.html#arrays
[10] соответствия типов C и ctypes: http://docs.python.org/library/ctypes.html#ctypes-fundamental-data-types-2
[11] OpenProcess: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspx
[12] CloseHandle: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
[13] pywin32: http://pypi.python.org/pypi/pywin32
[14] github: https://github.com/AterCattus/pycpuinfo
Нажмите здесь для печати.