Avsnitt fra Tanenbaum: 1.3-1.6 |
harek-haugeruds-macbook:~ hh$ uname -a Darwin dhcp-202-136.wlan.hio.no 9.5.1 Darwin Kernel Version 9.5.1: Fri Sep 19 16:19:24 PDT 2008; root:xnu-1228.8.30~1/RELEASE_I386 i386 |
$ top -o cpu Processes: 48 total, 5 running, 43 sleeping... 176 threads 21:43:01 Load Avg: 3.16, 1.85, 0.83 CPU usage: 89.27% user, 10.73% sys, 0.00% idle PID COMMAND %CPU TIME #TH #PRTS #MREGS RPRVT RSHRD RSIZE VSIZE 170 bash 63.9% 2:33.80 1 13 19 192K 704K 692K 18M 168 bash 63.8% 2:56.34 1 13 19 192K 704K 692K 18M 169 bash 62.1% 2:35.43 1 13 19 192K 704K 692K 18M |
Både CPU-registere og cache er vanligvis laget av SRAM (Static RAM). Aksess er meget hurtig og SRAM er statisk i den betydning at det ikke trenger å oppfriskes, slik DRAM (Dynamic RAM) må. Mer en 10 ganger i sekundet må DRAM opplades, ellers forsvinner informasjonen. SRAM består av 6 transistorer for hver bit som lagres, til sammenligning består en NOT-port av to transistorer og AND og OR-porter av 4. Men DRAM trenger bare en transistor og en kapasitator(lagrer elektrisk ladning) for å lagre en bit. Derfor er DRAM billigere, mindre og bruker mindre effekt og kan derfor lages i større enheter. Internminne består derfor av DRAM eller forbedrede varianter av DRAM. DDR3 SDRAM (Double-Data Rate type 3 Synchronus Dynamic RAM) er et av de foreløpig siste leddene av kjedene av forbedrede utgaver av DRAM.
|
Arkitekturen til en moderne prosessor kan da i grove trekk se ut som i Fig. 40.
|
Noen arkitekturer har i tillegg enda et lag i minnehierarkiet, en offchip L3 cache som sitter mellom mikroprosessoren og RAM.
|
IU-serveren huldra har to Intel Xeon prosessorer (på hver sin brikke, ikke multicore) som begge er hyperthreading. Som før bruker vi følgende CPU-slukende program:
#! /bin/bash
(( max = 150000 ))
(( i = 0 ))
(( sum = 0 ))
echo $0 : regner....
while (($i < $max))
do
(( i += 1 ))
(( sum += i ))
done
echo $0, resultat: $sum
|
Slik ser det ut på huldra om man starter 6 CPU-intensive regnejobber:
top - 08:31:21 up 114 days, 23:25, 2 users, load average: 6.04, 3.77, 1.62 Tasks: 221 total, 7 running, 214 sleeping, 0 stopped, 0 zombie Cpu0 : 96.6% us, 3.4% sy, 0.0% ni, 0.0% id, 0.0% wa, 0.0% hi, 0.0% si Cpu1 : 98.7% us, 1.3% sy, 0.0% ni, 0.0% id, 0.0% wa, 0.0% hi, 0.0% si Cpu2 : 99.3% us, 0.7% sy, 0.0% ni, 0.0% id, 0.0% wa, 0.0% hi, 0.0% si Cpu3 : 98.0% us, 2.0% sy, 0.0% ni, 0.0% id, 0.0% wa, 0.0% hi, 0.0% si PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ P COMMAND 27550 haugerud 25 0 3644 1340 1020 R 99 0.3 4:50.35 1 regn2 27549 haugerud 25 0 3648 1344 1020 R 97 0.3 4:46.08 0 regn2 27547 haugerud 25 0 3644 1340 1020 R 50 0.3 2:34.50 3 regn2 27548 haugerud 25 0 3644 1340 1020 R 50 0.3 2:31.34 2 regn2 27551 haugerud 25 0 3648 1344 1020 R 50 0.3 2:25.52 2 regn2 27552 haugerud 25 0 3648 1344 1020 R 50 0.3 2:23.75 3 regn2 |
Linux-kjernen betrakter dette som fire uavhengige CPU'er. Av kolonnen P kan vi se at OS fordeler jobbene på de fire prosessorene, en på CPU 0 og 1 og to hver på CPU 2 og 3. Prosessene på CPU 2 og 3 må dele på CPU'en og får derfor i gjennomsnitt bare 50% CPU-tid som %CPU kolonnen viser. Prosessene på CPU 0 og 1 trenger ikke å konkurrere med andre CPU-krevende jobber og får dermed nesten 100% CPU-tid (Når man kjører top må man taste 1 for å se 4 øverste CPU-linjene og f fulgt av p og return for å se hvilke prosessorer som brukes).
top at de jobber på hver sin CPU og at de hver får 100% CPU-tid. Men hvordan kan man finne ut om de virkelig gjør det?
På samme måte som om man ønsker å finne ut om fire potetskrellere man har ansatt for å skrelle poteter virkelig jobber samtidig. Man tar tiden på dem. Fire personer bør bruke like lang tid på å skrelle fire sekker poteter som to stykker bruker på to sekker poteter. Så vi setter igang to regnejobber som skreller ivei på hver sin av de to CPU'ene:
huldra:~/hh/task# for i in $(seq 1 2); do time ./regn1& done ./regn1, resultat: 11250075000 real 0m4.971s user 0m4.820s sys 0m0.140s ./regn1, resultat: 11250075000 real 0m5.054s user 0m4.928s sys 0m0.124s |
Jobben går unna på fem sekunder og det bør ikke ta lenger tid for fire prosesser hvis de reelt sett jobber samtidig:
huldra:~/hh/task# for i in $(seq 1 4); do time ./regn1& done ./regn1, resultat: 11250075000 real 0m10.462s user 0m10.189s sys 0m0.276s ./regn1, resultat: 11250075000 real 0m10.472s user 0m10.189s sys 0m0.244s ./regn1, resultat: 11250075000 real 0m10.481s user 0m10.137s sys 0m0.304s ./regn1, resultat: 11250075000 real 0m10.479s user 0m10.233s sys 0m0.208s |
Men det tar ganske nøyaktig dobbelt så lang tid. Som beskrevet tidligere, CPU'en har lastet inn to prosesser samtidig, men internt må de bytte på å bruke ALU'en og for slike prosesser som hele tiden bruker CPU har hyperthreading liten effekt.
|
Merk forøvrig: 30 MHz var maks klokkefrekvens for Intel i 1992 og den ble mer enn tredvedoblet pp åtte år fram til 2000 hvor de første GHz prosessorene kom. Men på de neste åtte årene ble frekvensen bare tredoblet. Det at det er vanskelig å øke klokkefrekvensen har gjort at man istedet har økt kapasiteten med multi core og hyperthreading.
For å undersøke hvorfor det er slik, ser vi på assemblerkoden for et program som regner ut Fibonacci-rekken, 1 1 2 3 5 8 13 21 34 55 ..... I denne rekken er neste tall summen av de to foregående. Assemblerkode ligger tett opp til maskinkode, det er en litt mer lesbar utgave av maskininstruksjoner og kan sees på som den koden som CPU'en utfører en for en:
1. MOV ax, 1 # ax = 1 2. MOV bx, 1 # bx = 1 3. ADD bx, ax # bx = bx + ax 4. ADD ax, bx # ax = ax + bx 5. JMP 3 |
Etter hver runde i denne evige løkken, vil ax og bx være siste og nest siste ledd i Fibonacci-rekken som er regnet ut. I instruksjon 3 settes bx lik summen av de to og i nummer 4 settes ax lik summen av de to og dermed har vi kommet to stepp videre i beregningen. Vi ser at det ikke er mulig for et operativsystem å fordele bergningene i en slik algoritme på flere CPU'er. Neste ledd i beregningen avhenger av det forrige og det tar uforholdsmessig lang tid å flytte verdier av registere fra en CPU til en annen. I dette tilfellet lar ikke algoritmen seg naturlig dele opp i separate bergningsdeler og da vil det også være vanskelig for en programmerer å dele opp beregningen i flere prosesser for å utnytte flere prosessorer. Følgende eksempel som regner ut summen
1. MOV ax, 2001 2. MOV bx, 1 3. MOV cx, 0 3. ADD cx, bx # cx += bx 4. INC bx # bx++ 5. CMP bx ax 6. JNE 3 # Hopp til linje 3 hvis bx ikke er lik 2001 |
Etter dette programmet er avsluttet vil registereret cx være lik
. Det er lett å se at denne algoritmen i prinsippet kan deles i to. En CPU kan regne ut
og en annen CPU kan regne ut
og så legger man sammen svarene til slutt. Men poenget er at operativsystemet ikke har noen anelse om hva som foregår i et vilkårlig program. Det bare sørger for at prosessene får utført sine instruksjoner. Derfor er det programmereren som eksplisitt må skrive programmet slik at det kjøres som to uavhengige prosesser for at det skal kunne utnytte flere CPU'er. En annen løsning er at programmet inneholder flere tråder(threads) som kan kjøres på hver sin CPU, dette kommer vi tilbake til senere. Threads i denne sammenhengen har ikke noe med hyperthreads å gjøre, disse threads styres av OS og ikke av prosessoren. Det har også blitt utviklet kompilatorer som til en viss grad klarer å parallelisere kode. Men operativsystemet kan ikke gjette seg til hva programmet gjør og kan derfor ikke på egenhånd få en enkelt prosess til å utnytte flere prosessorer.
Moderne spillkonsoller inneholder ofte mange CPU'er, XBOX 360 har tre og Playstation 3 har åtte CPU'er. For å kunne utnytte disse må spill-programmen som kjøres på dem skrives slik at de kan utnytte alle prosessorene. Programmererne deler da opp oppgavene i uavhengige deler slik at de kan beregnes hver for seg. Dette kalles å parallellisere koden. Tidligere var dette bare viktig i såkalte clustere satt sammen av mange datamaskiner, men med dagens utvikling hvor etterhvert alle datamakiner har flere CPU'er er dette viktig for alle programmer.
io1 (bruker lite CPU):
#! /bin/bash
echo "start" > fileio1
(( max = 600 ))
echo $0 : skriver til fileio1....
(( i = 0 ))
while (( $i < $max ))
do
echo $i >> fileio1
(( i += 1 ))
done
|
Men en stor effektivisering av hvordan bash utfører dette har gjort at filskrivingen blir så effektiv at CPU brukes nærmest 100%. For å illustrere effekten av et program som bruker tid fordi det venter på en prosess som ikke CPU'en har kontroll over, bruker vi istedet følgende kode
io1:
#! /bin/bash echo $0 : laster ned.... wget http://download.intel.com/products/processor/corei7/prod_brief.pdf |
Kjøring av de følgende programmer viser hvordan multitasking virker i praksis.
$ TIMEFORMAT="Real:%R User:%U System:%S %P%%" group11@ubuntu:~/task$ time regn1 ./regn1 : regner.... ./regn1, resultat: 5000050000 Real:3.303 User:2.916 System:0.336 98.45% |
Her angir 3.303 antall sekunder totaltid, 2.916 bruker-CPUtid, 0.336 OS-overhead og 98.45% andel av CPU'en prosessen har hatt tilgang til.
$ time regn1 |
$ time regn2 |
|---|---|
./regn1 : regner.... |
./regn2 : regner.... |
./regn1, resultat: 50005000 |
./regn2, resultat: 50005000 |
Real:6.962 User:2.992 System:0.160 45.27% |
Real:7.399 User:3.112 System:0.180 44.49% |
$ time io1 ./io1 : laster ned.... Real:3.738 User:0.000 System:0.008 0.21% |
Hvis man kjører io1 og regn1 samtidig
$ time io1 |
$ time regn1 |
|---|---|
./io1 : laster ned.... |
./regn1 : regner.... |
|
./regn1, resultat: 50005000 |
Real:3.837 User:0.012 System:0.000 0.31% |
Real:3.302 User:2.920 System:0.308 97.77% |
regn1 som bruker CPU'en, så det går omtrent like fort som når
de kjører hver for seg.
task1:
#! /bin/bash regn1 io1 regn2 io2 |
task2:
#! /bin/bash io1 regn1 io2 regn2 |
Når vi kjører task1 alene får vi føgende resultat:
$ time task1 ./regn1 : regner.... ./regn1, resultat: 50005000 ./io1 : laster ned.... ./regn2 : regner.... ./regn2, resultat: 50005000 ./io2 : laster ned.... Real:13.460 User:5.924 System:0.556 48.14% |
Man ville kanskje ventet at jobbene task1 og task2 til sammen ville bruke det dobbelte av
det task1 bruker alene, ca. 27 sekunder.
| $ time task1 | $ time task2 |
|---|---|
./regn1 : regner.... |
./io1 : laster ned.... |
./io1 : laster ned.... |
./regn1 : regner.... |
./regn2 : regner.... |
./io2 : laster ned.... |
./io2 : laster ned.... |
./regn2 : regner.... |
Real:15.605 User:6.048 System:0.496 41.93% |
Real:14.624 User:5.900 System:0.556 44.14% |