Kodėl greičiau apdoroti surūšiuotą masyvą nei nerūšiuota masyvas?

Čia yra C + + kodo, kuris atrodo labai savotiškas, gabalas. Dėl keistos priežasties duomenų rūšiavimas stebuklingai daro kodą beveik šešis kartus greičiau.

 import java.util.Arrays; import java.util.Random; public class Main { public static void main(String[] args) { // Generate data int arraySize = 32768; int data[] = new int[arraySize]; Random rnd = new Random(0); for (int c = 0; c < arraySize; ++c) data[c] = rnd.nextInt() % 256; // !!! With this, the next loop runs faster Arrays.sort(data); // Test long start = System.nanoTime(); long sum = 0; for (int i = 0; i < 100000; ++i) { // Primary loop for (int c = 0; c < arraySize; ++c) { if (data[c] >= 128) sum += data[c]; } } System.out.println((System.nanoTime() - start) / 1000000000.0); System.out.println("sum = " + sum); } } 

Su šiek tiek panašiu, bet mažiau ekstremaliu rezultatu.


Mano pirmoji mintis buvo ta, kad rūšiavimas atneša duomenis į talpyklą, bet tada aš maniau, kaip tai kvaila, nes masyvas buvo sukurtas.

  • Kas vyksta
  • Kodėl rūšiuojama masyvas apdorojamas greičiau nei nerūšiuota masyvas?
  • Kodas apibendrina keletą nepriklausomų terminų, o užsakymas nesvarbu.
22512
27 июня '12 в 16:51 2012-06-27 16:51 GManNickG yra nustatytas birželio 27 d. 12 val. 4:51 2012-06-27 16:51
@ 26 atsakymai

Jūs esate nukrypimo nuo šakos auka.


Kas yra šakos prognozavimas?

Apsvarstykite geležinkelio sankryžą:

2019

27 июня '12 в 16:56 2012-06-27 16:56 atsakymą Mysticial pateikė birželio 27 d. 12 d. 4:56 2012-06-27 16:56

Filialų prognozavimas.

Su rūšiuojamu masyvu data[c] >= 128 būklė yra pirmoji verte reikšmių eilutėje, o tada tampa true visoms vėlesnėms vertėms. Tai lengva prognozuoti. Su nerūšiuota masyvu mokate už filialų išlaidas.

3815
27 июня '12 в 16:54 2012-06-27 16:54 atsakymą pateikė Danielis Fišeris birželio 27 d. 12 val. 15.45 val. 2012-06-27 16:54

Priežastis, dėl kurios našumas gerokai pagerėja, kai rūšiuojami duomenys yra tai, kad nuobauda už šakos prognozavimą yra pašalinta, kaip Mysticial puikiai paaiškino.

Dabar, jei pažvelgsime į kodą

 if (data[c] >= 128) sum += data[c]; 

mes galime pastebėti, kad šio konkretaus, if... else... filialas reiškia kažką pridėti, kai įvykdoma sąlyga. Šis šakos tipas gali būti lengvai konvertuojamas į sąlyginį operatorių, kuris bus sukompiliuotas į sąlyginį judėjimo nurodymą: cmovl , x86 sistemoje. Filialas ir todėl galimas nuobaudos už filialo prognozavimą yra pašalintos.

Todėl C++ , C++ , operatorius, kuris bus tiesiogiai (be optimizavimo), sudarytas į sąlyginį perleidimo nurodymą x86 yra trivietis operatorius ...?... :... ...?... :... Todėl perrašome pirmiau pateiktą pareiškimą lygiaverčiu:

 sum += data[c] >=128 ? data[c] : 0; 

Išlaikydami skaitymą, galime patikrinti pagreičio koeficientą.

„Intel Core i7-2600K @ 3.4 GHz“ ir „Visual Studio 2010“ leidimo režimo atskaitos testas (formatas kopijuojamas iš „Mysticial“):

x86

 // Branch - Random seconds = 8.885 // Branch - Sorted seconds = 1.528 // Branchless - Random seconds = 3.716 // Branchless - Sorted seconds = 3.71 

x64

 // Branch - Random seconds = 11.302 // Branch - Sorted seconds = 1.830 // Branchless - Random seconds = 2.736 // Branchless - Sorted seconds = 2.737 

Rezultatas yra patikimas keliuose bandymuose. Mes gauname reikšmingą pagreitį, kai šakotojo rezultatas yra nenuspėjamas, tačiau šiek tiek kenčiame, kai tai yra nuspėjama. Iš tiesų, naudojant sąlyginį judėjimą, našumas išlieka tas pats, nepriklausomai nuo duomenų modelio.

Dabar pažvelkime, tyrinėdami sukurtus x86 kūrinius. Paprastumo dėlei naudojame dvi funkcijas max2 ir max2 .

max1 naudoja sąlyginį filialą, if... else... :

 int max1(int a, int b) { if (a > b) return a; else return b; } 

max2 naudoja max2 operatorių ...?... :... ...?... :... :

 int max2(int a, int b) { return a > b ? a : b; } 

„X86-64“ kompiuteryje GCC -S stato toliau pateiktą rinkinį.

 :max1 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax cmpl -8(%rbp), %eax jle .L2 movl -4(%rbp), %eax movl %eax, -12(%rbp) jmp .L4 .L2: movl -8(%rbp), %eax movl %eax, -12(%rbp) .L4: movl -12(%rbp), %eax leave ret :max2 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax cmpl %eax, -8(%rbp) cmovge -8(%rbp), %eax leave ret 

max2 naudoja daug mažesnį kodą dėl „ cmovge naudojimo. Tačiau tikrasis pelnas yra tas, kad max2 neapima perėjimų max2 , jmp , o tai gali lemti reikšmingą max2 našumą, jei numatomas rezultatas yra max2 .

Tad kodėl sąlyginis judėjimas veikia geriau?

Tipiškame x86 procesoriuje x86 vykdymas yra padalintas į kelis etapus. Apibendrinant, turime skirtingą aparatūrą skirtingiems etapams. Todėl nereikia laukti, kol baigsis vieno nurodymo pabaiga. Tai vadinama vamzdynu .

Filialų suskirstymo atveju kitas nurodymas nustatomas pagal ankstesnįjį, todėl negalime atlikti vamzdynų. Turime laukti arba numatyti.

Esant sąlyginiam judėjimui, sąlyginio perkėlimo komandos vykdymas yra suskirstytas į kelis etapus, tačiau ankstesni etapai, pvz., Fetch and Decode , nepriklauso nuo ankstesnio nurodymo rezultato; tik galutiniams etapams reikia rezultatų. Taigi, laukiame vieno instrukcijų vykdymo laiko. Štai kodėl sąlyginė perkėlimo versija yra lėčiau nei filialas, kai prognozavimas yra paprastas.

Išsamiai tai paaiškina knyga „ Kompiuterių sistemos: programuotojo perspektyva“, antrasis leidimas . Galite patikrinti 3.6.6 skyrių „Sąlyginiai judesio nurodymai“, visą 4 skyrių „Procesoriaus architektūra“ ir 5.11.2 skirsnį, skirtą specialioms operacijoms, skirtoms numatomoms sankcijoms ir klaidingai numatyti.

Kartais kai kurie šiuolaikiniai kompiliatoriai gali optimizuoti savo kodą pastatymui su didesniu našumu, kartais kai kurie kompiliatoriai negali (šis kodas naudoja savo „Visual Studio“ kompiliatorių). Žinant skirtumo tarp šakos ir sąlyginio judėjimo skirtumus, kai jis yra nenuspėjamas, gali padėti mums parašyti kodą su geresniu našumu, kai scenarijus tampa toks sudėtingas, kad kompiliatorius negali jų automatiškai optimizuoti.

3064
28 июня '12 в 5:14 2012-06-28 05:14 atsakymą pateikė „ WiSaGaN“ birželio 28, 12 d. 5:14 val. 2012-06-28 05:14

Jei domina dar daugiau optimizavimo, kurį galima atlikti su šiuo kodu, apsvarstykite šiuos dalykus:

Nuo pradinio ciklo:

 for (unsigned i = 0; i < 100000; ++i) { for (unsigned j = 0; j < arraySize; ++j) { if (data[j] >= 128) sum += data[j]; } } 

Su ciklo permutacija, galime saugiai pakeisti šį ciklą į:

 for (unsigned j = 0; j < arraySize; ++j) { for (unsigned i = 0; i < 100000; ++i) { if (data[j] >= 128) sum += data[j]; } } 

Tada galite matyti, kad if sąlyga yra pastovi vykdant kilpą i , todėl galite ištraukti, if :

 for (unsigned j = 0; j < arraySize; ++j) { if (data[j] >= 128) { for (unsigned i = 0; i < 100000; ++i) { sum += data[j]; } } } 

Tada pamatysite, kad vidinė kilpa gali būti sutraukta į vieną išraišką, darant prielaidą, kad slankiojo kablelio modelis leidžia (pvz., / Fp: greitas)

 for (unsigned j = 0; j < arraySize; ++j) { if (data[j] >= 128) { sum += data[j] * 100000; } } 

Tai 100 000 kartų greitesnis nei anksčiau.

2105
03 июля '12 в 5:25 2012-07-03 05:25 atsakymas pateikiamas vulcan raven liepos 3 d., 12 val

Be abejo, kai kurie iš mūsų bus suinteresuoti identifikuoti kodą, kuris yra problemiškas CPU prognozavimo procesoriui. cachegrind įrankis turi šakos prognozavimo šakos sintaksę, kuri aktyvuojama naudojant --branch-sim=yes . Vykdydami šio klausimo pavyzdžius, išorinių kilpų skaičius, sumažintas iki 10 000 ir sudarytas su g++ , duoda šiuos rezultatus:

Rūšiuoti pagal:

 ==32551== Branches: 656,645,130 ( 656,609,208 cond + 35,922 ind) ==32551== Mispredicts: 169,556 ( 169,095 cond + 461 ind) ==32551== Mispred rate: 0.0% ( 0.0% + 1.2% ) 

Nerūšiuota:

 ==32555== Branches: 655,996,082 ( 655,960,160 cond + 35,922 ind) ==32555== Mispredicts: 164,073,152 ( 164,072,692 cond + 460 ind) ==32555== Mispred rate: 25.0% ( 25.0% + 1.2% ) 

Pereinant prie cg_annotate sukurtos linijinės išvesties, matome atitinkamą ciklą:

Rūšiuoti pagal:

  Bc Bcm Bi Bim 10,001 4 0 0 for (unsigned i = 0; i < 10000; ++i) . . . . { . . . . // primary loop 327,690,000 10,016 0 0 for (unsigned c = 0; c < arraySize; ++c) . . . . { 327,680,000 10,006 0 0 if (data[c] >= 128) 0 0 0 0 sum += data[c]; . . . . } . . . . } 

Nerūšiuota:

  Bc Bcm Bi Bim 10,001 4 0 0 for (unsigned i = 0; i < 10000; ++i) . . . . { . . . . // primary loop 327,690,000 10,038 0 0 for (unsigned c = 0; c < arraySize; ++c) . . . . { 327,680,000 164,050,007 0 0 if (data[c] >= 128) 0 0 0 0 sum += data[c]; . . . . } . . . . } 

Tai leidžia lengvai identifikuoti probleminę eilutę - nesirinktoje versijoje, if (data[c] >= 128) sukelia Bcm neteisingai prognozuojamus sąlyginius filialus ( Bcm ) kaip dalį „cachegrind“ filialo prognozės modelio, o tik skambina 10 006 rūšiuojamuose versija.


Arba, Linux, galite naudoti našumo skaitiklių posistemį, kad atliktumėte tą pačią užduotį, tačiau naudodami savo našumą naudodami CPU skaitiklius.

 perf stat ./sumtest_sorted 

Rūšiuoti pagal:

  Performance counter stats for './sumtest_sorted': 11808.095776 task-clock # 0.998 CPUs utilized 1,062 context-switches # 0.090 K/sec 14 CPU-migrations # 0.001 K/sec 337 page-faults # 0.029 K/sec 26,487,882,764 cycles # 2.243 GHz 41,025,654,322 instructions # 1.55 insns per cycle 6,558,871,379 branches # 555.455 M/sec 567,204 branch-misses # 0.01% of all branches 11.827228330 seconds time elapsed 

Nerūšiuota:

  Performance counter stats for './sumtest_unsorted': 28877.954344 task-clock # 0.998 CPUs utilized 2,584 context-switches # 0.089 K/sec 18 CPU-migrations # 0.001 K/sec 335 page-faults # 0.012 K/sec 65,076,127,595 cycles # 2.253 GHz 41,032,528,741 instructions # 0.63 insns per cycle 6,560,579,013 branches # 227.183 M/sec 1,646,394,749 branch-misses # 25.10% of all branches 28.935500947 seconds time elapsed 

Jis taip pat gali sukurti šaltinio kodo komentarus su išardymu.

 perf record -e branch-misses ./sumtest_unsorted perf annotate -d sumtest_unsorted 
  Percent | Source code  Disassembly of sumtest_unsorted ------------------------------------------------ ... : sum += data[c]; 0.00 : 400a1a: mov -0x14(%rbp),%eax 39.97 : 400a1d: mov %eax,%eax 5.31 : 400a1f: mov -0x20040(%rbp,%rax,4),%eax 4.60 : 400a26: cltq 0.00 : 400a28: add %rax,-0x30(%rbp) ... 

Išsamesnės informacijos ieškokite naudojimo instrukcijoje .

1758 m
12 окт. atsakymas duodamas caf 12 oct. 2012-10-12 08:53 '12 at 8:53 2012-10-12 08:53

Aš perskaičiau šį klausimą ir jo atsakymus, ir manau, kad atsakymas nėra.

Įprastas būdas pašalinti šakos prognozavimą, kuris, mano nuomone, ypač gerai veikia valdomomis kalbomis, yra ieškoti lentelės, o ne naudoti šakotuvus (nors šiuo atveju tai neperžiūrėjau).

Šis požiūris apskritai veikia, jei:

  1. tai yra nedidelė lentelė ir greičiausiai bus talpinama procesoriuje, ir
  2. Jūs dirbate gana siauroje kilpoje ir / arba procesorius gali iš anksto įkelti duomenis.

Fonas ir kodėl

Procesoriaus požiūriu atmintis yra lėta. Norint kompensuoti greičio skirtumą, į procesorių įterpiamos kelios talpyklos (L1 / L2 talpyklos). Taigi įsivaizduokite, kad darote savo gerus skaičiavimus ir sužinosite, kad jums reikia atminties. Procesorius atliks apkrovos operaciją ir įkelia dalį atminties į talpyklą, o tada naudodami talpyklą atliks likusius skaičiavimus. Kadangi atmintis yra palyginti lėta, ši „apkrova“ sulėtins jūsų programą.

Kaip ir filialo prognozavimas, jis buvo optimizuotas „Pentium“ procesoriuose: procesorius prognozuoja, kad reikia įkelti kai kuriuos duomenis, ir bando juos įkelti į talpyklą, kol operacija iš tikrųjų patenka į talpyklą. Kaip jau matėme, filialo prognozavimas kartais baisiai neteisingas - blogiausiu atveju jums reikia grįžti ir iš tikrųjų laukti, kol atmintis bus laikoma amžinai ( kitaip tariant, nesėkminga šakos prognozė yra bloga, atminties apkrova po šakos prognozavimo gedimo yra baisi! ).

Laimei, jei atminties prieigos schema yra nuspėjama, procesorius ją įkelia į savo sparčiąją talpyklą ir viskas gerai.

Pirmas dalykas, kurį turime žinoti, yra mažai? Nors mažesnis dydis paprastai yra geresnis, nykščio taisyklė yra laikytis paieškos lentelių <= 4096 baitų. Kaip viršutinė riba: jei jūsų atskaitos lentelė yra didesnė nei 64 KB, ji tikriausiai turėtų būti peržiūrėta.

Sukurkite lentelę

Taigi, sužinojome, kad galime sukurti nedidelį stalą. Kitas dalykas yra pakeisti paieškos funkciją. Paieškos funkcijos paprastai yra mažos funkcijos, kuriose naudojamos kelios pagrindinės sveikojo skaičiaus operacijos (ir, arba, xor, shift, add, Remove ir galbūt daugybos). Norite, kad jūsų indėlis būtų išverstas ieškant tam tikro „unikalaus rakto“ jūsų lentelėje, o tai tiesiog suteiks jums atsakymą į visą norimą darbą.

Šiuo atveju:> = 128 reiškia, kad mes galime išsaugoti vertę, <128 reiškia, kad mes ją atsikratysime. Paprasčiausias būdas tai padaryti yra naudoti „IR“: jei jį išsaugosime, mes ir tai yra su 7FFFFFFF; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и большим количеством 7FFFFFFFF годов.

Управляемые языки