Permalänk
Medlem
Skrivet av pv2b:

Kod:

// ConsoleApplication4.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <algorithm> #include <ctime> #include <iostream> const int ITERATIONS = 100000; void test(const char *desc, int *data, unsigned arraySize) { // Test clock_t start = clock(); long long sum = 0; for (unsigned i = 0; i < ITERATIONS; ++i) { // Primary loop for (unsigned c = 0; c < arraySize; ++c) { if (data[c] >= 128) sum += data[c]; } } double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC; std::cout << desc << ": " << elapsedTime << std::endl; std::cout << "sum = " << sum << std::endl; } int main() { // Generate data (f = filtered) const unsigned arraySize = 32768; unsigned fArraySize = 0; int data[arraySize]; int fData[arraySize]; for (unsigned c = 0; c < arraySize; ++c) data[c] = std::rand() % 256; for (int i = 0; i < arraySize; i++) if (data[i] >= 128) fData[fArraySize++] = data[i]; test("filtered", fData, fArraySize); test("unsorted", data, arraySize); std::sort(data, data + arraySize); test("sorted", data, arraySize); return 0; }

Kompilator: Microsoft Visual Studio 2017.
Processor: AMD Ryzen 5 1600

Testresultat med optimering avstängd (/Od):

filtered: 4.726 sum = 312275400000 unsorted: 24.831 sum = 312275400000 sorted: 8.992 sum = 312275400000

Testresultat med optimering på (/O2):

filtered: 0.826 sum = 312275400000 unsorted: 1.027 sum = 312275400000 sorted: 1.021 sum = 312275400000

Min slutats är att "sortera arrayen" inte är en generellt bra lösning på den här typen av problem. Däremot är det bra att fundera på hur man lagrar och itererar över sin data.

Och att slå på kompilatoroptimeringar. Och att moderna kompilatorer/CPU:er är rätt smarta.

Kan man stänga av processorns branch prediction med instruktioner i kompilatorn verkligen?

Permalänk
Hedersmedlem
Skrivet av Mordekai:

Kan man stänga av processorns branch prediction med instruktioner i kompilatorn verkligen?

Nej, det har jag inte påstått heller.

Permalänk
Skrivet av Mordekai:

Kan man stänga av processorns branch prediction med instruktioner i kompilatorn verkligen?

Poängen med detta test är väl att om data är sorterat kommer BP gissa rätt oftare än när det är osorterat.

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Poängen med detta test är väl att om data är sorterat kommer BP gissa rätt oftare än när det är osorterat.

Då förstår jag inte vad kompilatoroptimeringar har med saken att göra.

Permalänk
Skrivet av Mordekai:

Då förstår jag inte vad kompilatoroptimeringar har med saken att göra.

Om en loop är nästan tom går det tydligen att optimera bort den. Det brukar hända med -O2 och -O3.
Då kan det var så att man får samma tid för 1 miljoner varv som för 100 miljoner varv.
För en RPi 3 handlar det om 5-10 ms.

Permalänk

Nej, jag ser inte heller att optimeringarna påverkar BP, men de påverkar körtiden. Att de synliga "effekterna" av BP blir tydligare när man slår på optimering handlar sannolikt om att kompilatorn genererar mindre korkad kod. Med färre instruktioner som exekveras kommer skillnaden mellan sorted och unsorted mätt i sekunder (som är ungefär lika stor oavsett om det är ooptimerad och optimerad kod) att bli större relativt den totala körtiden.

Permalänk
Skrivet av Greyguy1948:

Om en loop är nästan tom går det tydligen att optimera bort den.

Det beror på vad du gör i din nästan tomma loop. Ge några exempel på nästan tomma loopar så kan jag tala om vilka man kan optimera och varför.

Permalänk
Hedersmedlem
Skrivet av Mordekai:

Då förstår jag inte vad kompilatoroptimeringar har med saken att göra.

