html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

Python threading или GIL нам почти не помеха

Наверное всем, кто хоть раз интересовался Python, известно про GIL — его одновременно и сильное и слабое место.
Не мешая однопоточным скриптам работать, он ставит изрядные палки в колеса при многопоточной работе на CPU-bound задачах (когда потоки выполняются, а не висят попеременно в ожидании I/O и т.п.).
Подробности хорошо описаны в переводе двухгодичной давности. Побороть 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 и т.д. Пример определения можно посмотреть тут.

Непосредственно для работы с processor affinity были выбраны:


Linux Ubuntu


Чтобы достучаться до libc воспользуемся модулем ctypes. Для загрузки нужной библиотеки воспользуемся ctypes.CDLL:
libc = ctypes.CDLL( 'libc.so.6' )
libc.sched_setaffinity # наша функция

Все бы было хорошо, но есть два момента:
  • Жесткое задание имени libc.so.6 не переносимо, а файл libc.so, которому следовало бы являться симлинкой на реальную версию, на Debian/Ubuntu сделан текстовым файлом.
    На данный момент сделан костыль в виде поиска всех файлов, имена которых начинаются с «libc.so» и попытка подгрузить их с обработкой OSError. Загрузили — это наша библиотечка.
    Если кто-то знает лучшее и универсальное решение — буду раз увидеть в комментариях или в личке.
  • Указания имени функции недостаточно. Нужны же еще число параметров и их типы. Для этого воспользуемся заданием «магического» атрибута argtypes для нужных нам функций.

Наши функции:
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, но это выходит за рамки темы статьи.
Также стоит заметить, что mask передается как указатель, т.е. ctypes.POINTER(<тип самого значения>).
После проведения соответствия типов C и ctypes получаем:
__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 выполняется над текущим процессом.

Windows XP+


В документации к нужным нам функциям указано:
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 ну и «парной» к ней CloseHandle:
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


WindowsXP+ для ленивых


Чтобы немного сократить объем кода для Win-реализации можно поставить модуль pywin32. Он избавит нас от необходимости задавать константы и разбираться с библиотеками и параметрами вызова. Наш код выше мог бы выглядеть как-то так:
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.
Принимаю любые предложения и претензии.
Также интересуют результаты на процессоре с поддержкой HT и на других версиях Linux.

Всех с первым апреля! Этот код действительно работает :)
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

1

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • wrise007
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции