os.sched_getaffinity (0) против os.cpu_count ()

Oct 04 2020

Итак, я знаю разницу между двумя методами в названии, но не знаю их практического значения.

Насколько я понимаю: если вы используете больше NUM_WORKERS, чем реально доступно ядер, вы столкнетесь с большим падением производительности, потому что ваша ОС постоянно переключается туда и обратно, пытаясь поддерживать параллельность. Не знаю, насколько это правда, но я прочитал это здесь, на SO, где-то от кого-то умнее меня.

И в документации для os.cpu_count()этого говорится:

Возвращает количество процессоров в системе. Если не определено, возвращает None. Это число не эквивалентно количеству процессоров, которые может использовать текущий процесс. Количество используемых процессоров можно получить с помощью len (os.sched_getaffinity (0))

Итак, я пытаюсь понять, что означает «система», если в процессе может использоваться больше процессоров, чем в «системе».

Я просто хочу безопасно и эффективно реализовать multiprocessing.poolфункциональность. Итак, вот мой вопрос резюмирован:

Каковы практические последствия:

NUM_WORKERS = os.cpu_count() - 1
# vs.
NUM_WORKERS = len(os.sched_getaffinity(0)) - 1

Причина в -1том, что я обнаружил, что моя система намного меньше тормозит, если я пытаюсь работать во время обработки данных.

Ответы

2 Booboo Oct 04 2020 at 20:18

Если бы у вас были задачи, которые были полностью привязаны к ЦП, то есть не выполняли ничего, кроме вычислений, тогда, очевидно, ничего нельзя было бы получить, если бы размер пула процессов был больше, чем количество ЦП, доступных на вашем компьютере. Но что, если бы была добавлена ​​комбинация операций ввода-вывода, в результате чего процесс отказался бы от ЦП, ожидая завершения ввода-вывода (или, например, URL-адрес, который будет возвращен с веб-сайта, что занимает относительно много времени) ? Мне непонятно, нельзя ли добиться в этом сценарии улучшенной пропускной способности с размером пула процессов, который превышает os.cpu_count().

Обновить

Вот код, демонстрирующий суть дела. Этот код, который, вероятно, лучше всего обслуживать с помощью потоковой передачи, использует процессы. У меня на рабочем столе 8 ядер. Программа просто извлекает 54 URL одновременно (или в данном случае параллельно). Программе передается аргумент - размер используемого пула. К сожалению, начальные накладные расходы связаны только с созданием дополнительных процессов, поэтому экономия начинает падать, если вы создаете слишком много процессов. Но если бы задача выполнялась долго и имела много операций ввода-вывода, то накладные расходы на создание процессов в конце концов окупились бы:

from concurrent.futures import ProcessPoolExecutor, as_completed
import requests
from timing import time_it

def get_url(url):
    resp = requests.get(url, headers={'user-agent': 'my-app/0.0.1'})
    return resp.text


@time_it
def main(poolsize):
    urls = [
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
    ]
    with ProcessPoolExecutor(poolsize) as executor:
        futures = {executor.submit(get_url, url): url for url in urls}
        for future in as_completed(futures):
            text = future.result()
            url = futures[future]
            print(url, text[0:80])
            print('-' * 100)

if __name__ == '__main__':
    import sys
    main(int(sys.argv[1]))

8 процессов: (количество ядер у меня):

func: main args: [(8,), {}] took: 2.316840410232544 sec.

16 процессов:

func: main args: [(16,), {}] took: 1.7964842319488525 sec.

24 процесса:

func: main args: [(24,), {}] took: 2.2560818195343018 sec.
2 Darkonaut Oct 04 2020 at 19:36

Эти две функции очень разные, и NUM_WORKERS = os.sched_getaffinity(0) - 1они сразу же потерпят неудачу, TypeErrorпотому что вы попытаетесь вычесть целое число из набора. Пока os.cpu_count()сообщает вам, сколько ядер имеет система, os.sched_getaffinity(pid)сообщает вам, на каких ядрах разрешено запускать определенный поток / процесс .


os.cpu_count()

os.cpu_count()показывает количество доступных ядер, известных ОС ( виртуальных ядер). Скорее всего, у вас вдвое меньше физических ядер. Если имеет смысл использовать больше процессов, чем у вас есть физических ядер, или даже больше, чем виртуальных ядер, во многом зависит от того, что вы делаете. Чем жестче вычислительный цикл (небольшое разнообразие инструкций, несколько промахов в кэше и т. Д.), Тем больше вероятность того, что вы не выиграете от большего количества используемых ядер (при использовании большего количества рабочих процессов) или даже испытаете снижение производительности.

