os.sched_getaffinity (0) so với os.cpu_count ()

Oct 04 2020

Vì vậy, tôi biết sự khác biệt giữa hai phương pháp trong tiêu đề, nhưng không có ý nghĩa thực tế.

Theo những gì tôi hiểu: Nếu bạn sử dụng nhiều hơn NUM_WORKERS số lõi thực sự có sẵn, bạn sẽ phải đối mặt với sự sụt giảm hiệu suất lớn vì hệ điều hành của bạn liên tục chuyển đổi qua lại để cố gắng giữ mọi thứ song song. Không biết điều này đúng như thế nào, nhưng tôi đã đọc nó ở đây trên SO ở đâu đó từ một người thông minh hơn tôi.

Và trong tài liệu cho os.cpu_count()nó nói:

Trả về số lượng CPU trong hệ thống. Trả về Không nếu không xác định. Con số này không tương đương với số CPU mà tiến trình hiện tại có thể sử dụng. Số lượng CPU có thể sử dụng có thể được lấy bằng len (os.sched_getaffinity (0))

Vì vậy, tôi đang cố gắng tìm hiểu xem "hệ thống" đề cập đến điều gì nếu một tiến trình có thể sử dụng được nhiều CPU hơn số CPU trong "hệ thống".

Tôi chỉ muốn triển khai multiprocessing.poolchức năng một cách an toàn và hiệu quả . Vì vậy, đây là câu hỏi của tôi được tóm tắt:

Ý nghĩa thực tế của:

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

Lý -1do là bởi vì tôi nhận thấy rằng hệ thống của tôi ít bị lag hơn rất nhiều nếu tôi cố gắng làm việc trong khi dữ liệu đang được xử lý.

Trả lời

2 Booboo Oct 04 2020 at 20:18

Nếu bạn có các tác vụ được ràng buộc 100% CPU, tức là không làm gì ngoài tính toán, thì rõ ràng sẽ không / có thể đạt được gì bằng cách có kích thước nhóm quy trình lớn hơn số lượng CPU có sẵn trên máy tính của bạn. Nhưng điều gì sẽ xảy ra nếu có sự kết hợp của I / O được đưa vào, theo đó một quy trình sẽ khiến CPU phải chờ đợi I / O hoàn thành (hoặc, ví dụ, một URL được trả về từ một trang web, mất một thời gian tương đối dài ) ? Đối với tôi, không rõ là bạn không thể đạt được thông lượng được cải thiện trong trường hợp này với kích thước nhóm quy trình vượt quá os.cpu_count().

Cập nhật

Đây là mã để chứng minh quan điểm. Mã này, có lẽ sẽ được phân phát tốt nhất bằng cách sử dụng phân luồng, đang sử dụng các quy trình. Tôi có 8 lõi trên máy tính để bàn của mình. Chương trình chỉ cần truy xuất đồng thời 54 URL (hoặc song song trong trường hợp này). Chương trình được truyền một đối số, kích thước của nhóm sẽ sử dụng. Thật không may, có chi phí ban đầu chỉ để tạo các quy trình bổ sung, do đó, khoản tiết kiệm bắt đầu giảm nếu bạn tạo quá nhiều quy trình. Nhưng nếu tác vụ chạy lâu và có nhiều I / O, thì cuối cùng chi phí tạo các quy trình sẽ đáng giá:

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 quy trình: (số lõi tôi có):

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

16 quy trình:

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

24 quy trình:

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

Hai hàm này rất khác nhau và NUM_WORKERS = os.sched_getaffinity(0) - 1sẽ bị lỗi ngay lập tức TypeErrorvì bạn cố gắng trừ một số nguyên khỏi một tập hợp. Trong khi os.cpu_count()cho bạn biết hệ thống có bao nhiêu lõi, os.sched_getaffinity(pid)cho bạn biết lõi nào mà một luồng / quy trình nhất định được phép chạy.


os.cpu_count()

