Sıralanmamış bir arraydən başqa bir ardıcıllıqdan daha sürətli işləmə nədir?

Burada çox xüsusi görünən bir C ++ kod parçasıdır. Bəzi qəribə səbəblərə görə, məlumatları mürəkkəbləşdirərək kodu təxminən altı dəfə daha sürətli edir.

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

Bir az oxşar, lakin daha az həddindən artıq nəticə ilə.


İlk düşüncəm ki, sıralama məlumatları önbelleğe gətirir, amma sonra mən necə də axmaq düşündüm, çünki dizi yaradılıb.

  • Whats davam edir
  • Niyə sıralanan array sıralanmamış array daha sürətli işlənir?
  • Kod, bəzi müstəqil şərtləri ümumiləşdirir və sifariş əhəmiyyətsizdir.
22512
27 июня '12 в 16:51 2012-06-27 16:51 GManNickG 27 iyun ' ta saat 04 : 51- də təyin olunub . 2012-06-27 16:51
@ 26 cavab

Siz dallanmadan kənarlaşmanın qurbanısınız .


Branş proqnozu nədir?

Dəmir yolu qovşağını nəzərdən keçirin:

2019

27 июня '12 в 16:56 2012-06-27 16:56 Cavab Mystical tərəfindən 27 İyun 2012 günü saat 04:56 'da veriləcək 2012-06-27 16:56

Filialların proqnozlaşdırılması.

Sıralanan bir sıra ilə, data[c] >= 128 şərti dəyər düsturu üçün ilk false və sonra bütün sonrakı dəyərlər üçün true olur. Proqnozlaşdırmaq asandır. Seçməmiş bir sıra ilə, dallanma xərclərini ödəyirsiniz.

3815
27 июня '12 в 16:54 2012-06-27 16:54 Cavab Daniel Fischer tərəfindən 27 iyun 'da saat 16:54 ' da verilir. 2012-06-27 16:54

Məlumatların çeşidlənməsi zamanı performansın dramatik şəkildə yaxşılaşması səbəbi, Mistikanın mükəmməl izah etdiyi kimi, filialın proqnozlaşdırılması üçün cərimənin aradan qaldırılmasıdır.

İndi koda baxırıq

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

biz bu if... else... şöbəsinin şərtləri yerinə yetirildikdə bir şey əlavə etmək olduğunu bilirik. Bu növ filial asanlıqla x86 sistemində, cmovl , şərti hərəkət cmovl tərtib ediləcək şərti operatora asanlıqla çevrilə bilər. Şöbə və buna görə də, öngörən filial üçün potensial cəza çıxarılır.

Bu səbəbdən, C++ , x86 şərti hərəkət təlimatına tərtib edilmiş birbaşa (heç bir optimallaşdırma olmadan) işləyən operator üçtərəfli operatordur ...?... :... ...?... :... Buna görə də, yuxarıda göstərilən bəyanımı bərabər olaraq yenidən yazırıq:

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

Oxunanlığı davam etdirərək, sürətləndirici faktorları kontrol edə bilərik.

Intel Core i7-2600K @ 3.4 GHz və Visual Studio 2010 azad rejimi istinad testi (mistikdən kopyalanan format) üçün:

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 

Nəticədə bir neçə sınaqda etibarlıdır. Dallanmanın nəticəsi gözlənilməz olduğunda əhəmiyyətli bir sürət qazanırıq, ancaq öngörülebilir olduğunda bir az əziyyət çəkirik. Əslində, şərti bir hərəkət istifadə edərkən, məlumat nümunəsindən asılı olmayaraq performans eyni qalır.

İndi x86 qurduqları quruluşu araşdıraraq daha yaxından nəzər salaq. Sadəlik üçün max1max2 funksiyalarını istifadə max2 .

max1 şərti filialdan istifadə edir, if... else... :

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

max2 üçüncü operatoru istifadə edir ...?... :... ...?... :... :

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

X86-64 kompüterində GCC -S aşağıda bir qurul qurur.

 :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 təlimatından istifadə etməklə daha az kod istifadə edir. Amma real mənfəət max2max2 , jmp üzrə keçidləri nəzərdə max2 , proqnozlaşdırılan nəticə max2 əhəmiyyətli max2 performansına gətirib çıxara bilər.

Niyə şərti hərəkət daha yaxşı işləyir?

Tipik bir x86 prosessorunda x86 icraatı bir neçə mərhələdən ibarətdir. Sözsüz ki, müxtəlif mərhələlər üçün müxtəlif avadanlıqlar var. Buna görə də bir təlimatın sonunu yeni bir başlanğıc üçün gözləmək lazım deyil. Buna boru kəməri deyilir.

Dallanma halında, növbəti təlimat əvvəlcədən müəyyən edilir, beləliklə boru kəmərini yerinə yetirə bilmirik. Gözləmə və ya proqnozlaşdırmalıyıq.

Şərti hərəkət vəziyyətində, şərti hərəkət əmrinin icrası bir neçə mərhələyə bölünür, lakin FetchDecode kimi əvvəlki mərhələlər əvvəlki təlimatın nəticəsindən asılı deyil; yalnız son mərhələlər nəticə verməlidir. Beləliklə, biz bir göstəriş icra vaxtının bir hissəsini gözləyirik. Buna görə şərti hərəkət versiyası, proqnoz asan olduqda filialdan daha yavaşdır.

