Bakit mas mabilis itong iproseso ang isang nakaayos na array kaysa sa isang unsorted array?

Narito ang isang piraso ng C + + code na tila napaka kakaiba. Para sa i>

 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); } } 

Sa isang medyo katulad, ngunit mas kaunting resulta.


Ang aking unang pag-iisip ay ang pagsasama-sama ay nagdadala ng data sa cache, ngunit pagkatapos ay naisip ko kung gaano kabuktot ito, dahil ang array ay nabuo na lamang.

  • Ano ang nangyayari
  • Bakit ang pinagsanib na array ay naproseso nang mas mabilis kaysa sa unsorted array?
  • Ang code ay nagbubuod sa i>
22512
27 июня '12 в 16:51 2012-06-27 16:51 Ang GManNickG ay nakatakda sa Hunyo 27 '12 sa 4:51 2012-06-27 16:51
@ 26 sagot

Ang dahilan na ang pagpapabuti ng pagganap ay kapansin-pansing kapag ang pag-uuri ng data ay na ang parusa para sa prediksyon ng sangay ay inalis, gaya ng mistu> ganap na ipinaliwanag.

Ngayon, kung titingnan natin ang code

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

maaari naming makita na ang kahulugan ng partikular na ito if... else... sangay ay magdaragdag ng isang bagay kapag natugunan ang kalagayan. Maaaring madaling ma-convert ang ganitong uri ng sangay sa isang kondisyong operator, na kung saan ay cmovl sa isang kondisyon na pagtuturo ng kilusan: cmovl , sa x86 system. Ang sangay at, samakatuwid, ang potensyal na parusa para sa predicting branch ay aalisin.

Sa C , samakatuwid, ang C++ , ang operator na direkta (nang wa>x86 ay isang ternary operator ...?... :... ...?... :... Samakatuwid, isulat namin ang pahayag sa itaas na katumbas:

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

Sa pamamagitan ng pagpapanatili ng pagiging madaling mabasa, maaari naming suriin ang acceleration factor.

Para sa Intel Core i7 -2600K @ 3.4 GHz at Visual Studio 2010 release mode reference test (format na kinopya mula sa 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 

Ang resulta ay maaasahan sa maraming pagsubok. Makakakuha kami ng makabuluhang pag-accelerate kapag ang resulta ng pagsasanib ay hindi nahuhulaang, subalit nakakaranas kami ng kaunti kung ito ay predictable. Sa katunayan, kapag gumagamit ng isang kondisyong paglipat, ang pagganap ay nananatiling pareho anuman ang pattern ng data.

Ngayon tingnan natin, pagtuklas sa x86 build nila. Para sa pagiging simple, ginagamit namin ang dalawang mga function na max1 at max2 .

Ginagamit ng max1 ang conditional branch, if... else... :

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

ginagamit ng max2 ang ternary operator ...?... :... ...?... :... :

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

Sa x86-64 computer, ang GCC -S nagtatayo ng pagpupulong sa ibaba.

 :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 gumagamit ng mas mababa code dahil sa ang paggamit ng cmovge pagtuturo. Subalit ang tunay na pakinabang ay ang max2 ay hindi kasama ang mga transisyon sa max2 , jmp , na maaaring humantong sa makabuluhang pagganap ng max2 kung ang hinulaang resulta ay max2 .

Kaya bakit mas mahusay ang paglipat ng kondisyon?

Sa isang tipikal na x86 processor, ang pagpapatupad ng x86 ay nahahati sa maraming yugto. Halos nagsasalita, mayroon kaming iba't ibang hardware para sa iba't ibang yugto. Samakatuwid, hindi namin kai>pipelining .

Sa kaso ng sumasanga, ang susunod na pagtuturo ay natutukoy ng nakaraang isa, kaya hindi namin maaaring magsagawa ng pipelining. Dapat tayong maghintay o hulaan.

Sa kaso ng isang kondisyonal na paglipat, ang pagpapatupad ng command conditional move ay nahahati sa maraming yugto, ngunit ang mga naunang yugto, tulad ng Fetch at Decode , ay hindi umaasa sa resulta ng naunang pagtuturo; tanging ang mga huling yugto ay nangangai>

Ang aklat Computer Systems: Isang Prospect para sa isang Programmer, ikalawang edisyon, nagpapaliwanag na ito nang detalyado. Maaari mong suriin ang Seksiyon 3.6.6 para sa Conditional Movement Instructions, ang buong Kabanata 4 para sa Processor Architecture, at Seksyon 5.11.2 para sa espesyal na pangangasiwa para sa Mga Hula sa Hula at Maling Hula.

Minsan ay maaaring i-optimize ng i>

3064
28 июня '12 в 5:14 2012-06-28 05:14 ang sagot ay ibinigay ng WiSaGaN Hunyo 28, '12 sa 5:14 ng umaga 2012-06-28 05:14

Kung interesado ka sa mas maraming pag-optimize na maaaring gawin sa code na ito, isaa>

Simula mula sa unang pag-ikot:

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

Sa pag-uulit ng pag-ikot, maaari naming ligtas na baguhin ang siklo na ito sa:

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

Pagkatapos ay makikita mo na if kundisyon ay pare-pareho sa pagpapatupad ng loop i , upang maaari mong bunutin if out:

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

Pagkatapos ay makikita mo na ang panloob na loop ay maaaring gumuho sa isang solong expression, ipagpapalagay na ang modelo lumulutang point ay nagbibigay-daan ito (halimbawa, / fp: mabilis)

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

Ito ay 100,000 beses na mas mabilis kaysa sa dati.

2105
03 июля '12 в 5:25 2012-07-03 05:25 ang sagot ay ibinigay sa bulkan na uwak Hulyo 3, '12 sa 5:25 ng umaga 2012-07-03 05:25

Wa>cachegrind ay may syntax sangay ng branch predictor na na-activate gamit ang --branch-sim=yes flag. Tumatakbo ito sa pamamagitan ng mga halimbawa sa tanong na ito, ang bi>g++ , ay nagbibigay ng mga sumusunod na resulta:

Ayusin ayon sa:

 ==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% ) 

Unsorted:

 ==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% ) 

