os.sched_getaffinity (0) contre os.cpu_count ()

Oct 04 2020

Donc, je connais la différence entre les deux méthodes dans le titre, mais pas les implications pratiques.

D'après ce que je comprends: si vous utilisez plus de NUM_WORKERS que le nombre de cœurs réellement disponibles, vous faites face à de grandes baisses de performances, car votre système d'exploitation bascule constamment en essayant de garder les choses en parallèle. Je ne sais pas à quel point c'est vrai, mais je l'ai lu ici sur SO quelque part de quelqu'un de plus intelligent que moi.

Et dans la documentation, os.cpu_count()il est dit:

Renvoie le nombre de processeurs dans le système. Renvoie None si indéterminé. Ce nombre n'est pas équivalent au nombre de processeurs que le processus actuel peut utiliser. Le nombre de processeurs utilisables peut être obtenu avec len (os.sched_getaffinity (0))

Donc, j'essaie de déterminer à quoi le «système» fait référence s'il peut y avoir plus de processeurs utilisables par un processus qu'il n'y en a dans le «système».

Je veux juste mettre en œuvre des multiprocessing.poolfonctionnalités de manière sûre et efficace . Voici donc ma question résumée:

Quelles sont les implications pratiques de:

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

C'est -1parce que j'ai trouvé que mon système est beaucoup moins lent si j'essaie de travailler pendant le traitement des données.

Réponses

2 Booboo Oct 04 2020 at 20:18

Si vous aviez des tâches purement liées à 100% au processeur, c'est-à-dire ne faisant que des calculs, alors il est clair que rien ne serait / ne pourrait être gagné en ayant une taille de pool de processus supérieure au nombre de processeurs disponibles sur votre ordinateur. Mais que se passerait-il s'il y avait un mélange d'E / S dans lequel un processus abandonnerait le processeur en attendant la fin d'une E / S (ou, par exemple, une URL à retourner d'un site Web, ce qui prend un temps relativement long ) ? Pour moi, il n'est pas clair que vous ne puissiez pas obtenir dans ce scénario un débit amélioré avec une taille de pool de processus qui dépasse os.cpu_count().

Mettre à jour

Voici le code pour démontrer le point. Ce code, qui serait probablement mieux servi en utilisant le threading, utilise des processus. J'ai 8 cœurs sur mon bureau. Le programme récupère simplement 54 URL simultanément (ou en parallèle dans ce cas). Le programme reçoit un argument, la taille du pool à utiliser. Malheureusement, il y a une surcharge initiale juste pour créer des processus supplémentaires, de sorte que les économies commencent à diminuer si vous créez trop de processus. Mais si la tâche était longue et comportait beaucoup d'E / S, la surcharge de création des processus en vaudrait la peine à la fin:

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 processus: (le nombre de cœurs que j'ai):

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

16 processus:

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

24 processus:

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

Ces deux fonctions sont très différentes et NUM_WORKERS = os.sched_getaffinity(0) - 1échoueraient instantanément TypeErrorparce que vous essayez de soustraire un entier d'un ensemble. Tandis que os.cpu_count()vous indique le nombre de cœurs du système, os.sched_getaffinity(pid)vous indique sur quels cœurs un certain thread / processus est autorisé à s'exécuter.


os.cpu_count()

os.cpu_count()indique le nombre de cœurs disponibles connus du système d'exploitation ( cœurs virtuels ). Vous avez probablement la moitié de ce nombre de cœurs physiques . S'il est judicieux d'utiliser plus de processus que de cœurs physiques, ou même plus que de cœurs virtuels, cela dépend beaucoup de ce que vous faites. Plus la boucle de calcul est serrée (peu de diversité dans les instructions, peu d'erreurs de cache, ...), plus vous ne bénéficierez pas de cœurs plus utilisés (en utilisant plus de processus de travail) ou même de dégradation des performances.

De toute évidence, cela dépend également de ce que votre système exécute, car votre système essaie de donner à chaque thread (en tant qu'unité d'exécution réelle d'un processus) dans le système une part équitable du temps d'exécution sur les cœurs disponibles. Il n'y a donc pas de généralisation possible en termes de nombre de travailleurs que vous devriez utiliser. Mais si, par exemple, vous avez une boucle serrée et que votre système est au ralenti, un bon point de départ pour l'optimisation est

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

... et en augmentant à partir de là.

Comment @Frank Yellin déjà mentionné, multiprocessing.Poolutilise os.cpu_count()le nombre de travailleurs par défaut.

os.sched_getaffinity(pid)

os.sched_getaffinity(pid)

Renvoie l'ensemble de processeurs auquel le processus avec PID pid (ou le processus actuel si zéro) est limité.

Désormais, core / cpu / processor / -affinity concerne les cœurs concrets (virtuels) que votre thread (dans votre processus de travail) est autorisé à exécuter. Votre système d'exploitation donne à chaque noyau un identifiant, de 0 à (nombre de cœurs - 1) et le changement d'affinité permet de restreindre («épingler») sur quel (s) cœur (s) réel (s) un certain thread est autorisé à s'exécuter.

Au moins sur Linux, j'ai trouvé que cela signifiait que si aucun des cœurs autorisés n'est actuellement disponible, le thread d'un processus enfant ne fonctionnera pas, même si d'autres cœurs non autorisés seraient inactifs. Donc "l'affinité" est un peu trompeuse ici.

Le but en manipulant l'affinité est de minimiser les invalidations de cache des changements de contexte et des migrations de cœur. Votre système d'exploitation ici a généralement la meilleure compréhension et essaie déjà de garder les caches «chauds» avec sa politique de planification, donc à moins que vous ne sachiez ce que vous faites, vous ne pouvez pas vous attendre à des gains faciles en interférant.

Par défaut, l'affinité est définie sur tous les cœurs et pour multiprocessing.Pool, cela n'a pas trop de sens de se soucier de changer cela, du moins si votre système est inactif autrement.

Notez que malgré le fait que la documentation parle ici de "processus", la définition de l'affinité est vraiment une chose par thread. Ainsi, par exemple, définir l'affinité dans un thread "enfant" pour le "processus actuel si zéro" ne change pas l'affinité du thread principal ou des autres threads dans le processus. Mais , les threads enfants héritent de leur affinité du thread principal et les processus enfants (via leur thread principal) héritent de l'affinité du thread principal du processus parent. Cela affecte toutes les méthodes de démarrage possibles ("spawn", "fork", "forkserver"). L'exemple ci-dessous illustre cela et comment modifier l'affinité avec l'utilisation 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))

Production:

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

L'implémentation de multiprocessing.pool utilise

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

Je ne sais pas si cela répond à votre question, mais au moins c'est un point de données.