Min poäng är att slå på kompilatoroptimeringar i det här fallet gjorde koden upp till 24 gånger snabbare. Och om man ska kolla på att optimera ett program så är det lätt att dra helt fel slutsatser om man sitter och tittar på hur kod fungerar utan optimering på. Finns en massa saker man kan göra i ett program som ser väldigt ineffektiva ut men som en vettig kompilator optimerar, vilket gör att det som ser ut som en 3x speedup i praktiken faktiskt är noll.

Permalänk
Datavetare
Skrivet av Greyguy1948:

Du har säkert rätt. I regel ser man loopar i ASM till och med -O1 men inte med -O2 och -O3.
Raspberry Pi har samma program för 1176 och alla Cortex-varianter. Tinker board behöver inte vara kompatibel med 1176.
x86_32 och x86_64 beter sig helt olika. Antalet register är fler på 64 bitar!

Blev konfunderad varför vi får så olika resultat, svaret ligger i att du testar 32-bitar och jag testat 64-bitar (även RPi3 är kapabel att köra Aarch64 och finns flera fördelar att göra detta i.m.h.o).

Allt kokar ned till

long long sum = 0; ... sum += data[c]; // int *data

long long är typiskt 64-bitars på 32-bitars och på 64-bitars CPUer. Men de flesta 32-bitars arkitekturer saknar 64-bitars register. Och här ligger hunden!

Kör man en 64-bitars arkitektur kommer även en halvmedioker kompilatorn göra följande optimering

mov ecx, dword ptr [rdi + 4*rsi] cmp ecx, 127 cmovle ecx, r8d ;; r8d innehåller alltid noll här movsxd rcx, ecx add rax, rcx

och lite enklare (tycker i alla fall jag) att förstå med Aarch64 (men är exakt samma optimering)

ldr w10, [x8], #4 cmp w10, #127 csel w10, w10, wzr, gt add x0, x0, w10, sxtw

i.e. vad dessa gör är kolla om data <=127, om så är fallet adderar man noll till summan annars adderar man data[c].

D.v.s. koden tar exakt samma väg oavsett utgång hos jämförelsen. För 32-bitars verkar bara de absolut senaste versionerna av LLVM/clang gör motsvarande optimering. Den blir inte lika effekt då det krävs två cmovle (en för låga 32-bitarna och en gör eventuell carry om de låga 32-bitarna flödar över när man adderar).

Men i de flesta fall blir det ett hopp, då kommer då gå snabbare om man sorterat p.g.a. att det ger extremt nära 100 % rätt gissning hos branchpredictor.
Med hopp får man något likt detta

mov edi, dword ptr [edx] cmp edi, 128 jl .LBB0_4 ;; hoppa över nästa 4 instruktioner om <128 mov ebx, edi sar ebx, 31 add esi, edi adc eax, ebx .LBB0_4:

och med LLVM 9.0.0 får man på 32-bitars ARM detta redan vid -O1. Inget hopp precis som för 64-bitars, adc tar hand om höga 32-bitarna

ldr r2, [r0], #4 cmp r2, #127 movle r2, r12 ;; r12 innehåller alltid noll adds lr, lr, r2 adc r3, r3, r2, asr #31

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

Cacheminnet instruktioner

@Yoshman:
Intressanta synpunkter!
En fundering om man har 32bitars program med en 64bitars CPU.
Kan man bara bruka 50% av cacheminnet?

Permalänk

Nej, det har inte med varandra att göra. Att du på en 64-bits-CPU har möjlighet att köra ett 32-bits-instruktionsset innebär att man i den moden "bara" jobbar med den lägre halvan av registren. (Ja, detta är vansinnigt förenklat.)

Vi kan jämföra med att kompilatorn kan generera binärer för X86 och för ARM (två olika arkitekturer). Du förväntar dig att programmet skall bete sig likadant oavsett om kör det på din PC eller på din RPi. Om en 64-bits-CPU (i olika moder) kan köra både ett 64-bits- och ett 32-bits-instruktionsset så kan du kompilera samma program till 32-bits och 64-bits-instruktioner. Precis som att X86- och ARM-varianterna av programmet skall bete sig likadant och producera samma resultat så skall även 32- och 64-bits-varianterna bete likadant och producera samma resultat. I fallet vi diskuterar här kommer data att vara 32-bitar i båda fallen och hanteras likadant i cachen oavsett om du kör programmet i 32-bits- eller 64-bits-smak. På samma sätt som 32-bits-binären har instruktioner som laddar 32-bitar från minnet kommer 64-bits-binären använda instruktioner som laddar 32-bitar i taget från minnet. Det är dessa operationer som cachen ser.