Kompüter Sistemləri: Bir Proqramçı üçün Prospect, ikinci nəşr, bu kitabı ətraflı izah edir. Proqnozlaşdırma Cəzaları və Yanlış Proqnozlaşdırma üçün xüsusi müddəalar üçün Bölmə 3.6.6-ı Şərti Hərəkət Təlimatları, prosessor Mimarisi üçün bütün fəsil 4 və 5.11.2-də yoxlaya bilərsiniz.

Bəzən bəzi müasir kompüterlər daha yüksək performansı ilə bina üçün kodunuzu optimallaşdırır, bəzən bəzi kompilyatorlar (bu kod Visual Studio kompilyatorunu istifadə edir). Dallanma və şərti hərəkət arasındakı fərqləri bilmək, gözlənilməz zaman, scriptin kompleksi avtomatik olaraq optimallaşdırıla bilməyəcəyi qədər mürəkkəbləşdiyi zaman kodu daha yaxşı işləməyə kömək edə bilər.

3064
28 июня '12 в 5:14 2012-06-28 05:14 Cavab WiSaGaN tərəfindən 28 iyun '12, saat 05:14 'da 2012-06-28 05:14' də verilir

Bu kodla edilə biləcək daha da optimallaşdırma ilə maraqlanırsınızsa, aşağıdakıları nəzərdən keçirin:

İlkin dövrdən başlayaraq:

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

Dövrü permütasyonu ilə, bu dövrü təhlükəsiz olaraq dəyişə bilərik:

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

Sonra loop i icrası zamanı if vəziyyətin sabit olduğunu görə bilərsiniz, belə ki həyata çıxmaq if :

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

Sonra daxili loopun bir tək ifadəyə çökə biləcəyini görürsünüz, buna görə üzən nöqtə modelinə imkan verir (məsələn, / fp: sürətli)

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

Daha əvvəl 100.000 dəfə daha sürətli.

2105
03 июля '12 в 5:25 2012-07-03 05:25 Cavab vulcan raven üçün verilir 3 iyul, 2012 at 5:25 pm 2012-07-03 05:25

Şübhəsiz ki, bəzilərimiz bir CPU proqnozu prosessoru üçün problemli olan kodu müəyyən etmək yolları ilə maraqlanırlar. Valgrind cachegrind alətində --branch-sim=yes bayraqdan istifadə edərək aktivləşdirilən şöbə predictor filial sintaksisinə malikdir. Bu sualın nümunələri ilə işləyən, 10,000-ə endirilmiş və g++ ilə tərtib edilən xarici loopların sayı aşağıdakı nəticələr verir:

Sırala:

 ==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 tərəfindən yaradılan xətti çıxışa cg_annotate , sözügedən dövrü görürük:

Sırala:

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

Bu problem xəttini asanlıqla təsvir etməyə imkan verir - bir cüzi versiyada, if (data[c] >= 128) cachegrind filialı predmeti modelinin bir hissəsi kimi Bcm yanlış proqnozlaşdırılmış şərti şöbələrə ( Bcm ) səbəb olur, yalnız 10,006 sıralanır versiyası.


Alternativ olaraq, Linux-da, eyni vəzifəni yerinə yetirmək üçün bir performans sayacı alt sistemini istifadə edə bilərsiniz, ancaq CPU sayaçlarını istifadə edərək öz performansıyla.

 perf stat ./sumtest_sorted 

Sırala:

  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 

Həm də sökülməsi ilə mənbə kodu annotasiya yarada bilər.

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

Ətraflı məlumat üçün performans kitabçasına baxın .

1758
12 окт. 12 aylıq kafedə cavab verildi . 2012-10-12 08:53 '12 at 8:53 2012-10-12 08:53

Mən yalnız bu sualları və cavablarını oxuyuram və cavabın itkin olduğunu hiss edirəm.

Şəffaf proqnozu aradan qaldırmaq adi üsulu, xüsusilə də idarə olunan dillərdə işləməyimə baxmayaraq, dallanma əvəzinə masanın axtarışıdır (baxmayaraq ki, mən bunu yoxdum).

Bu yanaşma ümumiyyətlə işləyir:

  1. bu kiçik bir masa və çox güman ki prosessorda saxlanılacaq və
  2. Siz olduqca dar bir loopda işləyirsiniz və / və ya prosessor data yükləyə bilər.

Tarix və niyə

İşlemci baxımından yaddaşınız yavaş. Sürət fərqini kompensasiya etmək üçün prosessorunuza bir neçə önbellek (L1 / L2 önbellek) daxil edilir. Yəni, yaxşı hesablamaları etdiyinizi və bir parça yaddaşa ehtiyacınız olduğunu təsəvvür edin. İşlemci, yük əməliyyatını yerinə yetirir və yaddaşın bir hissəsini önbelleğe yükləyəcək və qalan hesablamaları yerinə yetirmək üçün önbelleği istifadə edin. Yaddaş nisbətən yavaş olduğundan, bu "yük" proqramınızı yavaşlatacaq.