os.cpu_count()hiển thị số lõi có sẵn như đã biết đối với Hệ điều hành ( lõi ảo ). Nhiều khả năng bạn có một nửa số lõi vật lý này . Việc sử dụng nhiều quy trình hơn số lõi vật lý hoặc thậm chí nhiều hơn lõi ảo có hợp lý hay không, phụ thuộc rất nhiều vào những gì bạn đang làm. Vòng lặp tính toán càng chặt chẽ (ít đa dạng về hướng dẫn, ít bộ nhớ cache, ...), bạn càng có nhiều khả năng không được hưởng lợi từ nhiều lõi được sử dụng hơn (bằng cách sử dụng nhiều quy trình công nhân hơn) hoặc thậm chí bị giảm hiệu suất.

Rõ ràng là nó cũng phụ thuộc vào những gì khác mà hệ thống của bạn đang chạy, bởi vì hệ thống của bạn cố gắng cung cấp cho mọi luồng (như đơn vị thực thi thực tế của một quy trình) trong hệ thống một phần thời gian chạy hợp lý trên các lõi có sẵn. Vì vậy, không có khả năng tổng quát hóa về số lượng công nhân bạn nên sử dụng. Nhưng ví dụ: nếu bạn có một vòng lặp chặt chẽ và hệ thống của bạn đang chạy không tải, thì điểm khởi đầu tốt để tối ưu hóa là

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

... và tăng dần từ đó.

Cách @Frank Yellin đã được đề cập, multiprocessing.Poolsử dụng os.cpu_count()cho số lượng công nhân làm mặc định.

os.sched_getaffinity(pid)

os.sched_getaffinity(pid)

Trả lại tập hợp các CPU mà quá trình có PID pid (hoặc quá trình hiện tại nếu không) bị hạn chế.

Bây giờ lõi / cpu / bộ xử lý / -affinity là về việc cụ thể (ảo) lõi luồng của bạn (trong quy trình công nhân của bạn) được phép chạy. Hệ điều hành của bạn cung cấp cho mọi lõi một id, từ 0 đến (số lõi - 1) và việc thay đổi mối quan hệ cho phép hạn chế ("ghim") mà (các) lõi thực tế mà một luồng nhất định được phép chạy.

Ít nhất trên Linux, tôi thấy điều này có nghĩa là nếu không có lõi nào được phép hiện có sẵn, thì luồng của quy trình con sẽ không chạy, ngay cả khi các lõi khác, không được phép sẽ không hoạt động. Vì vậy, "ái lực" là một chút sai lầm ở đây.

Mục tiêu khi tìm hiểu về mối quan hệ là giảm thiểu sự mất hiệu lực của bộ nhớ cache từ các chuyển mạch ngữ cảnh và di chuyển lõi. Hệ điều hành của bạn ở đây thường có cái nhìn sâu sắc hơn và đã cố gắng giữ cho bộ nhớ đệm luôn "nóng" bằng chính sách lập lịch của nó, vì vậy trừ khi bạn biết mình đang làm gì, bạn không thể mong đợi lợi nhuận dễ dàng từ việc can thiệp.

Theo mặc định, mối quan hệ được đặt thành tất cả các lõi và vì vậy multiprocessing.Pool, bạn không cần bận tâm nhiều đến việc thay đổi điều đó, ít nhất là nếu hệ thống của bạn không hoạt động.

Lưu ý rằng mặc dù thực tế các tài liệu ở đây nói về "quy trình", việc thiết lập mối quan hệ thực sự là một việc theo từng chuỗi. Vì vậy, ví dụ: đặt mối quan hệ trong luồng "con" cho "quy trình hiện tại nếu không", không thay đổi mối quan hệ của luồng chính hoặc các luồng khác trong quy trình. Tuy nhiên , các luồng con kế thừa mối quan hệ của chúng từ luồng chính và các quá trình con (thông qua luồng chính của chúng) kế thừa mối quan hệ từ luồng chính quy trình của cha. Điều này ảnh hưởng đến tất cả các phương thức bắt đầu có thể có ("spawn", "fork", "forkserver"). Ví dụ dưới đây minh họa điều này và cách sửa đổi mối quan hệ với việc sử dụng 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))

Đầu ra:

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

Việc triển khai multiprocessing.pool sử dụng

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

Không chắc liệu điều đó có trả lời câu hỏi của bạn hay không, nhưng ít nhất đó là một điểm dữ liệu.