os.sched_getaffinity(0)とos.cpu_count()

Oct 04 2020

したがって、タイトルの2つの方法の違いはわかっていますが、実際的な意味はわかりません。

私が理解していることから:実際に利用可能なコアよりも多くのNUM_WORKERSを使用すると、OSが常に前後に切り替わり、並列処理を維持しようとするため、パフォーマンスが大幅に低下します。これがどれほど真実かはわかりませんが、私より賢い人からどこかでSOで読んでいます。

そしてos.cpu_count()それのためのドキュメントで言う:

システム内のCPUの数を返します。不明な場合はNoneを返します。この数は、現在のプロセスが使用できるCPUの数と同じではありません。使用可能なCPUの数は、len(os.sched_getaffinity(0))で取得できます。

そこで、「システム」よりも多くのCPUをプロセスで使用できる場合、「システム」が何を指しているのかを解明しようとしています。

安全かつ効率的に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

純粋に100%CPUにバインドされたタスクがある場合、つまり計算のみを行った場合、プロセスプールのサイズをコンピューターで使用可能なCPUの数より大きくしても、明らかに何も得られません。しかし、I / Oが混在している場合、プロセスはI / Oが完了するのを待っているCPUを放棄します(または、たとえば、比較的長い時間がかかるWebサイトからURLが返される)。 ?私には、このシナリオでプロセスプールサイズがを超えるとスループットの向上を達成できなかったことは明らかではありませんos.cpu_count()。

更新

これがポイントを示すためのコードです。このコードは、おそらくスレッドを使用することで最も効果的に機能しますが、プロセスを使用しています。デスクトップに8つのコアがあります。プログラムは、54個のURLを同時に(この場合は並行して)取得するだけです。プログラムには、使用するプールのサイズである引数が渡されます。残念ながら、追加のプロセスを作成するためだけに初期オーバーヘッドが発生するため、作成するプロセスが多すぎると節約額が減少し始めます。ただし、タスクが長時間実行され、I / Oが多い場合、プロセスを作成するオーバーヘッドは、最終的にはそれだけの価値があります。

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

これらの2つの関数は非常に異なり、セットから整数を減算しようとNUM_WORKERS = os.sched_getaffinity(0) - 1するとTypeError、すぐに失敗します。一方でos.cpu_count()、システムが持っているどのように多くのコアを説明します、os.sched_getaffinity(pid)特定のスレッド/プロセスがされているコアを示しています許可実行します。


os.cpu_count()

os.cpu_count()は、OSに認識されている使用可能なコア(仮想コア)の数を示しています。ほとんどの場合、この数の半分の物理コアがあります。物理コアよりも多くのプロセスを使用することが理にかなっている場合、または仮想コアよりも多くのプロセスを使用することが理にかなっている場合は、何をしているかに大きく依存します。計算ループがタイトになるほど(命令の多様性が少なく、キャッシュミスが少ないなど)、(より多くのワーカープロセスを使用することで)より多くのコアを使用してもメリットが得られない可能性が高くなり、パフォーマンスが低下する可能性もあります。

明らかに、システムは他に何を実行しているかにも依存します。これは、システムがシステム内のすべてのスレッド(プロセスの実際の実行ユニットとして)に、使用可能なコアで実行時間のかなりの部分を与えようとするためです。だから、あなたはどのように多くの労働者の観点から可能何ら一般化が存在しないはずです使用は。しかし、たとえば、タイトなループがあり、システムがアイドリングしている場合、最適化の良い出発点は次のとおりです。

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を持つプロセス(またはゼロの場合は現在のプロセス)が制限されているCPUのセットを返します。

これで、core / cpu / processor / -affinityは、スレッド(ワーカープロセス内)の実行を許可される具体的な(仮想)コアについてです。OSはすべてのコアに0から(コア数-1)までのIDを与え、アフィニティを変更すると、特定のスレッドが実行できる実際のコアを制限(「ピン留め」)できます。

少なくともLinuxでは、これは、許可されたコアが現在使用できない場合、他の許可されていないコアがアイドル状態であっても、子プロセスのスレッドが実行されないことを意味することがわかりました。したがって、「親和性」はここでは少し誤解を招く可能性があります。

アフィニティをいじるときの目標は、コンテキストスイッチとコア移行によるキャッシュの無効化を最小限に抑えることです。ここにあるOSは通常、より優れた洞察を持っており、スケジューリングポリシーを使用してキャッシュを「ホット」に保とうとしているため、何をしているのかを理解していない限り、干渉による簡単な利益は期待できません。

デフォルトでは、アフィニティはすべてのコアに設定されmultiprocessing.Poolており、少なくともシステムがアイドル状態の場合は、それを変更することに煩わされることはあまり意味がありません。

ここのドキュメントでは「プロセス」について説明していますが、アフィニティの設定は実際にはスレッドごとのものであることに注意してください。したがって、たとえば、「現在のプロセスがゼロの場合」の「子」スレッドにアフィニティを設定しても、プロセス内のメインスレッドまたは他のスレッドのアフィニティは変更されません。ただし、子スレッドはメインスレッドからアフィニティを継承し、子プロセスは(メインスレッドを介して)親プロセスのメインスレッドからアフィニティを継承します。これは、考えられるすべてのstartメソッド( "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

それがあなたの質問に答えるかどうかはわかりませんが、少なくともそれはデータポイントです。