Почему обрабатывать отсортированный массив быстрее, чем несортированный?

22512

Вот кусок кода на С++, который кажется очень своеобразным. По какой-то странной причине сортировка данных чудом делает код почти в шесть раз быстрее.

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;

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

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}
  • Без std::sort(data, data + arraySize); код запускается через 11.54 секунды.
  • С отсортированными данными код запускается за 1,93 секунды.

Вначале я думал, что это может быть просто аномалия языка или компилятора. Поэтому я попробовал это на Java.

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

С несколько похожим, но менее экстремальным результатом.


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

  • Что происходит?
  • Почему быстрее обрабатывается отсортированный массив, чем несортированный массив?
  • Код суммирует некоторые независимые термины, и порядок не имеет значения.
  • 157
    Только для записи. На Windows / VS2017 / i7-6700K 4GHz нет никакой разницы между двумя версиями. Это занимает 0,6 с в обоих случаях. Если количество итераций во внешнем цикле увеличивается в 10 раз, то время выполнения увеличивается в 10 раз и до 6 с в обоих случаях.
  • 45
    @ user194715: любой компилятор, использующий cmov или другую реализацию без ветвей (например, автоматическую векторизацию с pcmpgtd ), будет иметь производительность, не зависящую от данных ни на одном процессоре. Но если он ветвистый, он будет зависеть от сортировки на любом процессоре с нестандартным спекулятивным выполнением. (Даже высокопроизводительные центральные процессоры используют предсказание ветвлений, чтобы избежать появления пузырей извлечения / декодирования на взятых ветвях; штраф за промах меньше).
Показать ещё 15 комментариев
Теги:
optimization
performance
branch-prediction

26 ответов

31040
Лучший ответ

Вы являетесь жертвой отклонение от ветвления.


Что такое предсказание ветвей?

Рассмотрим железнодорожный узел:

Изображение 1152 Изображение от Mecanismo, через Википедия. Используется под лицензией CC-By-SA 3.0.

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

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

Поезда тяжелые и имеют большую инерцию. Таким образом, они навсегда задерживаются и замедляются.

Есть ли лучший способ? Вы догадываетесь, в каком направлении поезд поедет!

  • Если вы догадались, он продолжается.
  • Если вы догадались, капитан остановится, поддержит вас и кричит на вас, чтобы перевернуть переключатель. Затем он может перезапустить другой путь.

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


Рассмотрим if-statement: На уровне процессора это инструкция перехода:

Изображение 1153

Вы процессор, и вы видите ветку. Вы не представляете, как он пойдет. Чем ты занимаешься? Вы прекратите выполнение и дождитесь завершения предыдущих инструкций. Затем вы продолжаете идти по правильному пути.

Современные процессоры сложны и имеют длинные конвейеры. Поэтому они навсегда наводят "разогрев" и "замедляют".

Есть ли лучший способ? Вы догадываетесь, в каком направлении идет ветка!

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

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


Это предсказание ветвления. Я признаю, что это не лучшая аналогия, так как поезд может просто сигнализировать направление флагом. Но в компьютерах процессор не знает, к какому направлению идет ветка до последнего момента.

Итак, как бы вы стратегически угадали, чтобы свести к минимуму количество раз, которое поезд должен поддерживать и идти по другому пути? Вы смотрите на прошлую историю! Если поезд уходит в 99% случаев, то вы угадаете, что осталось. Если он чередуется, вы чередуете свои догадки. Если он идет один путь каждые 3 раза, вы догадываетесь о том же...

Другими словами, вы пытаетесь идентифицировать шаблон и следовать ему. Это более или менее то, как работают предиктора отрасли.

В большинстве приложений есть ведомые ветки. Таким образом, современные отраслевые предсказатели обычно достигают > 90% ставок. Но, столкнувшись с непредсказуемыми ветвями без узнаваемых паттернов, предсказатели ветвей практически бесполезны.

Дополнительная литература: статья "Отраслевой прогноз" в Википедии.


Как указано выше, виновником является это if-statement:

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

