Чаму хутчэй апрацоўваць адсартаваны масіў, чым малокомплектных масіў?

Вось кавалак кода на З ++, які здаецца вельмі своеасаблівым. Па нейкай дзіўнай прычыне сартаванне дадзеных цудам робіць код амаль у шэсць разоў хутчэй.

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

З некалькі падобным, але менш экстрэмальным вынікам.


Мая першая думка заключалася ў тым, што сартаванне прыводзіць дадзеныя ў кэш, але потым я падумаў, як гэта глупства, таму што масіў толькі што згенераваны.

  • Што адбываецца?
  • Чаму хутчэй апрацоўваецца адсартаваны масіў, чым малокомплектных масіў?
  • Код сумуе некаторыя незалежныя тэрміны, і парадак не мае значэння.
22512
27 июня '12 в 16:51 2012-06-27 16:51 зададзены GManNickG 27 чэрвеня '12 у 16:51 2012-06-27 16:51
@ 26 адказаў

Вы з'яўляецеся ахвярай адхіленне ад галінавання .


Што такое прадказанне за галіны?

Разгледзім чыгуначны вузел:

2019

Прагназаванне галін.

З Адсартавана масівам ўмова data[c] >= 128 з'яўляецца першым false для радкі значэнняў, а затым становіцца true для ўсіх наступных значэнняў. Гэта лёгка прадказаць. Пры малокомплектных масіве вы плаціце за выдаткі на разгалінаванне.

3815
27 июня '12 в 16:54 2012-06-27 16:54 адказ дадзены Daniel Fischer 27 чэрвеня '12 у 16:54 2012-06-27 16:54

Прычына, па якой прадукцыйнасць рэзка падвышаецца пры сартаванні дадзеных, заключаецца ў тым, што штраф за прадказанне галінаванняў ліквідаваны, як выдатна растлумачана ў адказе Mysticial .

Цяпер, калі мы паглядзім на код

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

мы можам выявіць, што сэнс гэтай канкрэтнай галінкі if... else... складаецца ў тым, каб дадаць нешта, калі ўмова выканана. Гэты тып галінкі можа быць лёгка пераўтвораны ў аператар ўмоўнага перамяшчэння, які будзе скампіляваны ў інструкцыю ўмоўнага перамяшчэння: cmovl , у сістэме x86 . Галінаванне і, такім чынам, патэнцыйнае пакаранне за прадказанне галінавання выдаляюцца.

У C , такім чынам, C++ , аператар, які будзе наўпрост (без якой-небудзь аптымізацыі) кампілявацца ў інструкцыю ўмоўнага перамяшчэння ў x86 , з'яўляецца трынітарнай аператарам ...?... :... ...?... :... Таму мы перапісваем прыведзенае вышэй зацвярджэнне ў эквівалентнае:

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

Падтрымліваючы чытэльнасць, мы можам праверыць каэфіцыент паскарэння.

