Warum kann ein sortiertes Array schneller verarbeitet werden als ein unsortiertes Array?

Hier ist ein Stück C ++ - Code, das sehr eigenartig erscheint. Aus irgendeinem seltsamen Grund macht das Sortieren der Daten auf wundersame Weise den Code fast sechsmal schneller.

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

Mit einem etwas ähnlichen, aber weniger extremen Ergebnis.


Mein erster Gedanke war, dass das Sortieren die Daten in den Cache bringt, aber dann dachte ich, wie blöd es ist, weil das Array gerade erzeugt wurde.

  • Was ist los?
  • Warum wird das sortierte Array schneller verarbeitet als das unsortierte Array?
  • Der Code fasst einige unabhängige Begriffe zusammen und die Reihenfolge spielt keine Rolle.
22512
27 июня '12 в 16:51 2012-06-27 16:51 GManNickG wird am 27. Juni 12 um 04:51 Uhr 2012-06-27 16:51 eingestellt
@ 26 Antworten

Sie sind das Opfer einer Abweichung von der Verzweigung .


Was ist eine Branchenvorhersage?

Betrachten Sie den Eisenbahnknotenpunkt:

2019

Vorhersage von Zweigen.

Bei einem sortierten Array ist die Bedingung data[c] >= 128 das erste false für die Wertzeichenfolge und wird true für alle nachfolgenden Werte auf true . Es ist leicht vorherzusagen. Bei einem unsortierten Array bezahlen Sie die Verzweigungskosten.

3815
27 июня '12 в 16:54 2012-06-27 16:54 Die Antwort wird von Daniel Fischer am 27. Juni um 16:54 Uhr 2012-06-27 16:54 gegeben

Der Grund für die Verbesserung der Leistung beim Sortieren von Daten ist, dass die Strafe für die Verzweigungsvorhersage beseitigt wird, wie Mysticial perfekt erklärt hat.

Nun, wenn wir uns den Code ansehen

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

Wir können feststellen, dass die Bedeutung dieses bestimmten if... else... Ast ist, etwas hinzuzufügen, wenn die Bedingung erfüllt ist. Diese Art von Zweig kann leicht in einen bedingten Operator cmovl , der im x86 System in eine bedingte Bewegungsanweisung übersetzt wird: cmovl . Die Verzweigung und damit die mögliche Strafe für die Vorhersage der Verzweigung wird aufgehoben.

In C , also C++ , ist der Operator, der direkt (ohne Optimierung) in x86 in eine bedingte Verschiebungsanweisung kompiliert werden soll, ein ternärer Operator ...?... :... ...?... :... Deshalb schreiben wir die obige Aussage um, damit sie gleichwertig ist:

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

Durch die Aufrechterhaltung der Lesbarkeit können wir den Beschleunigungsfaktor überprüfen.

Für den Intel Core i7 -2600K @ 3,4 GHz und den Referenzmodus für den Release-Modus von Visual Studio 2010 (aus Mysticial kopiertes Format):

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 

Das Ergebnis ist in mehreren Tests zuverlässig. Wir erhalten eine signifikante Beschleunigung, wenn das Ergebnis der Verzweigung unvorhersehbar ist, wir leiden jedoch etwas, wenn es vorhersehbar ist. Wenn Sie eine bedingte Verschiebung verwenden, bleibt die Leistung unabhängig vom Datenmuster gleich.

Schauen wir uns nun den x86 Build genauer an, den sie generieren. Zur Vereinfachung verwenden wir zwei Funktionen max1 und max2 .

max1 verwendet den bedingten Zweig, if... else... :

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

max2 verwendet den ternären Operator ...?... :... ...?... :... :

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

Auf dem x86-64-Computer erstellt der GCC -S unten eine Baugruppe.

 :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 aufgrund der cmovge Anweisung viel weniger Code. Der wahre Gewinn ist jedoch, dass max2 keine Übergänge auf max2 , jmp , was zu einer erheblichen Leistung von max2 führen kann, wenn das vorhergesagte Ergebnis max2 .

Warum funktioniert der bedingte Umzug besser?

In einem typischen x86 Prozessor ist die x86 in mehrere Stufen unterteilt. Grob gesagt haben wir unterschiedliche Hardware für verschiedene Phasen. Daher müssen wir nicht auf das Ende einer Anweisung warten, um eine neue Anweisung zu starten. Dies wird als Pipelining bezeichnet .

Im Falle einer Verzweigung wird der nächste Befehl durch den vorherigen Befehl bestimmt, sodass kein Pipelining durchgeführt werden kann. Wir müssen entweder warten oder vorhersagen.

Im Falle einer bedingten Verschiebung ist die Ausführung des Befehls für eine bedingte Verschiebung in mehrere Stufen unterteilt, aber die früheren Stufen, wie etwa Fetch und Decode , hängen nicht vom Ergebnis der vorherigen Anweisung ab. Nur die letzten Stufen benötigen ein Ergebnis. Daher warten wir auf einen Teil der Ausführungszeit eines Befehls. Aus diesem Grund ist die Version der bedingten Verschiebung >

Das Buch Computersysteme: Eine Perspektive für einen Programmierer, zweite Auflage, erläutert dies ausführlich. Sie können Abschnitt 3.6.6 für bedingte Bewegungsanweisungen, das gesamte Kapitel 4 für die Prozessorarchitektur und Abschnitt 5.11.2 für besondere Behandlung von Vorhersagen, Strafen und falschen Vorhersagen, überprüfen.