Şəbəkə proqnozu kimi, Pentium prosessorlarında optimallaşdırılmışdır: prosessor bəzi məlumatların yüklənməsi üçün lazım olduğunu nəzərdə tutur və əməliyyatın həqiqətən önbellekə daxil edilməzdən əvvəl onları önbelleğe yükləməyə çalışır. Daha əvvəl gördüyümüz kimi, filialın proqnozlaşdırılması bəzən hədsiz səhv gedir - ən pis halda, geri qayıtmaq və əslində sonsuza qədər davam edəcək bir yaddaş yükünü gözləməlisiniz ( başqa bir sözlə: uğursuz bir şöbə proqnozu pis, bir şöbə proqnozu uğursuzluqdan sonra yaddaş yükü qorxudur! ).

Xoşbəxtlikdən bizim üçün, əgər yaddaşa giriş sxemi proqnozlaşdırılırsa, prosessor onu sürətli yaddaşına yükləyəcək və hər şey yaxşıdır.

Bilmək lazım olan ilk şey azdır? Kiçik bir ölçüsü daha yaxşı olsa da, başparmağın qayda-sütunu <= 4096 bayt axtarmağı dayandırmaqdır. Üst sərhəd kimi: Sizin istinad kitabçanız 64 KB-dən çox olarsa, ehtimal ki, yenidən nəzərdən keçirilməlidir.

Masa qurun

Beləliklə, biz kiçik bir masa yarada bilərik. Növbəti şey axtarış funksiyasını əvəz etməkdir. Axtarış funksiyaları, bir neçə əsas tamsayı əməliyyatları (və ya, xor, köçürmə, əlavə et, qaldırmaq və bəlkə vurma) istifadə edən adətən kiçik funksiyalardır. Girişinizin, tablonuza bir növ "unikal key" axtararaq tərcümə olunmasını istəyirsən, sonra sadəcə olaraq istədiyiniz bütün işləri cavablandırırsınız.

Bu vəziyyətdə:> = 128, dəyəri saxlaya biləcəyimiz anlamına gəlir, <128 biz bundan xilas olacağıq. Bunu etmək üçün ən asan yol "VƏ" istifadə etməkdir: biz onu saxlayırıqsa Və bu 7FFFFFFF ilə; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и большим количеством 7FFFFFFFF годов.

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

Вы можете удивиться, почему это хорошо работает на управляемых языках. В конце концов, управляемые языки проверяют границы массивов с помощью ветки, чтобы убедиться, что вы не ошиблись...

Ну, не совсем... :-)

Была проделана определенная работа по устранению этой ветки для управляемых языков. Məsələn:

 // Generate data int arraySize = 32768; int[] data = new int[arraySize]; Random random = new Random(0); for (int c = 0; c < arraySize; ++c) { data[c] = random.Next(256); }  int[] lookup = new int[256]; for (int c = 0; c < 256; ++c) { lookup[c] = (c >= 128) ? c : 0; } // Test DateTime startTime = System.DateTime.Now; long sum = 0; for (int i = 0; i < 100000; ++i) { // Primary loop for (int j = 0; j < arraySize; ++j) {  sum += lookup[data[j]]; } } DateTime endTime = System.DateTime.Now; Console.WriteLine(endTime - startTime); Console.WriteLine("sum = " + sum); Console.ReadLine(); 
1221
ответ дан atlaste 24 апр. '13 в 9:26 2013-04-24 09:26

Поскольку данные распределяются между 0 и 255 при сортировке массива, вокруг первой половины итераций не будет вводиться if -statement (ниже оператор if ).

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

Вопрос: что делает вышеуказанный оператор не выполняемым в некоторых случаях, как в случае отсортированных данных? Здесь идет "предиктор отрасли". Предиктор ветвления представляет собой цифровую схему, которая пытается угадать, к какой ветке (например, структура if-then-else ) будет идти, прежде чем это будет известно наверняка. Целью прогнозирования ветвей является улучшение потока в конвейере команд. Отраслевые предсказатели играют решающую роль в достижении высокой эффективности!

Позвольте сделать заметку, чтобы лучше понять ее

Производительность if -statement зависит от того, имеет ли ее состояние предсказуемый шаблон. Если условие всегда истинно или всегда ложно, логика предсказания ветвления в процессоре будет забирать шаблон. С другой стороны, если шаблон непредсказуем, состояние if будет намного дороже.

Позволяет измерять производительность этого цикла при разных условиях:

 for (int i = 0; i < max; i++) if (condition) sum++; 

Ниже приведены тайминги цикла с разными истинно-ложными шаблонами:

 Condition Pattern Time (ms) (i  0×80000000) == 0 T repeated 322 (i  0xffffffff) == 0 F repeated 276 (i  1) == 0 TF alternating 760 (i  3) == 0 TFFFTFFF… 513 (i  2) == 0 TTFFTTFF… 1675 (i  4) == 0 TTTTFFFFTTTTFFFF… 1275 (i  8) == 0 8T 8F 8T 8F … 752 (i  16) == 0 16T 16F 16T 16F … 490 