Очевидно, это также зависит от того, что еще работает в вашей системе, потому что ваша система пытается предоставить каждому потоку (как фактическому исполнительному блоку процесса) в системе справедливую долю времени выполнения на доступных ядрах. Таким образом, невозможно обобщить, сколько воркеров вам следует использовать. Но если, например, у вас жесткий цикл и ваша система простаивает, хорошей отправной точкой для оптимизации является

os.cpu_count() // 2 # same as mp.cpu_count() // 2 

... и увеличиваясь оттуда.

Как уже упоминал @Frank Yellin, по умолчанию multiprocessing.Poolиспользуется os.cpu_count()количество рабочих.

os.sched_getaffinity(pid)

os.sched_getaffinity(pid)

Возвращает набор процессоров, для которых ограничен процесс с PID pid (или текущий процесс, если он равен нулю).

Теперь core / cpu / processor / -affinity касается того, на каких конкретных (виртуальных) ядрах разрешено запускать ваш поток (в вашем рабочем процессе) . Ваша ОС присваивает каждому ядру идентификатор от 0 до (количество ядер - 1), а изменение привязки позволяет ограничить («закрепить»), на каком фактическом ядре (ах) определенный поток вообще может запускаться.

По крайней мере, в Linux я обнаружил, что это означает, что если ни одно из разрешенных ядер в настоящее время недоступно, поток дочернего процесса не будет работать, даже если другие, запрещенные ядра будут бездействовать. Так что "близость" здесь немного вводит в заблуждение.

Цель возиться со сходством - свести к минимуму недействительность кеша из-за переключения контекста и миграции ядра. Ваша ОС здесь обычно лучше понимает и уже пытается поддерживать кеши в «горячем состоянии» с помощью своей политики планирования, так что, если вы не знаете, что делаете, вы не можете ожидать легкой выгоды от вмешательства.

По умолчанию привязка установлена ​​для всех ядер, и для multiprocessing.Poolэтого не имеет особого смысла беспокоиться об изменении этого, по крайней мере, если в противном случае ваша система простаивает.

Обратите внимание: несмотря на то, что в документации здесь говорится о «процессе», установка сродства действительно зависит от потока. Так, например, установка сродства в «дочернем» потоке для «текущего процесса, если ноль» не изменяет сродство основного потока или других потоков внутри процесса. Но дочерние потоки наследуют свою привязку от основного потока, а дочерние процессы (через свой основной поток) наследуют привязку от основного потока родительского процесса. Это влияет на все возможные методы запуска («spawn», «fork», «forkserver»). Пример ниже демонстрирует это и то, как изменить привязку с помощью multiprocessing.Pool.

import multiprocessing as mp
import threading
import os


def _location():
    return f"{mp.current_process().name} {threading.current_thread().name}"


def thread_foo():
    print(f"{_location()}, affinity before change: {os.sched_getaffinity(0)}")
    os.sched_setaffinity(0, {4})
    print(f"{_location()}, affinity after change: {os.sched_getaffinity(0)}")


def foo(_, iterations=200e6):

    print(f"{_location()}, affinity before thread_foo:"
          f" {os.sched_getaffinity(0)}")

    for _ in range(int(iterations)):  # some dummy computation
        pass

    t = threading.Thread(target=thread_foo)
    t.start()
    t.join()

    print(f"{_location()}, affinity before exit is unchanged: "
          f"{os.sched_getaffinity(0)}")

    return _


if __name__ == '__main__':

    mp.set_start_method("spawn")  # alternatives on Unix: "fork", "forkserver"

    # for current process, exclude cores 0,1 from affinity-mask
    print(f"parent affinity before change: {os.sched_getaffinity(0)}")
    excluded_cores = {0, 1}
    os.sched_setaffinity(0, os.sched_getaffinity(0).difference(excluded_cores))
    print(f"parent affinity after change: {os.sched_getaffinity(0)}")

    with mp.Pool(2) as pool:
        pool.map(foo, range(5))

Вывод:

parent affinity before change: {0, 1, 2, 3, 4, 5, 6, 7}
parent affinity after change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-1, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-1, affinity after change: {4}
SpawnPoolWorker-1 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-1, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-1, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-2, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-2, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-2, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-2, affinity after change: {4}
SpawnPoolWorker-1 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-3, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-3, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
1 FrankYellin Oct 04 2020 at 05:10

Реализация multiprocessing.pool использует

        if processes is None:
            processes = os.cpu_count() or 1

Не уверен, что это ответ на ваш вопрос, но, по крайней мере, это дата.