Olika processorer jobbar på väldigt olika sätt, blandar man dessutom in pipelining så blir det ännu större variation.
Jag har t.ex. kodat för en processor som kunde köra upp till fyra operationer per klockcykel, men access av externt RAM (processorn hade internt RAM också) tog 11 klockcykler.
Jag har en gammal nerskriven förklaring av en bit kod där, koden räknar ut kvadratiskt medelvärde för ett antal bytes med en klockcykel per byte (under förutsättning att det är minst 4 bytes och en multipel av 4). Kan lägga in den här som lite kuriosa. Processorn är en TMS 320-C80 "multimedia processor" som alltså har totalt 5 kärnor och internt RAM (både "privat" och "delat" för kärnorna). En RISC-kärna och fyra "parallellprocessorer". Koden nedan är för EN parallellprocessor. Jag skulle alltså kunna köra detta parallellt på 4 kärnor i samma kapsel.
d0-d7 är register (32-bitars)
a0-a8 är adressregister (minns inte hur många som fanns, men a0 och a8 verkar användas i koden)
x0-x1 är indexregister (minns inte heller hur många det fanns)
|| först på en rad betyder "görs parallellt med föregående"
Nu är den här koden extremt "handoptimerad", det är inte jag som skrivit den, den var med som exempel i databladet. Det där med ealu är t.ex. en riktig "fuling". Och själva loopen är inte med heller.
Först har vi en "splittad" subtraktion. Registren är 32-bitars men är delade så att det bara är bytevis subtraktion. m:et står fär multiple och c står för att vi vill att multiple flags (mf) lägsta fyra bitar skall sättas som carry-flaggorna. I princip görs det alltså fyra 8-bitars subtraktioner parallellt.
Sen har vi en unsigned halfword (uh) extract av det lägre halfwordet (0).
Och slutligen en "fuskis". d5 används som ett dummyregister. Vi vill bara räkna upp adressregsitret med indexregistrer. Detta sköts av en av adressenheterna, och behöver alltså inte ALUn. d5 får visserligen också värdet av a0, men det används alltså inte. Notera habrovinken med &*, vi vill inte läsa från adress a0 och det finns ingen instruktion som är bara a0+=x0.
Kod: Markera allt
d4= m 0 + ((d7&@mf)|(-d7&~@mf))
|| x1=uh1 d4
|| d6=*a1++
Första arden där ser lite rörig ut, men det beror alltså på att det finns en färdig opkod för detta (d.v.s att få fram absolutbeloppet).
@mf betyder Expanded Multiple Flags. De fyra lägsta bitarna i mf sattes till carry. Expansionen innebär att var och en av de bitarna fyller upp en byte.
Alltså, binärt: 0000000000000000000000000000abcd blir aaaaaaaabbbbbbbbccccccccdddddddd
m:et står även här för multiple, d.v.s. att varje byte hanteras för sig. Resultatet blir att absolutbeloppet för varje byte i d7 hamnar som bytes i d4.
Kör man denna utan m och @mf så skulle d4 alltså få absolutbeloppet av d7 bara, men ALU kan alltså köra bytes eller halfwords "parallellt" istället för hela 32-bitars ord.
Sen har vi en till halfword extract, den här gången det övre halfwordet.
Slutligen laddas d6 från adressen som a1 pekar på, och a1 räknas upp (det funkar precis som i C).
Kod: Markera allt
d4= um d4*d4
|| d7=ealu(d3+d4>>u16)
|| x0=uh0 d2
|| d5=&*(a0+=x0)
En unsigned (u) split (m=multiple) multiply. Bytesen i d4 kvadreras alltså och blir i samma veva halfwords. Det är alltså bara de två lägsta bytesen som kvadreras.
ealu är en lite speciall sak. Operationen anges i d0-registret, och opkoden innehåller alltså bara information om att det är en ealu-operation och vilka register som skall användas. d0 är satt tidigare. >>u16 betyder 16 steg unsigned shift right. d3 är laddad med noll! Orsaken förklaras nedan, men resultatet blir i alla fall en halfword extract (övre halvordet från d4 till d7).
Och så en halfword extract och en adressaritmetisk beräkning igen.
Kod: Markera allt
d2= um d7*d7
|| d1=ealu(d1+d2>>u16)
|| d5=&*(a0+=x1)
|| d5=*a8++
En kvadrering till.
Och en ealu igen. Nu är grejen att den här ealun har SAMMA operationskod som den förra. Vi använder alltså samma d0, men har andra register (de stod ju i opkoden). Det var alltså anledningen till att vi hade d3 som nollregister tidigare. Här används den inte som halfword extract, utan den summerar kvadrerade värden (de blev ju halfwords när de kvadrerades).
Sen har vi en adressaritmetisk pryl igen, och en load från minnet. Nu råkar bägge två ha samma destination, vilket ser lite konstigt ut. Som tur är (jo, det visste de som skrev det här:-) så finns det nåt som heter Write Priority. Det fungerar som så att Global Transfer (och det är det här, eftersom a8 är ett globalt adressregister) har högre prioritet än ALU-operationer. Bägge prylarna utförs alltså, men bara den ena skriver till destinationen.
Så, summa summarum, på 4 instruktioner (om än väldigt komplexa! men de ryms i 64 bitars opkoden) så har vi summerat kvadraten av fyra bytes.
Litet sidospår, men poängen här är då att de bytes jag vill jobba på måste ligga i internt RAM, annars dröjer det 11 klockcykler innan man får datat (skulle ju motsvara nästan 3 varv i loopen).
I det program jag skrev (implementering av ett digitalt filter) så utnyttjade jag väntetiden till beräkningar.