Mitmekeermeline VS-korduvtöötlus Pythonis

Mitmekeermestamise tõelise näo paljastamine

Selles artiklis püüan arutada mõningaid väärarusaamu Multithreadingu kohta ja selgitada, miks need on valed.
Kõik katsed viidi läbi 4 südamikuga masinal (EC2 c5.xlarge).

Püütonid naudivad mõnusat niit-basseinipidu.

Olen juba pikemat aega tegelenud parallelismiga pythonis ja lugesin pidevalt artikleid ja ülekandmise vooge, et oma teemast paremini aru saada. Tavaliselt mida rohkem otsid, seda rohkem õpid. Mitmekeermelise / mitutöötluse puhul aga mida rohkem otsisin, seda rohkem ma segasin. Siin on näide:

Ehkki vastuse esimene osa on õige, on viimane täiesti vale.
Ma ei ründa vastuse kirjutanud isikut, vastupidi: austan kõige rohkem kõiki, kes püüavad teisi inimesi aidata. Kasutasin seda näidet ainult selleks, et näidata, et mõned seletused mitmekeermelisuse kohta võivad olla eksitavad. Veelgi enam, mõnes teises seletuses kasutatakse täpsemaid termineid ja see võib muuta asjad raskemaks, kui nad tegelikult pole.

PS: Püüan hoida asjad lihtsana: nii et ei räägita GIL-ist, mälust, marineerimisest, üldkuludest. (Kuigi ma räägin pea kohal natuke).

Alustame!

Mitutöötlus ja mitmekordne keermestamine on põhimõtteliselt üks ja sama asi.

VEEL!

[Link kogu katse koodi]

Alustan lihtsa eksperimendiga ja laenan koodi sellest artiklist, mille on kirjutanud Brendan Fortuner, mis on muide suurepärane lugemine.

Oletame, et meil on see ülesanne, mida täidame mitu korda.

def cpu_heavy (x):
    print ('ma olen', x)
    arv = 0
    i jaoks vahemikus (10 ** 8):
        arv + = i

Järgmisena proovime nii mitmeprotsessilist kui ka mitmekeelset

alates samaaegsetest.failidest impordi ProcessPoolExecutor, ThreadPoolExecutor
def mitmekiuline (func, args, töötajad):
    koos ThreadPoolExecutoriga (töötajad) nagu ex:
        res = ex.map (funktsioon, args)
    tagastusloend (res)
def multiprocessing (func, args, töötajad):
    protsessiga ProcessPoolExecutor (töötajad) nagu ex:
        res = ex.map (funktsioon, args)
    tagastusloend (res)

Pange tähele, et kui rakendate:
- mitmeprotsessiline või mitmeprotsessiline või samaaegne.
- mitmekordne keermestamine keermestamise või multiprocessing.dummy või samaaegsete toimingutega…
see ei mõjuta meie katseid.

Ilma täiendava tähtajata käivitame mõne koodi:

visualiseeri_runtimes (mitme keermega (cpu_heavy, vahemik (4), 4))
visualiseeri_runtimes (multiprocessing (cpu_heavy, range (4), 4))

Kui mitmekordne keermestamine võttis 20 sekundit, siis mitme töötlemine võttis ainult 5 sekundit.

Nüüd, kui oleme veendunud, et nad pole samad, tahaksime teada saada, miks. Liigume siis järgmise eksiarvamuse juurde mitme keerme kohta.

Mitmekeermestamisel jooksevad lõimed paralleelselt.

VEEL!
Tegelikult on ThreadPoolis igal ajahetkel t ainult üks niit.

[Link kogu katse koodi]

Ma ei tea sinust, aga minu jaoks oli see šokeeriv!
Arvasin alati, et niidid täidavad koodi samaaegselt, kuid see on Pyhtonis täiesti vale.

Teeme väikese katse. Erinevalt eelmisest ei jälgi me mitte ainult iga töö algust ja peatust, vaid ka iga ajahetke, kus töö töötab:

def live_tracker (x):
    print ('ma olen', x)
    l = []
    i jaoks vahemikus (10 ** 6):
        l.append (aeg.time ())
    tagasi l

Nagu varem, korraldame ka meie katse ja koostame uusi graafikuid.

visualiseeri_ülekande_aeg (mitme keermega (reaalajas jälgija, vahemik (4), 4))
visualiseeri_ülekande_aeg (mitmeprotsessoriline (reaalajas jälgija, vahemik (4), 4))

