In order CPU och Out of order CPU- test i c++

Permalänk

In order CPU och Out of order CPU- test i c++

När man läser om CPUer brukar man få reda på om de är "out-of-order" eller "in-order" dvs om de själva kan ändra på ordningen vid körning eller om det krävs en kompilator för detta. Dagen PC har alla out-of-order så det är svårt att hitta tydliga skillnader. Den sista med in-order var original Atom.
Mer spännande är det för ARM och Raspberry Pi. Jag har gjort några tester på Pi 3 med denna kod:

#include <ctime> #include <iostream> #include <math.h> using namespace std; int identity(int x) { return x; } int sum1(int num) { int a = 3, b, c, d, e, f, g, h, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, z1, z2, z3, z4, z5, z6 ; for (int i = 0; i < 2000000; i++) num += i; t= r/a; s= o/a; r= a/a; p= 2*a; o= b/a; h = b/a; g= h%a; f= b/a; e= c%a; d= c/a; b=3*a; c=a/a; p= k+a+d; q= p+b+m; n= k+a; m= 1+b+l; l=3*a+k; k=a+b+3; t= r/a; u = t/a; v= h/a; w= b/z6; x= c/a; y= c/a +p; z=3*a; z1=z5/a; z2= k+a+d; z3= p+b+m; z4= k+a; z5= 1+b+l; a= a+b; return num; } int sum2(int num) { int a1 = 3, b1, c1, d1, e1, f1, g1, h1, k1, l1, m1, n1, o1, p1, q1, r1, s1, t1; float x1, y1 ,z1, u1, w1, x3, y3 ,z3, u3, w3, x5, y5 ,z5, u5, w5; for (int i = 0; i < 2000000; i++) num += i; t1= r1/s1; s1= o1/p1; r1= a1/b1; p1= 2*a1; o1= a1/b1; h1 = b1/a1; g1= h1/a1; f1= b1/c1; e1= c1/a1; d1= c1/b1; b1=3*a1; c1=a1/b1; p1= k1+a1+d1; q1= p1+b1+m1; n1= k1+a1; m1= 1+b1+l1; l1=3*a1+k1; k1=a1+b1+3; x1= sqrt (w1); y1= 1/z1; z1= (float) c1; u1= 1/x1; w1= (float) h1; x3= sqrt (w1); y3= 1/z1; z3= (float) c1; u3= 1/x1; w3= (float) h1; x5= sqrt (w1); y5= 1/z3; z5= (float) m1; u5= 1/x1; w5= (float) e1; a1= a1+b1; return num; } int sum3(int num) { int j1 = 3, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15, j16, j17, j18; float s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15 ; for (int i = 0; i < 2000000; i++) num += i; j18= j17/j1; j17= j16/j3; j16= j15/j4; j15= j1/j3; j2 = j1/j1; j3= j2/j1; j4= j2/j2; j5= j1/j2; j6= j1/j3; j7=3*j1; j8=j1/j7; j9 = j1+j2+j3; j10= j1+j9+j5; j11= j1+j10+j3; j12= j1+j2; j13= 1+j8+j4; j14=3*j1+j11; s1= sqrt (10); s2= 1/s1; s3= (float) j7; s4= 1/s3; s5= (float) j1; s6= sqrt (10); s7= 1/s1; s8= (float) j7; s9= 1/s3; s10= (float) j3; s11= sqrt (10); s12= 1/s1; s13= (float) j7; s14= 1/s3; s15= (float) j8; j1=j1+j2; return num; } int sum4(int num) { int j1 = 3, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15, j16, j17, j18; float s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15 ; for (int i = 0; i < 2000000; i++) num += i; j7=3*j1; j3= j2/j1; j4= j2/j2; j6= j1/j3; j13= 1+j8+j4; j8=j1/j7; j9 = j1+j2+j3; j10= j1+j9+j5; j11= j1+j10+j3; j12= j1+j2; j5= j1/j2; j14=3*j1; j2 = j1/j1; s11= sqrt (5+j2); s9= 1/s3; s2= 1/s1; s3= (float) j7; s4= 1/s3; j18= j17/j1; j17= j16/j3; j16= j15/j4; j15= j1/j3; s6= sqrt (10); s7= 1/s1; s8= (float) j7; s5= (float) j1; s10= (float) j3; s1= sqrt (10+j2); s12= 1/s1; s13= (float) j7; s14= 1/s3; s15= (float) j8; j1= j1+j2; return num; } int sum5(int num) { float s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15, s16, s17, s18, s19, s20, s21, s22, s23, s24, s25, s26, s27, s28, s29, s30; for (int i = 0; i < 2000000; i++) num += i; s30= sqrt (5); s29= 1/s3; s28= 1/s1; s27= sqrt(s30); s26= sqrt (10); s25= sqrt (5); s24= 1/s3; s23= 1/s1; s22= 1/s30; s21= 1/s3; s20= sqrt(10); s19=s20/s30; s17=sqrt(s20); s16=s15+s23; s7= 1/s1; s8= s7*s7; s5= s1+s1; s10= s3+s1; s1= sqrt(10); s12= 1/s1; s13= s2/s3; s14= 1/s3; s15= 5; s2=s1/s5; s3=s10/s17; s4=s5/s2; return num; } double time_it(int (*action)(int), int arg) { clock_t start_time = clock(); action(arg); clock_t finis_time = clock(); return ((double) 1000*(finis_time - start_time)) /CLOCKS_PER_SEC; } int main() { cout << "Identity(100) takes " << time_it(identity, 100) << " mseconds." << endl; cout << "Sum1(100) takes " << time_it(sum1, 100) << " mseconds." << endl; cout << "Sum2(100) takes " << time_it(sum2, 100) << " mseconds." << endl; cout << "Sum3(100) takes " << time_it(sum3, 100) << " mseconds." << endl; cout << "Sum4(100) takes " << time_it(sum4, 100) << " mseconds." << endl; cout << "Sum5(100) takes " << time_it(sum4, 100) << " mseconds." << endl; return 0; }