A bad "true-false шаблон может сделать if -statement до шести раз медленнее, чем шаблон хороший ! Конечно, какой шаблон хорош, а что плохой, зависит от точных инструкций, генерируемых компилятором и конкретным процессором.

Таким образом, нет никаких сомнений относительно влияния прогноза ветвления на производительность!

1090
ответ дан Saqlain 15 февр. '13 в 10:24 2013-02-15 10:24

Один из способов избежать ошибок прогнозирования ветвлений - построить таблицу поиска и проиндексировать ее с использованием данных. Стефан де Бройн обсуждал это в своем ответе.

Но в этом случае мы знаем, что значения находятся в диапазоне [0, 255], и мы заботимся только о значениях> = 128. Это означает, что мы можем легко извлечь один бит, который скажет нам, хотим ли мы значения или нет: сдвигом данные вправо 7 бит, мы остаемся с 0 бит или 1 бит, и мы хотим только добавить значение, когда у нас есть 1 бит. Позвольте называть этот бит "бит решения".

Используя значение 0/1 бит решения как индекс в массиве, мы можем сделать код, который будет одинаково быстрым, будут ли данные отсортированы или не отсортированы. Наш код всегда будет добавлять значение, но когда бит решения равен 0, мы добавим значение где-то, на что нас не волнует. Kod burada:

 // Test clock_t start = clock(); long long a[] = {0, 0}; long long sum; for (unsigned i = 0; i < 100000; ++i) { // Primary loop for (unsigned c = 0; c < arraySize; ++c) { int j = (data[c] >> 7); a[j] += data[c]; } } double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC; sum = a[1]; 

Этот код отнимает половину добавок, но никогда не имеет ошибки предсказания ветвления. Это значительно быстрее по случайным данным, чем версия с фактическим выражением if.

Но в моем тестировании явная таблица поиска была немного быстрее, чем это, вероятно, потому, что индексирование в таблицу поиска было немного быстрее, чем смещение битов. Это показывает, как мой код настраивается и использует таблицу поиска (невообразимо называемую lut для "LookUp Table" в коде). Здесь C++ код:

 // declare and then fill in the lookup table int lut[256]; for (unsigned c = 0; c < 256; ++c) lut[c] = (c >= 128) ? c : 0; // use the lookup table after it is built for (unsigned i = 0; i < 100000; ++i) { // Primary loop for (unsigned c = 0; c < arraySize; ++c) { sum += lut[data[c]]; } } 

В этом случае таблица поиска составляла всего 256 байтов, поэтому она прекрасно вписывалась в кеш, и все было быстро. Этот метод не сработает, если данные будут 24-битными значениями, и нам нужно только половину из них... таблица поиска будет слишком большой, чтобы быть практичной. С другой стороны, мы можем комбинировать два метода, показанные выше: сначала сдвинуть бит, а затем индексировать таблицу поиска. Для 24-битного значения, которое нам нужно только для значения верхней половины, мы могли бы перенести данные на 12 бит и оставить 12-битное значение для индекса таблицы. 12-разрядный индекс таблицы подразумевает таблицу из значений 4096, что может быть практичным.

EDIT: Одна вещь, которую я забыл положить.

Методика индексирования в массив вместо использования оператора if может использоваться для определения того, какой указатель использовать. Я видел библиотеку, которая реализовала двоичные деревья, и вместо двух указателей ( pLeft и pRight или любого pRight ) имела массив указателей длины-2 и использовал метод "бит решения", чтобы решить, к какому из них следует следовать. Например, вместо:

 if (x < node->value) node = node->pLeft; else node = node->pRight; 

эта библиотека будет делать что-то вроде:

 i = (x < node->value); node = node->link[i]; 

Ссылка на этот код: Red Black Trees , Eternally Confuzzled

1017
ответ дан steveha 22 июля '13 в 11:29 2013-07-22 11:29

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

В самом деле, массив разбивается в смежной зоне с data < 128 , а другой с data >= 128 . Таким образом, вы должны найти точку раздела с дихотомическим поиском (используя сравнения Lg(arraySize) = 15 ), а затем выполнить прямое накопление из этой точки.

Что-то вроде (непроверено)

 int i= 0, j, k= arraySize; while (i < k) { j= (i + k) >> 1; if (data[j] >= 128) k= j; else i= j; } sum= 0; for (; i < arraySize; i++) sum+= data[i]; 

или, немного более запутанный

 int i, k, j= (i + k) >> 1; for (i= 0, k= arraySize; i < k; (data[j] >= 128 ? k : i)= j) j= (i + k) >> 1; for (sum= 0; i < arraySize; i++) sum+= data[i]; 

Еще более быстрый подход, который дает приблизительное решение для отсортированного или несортированного: sum= 3137536; (предполагая действительно равномерное распределение, 16384 образцов с ожидаемым значением 191,5) :-)

922
ответ дан Yves Daoust 24 июля '13 в 10:57 2013-07-24 10:57

Вышеприведенное поведение происходит из-за предсказания ветвей.

Чтобы понять предсказание ветвей, сначала нужно понять Трубопровод инструкций :

Любая инструкция разбивается на последовательность шагов, так что разные шаги могут выполняться параллельно параллельно. Этот метод известен как конвейер команд, и это используется для увеличения пропускной способности в современных процессорах. Чтобы лучше понять это, см. Пример в Википедии .

Как правило, современные процессоры имеют довольно длинные конвейеры, но для простоты можно рассмотреть только эти 4 шага.

  • IF - выбор команды из памяти
  • ID - декодировать инструкцию
  • EX - выполнить инструкцию
  • WB - Запись обратно в регистр CPU

4-этапный трубопровод в целом для 2 инструкций. 2019

744
ответ дан Harsh Sharma 03 июля '15 в 18:35 2015-07-03 18:35

Официальным ответом будет

Вы также можете видеть из этой симпатичной диаграммы почему предиктор ветки путается.

2019

ответ дан Surt 12 окт. '15 в 0:05 2015-10-12 00:05

В той же строке (я думаю, что это не было подчеркнуто каким-либо ответом), полезно отметить, что иногда (особенно в программном обеспечении, где важна производительность - например, в ядре Linux), вы можете найти некоторые операторы if, такие как:

 if (likely( everything_is_ok )) {  } 

или аналогичным образом:

 if (unlikely(very_improbable_condition)) {  } 

Оба likely() и unlikely() являются фактически макросами, которые определяются с помощью чего-то вроде GCC __builtin_expect , чтобы помочь коду предсказания вставки компилятора в пользу условия, учитывающего информацию, предоставленную пользователем. GCC поддерживает другие встроенные функции, которые могут изменять поведение запущенной программы или выдавать инструкции низкого уровня, такие как очистка кеша и т.д. См. эту документацию , которая проходит доступные встроенные GCC.

Обычно такие виды оптимизации в основном используются в приложениях с жестким режимом реального времени или встраиваемых системах, где время выполнения имеет значение, и оно критично. Например, если вы проверяете какое-то условие ошибки, которое происходит только 1/10000000 раз, то почему бы не сообщить компилятору об этом? Таким образом, по умолчанию предсказание ветвления предполагает, что условие ложно.

617
ответ дан rkachach 23 сент. '15 в 17:57 2015-09-23 17:57

Часто используемые логические операции в С++ производят много ветвей в скомпилированной программе. Если эти ветки находятся внутри циклов, и их трудно предсказать, они могут значительно замедлить выполнение. Булевы переменные сохраняются как 8-битные целые числа со значением 0 для false и 1 для true .

Булевы переменные переопределены в том смысле, что все операторы, которые имеют логические переменные в качестве входных, проверяют, имеют ли входы какое-либо другое значение, чем 0 или 1 , но операторы, которые имеют Booleans в качестве вывода, могут не вызывать другого значения, чем 0 или 1 . Это делает операции с булевыми переменными в качестве входных данных менее эффективными, чем необходимо. Рассмотрим пример:

 bool a, b, c, d; c = a  b; d = a || b; 

Это обычно реализуется компилятором следующим образом:

 bool a, b, c, d; if (a != 0) { if (b != 0) { c = 1; } else { goto CFALSE; } } else { CFALSE: c = 0; } if (a == 0) { if (b == 0) { d = 0; } else { goto DTRUE; } } else { DTRUE: d = 1; } 

Этот код далеко не оптимален. В случае неправильных прогнозов ветки могут занимать много времени. Булевы операции могут быть сделаны намного эффективнее, если известно с уверенностью, что операнды не имеют других значений, чем 0 и 1 . Причина, по которой компилятор не делает такого предположения, состоит в том, что переменные могут иметь другие значения, если они не инициализированы или происходят из неизвестных источников. Вышеупомянутый код можно оптимизировать, если a и b были инициализированы до допустимых значений или если они исходят от операторов, которые производят логический вывод. Оптимизированный код выглядит следующим образом:

 char a = 0, b = 1, c, d; c = a  b; d = a | b; 

char используется вместо bool , чтобы можно было использовать побитовые операторы ( > и | ) вместо булевых операторов ( > и || ). Побитовые операторы - это одиночные инструкции, которые занимают только один такт. Оператор OR ( | ) работает, даже если a и b имеют другие значения, чем 0 или 1 . Оператор AND ( > ) и оператор EXCLUSIVE OR ( ^ ) могут давать несогласованные результаты, если операнды имеют другие значения, чем 0 и 1 .

~ не может использоваться для NOT. Вместо этого вы можете сделать Boolean NOT для переменной, которая, как известно, является 0 или 1 , XOR'ing ее с помощью 1 :

 bool a, b; b = !a; 

можно оптимизировать для:

 char a = 0, b; b = a ^ 1; 

a b не может быть заменен на a b , если b - это выражение, которое не должно быть оценено, если a false ( > не будет оценивать b , > будет). Аналогично, a || b не может быть заменен на a | b , если b - это выражение, которое не должно быть оценено, если a равно true .

Использование побитовых операторов более выгодно, если операнды являются переменными, чем если сравнивать операнды:

 bool a; double x, y, z; a = x > y  z < 5.0; 