In manchen Fällen können moderne Compiler den Code für das Erstellen mit höherer Leistung optimieren, in manchen Fällen jedoch nicht (dieser Code verwendet einen eigenen Visual Studio-Compiler). Das Wissen um den Unterschied in der Leistung zwischen Verzweigungen und bedingten Verschiebungen, wenn es unvorhersehbar ist, kann uns dabei helfen, Code mit besserer Leistung zu schreiben, wenn das Skript so komplex wird, dass der Compiler sie nicht automatisch optimieren kann.

3064
28 июня '12 в 5:14 2012-06-28 05:14 Die Antwort gibt WiSaGaN am 28. Juni '12 um 5:14 Uhr 2012-06-28 05:14

Wenn Sie an noch mehr Optimierungen interessiert sind, die mit diesem Code durchgeführt werden können, sollten Sie Folgendes berücksichtigen:

Ausgehend vom Anfangszyklus:

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

Mit der Zykluspermutation können wir diesen Zyklus sicher ändern in:

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

Dann können Sie sehen, dass die if Bedingung während der Ausführung von Schleife i konstant ist. Sie können also herausziehen, if out:

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

Dann werden Sie sehen, dass die innere Schleife zu einem einzelnen Ausdruck zusammengefasst werden kann, vorausgesetzt, dass das Gleitkommamodell dies zulässt (beispielsweise / fp: fast).

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

Es ist 100.000 Mal schneller als zuvor.

2105
03 июля '12 в 5:25 2012-07-03 05:25 Die Antwort wird Vulkanraben am 3. Juli 12 um 05:25 Uhr gegeben. 2012-07-03 05:25

Zweifellos sind einige von uns an Möglichkeiten interessiert, Code zu identifizieren, der für einen CPU-Prädiktorprozessor problematisch ist. Das cachegrind Tool von Valgrind cachegrind über eine Verzweigungssyntax für Verzweigungsvorhersagen, die mit dem --branch-sim=yes aktiviert wird. Die Anzahl der externen Schleifen, die auf 10.000 reduziert und mit g++ kompiliert wurden, führt bei den Beispielen in dieser Frage zu folgenden Ergebnissen:

Sortieren nach:

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

Unsortiert:

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

In cg_annotate auf die lineare Ausgabe, die von cg_annotate erstellt wurde, sehen wir für den fraglichen Zyklus:

Sortieren nach:

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

Unsortiert:

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

Auf diese Weise können Sie die Problemzeile leicht identifizieren. In einer unsortierten Version bewirkt die Bcm if (data[c] >= 128) dass Bcm falsch vorausgesagte bedingte Verzweigungen ( Bcm ) als Teil des Cachegrind-Verzweigungsvorhersagemodells verursachen, während sie nur sortierte 10.006 aufruft Version.


Alternativ können Sie in Linux ein Leistungsindikator-Subsystem verwenden, um dieselbe Aufgabe auszuführen, jedoch mit eigener Leistung unter Verwendung von CPU-Indikatoren.

 perf stat ./sumtest_sorted 

Sortieren nach:

  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 

Unsortiert:

  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 

Es kann auch Quelltextanmerkungen mit Demontage erstellen.

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

Einzelheiten finden Sie im Leistungshandbuch .

1758
12 окт. antworten gegeben caf 12 oct. 2012-10-12 08:53 '12 um 08:53 2012-10-12 08:53

Ich lese gerade diese Frage und ihre Antworten, und ich habe das Gefühl, dass die Antwort fehlt.

Die übliche Methode, die Verzweigungsvorhersage, die in verwalteten Sprachen besonders gut zu funktionieren schien, auszuschalten, besteht darin, die Tabelle zu durchsuchen, anstatt Verzweigungen zu verwenden (obwohl ich sie in diesem Fall nicht geprüft habe).

Dieser Ansatz funktioniert im Allgemeinen, wenn:

  1. Dies ist eine kleine Tabelle, die höchstwahrscheinlich im Prozessor zwischengespeichert wird
  2. Sie arbeiten in einer ziemlich engen Schleife und / oder der Prozessor kann Daten vorladen.

Hintergrund und warum

Aus der Sicht des Prozessors ist Ihr Speicher >

Wie die Verzweigungsvorhersage wurde sie in Pentium-Prozessoren optimiert: Der Prozessor sagt voraus, dass er einige Daten laden muss, und versucht, sie in den Cache zu laden, bevor die Operation tatsächlich in den Cache ge>anderen Worten: Eine erfolglose Verzweigungsvorhersage ist schlecht, die Speicherbelastung nach einem Verzweigungsvorhersagefehler ist schrecklich! ).

Wenn das Speicherzugriffsschema vorhersehbar ist, lädt der Prozessor es in seinen schnellen Cache, und alles ist in Ordnung.

Das erste, was wir wissen müssen, ist, dass es wenig gibt? Obwohl eine kleinere Größe normalerweise besser ist, gilt als Faustregel, dass Nachschlagetabellen <= 4096 Bytes verwendet werden. Als obere Grenze: Wenn Ihre Referenztabelle größer als 64 KB ist, sollte sie wahrscheinlich überarbeitet werden.

Bauen Sie den Tisch auf

Wir haben also herausgefunden, dass wir eine kleine Tabelle erstellen können. Als nächstes müssen Sie die Suchfunktion ersetzen. Suchfunktionen sind normalerweise kleine Funktionen, die mehrere grundlegende Ganzzahloperationen verwenden (und xor, shift, add, remove und möglicherweise multiplication). Sie möchten, dass Ihre Eingaben übersetzt werden, indem Sie nach einer Art "eindeutigem Schlüssel" in Ihrer Tabelle suchen, der Ihnen dann einfach die Antwort auf alle gewünschten Aufgaben gibt.

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

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

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

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

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

 // 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, мы добавим значение где-то, на что нас не волнует. Здесь код:

 // 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

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