Vad som däremot kommer skilja är att i 64-bits-binären kommer pekare vara 64 bitar och att datastrukturer kan innehålla en del padding för att pekare, long och doubles skall ha 8-bytes-alignment. Detta gör att en del saker tar större plats i minnet (och i cachen) och att färre saker ryms i cachen. (Men å andra sidan har CPU ofta fler register i 64-bits-mode så det är en fråga om gungor och karuseller. I bland vinner man på att bygga 32-bits-binärer, ibland är det bättre med 64-bits-binärer.)

TL;DR: även 32-bitsbinärer använder cachen till 100%.

Permalänk
Skrivet av Ingetledigtnamn:

Nej, det har inte med varandra att göra. Att du på en 64-bits-CPU har möjlighet att köra ett 32-bits-instruktionsset innebär att man i den moden "bara" jobbar med den lägre halvan av registren. (Ja, detta är vansinnigt förenklat.)

Vi kan jämföra med att kompilatorn kan generera binärer för X86 och för ARM (två olika arkitekturer). Du förväntar dig att programmet skall bete sig likadant oavsett om kör det på din PC eller på din RPi. Om en 64-bits-CPU (i olika moder) kan köra både ett 64-bits- och ett 32-bits-instruktionsset så kan du kompilera samma program till 32-bits och 64-bits-instruktioner. Precis som att X86- och ARM-varianterna av programmet skall bete sig likadant och producera samma resultat så skall även 32- och 64-bits-varianterna bete likadant och producera samma resultat. I fallet vi diskuterar här kommer data att vara 32-bitar i båda fallen och hanteras likadant i cachen oavsett om du kör programmet i 32-bits- eller 64-bits-smak. På samma sätt som 32-bits-binären har instruktioner som laddar 32-bitar från minnet kommer 64-bits-binären använda instruktioner som laddar 32-bitar i taget från minnet. Det är dessa operationer som cachen ser.

Vad som däremot kommer skilja är att i 64-bits-binären kommer pekare vara 64 bitar och att datastrukturer kan innehålla en del padding för att pekare, long och doubles skall ha 8-bytes-alignment. Detta gör att en del saker tar större plats i minnet (och i cachen) och att färre saker ryms i cachen. (Men å andra sidan har CPU ofta fler register i 64-bits-mode så det är en fråga om gungor och karuseller. I bland vinner man på att bygga 32-bits-binärer, ibland är det bättre med 64-bits-binärer.)

TL;DR: även 32-bitsbinärer använder cachen till 100%.

Tack för denna förklaring. Jag ser att för x86 brukar 64bitars prog vara ca 20% större.
Men det är bara Apple som har gjort en riktigt stor L1 cache 128kB+128kb för 64 bitar.

Permalänk
Skrivet av Greyguy1948:

Tack för denna förklaring. Jag ser att för x86 brukar 64bitars prog vara ca 20% större.
Men det är bara Apple som har gjort en riktigt stor L1 cache 128kB+128kb för 64 bitar.

Jo, X86 instruktionskoding är lite speciell. För att göra 64-bits-versioner av instruktioner så lägger man typiskt på ett prefix (en eller flera bytes före instruktionen) som gör att 64-bitsprogrammen blir lite större.

Beträffande storleken på L1 så är det en balansgång. Ju större cachen blir desto längre tid tar en access. Så det är inte säkert att det blir snabbare bara för att den görs större.

Permalänk

PipeLen av Igor Pavlov

https://www.7-cpu.com/utils.html