Tegelikult niidid ei kulge paralleelselt ega üksteise järel. Nad jooksevad samaaegselt! Iga kord täidetakse üks töö natuke ja siis võetakse teine ​​tööle.

Paralleelsus ja parallelism on omavahel seotud terminid, kuid mitte samad, ja sageli mõistetakse neid sarnaste terminite valesti. Oluline erinevus samaaegsuse ja parallelismi vahel on see, et samaaegsus seisneb paljude asjade samaaegses käsitlemises (loob illusiooni samaaegsusest) või samaaegsete sündmuste käsitlemises, mis sisuliselt peidavad latentsust. Vastupidi, parallelism tähendab kiiruse suurendamiseks korraga paljude asjade tegemist. [Allikas: techdifferences.com]

Kui teil on cpu raske ülesanne ja soovite seda kiiremini mitmeotstarbelise töötlemise abil muuta, saate seda öelda!
Näiteks kui teil on 4 südamikku, nagu ma tegin oma testides, siis mitme keermega on iga südamiku mahutavus umbes 25%, mitmeprotsessimise korral saate 100% iga südamiku kohta. See tähendab, et 100-protsendilise 4 tuuma korral saate kiirenduse 4-ga. Kuidas oleks mitmekeermelise 25% -ga? kas saame kiirendamist? Vastus järgmises jaotises.

Mitmekeermelisus on alati kiirem kui seeriaviisiline.

VEEL!
Tegelikult, cpu raskete ülesannete jaoks, mitmekordne lõng ei too mitte ainult midagi head. Halvim: see muudab teie koodi veelgi aeglasemaks!

[Link kogu katse koodi]

CPU raske ülesande mitmesse lõime saatmine ei kiirenda täitmist. Vastupidi, see võib halvendada üldist jõudlust.
Kujutage seda ette nii: kui teil on 10 ülesannet ja igaüks võtab 10 sekundit, võtab seeriaviisiline täitmine kokku 100 sekundit. Mitmekeermelise kasutamise korral, kuna igal ajahetkel t teostatakse ainult üks niit, on see nagu seeriaviisiline täitmine PLUS, niidide vahel vahetamiseks kulunud aeg.

Nii et ma käivitan eksperimendi jaoks 4 rasket cpu-tööd 4-tuumalise masina 4 keermel (EC2 c5.xlarge) ja võrdlen seda seeriaviisilise täitmisega.

def cpu_heavy (x):
    arv = 0
    i jaoks vahemikus (10 ** 10):
        arv + = i


n_jobs = 4

marker = aeg.time ()
i jaoks vahemikus (n_jobs): cpu_heavy (i)
print ("Seriaal kulutatud", aeg.time () - marker)
marker = aeg.time ()
mitme keermega (cpu_heavy, vahemik (n_jobs), 4)
print ("Mitmekeermeline kulutatud", aeg.time () - marker)

Väljundid:

amiin @ c5-xlarge: ~ $ python3 eksperiment.py
Sarja kulutatud 1658,8452804088593
Mitmekeermesed kulutasid 1668,8857419490814

Nii et mitmekeermelisus on cpu rasketel ülesannetel 10 sekundit aeglasem kui Serial, isegi 4 keermega 4-tuumalisel masinal.

Tegelikult on erinevus tühine, kuna 27-minutilisel tööl on see 10 sekundit (0,6% aeglasem), kuid siiski näitab see, et mitmekordne keermestamine on sel juhul kasutu.

Kas mitmekeermeline on siis midagi head?

Mitmekeermeline on kasutu.

VEEL!
Tegelikult on CPU raskete ülesannete jaoks mitmeotstarbeline lõimimine tõepoolest kasutu. Kuid see sobib ideaalselt IO jaoks.

[Link kogu katse koodi]

IO-ülesannete jaoks, näiteks andmebaasist päringute tegemine või veebilehe laadimine, ei tee protsessor muud, kui ootab vastust. Proovime küsida 16 URL-i seeriaviisiliselt, mitte kasutades 4 lõime, seejärel kasutades 8:

URLid = [...] # 16 URL-i
def load_url (x):
    koos urllib.request.urlopeniga (URLid [x], ajalõpp = 5) ühendusega:
        naasma conn.read ()