Обратите внимание, что данные распределены равномерно между 0 и 255. Когда данные сортируются, примерно первая половина итераций не будет вводить оператор if. После этого все они войдут в оператор if.

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

Быстрая визуализация:

T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

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

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...   (completely random - hard to predict)

Итак, что можно сделать?

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

Заменить:

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

с:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

Это устраняет ветвь и заменяет ее некоторыми побитовыми операциями.

(Обратите внимание, что этот хак не является строго эквивалентным исходному if-statement, но в этом случае он действителен для всех входных значений data[].)

Тесты: Core i7 920 @3.5 ГГц

С++ - Visual Studio 2010 - выпуск x64

//  Branch - Random
seconds = 11.777

//  Branch - Sorted
seconds = 2.352

//  Branchless - Random
seconds = 2.564

//  Branchless - Sorted
seconds = 2.587

Java - Netbeans 7.1.1 JDK 7 - x64

//  Branch - Random
seconds = 10.93293813

//  Branch - Sorted
seconds = 5.643797077

//  Branchless - Random
seconds = 3.113581453

//  Branchless - Sorted
seconds = 3.186068823

замечания:

  • С веткой: Существует огромная разница между отсортированными и несортированными данными.
  • С Hack: Нет никакой разницы между отсортированными и несортированными данными.
  • В случае С++ хак на самом деле чуть медленнее, чем с веткой, когда данные сортируются.

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


Update:

  • GCC 4.6.1 с -O3 или -ftree-vectorize на x64 способен генерировать условное перемещение. Таким образом, нет никакой разницы между отсортированными и несортированными данными - оба быстро.

  • VС++ 2010 не может генерировать условные ходы для этой ветки даже в /Ox.

  • Intel Compiler 11 делает что-то чудесное. Он меняет две петли, тем самым поднимая непредсказуемую ветвь во внешний цикл. Таким образом, он не только защищает ошибочные предсказания, но и в два раза быстрее, чем любые VС++ и GCC могут генерировать! Другими словами, ICC воспользовался тестовым циклом, чтобы победить в тесте...

  • Если вы предоставите компилятору Intel нераспространяемый код, он просто не имеет права векторизовать его... и так же быстро, как с веткой (с обменом петлями).

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

  • 287
    @Mysticial Чтобы избежать взлома смещения, вы можете написать что-то вроде int t=-((data[c]>=128)) для генерации маски. Это тоже должно быть быстрее. Было бы интересно узнать, достаточно ли умен компилятор для вставки условного перемещения или нет.
  • 200
    @phonetagger Посмотрите на следующий вопрос: stackoverflow.com/questions/11276291/… Компилятор Intel подошел довольно близко к полному избавлению от внешнего цикла.
Показать ещё 77 комментариев
4005

Прогнозирование ветвей.

С отсортированным массивом условие data[c] >= 128 является первым false для строки значений, а затем становится true для всех последующих значений. Это легко предсказать. При несортированном массиве вы платите за затраты на разветвление.

  • 89
    Предсказание ветвлений работает лучше на отсортированных массивах по сравнению с массивами с разными шаблонами? Например, для массива -> {10, 5, 20, 10, 40, 20, ...} следующий элемент в массиве из шаблона - 80. Будет ли ускорен этот тип массива с помощью предсказания перехода в какой следующий элемент 80 здесь, если шаблон следует? Или это обычно помогает только с отсортированными массивами?
  • 106
    Таким образом, в основном все, что я обычно узнал о big-O, выходит за окно? Лучше понести стоимость сортировки, чем стоимость ветвления?
Показать ещё 7 комментариев
2911

Причина, по которой производительность резко повышается при сортировке данных, заключается в том, что штраф за предсказание ветвлений устранен, как прекрасно объяснено в ответе 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). Зная разницу в производительности между ветвлением и условным перемещением, когда он непредсказуем, может помочь нам написать код с лучшей производительностью, когда сценарий становится настолько сложным, что компилятор не может их оптимизировать автоматически.

  • 123
    Нет уровня оптимизации по умолчанию, если вы не добавите -O в командные строки GCC. (И вы не можете иметь худший английский, чем мой;)
  • 77
    Мне трудно поверить, что компилятор может оптимизировать троичный оператор лучше, чем эквивалентный оператор if. Вы показали, что GCC оптимизирует троичный оператор для условного перемещения; Вы не показали, что это не делает то же самое для оператора if. В самом деле, согласно Мистическим выше, GCC делает оптимизировать Условный оператор к условному ходу, что бы сделать этот ответ совершенно неправильно.
Показать ещё 9 комментариев
2000

Если вам интересно, что еще больше оптимизаций, которые могут быть сделаны с этим кодом, рассмотрите следующее:

Начиная с исходного цикла:

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 раз быстрее, чем раньше

  • 252
    Если вы хотите обмануть, вы можете также вывести умножение за пределы цикла и выполнить sum * = 100000 после цикла.
  • 63
    @Michael - я считаю, что этот пример на самом деле является примером оптимизации циклически-инвариантного подъема (LIH), а НЕ цикла подкачки . В этом случае весь внутренний цикл не зависит от внешнего цикла и поэтому может быть выведен из внешнего цикла, после чего результат просто умножается на сумму по i равную одной единице = 1e5. Это не имеет никакого значения для конечного результата, но я просто хотел установить рекорд, так как это такая часто посещаемая страница.
Показать ещё 5 комментариев
1671

Несомненно, некоторые из нас будут интересоваться способами идентификации кода, который является проблематичным для процессора-предсказателя 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)
...

Подробнее см. руководство по производительности.

  • 63
    Это страшно, в несортированном списке должна быть 50% вероятность попадания в адд. Каким-то образом предсказание ветвлений имеет только 25% промахов, как это может быть лучше, чем промах 50%?
  • 108
    @ tall.b.lo: 25% от всех ветвей - в цикле две ветви, одна для data[c] >= 128 (которая, как вы предлагаете, имеет 50% промахов) и одна для условия цикла c < arraySize который имеет ~ 0% промахов.
1160

Я просто прочитал этот вопрос и его ответы, и я чувствую, что ответ отсутствует.

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

Этот подход работает в целом, если:

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

Фон и почему

С точки зрения процессора, ваша память работает медленно. Чтобы компенсировать разницу в скорости, в ваш процессор встроена пара кешей (кеш L1/L2). Итак, представьте, что вы делаете свои хорошие вычисления и выясните, что вам нужен кусок памяти. Процессор выполнит операцию загрузки и загрузит часть памяти в кеш, а затем использует кеш для выполнения остальных вычислений. Поскольку память относительно медленная, эта "загрузка" замедлит вашу программу.

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

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

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

Построение стола

Итак, мы выяснили, что можем создать небольшую таблицу. Следующее, что нужно сделать, это установить на место функцию поиска. Функции поиска обычно представляют собой небольшие функции, которые используют несколько основных целочисленных операций (и, или, xor, shift, add, remove и, возможно, умножение). Вы хотите, чтобы ваш ввод был переведен с помощью функции поиска в какой-то "уникальный ключ" в вашей таблице, который затем просто дает вам ответ на всю работу, которую вы хотели, чтобы он делал.

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

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

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

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

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

for (int i = 0; i < array.Length; ++i)
{
   // Use array[i]
}

В этом случае для компилятора очевидно, что граничное условие никогда не будет выполнено. По крайней мере компилятор Microsoft JIT (но я ожидаю, что Java делает подобные вещи) заметит это и вообще уберет проверку. Вау, это означает, что нет ветки. Точно так же это будет иметь дело с другими очевидными случаями.

Если у вас возникли проблемы с поиском на управляемых языках - ключ заключается в том, чтобы добавить & 0x[something]FFF к вашей функции поиска, чтобы сделать проверку границ предсказуемой, - и наблюдать, как она идет быстрее.

Результат этого дела

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