PipeLen borde också passa i tråden. En version för 32bit och en för 64bit X86
Mest krävande är tydligen test64 och test65.
Min Jaguar är "worst case" med 42 och 31 "miss prediction pipeline" (normalt 15)
Sandy Bridge 26 och 25 (normalt 18)
Ryzen 1700 8 och 15 (normalt 15)
Pentium3@700 3 och 5 (normalt 5)

64 bitar ökar resultatet med ca 2 steg.

Permalänk
Skrivet av Greyguy1948:

https://www.7-cpu.com/utils.html

PipeLen borde också passa i tråden. En version för 32bit och en för 64bit X86
Mest krävande är tydligen test64 och test65.
Min Jaguar är "worst case" med 42 och 31 "miss prediction pipeline" (normalt 15)
Sandy Bridge 26 och 25 (normalt 18)
Ryzen 1700 8 och 15 (normalt 15)
Pentium3@700 3 och 5 (normalt 5)

64 bitar ökar resultatet med ca 2 steg.

Någon som provat detta på sin x86?
Beräkningarna är inte lätt att tolka men en sak är lättare att se:
Om en branch tas varannan gång så kan dagens CPUer lära sig detta.
Därför blir Random betydligt fler cykler.
Så är det inte på Pentium/100 mHz (P54C) som saknar branch prediction.

Permalänk

Effekten av mod

Jag tar upp en gammal tråd så får vi se om nån är intresserad.
Jag har gjort om koden med några mod eller som det heter i c/c++ %

#include <algorithm> #include <ctime> #include <iostream> int main() { // Generate data const unsigned arraySize = 32768; int data[arraySize]; int a1,a2,a3,a4,a5,a6,a7,a8,a9,a10; for (unsigned c = 0; c < arraySize; ++c) data[c] = std::rand() % 256; // !!! With this, the next loop runs faster // std::sort(data, data + arraySize); // Test clock_t start = clock(); long long sum = 0; for (unsigned i = 0; i < 10000; ++i) { // Primary loop for (unsigned c = 0; c < arraySize; ++c) { if (data[c] >= 128) sum += data[c]; a1=data[c] % 10; if (a1 < 1) sum += a1; a2=data[c] % 9; if (a2 < 1) sum += a2; a3=data[c] % 8; if (a3 < 1) sum += a3; a4=data[c] % 7; if (a4 < 1) sum += a4; a5=data[c] % 6; if (a5 < 1) sum += a5; a6=data[c] % 5; if (a6 < 1) sum += a6; a7=data[c] % 4; if (a1 < 1) sum += a7; a8=data[c] % 19; if (a8 < 1) sum += a8; a9=data[c] % 18; if (a9 < 1) sum += a9; a10=data[c] % 17; if (a10 < 1) sum += a10; } } double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC; std::cout << "time in sec = "<< elapsedTime << std::endl; std::cout << "sum = " << sum << std::endl; }

De här programmet ger tydlig skillnad på tre Raspberry Pi varianter:
Utan opt Pi2=127.7sek, Pi3=75,5sek, Pi4=36,9sek
Med -O3 Pi2=39,1sek, Pi3=27,5sek, Pi4=15,6sek

Permalänk

OK, I'll bite...

Vad vill du egentligen påvisa? Att programmet tar olika lång tid att köra på olika hårdvara är inte särskilt konstigt. Inte heller att den optimerade koden går fortare än icke-optimerad. Det känns inte som att uppsnabbningen är direkt kopplad till branch prediction.

Permalänk

Nej det är kanske inte är enbart olika branch prediction. Även med enbart en tråd gör vissa CPUer flera saker parallellt.
Om man räknar om allt till att klockan går i 1 GHz kanske det blir tydligare.
No opt(sek) Raspberry Pi2=114,9 Raspberry Pi 3=90,6, Raspberry Pi 4=55,4, Goldmont Plus=48,3 Core i7-8700=35,7 Jaguar=61,2
-O3(sek) Raspberry Pi2=35,2 Raspberry Pi 3=33, Raspberry Pi 4=23,6, Goldmont Plus=21,3 Core i7-8700=10,3 Jaguar=23,4