n_jobs = len (URL-id)

marker = aeg.time ()
i jaoks vahemikus (n_jobs): load_url (i)
print ("Seriaal kulutatud", aeg.time () - marker)
marker = aeg.time ()
mitme keermega (load_url, vahemik (n_jobs), 4)
print ("Mitmekeermeline 4 kulutatud", aeg.time () - marker)
marker = aeg.time ()
mitme keermega (load_url, vahemik (n_jobs), 8)
print ("Mitmekeermeline 8 kulutatud", aeg.time () - marker)

Väljundid

amine @ c5-xlarge: ~ $ python3 serial_comparaison_io.py
Sarja kulutatud 7,8587799072265625
Mitmekeermestamine 4 kulutatud 2,5494980812072754
Mitmekeermeline 8 kulutatud 1,1110448837280273
Mitmekeermestamine 16-ga kulutatud 0,6199102401733398

Pange tähele, et oleme saavutanud hulgilõnga suurendamise märkimisväärselt kiiremini kui seerianumber! Pange tähele ka seda, et mida rohkem niite on, seda kiirem on täitmine. Muidugi pole mõtet, et niite oleks rohkem kui URL-ide arv, see on põhjus, miks ma peatusin 16 lõime juures 16 lõime juures.

Pidage meeles ka seda, et teie parimal juhul on mitme keermega täitmise aeg võrdne maksimaalse ajaga, mis kulub ühe URL-i laadimiseks: kui teil on 16 URL-i, mille ühe laadimiseks kulub 10 sekundit, ja 15 muu URL-i, igaüks võtab 0,1 sekundit, kaheksa keermest koosneva keerukogumi kasutamisel kestab teie programm vähemalt 10 sekundit, jadamisi aga 11,5 sekundit. Nii et sel juhul pole kiiret tegemist.

Okei, nüüd teame, et kuigi mitmekeermelisus mõjub CPU-le halvasti, toimib see IO jaoks märkimisväärselt hästi.

Kui mitmekordne keermestamine on CPU-le halb ja IO-le hea, kas see tähendab, et mitutöötlus on CPU-le hea ja IO-le halb?
Vastus järgmises jaotises.

Multitöötlus on IO jaoks halb.

VEEL!

Kui rääkida IO-st, siis on mitutöötlus üldiselt sama hea kui mitmekordne. Sellel on lihtsalt rohkem üldkulusid, kuna hüpikprotsessid on kallimad kui hüpikniidid.

Kui teile meeldib eksperimenti teha, asendage mitmikniit eelmises mitmeprotsessimisega.

amine @ c5-xlarge: ~ $ python3 serial_comparaison_io.py
Seeria kulutatud 5.325972080230713
Mitme töötlemisega 4 kulutatud 1,2662420272827148
Mitme töötlemisega 8 kulutatud 0,8015711307525635
Mitme töötlemisega 16 kulutas 0,5572431087493896

[Boonus] Mitme töötlemine on alati kiirem kui jadaprotsess.

TÕESTI, aga ainult sina teed seda õigesti

Näiteks kui teil on 1000 cpu raske ülesanne ja ainult 4 südamikku, siis ärge avage rohkem kui 4 protsessi, vastasel juhul konkureerivad nad protsessori ressursside pärast.
(võistlema => võistlema => samaaegsus)

Järeldus

  • Python-protsessis võib igal ajal olla ainult üks niit.
  • Multitöötlus on parallelism. Mitmekeermeline on samaaegsus.
  • Mitme töötlemine on mõeldud kiiruse suurendamiseks. Mitmekeermeline on latentsuse peitmiseks.
  • Mitmetöötlus on arvutuste jaoks parim. Mitmekeermeline on IO jaoks parim.
  • Kui teil on protsessori raskeid ülesandeid, kasutage mitut töötlemist n_process = n_cores abil ja mitte kunagi enam. Mitte kunagi!
  • Kui teil on IO raskeid ülesandeid, kasutage mitmekeelset niitidega n_threads = m * n_core, mille m on suurem kui 1 ja mida saate ise kohandada. Proovige paljusid väärtusi ja valige kõige kiirem väärtus, kuna üldreeglid puuduvad. Näiteks vaikeväärtus m on ThreadPoolExecutoris seatud 5-le [Allikas], mis on minu arvates ausalt üsna juhuslik.

See selleks.

Viited