/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/

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)
    {
        /* Here you basically want to use simple operations - so no
        random branches, but things like &, |, *, -, +, etc. are fine. */
        sum += lookup[data[j]];
    }
}

DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();
  • 50
    Вы хотите обойти ветвь-предиктор, почему? Это оптимизация.
  • 95
    Потому что ни одна ветка не лучше, чем ветка :-) Во многих ситуациях это просто намного быстрее ... если вы оптимизируете, это определенно стоит попробовать. Они также используют его в f.ex. graphics.stanford.edu/~seander/bithacks.html
Показать ещё 8 комментариев
1144

Поскольку данные распределяются между 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 до шести раз медленнее, чем шаблон хороший! Конечно, какой шаблон хорош, а что плохой, зависит от точных инструкций, генерируемых компилятором и конкретным процессором.

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

  • 46
    Вы не показываете время «случайного» паттерна TF.
  • 17
    @MooingDuck 'Потому что это не будет иметь значения - это значение может быть чем угодно, но оно все равно будет в пределах этих порогов. Так зачем показывать случайное значение, когда вы уже знаете пределы? Хотя я согласен с тем, что вы могли бы показать один из них для полноты картины и «просто ради этого».
Показать ещё 6 комментариев
1017

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

Но в этом случае мы знаем, что значения находятся в диапазоне [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

  • 25
    Правильно, вы также можете просто использовать бит напрямую и умножить ( data[c]>>7 - что также обсуждается где-то здесь); Я намеренно пропустил это решение, но, конечно, вы правы. Небольшое примечание: практическое правило для справочных таблиц состоит в том, что если он умещается в 4 КБ (из-за кэширования), он будет работать - желательно, чтобы таблица была как можно меньше. Для управляемых языков я бы увеличил это до 64 КБ, для низкоуровневых языков, таких как C ++ и C, я бы, вероятно, пересмотрел (это только мой опыт). Поскольку typeof(int) = 4 , я бы попробовал придерживаться до 10 бит.
  • 16
    Я думаю, что индексирование со значением 0/1, вероятно, будет быстрее, чем целочисленное умножение, но я думаю, что если производительность действительно важна, вы должны профилировать ее. Я согласен с тем, что маленькие таблицы поиска необходимы, чтобы избежать нагрузки на кеш, но ясно, что если у вас больший кэш, вы можете справиться с большей таблицей поиска, поэтому 4 КБ - это скорее практическое правило, чем жесткое правило. Я думаю, что вы имели в виду sizeof(int) == 4 ? Это было бы верно для 32-разрядных. Мой двухлетний сотовый телефон имеет кэш-память L1 объемом 32 КБ, поэтому даже таблица поиска 4K может работать, особенно если значения поиска были байтами, а не целыми.
Показать ещё 8 комментариев
876

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

В самом деле, массив разбивается в смежной зоне с 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) :-)

  • 23
    sum= 3137536 - умная. Это, очевидно, не в этом вопрос. Вопрос в том, чтобы объяснить удивительные характеристики производительности. Я склонен сказать, что добавление выполнения std::partition вместо std::sort ценно. Хотя актуальный вопрос распространяется не только на синтетический тест.
  • 12
    @DeadMG: это действительно не стандартный дихотомический поиск по заданному ключу, а поиск по индексу разделения; требуется одно сравнение на одну итерацию. Но не полагайтесь на этот код, я не проверял его. Если вы заинтересованы в гарантированно правильной реализации, дайте мне знать.
707

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

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

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

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

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

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

Возвращаясь к вышеуказанному вопросу, рассмотрим следующие инструкции:

                        A) if (data[c] >= 128)
                                /\
                               /  \
                              /    \
                        true /      \ false
                            /        \
                           /          \
                          /            \
                         /              \
              B) sum += data[c];          C) for loop or print().

Без предсказания ветвления произойдет следующее:

Для выполнения инструкции B или инструкции C процессору придется ждать, пока инструкция A не достигнет стадии EX в конвейере, так как решение перейти к инструкции B или инструкции C зависит от результата команды A. Таким образом, трубопровод будет выглядеть следующим образом.