Med A-53 så har vi in-order så utan optimering fungerar enbart Sum1.
Med -O1, -O2 eller -O3 går det bättre
RPi1, RPi2 och RPi3 har in-order medan RPi4 har "deeply-out-of-order".
Tinker Board gen 1 har A-17 som är out-of-order men inte "deeply".
Tinker Board gen 2 har A-53 dvs in-order.
Banana Pi har in-order.

Någon som vill testa denna kod?

Permalänk

@Greyguy1948: Vad är det egentligen du vill mäta? En optimerande kompilator kommer städa bort all kod i funktionen utom just loopen. Det är det enda som har betydelse för funktionens returvärde. All annan kod kan tas bort utan att resultatet påverkas.

int sum1(int num) { for (int i = 0; i < 2000000; i++) num += i; return num; }

Har du en riktigt duktig kompilator kan den även optimera bort loopen eftersom den bara lägger på ett konstantvärde på num.

int sum1(int num) { return num + 2000001000000; }

Permalänk
Medlem

@Greyguy1948 Jag tänkte själv på ARM här om dagen: att mycket av optimeringen ligger i compile-time. ARM har tidigare varit dåliga på cachemissar men vet inte hur det ser ut för de senaste arkitekturerna. Och outoforder har också varit en svag sida likaså hantering/konvertering av 16/32/64/128-bit (ARMv7). Vet som sagt inte hur status är för moderna ARM.

Vad har du själv testat /kommit fram till?

Som Ingetledigtnamn säger så finns det risk att kompilatorn "optimerar" bort ineffektiv kod.

Visa signatur

Ryzen 9 5950X, 32GB 3600MHz CL16, SN850 500GB SN750 2TB, B550 ROG, 3090 24 GB
Har haft dessa GPUer: Tseng ET6000, Matrox M3D, 3DFX Voodoo 1-3, nVidia Riva 128, TNT, TNT2, Geforce 256 SDR+DDR, Geforce 2mx, 3, GT 8600m, GTX460 SLI, GTX580, GTX670 SLI, 1080 ti, 2080 ti, 3090 AMD Radeon 9200, 4850 CF, 6950@70, 6870 CF, 7850 CF, R9 390, R9 Nano, Vega 64, RX 6800 XT
Lista beg. priser GPUer ESD for dummies

