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

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

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

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


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

  • Что происходит?
  • Почему быстрее обрабатывается отсортированный массив, чем несортированный массив?
  • Код суммирует некоторые независимые термины, и порядок не имеет значения.
21290
задан GManNickG 27 июня '12 в 16:51
источник поделиться

22 ответов

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


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

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

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

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

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

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

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

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

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


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

image2

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

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

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

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

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


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

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

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

28153
ответ дан Mysticial 27 июня '12 в 16:56
источник поделиться

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

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

3563
ответ дан Daniel Fischer 27 июня '12 в 16:54
источник поделиться

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

Теперь, если мы посмотрим на код

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

мы можем обнаружить, что значение этой конкретной ветки if... else... заключается в добавлении чего-то, когда выполняется условие. Этот тип ветки можно легко преобразовать в оператор условного перемещения, который будет скомпилирован в условную инструкцию перемещения: cmovl в системе x86. Отверстие и, следовательно, штраф прогнозирования потенциальной ветки удаляются.

В C, таким образом C++, оператор, который непосредственно компилируется (без какой-либо оптимизации) в команду условного перемещения в x86, является тройным оператором ... ? ... : .... Поэтому мы переписываем приведенное выше утверждение в эквивалентное:

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

При сохранении читаемости мы можем проверить коэффициент ускорения.

На Intel Core i7 -2600K @3.4 GHz и режиме выпуска Visual Studio 2010 эталонный формат (формат, скопированный с Mystical)

x86

//  Branch - Random
seconds = 8.885

//  Branch - Sorted
seconds = 1.528

//  Branchless - Random
seconds = 3.716

//  Branchless - Sorted
seconds = 3.71

64

//  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 не включает ответвления на ветки, jmp, который имел бы существенное снижение производительности, если предсказанный результат неверен.

Итак, почему условный ход работает лучше?

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

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

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

В книге "Компьютерные системы: перспектива программиста" , второе издание подробно объясняется. Вы можете проверить раздел 3.6.6 для инструкций по условному перемещению, целую главу 4 для архитектуры процессора и раздел 5.11.2, для особого режима для штрафных санкций в ветки и санкции за мессидаж.

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

2897
ответ дан WiSaGaN 28 июня '12 в 5:14
источник поделиться

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

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

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

С перестановкой цикла мы можем безопасно изменить этот цикл на:

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

Затем вы можете видеть, что условие if является постоянным во время выполнения цикла i, поэтому вы можете вытащить if out:

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

Затем вы увидите, что внутренний цикл может быть свернут в одно единственное выражение, если предположить, что модель с плавающей запятой допускает ее (например,/fp: fast)

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

Это на 100 000 раз быстрее, чем раньше

1983
ответ дан vulcan raven 03 июля '12 в 5:25
источник поделиться

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

Сортировка:

==32551== Branches:        656,645,130  (  656,609,208 cond +    35,922 ind)
==32551== Mispredicts:         169,556  (      169,095 cond +       461 ind)
==32551== Mispred rate:            0.0% (          0.0%     +       1.2%   )

Unsorted:

==32555== Branches:        655,996,082  (  655,960,160 cond +  35,922 ind)
==32555== Mispredicts:     164,073,152  (  164,072,692 cond +     460 ind)
==32555== Mispred rate:           25.0% (         25.0%     +     1.2%   )

Свернув в линейный вывод, созданный cg_annotate, мы видим для рассматриваемого цикла:

Сортировка:

          Bc    Bcm Bi Bim
      10,001      4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .      .  .   .      {
           .      .  .   .          // primary loop
 327,690,000 10,016  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .      .  .   .          {
 327,680,000 10,006  0   0              if (data[c] >= 128)
           0      0  0   0                  sum += data[c];
           .      .  .   .          }
           .      .  .   .      }

Unsorted:

          Bc         Bcm Bi Bim
      10,001           4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .           .  .   .      {
           .           .  .   .          // primary loop
 327,690,000      10,038  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .           .  .   .          {
 327,680,000 164,050,007  0   0              if (data[c] >= 128)
           0           0  0   0                  sum += data[c];
           .           .  .   .          }
           .           .  .   .      }

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


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

perf stat ./sumtest_sorted

Сортировка:

 Performance counter stats for './sumtest_sorted':

  11808.095776 task-clock                #    0.998 CPUs utilized          
         1,062 context-switches          #    0.090 K/sec                  
            14 CPU-migrations            #    0.001 K/sec                  
           337 page-faults               #    0.029 K/sec                  
26,487,882,764 cycles                    #    2.243 GHz                    
41,025,654,322 instructions              #    1.55  insns per cycle        
 6,558,871,379 branches                  #  555.455 M/sec                  
       567,204 branch-misses             #    0.01% of all branches        

  11.827228330 seconds time elapsed

