os.sched_getaffinity (0) contro os.cpu_count ()
Quindi, conosco la differenza tra i due metodi nel titolo, ma non le implicazioni pratiche.
Da quello che ho capito: se utilizzi più NUM_WORKERS di quanti sono i core effettivamente disponibili, devi affrontare grandi cali di prestazioni perché il tuo sistema operativo cambia costantemente avanti e indietro cercando di mantenere le cose in parallelo. Non so quanto sia vero, ma l'ho letto qui su SO da qualche parte da qualcuno più intelligente di me.
E nei documenti per os.cpu_count()esso dice:
Restituisce il numero di CPU nel sistema. Restituisce Nessuno se indeterminato. Questo numero non è equivalente al numero di CPU che il processo corrente può utilizzare. Il numero di CPU utilizzabili può essere ottenuto con len (os.sched_getaffinity (0))
Quindi, sto cercando di capire a cosa si riferisce il "sistema" se ci possono essere più CPU utilizzabili da un processo di quante ce ne siano nel "sistema".
Voglio solo implementare in modo sicuro ed efficiente le multiprocessing.poolfunzionalità. Quindi ecco la mia domanda riassunta:
Quali sono le implicazioni pratiche di:
NUM_WORKERS = os.cpu_count() - 1
# vs.
NUM_WORKERS = len(os.sched_getaffinity(0)) - 1
Il -1motivo è che ho scoperto che il mio sistema è molto meno lento se provo a lavorare mentre i dati vengono elaborati.
Risposte
Se avessi un'attività legata al 100% alla CPU, cioè non facessi altro che calcoli, allora chiaramente non si otterrebbe / non si potrebbe ottenere nulla con una dimensione del pool di processo maggiore del numero di CPU disponibili sul computer. Ma cosa succederebbe se ci fosse un mix di I / O lanciato in cui un processo rinuncerebbe alla CPU in attesa del completamento di un I / O (o, ad esempio, un URL da restituire da un sito Web, che richiede un tempo relativamente lungo ) ? Per me non è chiaro che non è possibile ottenere in questo scenario un throughput migliorato con una dimensione del pool di processi che supera os.cpu_count().
Aggiornare
Ecco il codice per dimostrare il punto. Questo codice, che probabilmente sarebbe meglio servito utilizzando il threading, utilizza i processi. Ho 8 core sul mio desktop. Il programma recupera semplicemente 54 URL contemporaneamente (o in parallelo in questo caso). Al programma viene passato un argomento, la dimensione del pool da utilizzare. Sfortunatamente, c'è un sovraccarico iniziale solo per creare processi aggiuntivi, quindi i risparmi iniziano a diminuire se si creano troppi processi. Ma se l'attività fosse di lunga durata e avesse molto I / O, alla fine ne varrebbe la pena il sovraccarico di creare i processi:
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 processi: (il numero di core che ho):
func: main args: [(8,), {}] took: 2.316840410232544 sec.
16 processi:
func: main args: [(16,), {}] took: 1.7964842319488525 sec.
24 processi:
func: main args: [(24,), {}] took: 2.2560818195343018 sec.
Queste due funzioni sono molto diverse e NUM_WORKERS = os.sched_getaffinity(0) - 1fallirebbero immediatamente TypeErrorperché si tenta di sottrarre un numero intero da un insieme. Mentre os.cpu_count()ti dice quanti core ha il sistema, os.sched_getaffinity(pid)ti dice su quali core è consentito eseguire un determinato thread / processo .
os.cpu_count()
os.cpu_count()mostra il numero di core disponibili come noto al sistema operativo ( core virtuali ). Molto probabilmente hai la metà di questo numero di core fisici . Se ha senso utilizzare più processi di quelli che hai core fisici, o anche più di core virtuali, dipende molto da ciò che stai facendo. Più stretto è il ciclo di calcolo (poca diversità nelle istruzioni, pochi errori nella cache, ...), più è probabile che non trarrai vantaggio da più core utilizzati (utilizzando più processi di lavoro) o addirittura sperimenterai un degrado delle prestazioni.
Ovviamente dipende anche da cos'altro sta eseguendo il tuo sistema, perché il tuo sistema cerca di dare a ogni thread (come l'effettiva unità di esecuzione di un processo) nel sistema una giusta quota di tempo di esecuzione sui core disponibili. Quindi non v'è alcuna generalizzazione possibile in termini di quanti lavoratori si dovrebbe usare. Ma se, ad esempio, hai un ciclo stretto e il tuo sistema è inattivo, un buon punto di partenza per l'ottimizzazione lo è
os.cpu_count() // 2 # same as mp.cpu_count() // 2
... e aumentando da lì.
Come @Frank Yellin già citato, multiprocessing.Poolutilizza os.cpu_count()per il numero di lavoratori come impostazione predefinita.
os.sched_getaffinity(pid)
os.sched_getaffinity(pid)
Restituisce il set di CPU a cui è limitato il processo con PID pid (o il processo corrente se zero).
Ora core / cpu / processor / -affinity riguarda i core concreti (virtuali) su cui è consentito eseguire il thread (all'interno del processo di lavoro) . Il tuo sistema operativo assegna a ogni core un ID, da 0 a (numero di core - 1) e la modifica dell'affinità consente di limitare ("bloccare") i core effettivi a cui è consentito eseguire un determinato thread.
Almeno su Linux ho scoperto che questo significa che se nessuno dei core consentiti è attualmente disponibile, il thread di un processo figlio non verrà eseguito, anche se altri core non consentiti sarebbero inattivi. Quindi "affinità" è un po 'fuorviante qui.
L'obiettivo quando si gioca con l'affinità è ridurre al minimo gli invalidamenti della cache dovuti a cambi di contesto e migrazioni di base. Il tuo sistema operativo qui di solito ha una visione migliore e cerca già di mantenere le cache "calde" con la sua politica di pianificazione, quindi a meno che tu non sappia cosa stai facendo, non puoi aspettarti facili guadagni dall'interferenza.
Per impostazione predefinita, l'affinità è impostata su tutti i core e per multiprocessing.Pool, non ha molto senso preoccuparsi di cambiarlo, almeno se il sistema è inattivo altrimenti.
Nota che nonostante il fatto che i documenti qui parlino di "processo", l'impostazione dell'affinità è davvero una cosa per thread. Quindi, ad esempio, l'impostazione dell'affinità in un thread "figlio" per il "processo corrente se zero", non modifica l'affinità del thread principale o di altri thread all'interno del processo. Tuttavia , i thread secondari ereditano la loro affinità dal thread principale e i processi figlio (tramite il loro thread principale) ereditano l'affinità dal thread principale del processo del padre. Ciò influisce su tutti i possibili metodi di avvio ("spawn", "fork", "forkserver"). L'esempio seguente mostra questo e come modificare l'affinità con l'utilizzo 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))
Produzione:
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}
L'implementazione di multiprocessing.pool utilizza
if processes is None:
processes = os.cpu_count() or 1
Non sono sicuro che questo risponda alla tua domanda, ma almeno è un punto dati.