Permalänk
Skrivet av Ingetledigtnamn:

@Greyguy1948: Vad är det egentligen du vill mäta? En optimerande kompilator kommer städa bort all kod i funktionen utom just loopen. Det är det enda som har betydelse för funktionens returvärde. All annan kod kan tas bort utan att resultatet påverkas.

int sum1(int num) { for (int i = 0; i < 2000000; i++) num += i; return num; }

Har du en riktigt duktig kompilator kan den även optimera bort loopen eftersom den bara lägger på ett konstantvärde på num.

int sum1(int num) { return num + 2000001000000; }

Det är CPUn jag vill testa. Klarar den detta utan optimering?

Permalänk
Skrivet av Herr Kantarell:

@Greyguy1948 Jag tänkte själv på ARM här om dagen: att mycket av optimeringen ligger i compile-time. ARM har tidigare varit dåliga på cachemissar men vet inte hur det ser ut för de senaste arkitekturerna. Och outoforder har också varit en svag sida likaså hantering/konvertering av 16/32/64/128-bit (ARMv7). Vet som sagt inte hur status är för moderna ARM.

Vad har du själv testat /kommit fram till?

Som Ingetledigtnamn säger så finns det risk att kompilatorn "optimerar" bort ineffektiv kod.

En bra Intel eller AMD CPU ska klara koden utan optimering.
Jag är inte hemma just nu så jag har endast provat på ARM A-53.

Permalänk

Enbart heltal

Timern mäter inte det intressanta så jag tar något enklare:

int main() { int a = 3, b, c, d, e, f, g, h, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, z1, z2, z3, z4, z5, z6 ; for (int i = 0; i < 2; i++) { t= r/a; s= o/a; r= a/a; p= 2*a; o= b/a; h= b/a; g= h%a; f= b/a; e= c%a; d= c/a; b= 3*a; c= a/a; p= k+a+d; q= p+b+m; n= k+a; m= 1+b+l; l= 3*a+k; k= a+b+3; t= r/a; u = t/a; v= h/a; w= b/z6; x= c/a; y= c/a +p; z= 3*a; z6= u/v; z1= z5/a; z2= k+a+d; z3= p+b+m; z4= k+a; z5= 1+b+l; z6= 3*z; a= a+b; } }

Med ARM A-53 får jag floating point exeption utan optimering.
Skriver jag // framför for-slingan får jag inga fel.

Permalänk

Flyttal

Flyttal:

include <ctime> #include <iostream> #include <math.h> using namespace std; int main() { float s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15, s16, s17, s18, s19, s20, s21, s22, s23, s24, s25, s26, s27, s28, s29, s30; for (int i = 0; i < 2000000; i++) { s30= sqrt(5); s29= 1/s3; s28= 1/s1; s27= sqrt(s30); s26= sqrt(10); s25= sqrt(5); s24= 1/s3; s23= 1/s1; s22= 1/s30; s21= 1/s3; s20= sqrt(10); s19=s20/s30; s17=sqrt(s20); s16=s15+s23; s7= 1/s1; s8= s7*s7; s5= s1+s1; s10= s3+s1; s1= sqrt(10); s12= 1/s1; s13= s2/s3; s14= 1/s3; s15= 5+s7; s2=s1/s5; s3=s10/s17; s4=s5/s2; } }

Detta fungerar på ARM A-53 utan optimering!

Permalänk

@Greyguy1948: Du massor med oinitierade variabler. När du dividerar med vad som råkar ligga på stacken/i registret är det föga förvånande om du råkar dividera med 0.

Permalänk
Skrivet av Ingetledigtnamn:

@Greyguy1948: Du massor med oinitierade variabler. När du dividerar med vad som råkar ligga på stacken/i registret är det föga förvånande om du råkar dividera med 0.

På heltalen har jag lagt till z6= 1/z;