является оптимальным в большинстве случаев (если вы не ожидаете выражения > для генерации многих неверных предсказаний ветвления).

587
ответ дан Maciej 10 окт. '15 в 3:30 2015-10-10 03:30

Это точно!...

Прогнозирование ветвей заставляет логику работать медленнее из-за переключения, которое происходит в вашем коде! Это похоже на то, что вы идете по прямой улице или улице с множеством повозок, наверняка, прямой будет сделан быстрее!...

Если массив отсортирован, ваше условие является ложным на первом шаге: data[c] >= 128 , а затем становится истинным значением для всего пути до конца улицы. То, как вы добираетесь до конца логики быстрее. С другой стороны, используя несортированный массив, вам нужно много поворота и обработки, которые заставляют ваш код работать медленнее наверняка...

Посмотрите изображение, которое я создал для вас ниже. Какая улица будет закончена быстрее?

2019

ответ дан Alireza 18 июня '17 в 14:40 2017-06-18 14:40

Этот вопрос уже ответил много раз. Тем не менее, я хотел бы привлечь внимание группы к еще одному интересному анализу.

Недавно этот пример (немного модифицированный) также использовался в качестве способа продемонстрировать, как часть кода может быть профилирована внутри самой программы в Windows. По пути автор также показывает, как использовать результаты, чтобы определить, где код проводит большую часть своего времени как в отсортированном, так и в несортированном случае. Наконец, в части также показано, как использовать малоизвестную особенность HAL (Hardware Abstraction Layer), чтобы определить, насколько происходит неверное предсказание отрасли в несортированном случае.

Ссылка находится здесь: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm

247
ответ дан ForeverLearning 12 янв. '17 в 4:50 2017-01-12 04:50

Усиление прогноза ветвления!

Важно понимать, что неверное предсказание отрасли не замедляет программы. Стоимость пропущенного предсказания точно так же, как если бы предсказание ветвления не существовало, и вы ожидали, что оценка выражения решит, какой код будет выполняться (дальнейшее объяснение в следующем абзаце).

 if (expression) { // Run 1 } else { // Run 2 } 

Всякий раз, когда существует оператор if-else \ switch , выражение должно быть оценено для определения того, какой блок должен быть выполнен. В коде сборки, сгенерированном компилятором, вставлены условные branch .

Инструкция ветки может привести к тому, что компьютер начнет выполнение другой последовательности команд и, таким образом, отклонится от поведения по умолчанию выполнения команд по порядку (т.е. если выражение ложно, программа пропускает код блока if ) в зависимости от при некотором условии, которое является оценкой выражения в нашем случае.

При этом компилятор пытается предсказать результат до того, как он будет фактически оценен. Он будет извлекать команды из блока if , и если выражение получится истинным, то замечательно! Мы получили время, необходимое для его оценки и прогресса в коде; если нет, то мы запускаем неправильный код, конвейер очищается, и выполняется правильный блок.

Визуализация:

Скажем, вам нужно выбрать маршрут 1 или маршрут 2. Ожидая, что ваш партнер проверит карту, вы остановились на ## и ждали, или вы можете просто выбрать маршрут1, и если вам повезет (маршрут 1 правильный маршрут), тогда вам не пришлось ждать, пока ваш партнер проверит карту (вы сохранили время, которое потребовалось бы ему, чтобы проверить карту), иначе вы просто вернетесь.

В то время как промывные трубопроводы очень быстрые, в настоящее время эта игра стоит того. Предсказание отсортированных данных или данных, которые меняются медленно, всегда проще и лучше, чем предсказать быстрые изменения.

  O Route 1 /------------------------------- /|\ / | ---------##/ / \ \ \ Route 2 \-------------------------------- 
156
ответ дан Tony Tannous 04 авг. '17 в 13:07 2017-08-04 13:07

Как и то, что уже было упомянуто другими, то, что скрывается за тайной, - это предсказатель отрасли .

Я не пытаюсь что-то добавить, а объясняю концепцию по-другому. Существует краткое введение в вики, которое содержит текст и диаграмму. Мне нравится объяснение ниже, которое использует диаграмму для интуитивного развития Предиктора ветвей.

В компьютерной архитектуре предиктор ветвления - это цифровая схема, которая пытается угадать, каким образом пойдет ветвь (например, структура if-then-else), прежде чем это станет известно наверняка. Целью предиктора ветвления является улучшение потока в конвейере команд. Предсказатели ветвлений играют решающую роль в достижении высокой эффективной производительности во многих современных конвейерных микропроцессорных архитектурах, таких как x86.

Двустороннее ветвление обычно реализуется с помощью инструкции условного перехода. Условный переход может быть либо "не взят" и продолжен с первой ветвью кода, которая следует сразу после условного перехода, либо его можно "взять" и перейти в другое место в памяти программ, где находится вторая ветвь кода. сохраняются. Точно неизвестно, будет ли выполнен условный переход или нет, пока условие не будет вычислено и условный переход не пройдет этап выполнения в конвейере команд (см. Рис. 1).

2019

ответ дан Gearon 06 нояб. '17 в 19:15 2017-11-06 19:15

Это о предсказании ветки. Что это?

  • Предиктор ветвления является одной из древних технологий повышения производительности, которые все еще имеют отношение к современным архитектурам. В то время как простые методы прогнозирования обеспечивают быстрый поиск и эффективность использования энергии, они страдают от высокой частоты ошибочного предсказания.

  • С другой стороны, предсказания комплексных ветвей - либо нейронные, либо варианты двухуровневого предсказания ветвей - обеспечивают лучшую точность предсказания, но они потребляют больше энергии, а сложность возрастает экспоненциально.

  • В дополнение к этому в сложных методах прогнозирования время, затрачиваемое на прогнозирование ветвей, само по себе очень велико - от 2 до 5 циклов, что сопоставимо с временем выполнения фактических ветвей.

  • Прогнозирование отрасли - это, по сути, проблема оптимизации (минимизации), в которой акцент делается на достижение минимально возможной скорости промаха, низкое энергопотребление и низкую сложность с минимальными ресурсами.

На самом деле существует три разных типа ветвей:

Форвардные условные ветки - на основе состояния времени выполнения ПК (программный счетчик) изменяется, чтобы указать на адрес вперед в потоке команд.

Отказоустойчивые ветки - ПК изменяется на обратную сторону в потоке команд. Филиал основан на некоторых условиях, таких как ветвление назад к началу цикла программы, когда тест в конце цикла указывает, что цикл должен быть выполнен снова.

Безусловные ветки - это включает в себя переходы, вызовы процедур и возвраты, которые не имеют определенного условия. Например, инструкция безусловного перехода может быть закодирована на языке ассемблера как просто "jmp", и поток команд должен быть немедленно направлен в целевое местоположение, на которое указывает команда перехода, тогда как условный переход, который может быть закодирован как "jmpne", будет перенаправлять поток команд только в том случае, если результат сравнения двух значений в предыдущих инструкциях "сравнения" показывает, что значения не равны. (Сегментированная схема адресации, используемая архитектурой x86, добавляет дополнительную сложность, поскольку переходы могут быть либо "ближе" (внутри сегмента), либо "далеко" (вне сегмента). Каждый тип имеет разные эффекты для алгоритмов предсказания ветвлений.)

Статическое/динамическое предсказание ветвей . Статическое предсказание ветвления используется микропроцессором при первом возникновении условной ветки, а предсказание динамической ветки используется для последующих исполнений условного кода ветвления.

Литература:

103
ответ дан aghilpro 03 окт. '17 в 12:47 2017-10-03 12:47

В ARM нет необходимости в ветке, потому что каждая инструкция имеет 4-битное поле условия, которое проверяется с нулевой стоимостью. Это устраняет необходимость в коротких ветвях, и не будет никакого хита предсказания ветвления. Поэтому отсортированная версия будет работать медленнее, чем несортированная версия на ARM, из-за дополнительных накладных расходов на сортировку. Внутренний цикл будет выглядеть примерно так:

91
ответ дан Luke Hutchison 22 дек. '17 в 16:13 2017-12-22 16:13

Помимо того, что предсказание ветки может замедлить работу, сортированный массив имеет еще одно преимущество:

У вас может быть условие остановки, а не просто проверка значения, таким образом, вы только перебираете соответствующие данные и игнорируете остальные.
Прогнозирование ветвления пропустит только один раз.

  // sort backwards (higher values first) std::sort(data, data + arraySize, std::greater<int>()); for (unsigned c = 0; c < arraySize; ++c) { if (data[c] < 128) { break; } sum += data[c]; } 
87
ответ дан Yochai Timmer 23 нояб. '17 в 17:28 2017-11-23 17:28

Сортированные массивы обрабатываются быстрее, чем несортированный массив, из-за явлений, называемых предсказанием ветвления.

Отражающий предиктор - это цифровая схема (в компьютерной архитектуре), которая пытается предсказать, по какой ветке будет идти ветка, улучшая поток в конвейере команд. Схема/компьютер предсказывает следующий шаг и выполняет его.

Неправильное предсказание приводит к возврату к предыдущему шагу и выполнению с другим прогнозом. Предполагая, что предсказание верное, код продолжит следующий шаг. Неправильное предсказание приводит к повторению того же шага, пока не произойдет правильное предсказание.

Ответ на ваш вопрос очень прост.

В несортированном массиве компьютер делает несколько прогнозов, что приводит к увеличению вероятности ошибок. Принимая во внимание, что при сортировке компьютер делает меньше прогнозов, уменьшая вероятность ошибок. Для создания большего количества прогнозов требуется больше времени.

Сортированный массив: прямой путь

 ____________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT 

Unsorted Array: кривая дорога

 ______ ________ | |__| 

Прогнозирование ветвей: Угадывание/прогнозирование того, какая дорога прямая и после нее без проверки

 ___________________________________________ Straight road |_________________________________________|Longer road 

Хотя обе дороги достигают одного и того же пункта назначения, прямая дорога короче, а другая - длиннее. Если тогда вы выбираете другого по ошибке, нет возврата назад, и поэтому вы будете тратить лишнее время, если вы выберете более длинную дорогу. Это похоже на то, что происходит на компьютере, и я надеюсь, что это помогло вам лучше понять.

Обновление: после того, что сказал @Simon_Weaver, я хочу добавить еще один факт, что... "он не делает меньше прогнозов - он делает меньше ошибочных прогнозов. Он все равно должен прогнозировать каждый раз через цикл".

80
ответ дан Omkaar.K 07 дек. '17 в 20:28 2017-12-07 20:28

Хотя, как уже упоминалось в других ответах, современные компиляторы или архитектуры (ARM) делают этот конкретный пример спорным, в общем случае предположение о том, что для сортировки данных нужны другие ответы, не совсем корректно.

Следующий код сортирует не весь массив, а только его 200-элементные сегменты и, следовательно, работает быстрее всего.

 #include <algorithm> #include <ctime> #include <iostream> int main() { int data[32768]; const int l = sizeof data / sizeof data[0]; for (unsigned c = 0; c < l; ++c) data[c] = std::rand() % 256; // sort 200-element segments, not the whole array for (unsigned c = 0; c + 200 <= l; c += 200) std::sort(  + 200]); clock_t start = clock(); long long sum = 0; for (unsigned i = 0; i < 100000; ++i) { for (unsigned c = 0; c < sizeof data / sizeof(int); ++c) { if (data[c] >= 128) sum += data[c]; } } std::cout << static_cast<double>(clock() - start) / CLOCKS_PER_SEC << std::endl; std::cout << "sum = " << sum << std::endl; } 

Сортировка только k-элементных разделов завершает предварительную обработку за линейное время, а не n.log(n) общем случае.

Это также "доказывает", что это не имеет никакого отношения к каким-либо алгоритмическим проблемам, таким как порядок сортировки, и это действительно предсказание ветвлений.

3
ответ дан user2297550 09 дек. '18 в 9:18 2018-12-09 09:18

Если под обработкой подразумевается поиск, то в этом случае отсортированные массивы позволяют использовать бинарный поиск и несортированный используемый линейный поиск. Поскольку мы знаем, что мы можем использовать Бинарный поиск, имеет сложность O (log n), а линейный поиск имеет сложность O (n), где n - это размер массива в обоих случаях.

2
ответ дан Ashish Agrawal 02 янв. '19 в 9:44 2019-01-02 09:44

Здесь вам понадобится предсказание ветвления. Вы можете обратиться к этой статье за дополнительной информацией. Там также есть быстрый обзор по теме прогнозирования ветвей, который можно найти здесь .

0
ответ дан kss 04 дек. '18 в 9:06 2018-12-04 09:06

Предложение "предсказание ветвей благоприятствует наиболее часто используемому условию" ничего не говорит о значении оцениваемого условия, будь то положительное или отрицательное. Он говорит только, что предсказание ветки может помочь условным ветвям, которые используются чаще, т.е. те, которые находятся в цикле. Поэтому он в основном говорит, используя, если внутри цикла в порядке.

Хотя вывод верен, вы не должны беспокоиться о том, что ifs в цикле (вы не должны беспокоиться ни о чем, если профайлер не говорит вам, что есть узкое место), само предложение довольно бессмысленно в контексте Java.

Прогнозирование ветвей является функцией ЦП, поэтому в интерпретируемом выполнении оно не имеет отношения к ветвям уровня Java, поскольку они просто изменяют состояние интерпретаторов (то есть указатель, который будет читать следующую инструкцию), но не связаны с инструкцией ЦП, которая была бы специфичной для конкретной ветки.

Как только HotSpot начинает играть, картина совершенно другая. Если этот код является "горячей точкой", оптимизатор будет применять множество преобразований кода, которые делают большинство предположений о том, как будет выполняться код Java, неправильно.

Одной общей оптимизацией является разворачивание цикла. Вместо того, чтобы иметь один блок кода, представляющий тело циклов, будет несколько экземпляров его, следующих друг за другом, оптимизированный относительно инвариантов их конкретной итерации. Эти установки позволяют полностью исключить связанные ветки, поскольку вполне предсказуемо, что после первого перехода firstManagedChild от true к false он никогда не вернется, поэтому, когда первая итерация неизменно видит истинное значение для него, код для последующие итерации могут быть оптимизированы для обработки переменной как постоянно ложной.

Таким образом, в этом случае предсказание ветвления снова не будет иметь никакого смысла, поскольку не будет ветвей для оператора if, результат которого можно заранее предсказать.

0
ответ дан Sivaprakash B 17 окт. '18 в 8:49 2018-10-17 08:49

Рассмотрим это:

Случай 1: Вы ищете книгу в библиотеке. Книга называется "Почему быстрее обрабатывать отсортированный массив, чем несортированный массив?"

Книги упорядочены по алфавиту по названию.

Случай 2: книги помещаются на полках в библиотеке.

Случай 2 потребует, чтобы вы расчесали всю библиотеку в худшем случае, пока Случай 1 потребует от вас перейти в раздел, где книги начинаются с "W".

Я считаю, что это тот же случай с вашим массивом. Нет разветвления до достижения w

-3
ответ дан TimeTrax 04 июня '18 в 12:14 2018-06-04 12:14

Другие вопросы по меткам или Задайте вопрос