os.sched_getaffinity (0) เทียบกับ os.cpu_count ()

Oct 04 2020

ดังนั้นฉันรู้ความแตกต่างระหว่างสองวิธีในชื่อเรื่อง แต่ไม่ใช่ผลในทางปฏิบัติ

จากสิ่งที่ฉันเข้าใจ: หากคุณใช้ NUM_WORKERS มากกว่าจำนวนคอร์ที่มีอยู่จริงคุณจะต้องเผชิญกับประสิทธิภาพที่ลดลงอย่างมากเนื่องจากระบบปฏิบัติการของคุณสลับไปมาอยู่ตลอดเวลาโดยพยายามทำให้สิ่งต่างๆขนานกัน ไม่รู้ว่าจริงแค่ไหน แต่ฉันอ่านที่นี่ใน SO จากคนที่ฉลาดกว่าฉัน

และในเอกสารos.cpu_count()ระบุว่า:

ส่งคืนจำนวนซีพียูในระบบ ส่งคืนไม่มีหากไม่กำหนด ตัวเลขนี้ไม่เทียบเท่ากับจำนวน CPU ที่กระบวนการปัจจุบันสามารถใช้ได้ สามารถรับจำนวน CPU ที่ใช้งานได้ด้วย len (os.sched_getaffinity (0))

ดังนั้นฉันกำลังพยายามหาสิ่งที่ "ระบบ" อ้างถึงหากกระบวนการสามารถใช้ซีพียูได้มากกว่าที่มีอยู่ใน "ระบบ"

ฉันแค่ต้องการใช้multiprocessing.poolฟังก์ชันการทำงานอย่างปลอดภัยและมีประสิทธิภาพ คำถามของฉันสรุปได้ดังนี้:

ผลกระทบในทางปฏิบัติของ:

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

-1เป็นเพราะฉันพบว่าระบบของฉันเป็นจำนวนมากน้อย laggy ถ้าฉันพยายามที่จะทำงานในขณะที่ข้อมูลจะถูกประมวลผล

คำตอบ

2 Booboo Oct 04 2020 at 20:18

หากคุณมีงานที่ผูกกับ CPU แท้ 100% กล่าวคือไม่ได้ทำอะไรเลยนอกจากการคำนวณก็ไม่มีอะไรที่จะได้รับจากการมีขนาดพูลกระบวนการมากกว่าจำนวน CPU ที่มีอยู่ในคอมพิวเตอร์ของคุณ แต่จะเกิดอะไรขึ้นถ้ามีการผสมผสานของ I / O เข้ามาโดยกระบวนการจะยกเลิกการใช้งาน CPU ที่รอให้ I / O เสร็จสมบูรณ์ (หรือตัวอย่างเช่น 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

ฟังก์ชันทั้งสองนี้แตกต่างกันมากและ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)

ส่งคืนชุดของ CPU ที่กระบวนการด้วย 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

การใช้งานหลายกระบวนการใช้พูล

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

ไม่แน่ใจว่าจะตอบคำถามของคุณหรือไม่ แต่อย่างน้อยก็เป็นจุดข้อมูล