Menar du att det händre något när jag kör for-slingan?
Om nämnare =0 hur kan något fungera?
Optimeringen skulle aldrig hjälpa!

Permalänk

Vad innehåller z6 första varvet i loopen?

Permalänk
Skrivet av Ingetledigtnamn:

Vad innehåller z6 första varvet i loopen?

Tack för tipset 1/9 blir ju = 0 map heltal.
Så nu har jag ändrat z6= 3*z;

Permalänk

@Greyguy1948: Det var inte det jag tänkte på. Jag ställer samma fråga för variablerna r, o, b, c, k, m och z5. Vad innehåller dessa när de förekommer i uttryck innan de har blivit tilldelade ett värde? Icke-statiska funktionslokala variabler är inte noll-initierade. De innehåller vad som råkar ligga i registret eller den plats på stacken där variabeln blir allokerad.

Permalänk
Skrivet av Ingetledigtnamn:

@Greyguy1948: Det var inte det jag tänkte på. Jag ställer samma fråga för variablerna r, o, b, c, k, m och z5. Vad innehåller dessa när de förekommer i uttryck innan de har blivit tilldelade ett värde? Icke-statiska funktionslokala variabler är inte noll-initierade. De innehåller vad som råkar ligga i registret eller den plats på stacken där variabeln blir allokerad.

Tack för info!

Permalänk
Skrivet av Herr Kantarell:

@Greyguy1948 Jag tänkte själv på ARM här om dagen: att mycket av optimeringen ligger i compile-time. ARM har tidigare varit dåliga på cachemissar men vet inte hur det ser ut för de senaste arkitekturerna. Och outoforder har också varit en svag sida likaså hantering/konvertering av 16/32/64/128-bit (ARMv7). Vet som sagt inte hur status är för moderna ARM.

Vad har du själv testat /kommit fram till?

Som Ingetledigtnamn säger så finns det risk att kompilatorn "optimerar" bort ineffektiv kod.

Jag tycker Raspberry Pi är rktigt intressant nu när man kan köra alla modeller med samma kompilator.
ARM1176 är ju rätt primitiv med 128 kB L2 cache så det är väl egentligen under K6-2+ med samma L2.
Men minnet är snabbre (inbyggd memory controller?).
Raspberry Pi 4 duger rätt bra till dagens internet med 1024kB L2.
Dessutom ska det bli intressant att se vad "deeply-out-order" betyder.

Permalänk

Skall vi nu backa till min ursprungliga fråga, vad är det egentligen du vill mäta?

Vilka effekter förväntar du dig att som processorn är OOO?

Hur kommer det visa sig i ditt program? Vilka instruktioner förväntar du dig att processorn skall exekvera när det väntar på resultatet från en tidigare instruktion?

Kan du avgöra om processorn har möjlighet att stuva om instruktioner genom att studera källkoden?

Om du bara kör två varv i loopen kommer inte alla effekter av OOO execution drunkna i en cachemiss?

Om du kör på olika processorer kommer resultaten skilja sig åt, men vad betyder det? Hur mycket beror på att X har en bättre flyttalsimplementation än Y? Hur mycket beror på att X är superskalär och inte Y? Hur mycket beror på att X har snabbare minne än Y? Det är inte säkert att skillnaderna du ser beror på X är OOO.

Ursäkta om jag ställer en massa elaka frågor, men det är ofta knepigt att skriva bänkmärken. Mäta hur lång tid det tar att boota Windows klarar vem som helst, men vill du mäta sådant här får man tänka efter ordentligt för att vara säker på att man faktiskt mäter det man vill mäta och inte bara ser effekterna av något annat.

Permalänk
Skrivet av Ingetledigtnamn:

Skall vi nu backa till min ursprungliga fråga, vad är det egentligen du vill mäta?

Vilka effekter förväntar du dig att som processorn är OOO?

Hur kommer det visa sig i ditt program? Vilka instruktioner förväntar du dig att processorn skall exekvera när det väntar på resultatet från en tidigare instruktion?

Kan du avgöra om processorn har möjlighet att stuva om instruktioner genom att studera källkoden?