Для Intel Core i7 -2600K @ 3,4 Ггц і рэжыму выпуску Visual Studio 2010 эталонны тэст (фармат скапіяваны з 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 

Вынік з'яўляецца надзейным ў некалькіх тэстах. Мы атрымліваем значнае паскарэнне, калі вынік галінавання непрадказальны, але мы крыху пакутуем, калі ён прадказальны. Фактычна, пры выкарыстанні ўмоўнага перамяшчэння прадукцыйнасць застаецца аднолькавай незалежна ад шаблону дадзеных.

Зараз давайце паглядзім больш падрабязна, даследуючы зборку x86 яны генеруюць. Для прастаты мы выкарыстоўваем дзве функцыі max1 і max2 .

max1 выкарыстоўвае ўмоўную галіна, if... else... :

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

max2 выкарыстоўвае трынітарнай аператар ...?... :... ...?... :... :

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

На кампутары x86-64 GCC -S стварае зборку ніжэй.

 :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 выкарыстоўвае нашмат менш кода з-за выкарыстання інструкцыі cmovge . Але рэальны выйгрыш у тым, што max2 не ўключае пераходы па max2 , jmp , што можа прывесці да значнага max2 прадукцыйнасці, калі прагназуемы вынік max2 .

Дык чаму ж умоўны ход працуе лепш?

У тыповым працэсары x86 выкананне інструкцыі дзеліцца на некалькі этапаў. Груба кажучы, у нас розныя апаратныя сродкі для розных этапаў. Таму нам не трэба чакаць заканчэння адной інструкцыі, каб пачаць новую. Гэта называецца канвеернай апрацоўкай .

У выпадку галінавання наступная інструкцыя вызначаецца папярэдняй, таму мы не можам выканаць конвейеризацию. Мы павінны альбо чакаць, альбо прадказваць.

У выпадку ўмоўнага перамяшчэння выкананне каманды ўмоўнага перамяшчэння дзеліцца на некалькі этапаў, але больш раннія этапы, такія як Fetch і Decode , не залежаць ад выніку папярэдняй інструкцыі; толькі апошнія этапы маюць патрэбу ў выніку. Такім чынам, мы чакаем частку часу выканання адной інструкцыі. Вось чаму версія ўмоўнага перамяшчэння павольней, чым галіна, калі прадказанне лёгка.

Кніга " Камп'ютэрныя сістэмы: перспектыва для праграміста", другое выданне, тлумачыць гэта падрабязна. Вы можаце праверыць Раздзел 3.6.6 для ўмоўна Інструкцый Перамяшчэння, усю Кіраўніка 4 для архітэктуры працэсара і Раздзел 5.11.2 для спецыяльнай апрацоўкі для Штрафаў Прадказанні і памылкова прадказанні.

Часам некаторыя сучасныя кампілятары могуць аптымізаваць наш код для зборкі з большай прадукцыйнасцю, часам некаторыя кампілятары не могуць (разгляданы код выкарыстоўвае уласны кампілятар Visual Studio). Ведаючы розніцу ў прадукцыйнасці паміж галінаваннем і умоўнай перамяшчэннем, калі ён непрадказальны, можа дапамагчы нам напісаць код з лепшай прадукцыйнасцю, калі сцэнар становіцца настолькі складаным, што кампілятар не можа іх аптымізаваць аўтаматычна.

3064
28 июня '12 в 5:14 2012-06-28 05:14 адказ дадзены WiSaGaN 28 чэрвеня '12 у 05:14 2012-06-28 05:14

Калі вам цікава, што яшчэ больш аптымізацый, якія могуць быць зроблены з гэтым кодам, разгледзьце наступнае:

Пачынаючы з зыходнага цыкла:

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

З перастановай цыклу мы можам бяспечна змяніць гэты цыкл на:

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

Затым вы можаце бачыць, што ўмова if з'яўляецца пастаянным падчас выканання цыкла i , таму вы можаце выцягнуць if Out:

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

Затым вы ўбачыце, што ўнутраны цыкл можа быць згорнуты ў адно адзінае выраз, калі выказаць здагадку, што мадэль з якая плавае коскі дапускае яе (напрыклад, / fp: fast)

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

Гэта на 100 000 разоў хутчэй, чым раней

2105
03 июля '12 в 5:25 2012-07-03 05:25 адказ дадзены vulcan raven 03 ліпеня '12 ў 05:25 2012-07-03 05:25

Несумненна, некаторыя з нас будуць цікавіцца спосабамі ідэнтыфікацыі кода, які з'яўляецца праблематычным для працэсара-прадказальніка CPU. Інструмент Valgrind cachegrind мае сінтаксіс галінавання-прадказальніка, які актывуецца з дапамогай сцяга --branch-sim=yes . Запусціўшы яго па прыкладах ў гэтым пытанні, колькасць знешніх цыклаў, паменшаных да 10000 і скампіляваных з дапамогай g++ , дае наступныя вынікі:

Сартаванне:

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

Згарнуўшы ў лінейны выснову, створаны cg_annotate , мы бачым для разгляданага цыкла:

Сартаванне:

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

Гэта дазваляе вам лёгка ідэнтыфікаваць праблемную радок - у малокомплектных версіі радок if (data[c] >= 128) выклікае 164 050 007 няслушна прадказаны умоўных галін ( Bcm ) у рамках мадэлі галінавання-прадказальніка cachegrind, тады як яна выклікае толькі 10 006 у адсартаванай версіі.


У якасці альтэрнатывы, у Linux вы можаце выкарыстоўваць падсістэму лічыльнікаў прадукцыйнасці для выканання той жа задачы, але з уласнай прадукцыйнасцю з выкарыстаннем лічыльнікаў CPU.

 perf stat ./sumtest_sorted 

Сартаванне:

  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 

Ён таксама можа ствараць анатацыю зыходнага кода з дызасэмбляванне.

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

Падрабязней гл. Кіраўніцтва па прадукцыйнасці .

1758
12 окт. адказ дадзены caf 12 каст. 2012-10-12 08:53 '12 у 8:53 2012/10/12 08:53

Я проста прачытаў гэтае пытанне і яго адказы, і я адчуваю, што адказ адсутнічае.

Звычайны спосаб ліквідаваць прадказанне галінавання, які, як мне падалося, асабліва добра працуе ў кіраваных мовах, - гэта пошук у табліцы замест выкарыстання галінавання (хоць у гэтым выпадку я яго не правяраў).

Гэты падыход працуе ў цэлым, калі:

  1. гэта невялікая табліца і, хутчэй за ўсё, будзе кэшыраваць ў працэсары, і
  2. вы працуеце ў даволі вузкім цыкле і / або працэсар можа папярэдне загрузіць дадзеныя.

Фон і чаму

З пункту гледжання працэсара, ваша памяць працуе павольна. Каб кампенсаваць розніцу ў хуткасці, у ваш працэсар ўбудаваная пара кэш сэрвэра (кэш L1 / L2). Такім чынам, уявіце, што вы робіце свае добрыя вылічэнні і высвятліце, што вам патрэбен кавалак памяці. Працэсар выканае аперацыю загрузкі і загрузіць частка памяці ў кэш, а затым выкарыстоўвае кэш для выканання астатніх вылічэнняў. Паколькі памяць адносна павольная, гэтая "загрузка" замарудзіць вашу праграму.

Як і прагназаванне галінаванняў, гэта было аптымізавана ў працэсарах Pentium: працэсар прадказвае, што яму трэба загрузіць частка дадзеных, і спрабуе загрузіць іх у кэш, перш чым аперацыя сапраўды патрапіць у кэш. Як мы ўжо бачылі, прадказанне галінавання часам ідзе жудасна няправільна - у горшым выпадку вам трэба вярнуцца назад і фактычна чакаць загрузкі памяці, якая будзе доўжыцца вечна (іншымі словамі: няўдалае прадказанне галінавання дрэнна, памяць загрузка пасля збою прадказанні галінкі проста жудасная!).

На шчасце для нас, калі схема доступу да памяці прадказальная, працэсар загрузіць яе ў свой хуткі кэш, і ўсё ў парадку.

Першае, што нам трэба ведаць, гэта тое, што мала? Хоць меншы памер, як правіла, лепш, практычнае правіла заключаецца ў тым, каб прытрымлівацца табліц пошуку памерам <= 4096 байт. У якасці верхняга мяжы: калі ваша даведачная табліца больш 64 КБ, яе, верагодна, варта перагледзець.

пабудова стала

Такім чынам, мы высветлілі, што можам стварыць невялікую табліцу. Наступнае, што трэба зрабіць, гэта ўсталяваць на месца функцыю пошуку. Функцыі пошуку звычайна ўяўляюць сабой невялікія функцыі, якія выкарыстоўваюць некалькі асноўных цэлалікавых аперацый (і, ці, xor, shift, add, remove і, магчыма, множанне). Вы хочаце, каб ваш увод быў пераведзены з дапамогай функцыі пошуку ў нейкі "унікальны ключ" у вашай табліцы, які затым проста дае вам адказ на ўсю працу, якую вы хацелі, каб ён рабіў.

У гэтым выпадку:> = 128 азначае, што мы можам захаваць значэнне, <128 азначае, што мы пазбавімся ад яго. Самы просты спосаб зрабіць гэта - выкарыстоўваць 'І': калі мы захоўваем гэта, мы І гэта з 7FFFFFFF; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и большим количеством 7FFFFFFFF годов.

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