Unsorted:

 Performance counter stats for './sumtest_unsorted':

  28877.954344 task-clock                #    0.998 CPUs utilized          
         2,584 context-switches          #    0.089 K/sec                  
            18 CPU-migrations            #    0.001 K/sec                  
           335 page-faults               #    0.012 K/sec                  
65,076,127,595 cycles                    #    2.253 GHz                    
41,032,528,741 instructions              #    0.63  insns per cycle        
 6,560,579,013 branches                  #  227.183 M/sec                  
 1,646,394,749 branch-misses             #   25.10% of all branches        

  28.935500947 seconds time elapsed

Он также может создавать аннотацию исходного кода с дизассемблированием.

perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
 Percent |      Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
         :                      sum += data[c];
    0.00 :        400a1a:       mov    -0x14(%rbp),%eax
   39.97 :        400a1d:       mov    %eax,%eax
    5.31 :        400a1f:       mov    -0x20040(%rbp,%rax,4),%eax
    4.60 :        400a26:       cltq   
    0.00 :        400a28:       add    %rax,-0x30(%rbp)
...

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

1649
ответ дан caf 12 окт. '12 в 8:53
источник поделиться

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

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

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

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

Фон и почему

Pfew, так что, черт возьми, это должно означать?

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

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

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

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

Построение таблицы

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

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

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

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

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

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

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

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

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

Результат этого случая

// 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.Next(256);

//To keep the spirit of the code in-tact 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();
1126
ответ дан atlaste 24 апр. '13 в 9:26
источник поделиться

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

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

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

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

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

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

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

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

Condition            Pattern                 Time (ms)

(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0            TF alternating    760

(i & 3) == 0            TFFFTFFF…          513

(i & 2) == 0            TTFFTTFF…          1675

(i & 4) == 0            TTTTFFFFTTTTFFFF… 1275

(i & 8) == 0            8T 8F 8T 8F …     752

(i & 16) == 0            16T 16F 16T 16F … 490

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

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

1003
ответ дан Saqlain 15 февр. '13 в 10:24
источник поделиться

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

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

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

// Test
clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

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

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

// 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 или что-то еще) имела массив указателей длины-2 и использовал метод "принятия решения", чтобы решить, какой из них следует следовать, Например, вместо:

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

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

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

Вот ссылка на этот код: Красные Черные Деревья, Вечно Confuzzled

936
ответ дан steveha 22 июля '13 в 11:29
источник поделиться

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

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

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

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

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

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

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

863
ответ дан Yves Daoust 24 июля '13 в 10:57
источник поделиться

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

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

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

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

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

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

671
ответ дан Harsh Sharma 03 июля '15 в 18:35
источник поделиться

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

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

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

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

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

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

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


595
ответ дан Surt 12 окт. '15 в 0:05
источник поделиться

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

562
ответ дан rkachach 23 сент. '15 в 17:57
источник поделиться

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

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

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

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

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

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

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

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

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

bool a, b;
b = !a;

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

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

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

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

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

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

536
ответ дан Maciej 10 окт. '15 в 3:30
источник поделиться

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

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

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

205
ответ дан ForeverLearning 12 янв. '17 в 4:50
источник поделиться

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

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

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

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

Branch Prediction

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

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

1. Static

2. Dynamic

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

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

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

202
ответ дан Alireza 18 июня '17 в 14:40
источник поделиться

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

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

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

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

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

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

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

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

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

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------
111
ответ дан Tony Tannous 04 авг. '17 в 13:07
источник поделиться

Как уже упоминалось другими, за тайной стоит Branch Predictor.

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

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

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

figure 1

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

  • Без Branch Predictor.

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

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

без предиктора ветвления

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

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

введите описание изображения здесь

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

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

введите описание изображения здесь

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

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

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

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

88
ответ дан Gearon 06 нояб. '17 в 19:15
источник поделиться

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

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

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

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

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

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

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

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

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

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

Литература:

68
ответ дан aghilpro 03 окт. '17 в 12:47
источник поделиться

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

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

 // 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];               
 }
48
ответ дан Yochai Timmer 23 нояб. '17 в 17:28
источник поделиться

В 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
37
ответ дан Luke Hutchison 22 дек. '17 в 16:13
источник поделиться

Простыми словами: Резюме всех замечательных ответов для начинающих...

Это происходит из-за того, что называется предсказанием ветвления.

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

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

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

____________________________________________________________________________
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

Unsorted Array: Curved Road
______     _______
|           | __ |

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

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

23
ответ дан Omkaar.K 07 дек. '17 в 20:28
источник поделиться

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

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

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

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

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

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

-3
ответ дан TimeTrax 04 июня '18 в 12:14
поделиться

Другие вопросы по меткам