Om du bara kör två varv i loopen kommer inte alla effekter av OOO execution drunkna i en cachemiss?

Om du kör på olika processorer kommer resultaten skilja sig åt, men vad betyder det? Hur mycket beror på att X har en bättre flyttalsimplementation än Y? Hur mycket beror på att X är superskalär och inte Y? Hur mycket beror på att X har snabbare minne än Y? Det är inte säkert att skillnaderna du ser beror på X är OOO.

Ursäkta om jag ställer en massa elaka frågor, men det är ofta knepigt att skriva bänkmärken. Mäta hur lång tid det tar att boota Windows klarar vem som helst, men vill du mäta sådant här får man tänka efter ordentligt för att vara säker på att man faktiskt mäter det man vill mäta och inte bara ser effekterna av något annat.

Det första är naturligtvis att jag kör minst två program av varje slag på varje CPU.
Ett som inte behöver stuvas om och ett som vinner på att stuvas om.
För att mäta handlar det om typ 20 miljoner cykler. Då kan man ju fundera på om slingan ryms i L1 cache.
Den enklaste CPUn har bara en pipe för int och en för fp. Då är det inte svårt att slöa ner körningen.
På de bästa blir det svårare!
Bra att veta är så klart hur många cykler som krävs för ADD, MUL, DIV, FADD, FMUL, FDIV, FSIN, och FSQRT.
Cortex A-72 i RPi4 har 4 pipeline(2 int +2fp). Det är egentligen minkravet för att vinna något med "out of order".
Optimalt kan tex vara om 10 FADD går parallellt med 1 FDIV.

För Raspberry Pi ser det ut som g++ utan optimering lägger alla variabler i RAM. Så långsamt går det faktiskt. Allt borde hamna i L1 efter några cykler men jag tvivlar!

Permalänk
Medlem
Skrivet av Greyguy1948:

En bra Intel eller AMD CPU ska klara koden utan optimering.
Jag är inte hemma just nu så jag har endast provat på ARM A-53.

Jo. Jag är heller inte hemma just nu. Jag har massa ARM dev kit hemma.
Kanske roligare att köra på de som kan köra linux. T.ex. RPi och BeagleBone

Visa signatur

Ryzen 9 5950X, 32GB 3600MHz CL16, SN850 500GB SN750 2TB, B550 ROG, 3090 24 GB
Har haft dessa GPUer: Tseng ET6000, Matrox M3D, 3DFX Voodoo 1-3, nVidia Riva 128, TNT, TNT2, Geforce 256 SDR+DDR, Geforce 2mx, 3, GT 8600m, GTX460 SLI, GTX580, GTX670 SLI, 1080 ti, 2080 ti, 3090 AMD Radeon 9200, 4850 CF, 6950@70, 6870 CF, 7850 CF, R9 390, R9 Nano, Vega 64, RX 6800 XT
Lista beg. priser GPUer ESD for dummies

Permalänk
Medlem

Tveksamt om det där testet säger någonting. Dels så har koden mängder med odefinierat beteende, men även om man fixar det så verkar det mest vara en fråga om att jämföra äpplen och päron.

Mäta hur lång tid koden tar att exekvera är en sak - men vad kan man dra för slutsatser om det annat än just att en viss CPU tar ett visst antal sekunder på sig att köra koden?

Ta två hypotetiska processorer A och B. A är strikt in-order och använder inte ens någon pipelining för instruktioner. B är out-of-order, och använder en massa andra optimeringstrick.
Men, A körs med en klockfrekvens som är 20 gånger högre än B, och har en stor snabb cache, medan B inte har någon cache alls.
Resultat kommer att bli att A är snabbare än B, trots att A inte har samma typer av optimeringar som B.

Ett benchmark kan avgöra vilken CPU som är snabbast på att exekvera just det benchmarket. Det säger väldigt lite om varför den CPUn är snabbast.

Permalänk
Skrivet av Erik_T:

Tveksamt om det där testet säger någonting. Dels så har koden mängder med odefinierat beteende, men även om man fixar det så verkar det mest vara en fråga om att jämföra äpplen och päron.

Mäta hur lång tid koden tar att exekvera är en sak - men vad kan man dra för slutsatser om det annat än just att en viss CPU tar ett visst antal sekunder på sig att köra koden?

Ta två hypotetiska processorer A och B. A är strikt in-order och använder inte ens någon pipelining för instruktioner. B är out-of-order, och använder en massa andra optimeringstrick.
Men, A körs med en klockfrekvens som är 20 gånger högre än B, och har en stor snabb cache, medan B inte har någon cache alls.
Resultat kommer att bli att A är snabbare än B, trots att A inte har samma typer av optimeringar som B.

Ett benchmark kan avgöra vilken CPU som är snabbast på att exekvera just det benchmarket. Det säger väldigt lite om varför den CPUn är snabbast.

Jo det går allt med rätt kod. Har du läst Agner Fog?
Han har testat det mesta på X86 men ännu inte på ARM.
Om allt körs i en pipeline dvs seriellt så är det en enkel summa.
Om det svåra tex DIV körs i en egen pipeline så får du lite andra effekter.
Normalt för ADD är 1 cykel.
För 64bitar kan DIV ta 80 cykler.
Så ibland är 32bitar betydligt bättre!
Så i detta exempel skulle 80ADD gå lika fort som 1DIV.
75ADD skulle inte gå fortare än 80ADD med 2 pipe men alltid seriellt om även DIV ska köras.

Permalänk
Medlem
Skrivet av Greyguy1948:

Jo det går allt med rätt kod. Har du läst Agner Fog?
Han har testat det mesta på X86 men ännu inte på ARM.
Om allt körs i en pipeline dvs seriellt så är det en enkel summa.
Om det svåra tex DIV körs i en egen pipeline så får du lite andra effekter.
Normalt för ADD är 1 cykel.
För 64bitar kan DIV ta 80 cykler.
Så ibland är 32bitar betydligt bättre!
Så i detta exempel skulle 80ADD gå lika fort som 1DIV.
75ADD skulle inte gå fortare än 80ADD med 2 pipe men alltid seriellt om även DIV ska köras.

Jämföra X86 med ARM är verkligen att jämföra äpplen med päron - helt oaktat att bägge processorfamiljerna rymmer en hel mängd olika mikroarkitekturer.

Det finns tre situationer där det är intressant att veta detaljerna om hur en processor exekverar kod:
1) Du arbetar med att utveckla processorn i fråga
2) Du gör mikro-optimeringar på handskriven assembler.
3) Du utvecklar optimeringsrutiner för en kompilators kodgenerering.

I alla övriga fall så är det bara trivia kunskaper utan praktisk nytta.

Din kod visar egentligen ingenting utom att moderna kompilatorer generellt har bra optimering. Om man fixade buggarna dvs, för just nu så har den så mycket odefinierade beteenden i sig att den är fullständigt oanvändbar och opålitlig.

Permalänk
Skrivet av Erik_T:

Jämföra X86 med ARM är verkligen att jämföra äpplen med päron - helt oaktat att bägge processorfamiljerna rymmer en hel mängd olika mikroarkitekturer.

Det finns tre situationer där det är intressant att veta detaljerna om hur en processor exekverar kod:
1) Du arbetar med att utveckla processorn i fråga
2) Du gör mikro-optimeringar på handskriven assembler.
3) Du utvecklar optimeringsrutiner för en kompilators kodgenerering.

I alla övriga fall så är det bara trivia kunskaper utan praktisk nytta.

Din kod visar egentligen ingenting utom att moderna kompilatorer generellt har bra optimering. Om man fixade buggarna dvs, för just nu så har den så mycket odefinierade beteenden i sig att den är fullständigt oanvändbar och opålitlig.

Ja det var naturligtvis en stor tabbe att bifoga kod.
Det verkar inte som du är speciellt intresserad av hur CPU:er verkligen fungerar.
Det är jag.
Idag har jag även lärt mig en del om optimeringar typ -O1, -O2 och -O3
jämfört med hur lång tid det ska ta enligt tabeller på latenser.
Men ingen är väl intresserad?