Почему обработка отсортированного массива быстрее, чем обработка несортированного массива? - программирование

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

Вот фрагмент кода C++, который демонстрирует очень специфическое поведение. По какой-то странной причине сортировка данных чудесным образом делает код почти в шесть раз быстрее:

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

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


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

  • Что происходит?
  • Почему обработка отсортированного массива быстрее, чем обработка несортированного массива?

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

4b9b3361

Ответ 1

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


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

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

Image showing a railroad junction Изображение Mecanismo, через Wikimedia Commons.Используется по лицензии CC-By-SA 3.0.

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

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

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

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

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

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


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

Screenshot of compiled code containing an if statement

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

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

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

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

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


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

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

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

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

Дальнейшее чтение: статья "Ветка предиктора" в Википедии.


Как указывалось выше, виновником является следующее if-утверждение:

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

Тесты: Core i7 920 @3,5 ГГц

C++ - 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: нет разницы между отсортированными и несортированными данными.
  • В случае C++ хак на самом деле медленнее, чем с веткой, когда данные сортируются.

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


Обновить:

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

Начиная с оригинального цикла:

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:

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

Ответ 5

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

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

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

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

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

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

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

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

Ответ 8

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

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

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

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

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

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

Вот ссылка на этот код: Красные черные деревья, вечно сбитые с толку

Ответ 9

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

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

Ответ 10

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

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

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

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

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

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

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

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

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

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

, если условие возвращает true: enter image description here

Если условие возвращает false: enter image description here

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

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

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

В случае правильной догадки конвейер выглядит примерно так: enter image description here

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

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

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

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

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

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

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

Ответ 11

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

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

2-разрядная диаграмма состояний

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

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

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

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


Ответ 12

В той же строке (я думаю, что это не было подчеркнуто каким-либо ответом), полезно отметить, что иногда (особенно в программном обеспечении, где важна производительность - например, в ядре 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 раз, то почему бы не сообщить компилятору об этом? Таким образом, по умолчанию предсказание ветвления предполагает, что условие ложно.

Ответ 13

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

Булевы переменные переопределены в том смысле, что все операторы, которые имеют булевы переменные в качестве входных данных, проверяют, имеют ли входные значения любое другое значение, кроме 0 или 1, но операторы, которые имеют логические переменные в качестве выходных данных, не могут производить никаких других значений, кроме 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.

~ нельзя использовать для НЕ. Вместо этого вы можете сделать логическое НЕ для переменной, которая, как известно, равна 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, & will). Аналогично, a || b a || b не может быть заменен a | b a | b если b является выражением, которое не должно оцениваться, если a true.

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

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

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

Ответ 14

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

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

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

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

Branch Prediction

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

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

1. Static

2. Dynamic

Отраслевое предсказание

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

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

Ответ 15

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

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

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

Ответ 16

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

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

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

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

figure 1

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

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

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

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

without branch predictor

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

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

enter image description here

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

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

enter image description here

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

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

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

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

Ответ 17

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

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

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

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

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

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

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

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

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

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

Ответ 18

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

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

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

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

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

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

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

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

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

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

Литература:

Ответ 19

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

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

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }

Ответ 20

В 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

Ответ 21

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

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

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

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

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

Сортированный массив: Прямая дорога ____________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Несортированный массив: Кривая дорога

______   ________
|     |__|

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

___________________________________________ Straight road
 |_________________________________________|Longer road

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


Также я хочу процитировать @Simon_Weaver из комментариев:

Он не делает меньше прогнозов - он делает меньше неправильных прогнозов. Это все еще должно предсказывать для каждого раза через цикл...

Ответ 22

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

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

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

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

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

Ответ 23

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

Ответ 24

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

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

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

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

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

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

Ответ 25

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

Ответ 26

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

Ответ 27

Это интуитивно понятно как ф ***. Попробуйте задать свой вопрос этот вопрос. Вы найдете ответ.

Скажите, если есть колода карт и вас попросят найти конкретную карту (скажем, 7 червей) Не будет ли это дороже в несортированной колоде, чем если бы вам было предложено найти ту же карту в отсортированном виде? колода. (В отсортированной колоде вам просто нужно будет скользить глазами, пока вы не начнете видеть Червы, а затем, пока не увидите число "7"). Ты можешь это почувствовать?

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

Ответ 28

Бьярне Страуструп Ответ на этот вопрос:

Это звучит как вопрос интервью. Это правда? Как бы вы узнали? Плохо отвечать на вопросы об эффективности, не проводя предварительных измерений, поэтому важно знать, как их измерять.

Итак, я попытался с вектором миллиона целых чисел и получил:

Already sorted    32995 milliseconds
Shuffled          125944 milliseconds

Already sorted    18610 milliseconds
Shuffled          133304 milliseconds

Already sorted    17942 milliseconds
Shuffled          107858 milliseconds

Я запускал это несколько раз, чтобы быть уверенным. Да, феномен настоящий. Мой код ключа был:

void run(vector<int>& v, const string& label)
{
    auto t0 = system_clock::now();
    sort(v.begin(), v.end());
    auto t1 = system_clock::now();
    cout << label 
         << duration_cast<microseconds>(t1 — t0).count() 
         << " milliseconds\n";
}

void tst()
{
    vector<int> v(1'000'000);
    iota(v.begin(), v.end(), 0);
    run(v, "already sorted ");
    std::shuffle(v.begin(), v.end(), std::mt19937{ std::random_device{}() });
    run(v, "shuffled    ");
}

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

Одной из причин является предсказание ветвлений: ключевая операция в алгоритме сортировки - 'if(v[i] < pivot]) …' или эквивалентная. Для отсортированной последовательности этот тест всегда верен, тогда как для случайной последовательности выбранная ветвь изменяется случайным образом.

Другая причина в том, что когда вектор уже отсортирован, нам никогда не нужно перемещать элементы в правильное положение. Эффект этих маленьких деталей - фактор пяти или шести, который мы видели.

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

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

Ответ 29

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

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

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

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

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

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

Ответ 30

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

  // CPP program to demonstrate processing 
  // time of sorted and unsorted array 
  #include <iostream> 
  #include <algorithm> 
  #include <ctime> 
 using namespace std; 

const int N = 100001; 

int main() 
{ 
int arr[N]; 

// Assign random values to array 
for (int i=0; i<N; i++) 
    arr[i] = rand()%N; 

// for loop for unsorted array 
int count = 0; 
double start = clock(); 
for (int i=0; i<N; i++) 
    if (arr[i] < N/2) 
        count++; 

double end = clock(); 
cout << "Time for unsorted array :: "
    << ((end - start)/CLOCKS_PER_SEC) 
    << endl; 
sort(arr, arr+N); 

// for loop for sorted array 
count = 0; 
start = clock(); 

for (int i=0; i<N; i++) 
    if (arr[i] < N/2) 
        count++; 

end = clock(); 
cout << "Time for sorted array :: "
    << ((end - start)/CLOCKS_PER_SEC) 
    << endl; 

return 0; 
} 

ВЫХОД

Execution 1:
Time for unsorted array :: 0.00108
Time for sorted array :: 0.00053

Execution 2:
Time for unsorted array :: 0.001101
Time for sorted array :: 0.000593

Execution 3:
Time for unsorted array :: 0.0011
Time for sorted array :: 0.000418