, если условие возвращает true: Изображение 1155

Если условие возвращает false: Изображение 1156

В результате ожидания результата команды А общие циклы ЦП, проведенные в вышеуказанном случае (без предсказания ветвления, для истины и false) равны 7.

Итак, что такое предсказание ветвей?

Предисловие ветки будет пытаться угадать, к какой ветке (структура if-then-else) будет идти, прежде чем это будет известно наверняка. Он не будет ждать, пока инструкция А достигнет стадии EX конвейера, но она угадает решение и перейдет к этой инструкции (B или C в случае нашего примера).

В случае правильной догадки конвейер выглядит примерно так: Изображение 1157

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

В OP-коде, в первый раз, когда условие условно, предсказатель ветвления не имеет никакой информации для прогнозирования базы, поэтому в первый раз он будет случайным образом выбирать следующую команду. Позже в цикле for он может основывать предсказание на истории. Для массива, отсортированного по возрастанию, существует три возможности:

  • Все элементы меньше 128
  • Все элементы больше 128
  • Некоторые начинающие новые элементы меньше 128, а затем становятся больше 128

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

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

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

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

  • 1
    как две инструкции выполняются вместе? это сделано с отдельными ядрами процессора, или инструкция конвейера интегрирована в одно ядро процессора?
  • 1
    @ M.kazemAkhgary Это все внутри одного логического ядра. Если вам интересно, это хорошо описано, например, в Руководстве разработчика программного обеспечения Intel
619

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

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

Изображение 1158

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

data[c] = std::rand() % 256;

чтобы предиктор изменил стороны как удар std::rand().

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


587

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

if (likely( everything_is_ok ))
{
    /* Do something */
}

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

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

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

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

558

Часто используемые логические операции в С++ производят много ветвей в скомпилированной программе. Если эти ветки находятся внутри циклов, и их трудно предсказать, они могут значительно замедлить выполнение. Булевы переменные сохраняются как 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;

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

277

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

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

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

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

Изображение 1159

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

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

1. Static

2. Dynamic

Изображение 1160

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

Чтобы эффективно писать свой код, чтобы воспользоваться этими правила при написании операторов if-else или switch, проверьте наиболее в первую очередь, с обычными делами и постепенным сокращением до минимума. Циклы не обязательно требуют специального заказа кода для статическое предсказание ветвления, поскольку только условие итератора цикла обычно используется.

259

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

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

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

  • 2
    Это очень интересная статья (на самом деле, я только что все прочитал), но как она отвечает на вопрос?
  • 2
    @PeterMortensen Я немного озадачен вашим вопросом. Например, вот одна соответствующая строка из этого фрагмента: When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping. Автор пытается обсудить профилирование в контексте кода, размещенного здесь, и в процессе пытается объяснить, почему отсортированный случай намного быстрее.
156

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

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

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

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

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

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

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

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

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

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------
149

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

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

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

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

Изображение 1161

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

  1. Без предсказателя отрасли.

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

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

Изображение 1162

Для выполнения 3-х инструкций потребуется 9 тактов.

  1. Используйте Branch Predictor и не делайте условный переход. Предположим, что прогноз не принимает условный переход.

Изображение 1163

Для выполнения 3-х инструкций потребуется 7 тактов.

  1. Используйте Branch Predictor и сделайте условный прыжок. Предположим, что прогноз не принимает условный переход.

Изображение 1164

Для выполнения 3-х инструкций потребуется 9 тактов.

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

Как видите, у нас нет причин не использовать Branch Predictor.

Это довольно простая демонстрация, которая разъясняет самую основную часть Branch Predictor. Если эти картинки раздражают, пожалуйста, удалите их из ответа, и посетители также могут получить демо из git

103

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

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

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

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

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

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

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

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

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

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

Литература:

91

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

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

 // 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];               
 }
  • 0
    Правильно, но стоимость установки сортировки массива составляет O (N log N), поэтому раннее прерывание не поможет вам, если единственная причина, по которой вы сортируете массив, заключается в возможности преждевременного прерывания. Однако, если у вас есть другие причины для предварительной сортировки массива, то да, это ценно.
  • 0
    @LukeHutchison хорошее наблюдение; пожалуйста, смотрите мой ответ ниже для другого дубля.
Показать ещё 3 комментария
87

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

MOV R0, #0     // R0 = sum = 0
MOV R1, #0     // R1 = c = 0
ADR R2, data   // R2 = addr of data array (put this instruction outside outer loop)
.inner_loop    // Inner loop branch label
    LDRB R3, [R2, R1]     // R3 = data[c]
    CMP R3, #128          // compare R3 to 128
    ADDGE R0, R0, R3      // if R3 >= 128, then sum += data[c] -- no branch needed!
    ADD R1, R1, #1        // c++
    CMP R1, #arraySize    // compare c to arraySize
    BLT inner_loop        // Branch to inner_loop if c < arraySize
  • 1
    Вы говорите, что каждая инструкция может быть условной? Таким образом, несколько инструкций с суффиксом GE могут выполняться последовательно, без изменения значения R3 между?
  • 1
    Да, правильно, каждая инструкция может быть обусловлена ARM, по крайней мере, в 32- и 64-битных наборах команд. Есть выделенное 4-битное поле условия. У вас может быть несколько инструкций подряд с одним и тем же условием, но в какой-то момент, если вероятность ложного условия не пренебрежимо мала, эффективнее будет добавить ветвь.
Показать ещё 2 комментария
84

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

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

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

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

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

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

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

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

______   ________
|     |__|

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

___________________________________________ Straight road
 |_________________________________________|Longer road

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

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

  • 9
    «Простыми словами» - я нахожу ваше объяснение менее простым, чем в других поездах, и гораздо менее точным, чем любой другой ответ, хотя я не новичок. Мне очень любопытно, почему так много отрицательных отзывов, может быть, кто-то из будущих доверенных лиц скажет мне?
  • 6
    @Sinatr, вероятно, это действительно основано на мнении, я сам нашел это достаточно хорошим, чтобы выразить свое мнение, это, конечно, не так точно, как другие примеры, в этом весь смысл: раздавать ответ (как мы все можем согласиться, что здесь используется предсказание ветвлений) без заставлять читателей искать технические объяснения, как это делали другие (очень хорошо). И я думаю, что он сделал это достаточно хорошо.
Показать ещё 3 комментария
3

Хотя, как уже упоминалось в других ответах, современные компиляторы или архитектуры (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(&data[c], &data[c + 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) общем случае.

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

  • 0
    Я действительно не вижу, как это доказывает что-нибудь? Единственное, что вы показали, это то, что «не вся работа по сортировке всего массива занимает меньше времени, чем сортировка всего массива». Ваше утверждение, что это «также работает быстрее всего», очень зависит от архитектуры. Смотрите мой ответ о том, как это работает на ARM. PS вы могли бы ускорить свой код на архитектурах без ARM, поместив суммирование внутри цикла из 200 элементов, отсортировав его в обратном порядке, а затем воспользовавшись предложением Йохая Тиммера об разрыве, как только вы получите значение вне диапазона. Таким образом, каждое суммирование блока из 200 элементов может быть прекращено досрочно.
  • 0
    @LukeHutchison Доказательство - для ОП, а не для такого хорошо информированного автора, как вы. Для ОП это сводит на нет гипотезу о том, что сортировка имеет какое-либо отношение к более быстрой обработке (см. Формулировку названия вопроса). «Работает быстрее всего» в алгоритмическом смысле в архитектуре общего назначения - ARM - особый случай. Предложение Йохая Тиммера - это оптимизация, которая не алгоритмична в смысле «большой-О». Кроме того, в общем, люди будут делать что-то и в истинном, и в ложном случае, чтобы взлом Йохая не применялся и, вероятно, что-то более важное, чем суммирование.
2

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

0

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

0

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

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

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

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

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

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

-4

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

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню