Simpel pipeline
Pipeline faser og ressourcer
Siden slutningen af 70'erne hvor de første pipelinede arkitekturer blev introduceret, har en instruktion gennemgået flere faser når den afvikles. Nogle faser er generiske; nogle afhænger af instruktionen.
Faserne gennemløbes i rækkefølge bestemt af instruktionstype og mikroarkitektur.
Betragt for eksempel afviklingen på en simpel pipeline, typisk for de første RISC maskiner konstrueret i 80'erne. Her er der fem faser:
F: Fetch, indlæsning af instruktionen fra hukommelse,D: Decode, afkodning af instruktionen og udlæsning fra registerfil,X: eXecute, udførsel af aritmetisk/logisk operation, samt beregning af mulig adresse,M: Memory, læsning fra eller skrivning til hukommelsen,W: Writeback, tilbageskrivning til registerfilen.
Her ses nogle begrænsninger for en 5-trins pipeline:
- Alle instruktioner gennemløber:
FDXMW - Tilgængelige ressourcer:
F:1,D:1,X:1,M:1,W:1
Ovenstående skal læses som: alle instruktioner passerer gennem samtlige fem trin ordnet som beskrevet; der findes en ressource for hvert trin, altså der kan kun være en instruktion i hver trin.
Bemærk at det er en voldsom forenkling at udtrykke begrænsningen for instruktionshentning i et antal af instruktioner. Især hvis instruktionen kommer til at ligge over to cache linier. For en maskine med instruktioner af forskellig længde er bindingen mere korrekt udtrykt som et antal bytes. Først i forbindelse med afkodning er det klart, hvor en instruktion begynder og slutter. Den lille detalje vil vi se bort fra.
Eksempel: Simpel pipeline mikroarkitektur
I en simpel pipeline vil afviklingsplottet for et mindre program være følgende:
012345678
movq (r10),r11 FDXMW
mulq r10,r12 FDXMW
addq $100,r13 FDXMW
movq r14,(r10) FDXMW
subq $1,r10 FDXMW
Her ses at første instruktion bliver indhentet i første clock periode. I anden clock periode vil anden instruktion blive indhentet samtidig med at første instruktion bliver afkodet, osv.
Det er vigtigt at vi overholde begrænsningerne. For at tjekke det ser vi at: * første begrænsning bliver overholdt, da alle linier i plottet indeholder alle fem trin, og * anden begrænsning bliver overholdt da hver søjle (clock periode) kun indeholder hvert trin en gang.
Hvis vi prøver at udregne ydeevnen for programmet, kan vi se at det samlet bruger 9 clock perioder: dette svare til antallet af instruktioner (5) + antallet af trin minus 1. Vi kan derefter udregne CPI = 9/5 = 1,8. Dette er dog misvisende fordi det ville have været muligt for efterfølgende instruktioner at starte tidligere. Vi skal i stedet tælle clock perioder fra start til slut af 'X' fasen. Hvis vi gør det, får vi en CPI på 1. Det er det sammen som for vores enkelt-cyklus maskine, men da hver fase udfører mindre arbejde, kan den pipelinede arkitektur nå en væsentlig højere clock-frekvens. Sandsynligvis omkring 4 gange højere.
Instruktioners latenstid
På en pipelined arkitektur bruger instruktioner en eller flere clock-perioder til at producere et resultat. Det kaldes instruktionens latenstid. Latenstiden er den tid der går fra instruktionen modtager/fremfinder sin sidste indgående operand og til en efterfølgende instruktion som afhænger af resultatet kan begynde sin beregning.
Man planlægger normalt en mikroarkitektur således at de grundlæggende aritmetisk og logiske instruktioner har en latenstid på en enkelt clock periode.
Andre instruktioner kan så få længere latenstid, fordi de udfører et mere kompliceret stykke arbejde. For eksempel er multiplikation mere kompliceret end addition og har derfor en latenstid på 3-4 clock perioder.
Tilgang til lageret er også mere kompliceret og tager længere tid end en enkelt clock periode. I den pipeline vi præsenterede ovenfor har instruktioner der læser fra lageret en latenstid på 2 clock-perioder, for eksempel. Det er i tilfælde af cache-hit. Situationen omkring et cache-miss er væsentlig mere kompliceret og vil ikke blive beskrevet her.
En latenstid på mere end en cyklus for en instruktion kan både opstå fordi selve eksekverings-delen er splittet i flere faser (pipelined eksekvering) eller fordi eksekverings-delen foregår iterativt så instruktionen bliver i samme fase i flere clock-perioder. I denne note antager vi at eksekvering altid er fuldt pipelinet. I virkelige maskiner gælder det for de fleste instruktioner med undtagelse af division.
Data afhængigheder og forwarding
Mere signifikant end latenstiden er data afhængigheder. Det har ikke været et problem i vores tidligere eksempler (ikke en tilfældighed), men kan hurtigt blive det for normale programmer.
Overvej følgende program:
movq (r10),r11
addq $100,r11
movq r11,(r10)
addq $8,r10
subq $1,r12
Her bliver register r11 opdateret i instruktionen lige før det bliver læst; endda to gange. F.eks. indlæser første instruktion en værdi til r11, som anden instruktion straks lægger noget til; men også fra anden til tredje instruktion. Vi kan lave data-flow graph, som beskrevet i CSapp, som vil tydeliggøre de data afhængigheder, som eksisterer i programmet. Instruktionsnummeret er indsat efter navnet.
r10 r11 r12
| \ | |
| movq1 |
| | |
| addq2 |
| / | |
movq3 | |
| | |
addq4 | subq5
| | |
r10 r11 r12
Hvis vi laver et simpelt afviklingsplot som før, vil vi få følgende:
movq (r10),r11 FDXMW
addq $100,r11 FDXMW
movq r11,(r10) FDXMW
addq $8,r10 FDXMW
subq $1,r12 FDXMW
(OVENSTÅENDE VIRKER IKKE)
Ud over den tydelige tekst, som indikerer det, kan vi overbevise os selv om at ovenstående ikke virker. Vi har en data afhængighed mellem læsningen fra lageret og første addition og vi ved fra den tidligere uformelle beskrivelse at læsning fra hukommelse sker i Mfasen. Men i plottet laver vi additionen i X samtidig med M; altså før vi har værdien til rådighed.
For at undgå dette er vi nødt til at tilføje afhængighederne til vores instruktioner. Det kan vi skrive på følgende måde:
- Aritmetik
op a b:depend(X,a), depend(X,b), produce(X,b) - Læsning
movq (a),b:depend(X,a), produce(M,b) - Skrivning
movq b,(a):depend(X,a), depend(M,b)
Her står at aritmetiske instruktioner er afhængige af, at værdierne for både a og b er klar til fase X, samt at de producerer deres resultat til register b i fase X, så resultatet kan bruges fra starten af den efterfølgende fase.
Læsning fra hukommelsen kræver at adressen der skal læses fra register a er klar til fase X (husk at vi har beregningen af adressen i X fasen, selvom læsningen først foregår i M fasen), mens resultatet af læsningen til register b er klar i fase M altså til fase W.
Ved skrivning til hukommelsen skal adressen i register a være klar til fase X, mens værdien først skal være klar til fase M. Skrivning til hukommelsen har ikke noget resultat.
Vær opmærksom på at ovenstående implementerer en arkitektur med fuld forwarding. Altså at alle værdier kan bruges umiddelbart i næste clock periode i alle efterfølgende instruktioner; dvs. før de reelt set er skrevet tilbage til registerfilen.
Hvis vi i stedet ville have en maskine uden forwarding, ville alle værdier bliver produceret til fase W, hvor vi reelt skriver værdien tilbage.
Eksempel: Data afhængigheder
Lad os nu definere det korrekte afviklingspot for eksemplet. Først, lad os dog opsummere alt vi har defineret for maskinen:
- Tilgængelige ressourcer:
F:1,D:1,X:1,M:1,W:1
| Instruktion | Faser | Dataafhængigheder | |
|---|---|---|---|
| Aritmetik | op a b |
FDXMW |
depend(X,a), depend(X,b), produce(X,b) |
| Læsning | movq (a),b |
FDXMW |
depend(X,a), produce(M,b) |
| Skrivning | movq b,(a) |
FDXMW |
depend(X,a), depend(M,b) |
012345678901 -- Beskrivelse
movq (r10),r11 FDXMW -- produce(M,r11)
addq $100,r11 FDDXMW -- depend(X,r11), produce(X,r11), stall i D
movq r11,(r10) FFDXMW -- Stall i F, depend(X,r11)
addq $8,r10 FDXMW -- Forsinket start på F
subq $1,r12 FDXMW --
Bemærk hvorledes instruktion nr. 2 bliver forsinket en clock periode i sin D-fase,
fordi den afhænger af r11 som bliver produceret af den forudgående instruktion
der har en latenstid på 2 clock-perioder.
Dette vil give en CPI = 6/5 = 1,2.
In-order udførsel af instruktioner
Men hov! Vi har lige fundet ud af at sidste instruktion ikke har dataafhængigheder til de øvrige, så hvorfor kan vi ikke spare en clock periode ved at lave:
012345678901 -- Beskrivelse
movq (r10),r11 FDXMW -- produce(M,r11)
addq $100,r11 FDDXMW -- depend(X,r11), produce(X,r11), stall i D
movq r11,(r10) FFDXMW -- Stall i F, depend(X,r11)
addq $8,r10 FDXMW -- Forsinket F
subq $1,r12 X -- <-- kunne være smart (X er jo ledigt i denne clock-periode), men hvordan?
Vi har måske lidt svært ved at se, hvordan en maskine overhovedet skulle kunne konstrueres således at ovenstående afviklingsrækkefølge kunne finde sted og en maskine er nødt til at læse instruktionerne i den rækkefølge, som er specificeret i vores program.
Vi tydeliggør denne begrænsning: Hver fase gennemføres i instruktions-rækkefølge.
inorder(F,D,X,M,W)
Vi har overholdt dette i tidligere eksempler. Vi kan tjekke det ved at når vi læser hver søjle oppefra, skal vi se faserne bagfra.
I det her tilfælde kan vores oversætter forbedre situationen, ved at flytte sidste instruktion frem. Dermed kan vi opnå ovenstående udførsel:
01234567890 -- Beskrivelse
movq (r10),r11 FDXMMW -- produce(M,r11)
subq $1,r12 FDXXMW --
addq $100,r11 FDDXMW -- depend(X,r11), produce(X,r11), stall i D
movq r11,(r10) FFDXMMW -- Stall i F, depend(X,r11)
addq $8,r10 FDXXMW -- Forsinket F
Vi kan altså nøjes med at benytte 5 clock perioder og får dermed en CPI = 5/5 = 1,0.
Kontrolafhængigheder
Vi modellerer effekten af hop, kald og retur ved at forsinke F.
For betingede hop skelner vi mellem om hoppet tages eller ej.
Vi udtrykker effekten ved tildelinger til en ny tidsvariabel: NextF.time Og for enhver instruktion gælder altid at F.time >= NextF.time
call a,b: FDXMW
ret a: FDXMW
cbcc a,b,x: FDXMW
Her er nogle mulige regler for kontrol-instruktioner i en simpel pipeline som beskrevet ovenfor
Instruktion Taget Effekt
Call ja produce(F+2,pc)
Ret ja produce(F+3,pc)
CBcc nej produce(F+1,pc)
ja produce(F+3,pc)
Husk at pc er vores specielle register til at pege på næste instruktion, så ovenstående peger på hvor hurtigt næste instruktion kan være klar.
Herunder ses to gennemløb af en indre løkke, hvor hop tages til sidst
012345678901234567
loop: movq (r10),r11 FDXMW -- produce(M,r11)
addq $100,r11 FDDXMW -- depend(X,r11), produce(X,r11)
movq r11,(r10) FFDXMW -- depend(X,r10), depend(M,r11), produce(M,r11)
addq $8,r10 FDXMW -- produce(X,r10)
cbl r10,r12,loop FDXMW -- produce(F+3, pc) (hop taget)
loop: movq (r10),r11 FDXMW -- produce(M,r11)
addq $100,r11 FDDXMW -- depend(X,r11), produce(X,r11)
movq r11,(r10) FFDXMW -- depend(A,r10), depend(M,r11), produce(M,r11)
addq $8,r10 FDXMW -- produce(X,r10)
cbl r10,r12,loop FDXMW