Pagbabalik sa linear na output na nilikha ng cg_annotate , nakikita natin ang siklo na cg_annotate uusapan:

Ayusin ayon sa:

  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]; . . . . } . . . . } 

Unsorted:

  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]; . . . . } . . . . } 

Pinapayagan ka nitong madaling tukuyin ang linya ng problema - sa isang unsorted na bersyon, if (data[c] >= 128) nagiging sanhi ng Bcm hindi tama ang hinulaang mga kondisyong sanga ( Bcm ) bi>


Bi>

 perf stat ./sumtest_sorted 

Ayusin ayon sa:

  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 

Unsorted:

  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 

Maaari rin itong lumikha ng mga annotation ng source code na may disassembly.

 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) ... 

Para sa mga detalye, tingnan ang manual ng pagganap .

1758
12 окт. tugon na ibinigay na cafe 12 Oktubre. 2012-10-12 08:53 '12 sa 8:53 2012-10-12 08:53

Nabasa ko >

Ang karaniwan na paraan upang maalis ang prediksyon ng sangay, na tila nakapagtatrabaho ako nang mahusay sa mga pinamamahalaang wika, ay upang maghanap ng talahanayan sa halip na gamitin ang sumasanga (bagaman sa kasong ito ay hindi ko ito nasuri).

Ang pamamaraan na ito ay gumagana sa pangkalahatan kung:

  1. ito ay isang maliit na talahanayan at malamang na mai-cache sa processor, at
  2. Nagtatrabaho ka sa isang makitid na loop at / o ang preload ay maaaring preload data.

Background at bakit

Mula sa punto ng view ng processor, ang iyong memorya ay mabagal. Upang matugunan ang pagkakaiba sa bilis, ang isang pares ng mga cache (L1 / L2 cache) ay binuo sa iyong processor. Kaya isipin na ginagawa mo ang iyong mahusay na kalkulasyon at malaman na kai>

Tulad ng prediksyon ng sangay, na-optimize ito sa mga processor ng Pentium: hinuhulaan ng processor na kai>madaling salita: ang isang hindi matagumpay na prediksyon ng sangay ay masama, ang memorya ng pagkarga pagkatapos ng kabiguan ng prediksiyon ng sangay ay kakila-kilabot! ).

Sa kabutihang palad para sa amin, kung ang memory access scheme ay predictable, ang processor ay i-load ito sa mabilis cache nito, at lahat ng bagay ay pagmultahin.

Ang unang bagay na dapat nating malaman ay may maliit? Kahit na ang mas maliit na sukat ay karaniwang mas mahusay, ang panuntunan ng hinlalaki ay upang manatili sa mga lookup table <= 4096 bytes. Bi>

Buuin ang mesa

Kaya, nalaman namin na maaari kaming lumikha ng isang maliit na talahanayan. Ang susunod na gawin ay upang palitan ang pag-andar ng paghahanap. Ang mga pag-andar ng paghahanap ay kadalasang maliliit na function na gumagamit ng i>

Sa kasong ito:> = 128 ay nangangahulugang maaari naming i-save ang halaga, <128 ay nangangahulugan na mapupuksa namin ito. Ang pinakamadaling paraan upang gawin ito ay ang paggamit ng 'AT': kung i-save namin ito, kami At ito ay may 7FFFFFFF; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и большим количеством 7FFFFFFFF годов.

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