Вот фрагмент кода 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);
}
}
С похожим, но менее экстремальным результатом.
Моей первой мыслью было, что сортировка переносит данные в кеш, но потом я подумал, насколько это глупо, потому что массив только что сгенерирован.
- Что здесь происходит?
- Почему обработка отсортированного массива быстрее, чем обработка несортированного массива?
Код суммирует некоторые независимые термины, поэтому порядок не должен иметь значения.
java
c++
performance
optimization
branch-prediction
GManNickG
источник
источник
Ответы:
Вы являетесь жертвой неудачного предсказания ветвлений .
Что такое предсказание отрасли?
Рассмотрим железнодорожный узел:
Изображение Mecanismo, через Wikimedia Commons. Используется по лицензии CC-By-SA 3.0 .
Теперь, ради аргумента, предположим, что это еще в 1800-х годах - до дальней связи или радиосвязи.
Вы оператор развязки и слышите прибывающий поезд. Вы понятия не имеете, в каком направлении это должно идти. Вы останавливаете поезд, чтобы спросить водителя, в каком направлении они хотят. И тогда вы установите переключатель соответствующим образом.
Поезда тяжелые и обладают большой инерцией. Таким образом, им требуется вечность, чтобы начать и замедлить.
Есть ли способ лучше? Вы угадываете, в каком направлении будет идти поезд!
Если вы угадаете каждый раз , поезд никогда не остановится.
Если вы слишком часто угадываете неправильно , поезд будет тратить много времени на остановку, резервное копирование и перезапуск.
Рассмотрим оператор if: на уровне процессора это инструкция перехода:
Вы процессор, и вы видите ветку. Вы понятия не имеете, по какому пути это пойдет. Чем ты занимаешься? Вы останавливаете выполнение и ждете, пока предыдущие инструкции не будут выполнены. Затем вы продолжаете идти по правильному пути.
Современные процессоры сложны и имеют длинные трубопроводы. Таким образом они берут навсегда, чтобы "согреться" и "замедлить".
Есть ли способ лучше? Вы угадываете, в каком направлении пойдет филиал!
Если вы каждый раз угадаете , выполнение никогда не остановится.
Если вы слишком часто угадываете неправильно , вы тратите много времени на остановку, откат и перезапуск.
Это прогноз отрасли. Я признаю, что это не лучшая аналогия, так как поезд мог просто сигнализировать направление с флагом. Но в компьютерах процессор не знает, в каком направлении пойдет ветка, до последнего момента.
Итак, как вы могли бы стратегически угадать, сколько раз поезд должен вернуться и пойти по другому пути? Вы смотрите на прошлую историю! Если поезд идет влево 99% времени, значит, вы уехали. Если это чередуется, то вы чередуете свои догадки. Если это происходит в одну сторону каждые три раза, вы догадаетесь, то же самое ...
Другими словами, вы пытаетесь определить шаблон и следовать ему. Это более или менее то, как работают предсказатели ветвлений.
Большинство приложений имеют хорошо управляемые ветви. Таким образом, современные отраслевые предикторы, как правило, достигают> 90% показателей успеха. Но когда они сталкиваются с непредсказуемыми ветвями без распознаваемых шаблонов, предикторы ветвей практически бесполезны.
Дальнейшее чтение: статья "Ветка предиктора" в Википедии .
Как указывалось выше, виновником является следующее if-утверждение:
Обратите внимание, что данные равномерно распределены между 0 и 255. Когда данные отсортированы, примерно первая половина итераций не войдет в оператор if. После этого все они введут оператор if.
Это очень удобно для предиктора ветвления, поскольку ветвь последовательно идет в одном и том же направлении много раз. Даже простой насыщающий счетчик будет правильно предсказывать ветвь, за исключением нескольких итераций после переключения направления.
Быстрая визуализация:
Однако, когда данные полностью случайны, предиктор ветвления становится бесполезным, потому что он не может предсказать случайные данные. Таким образом, вероятно, будет около 50% неправильного прогноза (не лучше, чем случайное угадывание).
Так что же можно сделать?
Если компилятор не может оптимизировать переход в условный ход, вы можете попробовать некоторые методы, если вы готовы пожертвовать удобочитаемостью для производительности.
Заменить:
с:
Это исключает ветвление и заменяет его на некоторые побитовые операции.
(Обратите внимание, что этот хак не является строго эквивалентным исходному оператору if. Но в этом случае он действителен для всех входных значений
data[]
.)Тесты: Core i7 920 @ 3,5 ГГц
C ++ - Visual Studio 2010 - выпуск x64
Java - NetBeans 7.1.1 JDK 7 - x64
Замечания:
Общее правило - избегать зависимого от данных ветвления в критических циклах (например, в этом примере).
Обновить:
GCC 4.6.1 с
-O3
или-ftree-vectorize
на x64 может генерировать условный ход. Таким образом, нет никакой разницы между отсортированными и несортированными данными - оба быстры.(Или несколько быстро: для уже отсортированного случая
cmov
может быть медленнее, особенно если GCC ставит его на критический путь, а не простоadd
, особенно на Intel до Broadwell, гдеcmov
имеет 2 цикла задержки: флаг оптимизации gcc -O3 делает код медленнее, чем -O2 )VC ++ 2010 не может генерировать условные перемещения для этой ветви даже в
/Ox
.Компилятор Intel C ++ (ICC) 11 совершает чудеса. Он чередует две петли , тем самым поднимая непредсказуемую ветвь на внешнюю петлю. Таким образом, он не только защищен от неправильных прогнозов, но также в два раза быстрее, чем то, что могут генерировать VC ++ и GCC! Другими словами, ICC воспользовалась тестовым циклом, чтобы победить тест ...
Если вы дадите компилятору Intel код без ответвлений, он просто сразу его векторизует ... и так же быстро, как и с ветвью (с циклическим обменом).
Это говорит о том, что даже зрелые современные компиляторы могут сильно различаться по своей способности оптимизировать код ...
источник
Отраслевой прогноз.
В отсортированном массиве условие
data[c] >= 128
сначалаfalse
для последовательности значений, а затем становитсяtrue
для всех последующих значений. Это легко предсказать. С несортированным массивом вы платите за стоимость ветвления.источник
Причина, по которой производительность резко улучшается при сортировке данных, заключается в том, что штраф за предсказание ветвлений снят, как прекрасно объясняется в ответе Mysticial .
Теперь, если мы посмотрим на код
мы можем обнаружить, что смысл этой конкретной
if... else...
ветви состоит в том, чтобы добавить что-то, когда условие выполнено. Этот тип ветки может быть легко преобразован в оператор условного перемещения , который будет скомпилирован в инструкцию условного перемещения:cmovl
вx86
системе. Ветвление и, следовательно, потенциальное наказание за предсказание ветвления удаляются.Таким
C
образомC++
, оператор, который будет напрямую (без какой-либо оптимизации) компилироваться в инструкцию условного перемещенияx86
, является троичным оператором... ? ... : ...
. Поэтому мы переписываем приведенное выше утверждение в эквивалентное:Поддерживая читабельность, мы можем проверить коэффициент ускорения.
Для Intel Core i7 -2600K с частотой 3,4 ГГц и режима выпуска Visual Studio 2010 эталонный тест (формат скопирован из Mysticial):
x86
x64
Результат надежен в нескольких тестах. Мы получаем значительное ускорение, когда результат ветвления непредсказуем, но мы немного страдаем, когда он предсказуем. Фактически, при использовании условного перемещения производительность одинакова независимо от шаблона данных.
Теперь давайте более подробно рассмотрим
x86
сборку, которую они генерируют. Для простоты мы используем две функцииmax1
иmax2
.max1
использует условную ветвьif... else ...
:max2
использует троичный оператор... ? ... : ...
:На машине x86-64
GCC -S
генерирует сборку ниже.max2
использует гораздо меньше кода из-за использования инструкцииcmovge
. Но реальная выгода заключается в том,max2
что не включает переходы по ветвям,jmp
что может привести к значительному снижению производительности, если прогнозируемый результат неверен.Так почему же условный ход работает лучше?
В типичном
x86
процессоре выполнение инструкции делится на несколько этапов. Грубо говоря, у нас разные аппаратные средства для разных этапов. Поэтому нам не нужно ждать окончания одной инструкции, чтобы начать новую. Это называется конвейерной обработкой .В случае ветвления следующая инструкция определяется предыдущей, поэтому мы не можем выполнить конвейеризацию. Мы должны либо ждать, либо предсказывать.
В случае условного перемещения исполняемая инструкция условного перемещения делится на несколько этапов, но более ранние этапы похожи
Fetch
иDecode
не зависят от результата предыдущей инструкции; только последним этапам нужен результат. Таким образом, мы ждем часть времени выполнения одной инструкции. Вот почему версия условного перемещения медленнее, чем ветвь, когда предсказание легко.Книга « Компьютерные системы: взгляд программиста», второе издание, объясняет это подробно. Вы можете проверить Раздел 3.6.6 для Условных Инструкций Перемещения , всю Главу 4 для Архитектуры процессора и Раздел 5.11.2 для специальной обработки для Штрафов Предсказания и Ошибочного предсказания .
Иногда некоторые современные компиляторы могут оптимизировать наш код для сборки с большей производительностью, иногда некоторые компиляторы не могут (рассматриваемый код использует собственный компилятор Visual Studio). Зная разницу в производительности между ветвлением и условным перемещением, когда он непредсказуем, может помочь нам написать код с лучшей производительностью, когда сценарий становится настолько сложным, что компилятор не может оптимизировать их автоматически.
источник
-O0
пример и показать разницу в оптимизированном asm на двух ваших тестовых примерах.Если вам интересно еще больше оптимизаций, которые можно сделать с этим кодом, подумайте об этом:
Начиная с оригинального цикла:
С помощью обмена циклами мы можем смело изменить этот цикл на:
Затем вы можете видеть, что
if
условие является постоянным на протяжении всегоi
цикла, поэтому вы можете поднятьif
out:Затем вы видите, что внутренний цикл можно свернуть в одно единственное выражение, предполагая, что модель с плавающей запятой это позволяет (
/fp:fast
например, выбрасывается)Это в 100 000 раз быстрее, чем раньше.
источник
i
в одну единицу = 1e5. Это не имеет никакого значения для конечного результата, но я просто хотел установить рекорд, так как это такая часто посещаемая страница.if
в этой точке может быть преобразовано в:sum += (data[j] >= 128) ? data[j] * 100000 : 0;
которое компилятор может уменьшитьcmovge
или эквивалентно.Несомненно, некоторые из нас будут заинтересованы в способах идентификации кода, который является проблематичным для предсказателя ветвления ЦП. Инструмент Valgrind
cachegrind
имеет симулятор предсказания ветвлений, включаемый с помощью--branch-sim=yes
флага. Выполнение этого в примерах из этого вопроса с уменьшением числа внешних циклов до 10000 и скомпилированным с нимg++
дает следующие результаты:Сортировка:
Unsorted:
Детализация до построчного вывода, полученного
cg_annotate
нами, для рассматриваемого цикла:Сортировка:
Unsorted:
Это позволяет легко определить проблемную строку - в несортированной версии эта
if (data[c] >= 128)
строка вызывает 164 050 007 неправильно предсказанных условных переходов (Bcm
) в модели предсказателя ветвления в cachegrind, тогда как в отсортированной версии она вызывает только 10 006.В качестве альтернативы, в Linux вы можете использовать подсистему счетчиков производительности для выполнения той же задачи, но с собственной производительностью с использованием счетчиков ЦП.
Сортировка:
Unsorted:
Он также может делать аннотации исходного кода с разборкой.
Смотрите руководство по производительности для более подробной информации.
источник
data[c] >= 128
(которая, как вы предлагаете, имеет промах 50%), а другая для условия цикла,c < arraySize
которая имеет промах ~ 0%. ,Я просто прочитал этот вопрос и его ответы, и я чувствую, что ответ отсутствует.
Обычный способ устранить предсказание ветвления, который, как мне показалось, особенно хорошо работает в управляемых языках, - это поиск в таблице вместо использования ветвления (хотя в этом случае я его не проверял).
Этот подход работает в целом, если:
Фон и почему
С точки зрения процессора, ваша память работает медленно. Чтобы компенсировать разницу в скорости, в ваш процессор встроена пара кешей (кеш L1 / L2). Так что представьте, что вы делаете свои хорошие вычисления и выясните, что вам нужен кусок памяти. Процессор выполнит операцию загрузки и загрузит часть памяти в кеш, а затем использует кеш для выполнения остальных вычислений. Поскольку память относительно медленная, эта «загрузка» замедлит вашу программу.
Как и предсказание ветвлений, это было оптимизировано в процессорах Pentium: процессор предсказывает, что ему нужно загрузить часть данных, и пытается загрузить их в кеш, прежде чем операция действительно попадет в кеш. Как мы уже видели, предсказание ветвления иногда идет ужасно неправильно - в худшем случае вам нужно вернуться назад и фактически ждать загрузки памяти, которая будет длиться вечно ( другими словами: неудачное предсказание ветвления плохо, память загрузка после сбоя предсказания ветки просто ужасна! ).
К счастью для нас, если схема доступа к памяти предсказуема, процессор загрузит ее в свой быстрый кеш, и все в порядке.
Первое, что нам нужно знать, это то, что мало ? Хотя меньший размер, как правило, лучше, практическое правило - придерживаться таблиц поиска размером <= 4096 байт. В качестве верхнего предела: если ваша таблица поиска больше 64 КБ, вероятно, стоит пересмотреть.
Построение стола
Итак, мы выяснили, что можем создать небольшую таблицу. Следующее, что нужно сделать, это установить на место функцию поиска. Функции поиска обычно представляют собой небольшие функции, которые используют несколько основных целочисленных операций (и, или, xor, shift, add, remove и, возможно, умножение). Вы хотите, чтобы ваш ввод был переведен с помощью функции поиска на какой-то «уникальный ключ» в вашей таблице, который затем просто дает вам ответ на всю работу, которую вы хотели, чтобы он делал.
В этом случае:> = 128 означает, что мы можем сохранить значение, <128 означает, что мы избавимся от него. Самый простой способ сделать это - использовать 'И': если мы сохраняем это, мы И это с 7FFFFFFF; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и множеством 7FFFFFFFF годов.
Управляемые языки
Вы можете спросить, почему это хорошо работает на управляемых языках. В конце концов, управляемые языки проверяют границы массивов с помощью ветки, чтобы убедиться, что вы не ошиблись ...
Ну, не совсем ... :-)
Была проведена большая работа по устранению этой ветки для управляемых языков. Например:
В этом случае для компилятора очевидно, что граничное условие никогда не будет выполнено. По крайней мере компилятор Microsoft JIT (но я ожидаю, что Java делает подобные вещи) заметит это и вообще уберет проверку. Вау, это означает, что нет ветви. Точно так же это будет иметь дело с другими очевидными случаями.
Если у вас возникли проблемы с поиском на управляемых языках - ключ заключается в том, чтобы добавить
& 0x[something]FFF
к вашей функции поиска, чтобы сделать проверку границ предсказуемой, - и наблюдать, как она идет быстрее.Результат этого дела
источник
sum += lookup[data[j]]
гдеlookup
массив с 256 записей, первые из них равно нулю , а последние, будучи равен индексу?Поскольку данные распределяются между 0 и 255 при сортировке массива, около первой половины итераций не будет входить в
if
-statement (if
оператор разделяется ниже).Вопрос в том, что делает вышеприведенное утверждение не выполненным в некоторых случаях, например, в случае отсортированных данных? Здесь появляется «предсказатель ветви». Предиктор ветвления - это цифровая схема, которая пытается угадать, каким образом
if-then-else
пойдет ветвь (например, структура), прежде чем это станет известно наверняка. Целью предиктора ветвления является улучшение потока в конвейере команд. Предсказатели ветвлений играют решающую роль в достижении высокой эффективности!Давайте сделаем несколько тестов, чтобы понять это лучше
Производительность
if
-элемента зависит от того, имеет ли его состояние предсказуемый характер. Если условие всегда истинно или всегда ложно, логика предсказания ветвления в процессоре подберет шаблон. С другой стороны, если шаблон непредсказуемый,if
-состояние будет намного дороже.Давайте измерим производительность этого цикла с различными условиями:
Вот временные параметры цикла с различными паттернами истина-ложь:
« Плохой » паттерн «истинно-ложно» может сделать
if
утверждение в шесть раз медленнее, чем « хороший » паттерн! Конечно, какой шаблон хорош, а какой плох, зависит от точных инструкций, сгенерированных компилятором, и от конкретного процессора.Таким образом, нет никаких сомнений в отношении влияния отраслевого прогнозирования на производительность!
источник
Один из способов избежать ошибок предсказания ветвлений - это создать справочную таблицу и проиндексировать ее, используя данные. Стефан де Брюин обсуждал это в своем ответе.
Но в этом случае мы знаем, что значения находятся в диапазоне [0, 255], и мы заботимся только о значениях> = 128. Это означает, что мы можем легко извлечь единственный бит, который скажет нам, хотим ли мы значение или нет: сдвигая данные справа 7 бит, у нас осталось 0 бит или 1 бит, и мы хотим добавить значение только тогда, когда у нас есть 1 бит. Давайте назовем этот бит «битом решения».
Используя значение 0/1 бита решения в качестве индекса массива, мы можем создать код, который будет одинаково быстрым, независимо от того, отсортированы данные или нет. Наш код всегда добавляет значение, но когда бит принятия решения равен 0, мы добавим значение туда, где нам все равно. Вот код:
Этот код тратит впустую половину добавления, но никогда не имеет ошибки предсказания ветвления. Это значительно быстрее на случайных данных, чем версия с фактическим оператором if.
Но в моем тестировании явная таблица поиска была немного быстрее, чем эта, вероятно, потому что индексирование в таблицу поиска было немного быстрее, чем битовое смещение. Это показывает, как мой код устанавливает и использует таблицу подстановки (
lut
в коде невообразимо вызывается «таблица подстановки»). Вот код C ++:В этом случае таблица поиска составляла всего 256 байт, поэтому она хорошо помещалась в кеш, и все было быстро. Этот метод не сработает, если данные будут 24-битными значениями, а нам нужна только половина из них ... таблица поиска была бы слишком большой, чтобы быть практичной. С другой стороны, мы можем объединить два метода, показанных выше: сначала сдвинуть биты, а затем проиндексировать таблицу поиска. Для 24-битного значения, для которого нам нужно только верхнее половинное значение, мы могли бы сдвинуть данные вправо на 12 бит и оставить 12-битное значение для индекса таблицы. 12-битный индекс таблицы подразумевает таблицу из 4096 значений, что может быть практичным.
Техника индексации в массив, вместо использования
if
оператора, может быть использована для решения, какой указатель использовать. Я видел библиотеку , которая реализована бинарных деревьев, и вместо того , чтобы два именованных указателей (pLeft
иpRight
или любой другой ) , имели массив длины 2 указателей и использовали метод «решения бит» , чтобы решить , какой из них следовать. Например, вместо:эта библиотека будет делать что-то вроде:
Вот ссылка на этот код: Красные черные деревья , вечно сбитые с толку
источник
data[c]>>7
- что также обсуждается где-то здесь); Я намеренно оставил это решение, но, конечно, вы правы. Небольшое примечание: практическое правило для справочных таблиц заключается в том, что если он умещается в 4 КБ (из-за кэширования), он будет работать - желательно, чтобы таблица была как можно меньше. Для управляемых языков я бы увеличил это до 64 КБ, для низкоуровневых языков, таких как C ++ и C, я бы, вероятно, пересмотрел (это только мой опыт). С тех порtypeof(int) = 4
я бы попробовал придерживаться до 10 бит.sizeof(int) == 4
? Это было бы верно для 32-разрядных. Мой двухлетний сотовый телефон имеет кэш-память L1 объемом 32 КБ, поэтому даже таблица поиска 4K может работать, особенно если значения поиска были байтами, а не целыми.j
методе равно 0 или 1, почему бы вам просто не умножить свое значение,j
прежде чем добавлять его, а не использовать индексирование массива (возможно, следует умножить на,1-j
а не наj
)int c = data[j]; sum += c & -(c >> 7);
который не требует умножения вообще.В отсортированном случае вы можете добиться большего успеха, чем полагаться на успешное предсказание ветвлений или любой метод сравнения без ветвей: полностью удалить ветвь.
Действительно, массив разделен на смежные зоны с,
data < 128
а другой сdata >= 128
. Таким образом, вы должны найти точку разделения с помощью дихотомического поиска (используяLg(arraySize) = 15
сравнения), а затем выполнить прямое накопление с этой точки.Что-то вроде (не проверено)
или, немного более запутанный
Еще более быстрый подход, который дает приблизительное решение как для отсортированного, так и для несортированного:
sum= 3137536;
(при условии действительно равномерного распределения, 16384 выборки с ожидаемым значением 191,5) :-)источник
sum= 3137536
- умная. Это явно не в этом вопрос. Вопрос в том, чтобы объяснить удивительные характеристики производительности. Я склонен сказать, что добавление делатьstd::partition
вместо того, чтобыstd::sort
ценно. Хотя актуальный вопрос распространяется не только на синтетический тест.Вышеупомянутое поведение происходит из-за предсказания Ветви.
Чтобы понять предсказание ветвления, нужно сначала понять конвейер инструкций :
Любая инструкция разбита на последовательность шагов, так что разные шаги могут выполняться параллельно параллельно. Этот метод известен как конвейер команд и используется для увеличения пропускной способности современных процессоров. Чтобы лучше это понять, посмотрите этот пример в Википедии .
Как правило, современные процессоры имеют довольно длинные конвейеры, но для простоты давайте рассмотрим только эти 4 шага.
4-х ступенчатый конвейер в целом по 2 инструкции.
Возвращаясь к вышеупомянутому вопросу, давайте рассмотрим следующие инструкции:
Без прогнозирования ветвления может произойти следующее:
Чтобы выполнить инструкцию B или инструкцию C, процессор должен будет ждать, пока инструкция A не достигнет стадии EX в конвейере, поскольку решение перейти к инструкции B или инструкции C зависит от результата инструкции A. Таким образом, конвейер будет выглядеть так
когда условие возвращает true:
Когда условие возвращает ложное:
В результате ожидания результата команды A общее количество циклов ЦП, проведенных в вышеупомянутом случае (без прогнозирования ветвления; как для истинного, так и для ложного), равно 7.
Так что же такое прогноз отрасли?
Предсказатель ветвления попытается угадать, каким образом пойдет ветвь (структура if-then-else), прежде чем это станет известно наверняка. Он не будет ждать, пока инструкция A достигнет стадии EX конвейера, но он угадает решение и перейдет к этой инструкции (B или C в случае нашего примера).
В случае правильного предположения, конвейер выглядит примерно так:
Если позднее обнаруживается, что предположение было неверным, то частично выполненные инструкции отбрасываются, и конвейер начинается с правильной ветви, что вызывает задержку. Время, которое теряется в случае ошибочного прогнозирования ветвления, равно числу этапов в конвейере от этапа выборки до этапа выполнения. Современные микропроцессоры, как правило, имеют довольно длинные конвейеры, поэтому задержка неверного прогнозирования составляет от 10 до 20 тактов. Чем длиннее конвейер, тем больше потребность в хорошем предикторе ветвления .
В коде OP первый раз, когда выполняется условие, предсказатель ветвления не имеет никакой информации для обоснования предсказания, поэтому в первый раз он случайным образом выберет следующую инструкцию. Позже в цикле for он может основывать прогноз на истории. Для массива, отсортированного по возрастанию, есть три возможности:
Предположим, что предиктор всегда будет принимать истинную ветвь при первом запуске.
Так что в первом случае он всегда будет принимать истинную ветвь, поскольку исторически все его предсказания верны. Во 2-м случае изначально он будет предсказывать неправильно, но после нескольких итераций он будет предсказывать правильно. В третьем случае он изначально будет правильно прогнозировать до тех пор, пока элементы не станут меньше 128. После чего он потерпит неудачу в течение некоторого времени и сам исправится, когда увидит ошибку прогнозирования ветвления в истории.
Во всех этих случаях количество ошибок будет меньше, и в результате всего лишь несколько раз потребуется отменить частично выполненные инструкции и начать заново с правильной ветвью, что приведет к уменьшению циклов ЦП.
Но в случае случайного несортированного массива прогноз должен отбросить частично выполненные инструкции и большую часть времени начинать с правильной ветви, что приведет к большему количеству циклов ЦП по сравнению с отсортированным массивом.
источник
Официальный ответ будет от
Вы также можете увидеть из этой прекрасной диаграммы, почему предсказатель ветвления запутывается.
Каждый элемент в исходном коде является случайным значением
поэтому предиктор будет меняться сторонами как
std::rand()
удар.С другой стороны, после сортировки предиктор сначала переходит в состояние строго не принятого, а когда значения изменяются до высокого значения, предиктор за три прогона изменяется полностью от сильно не принятого до строго принятого.
источник
В той же строке (я думаю, что это не было выделено ни одним ответом) хорошо упомянуть, что иногда (особенно в программном обеспечении, где производительность имеет значение - как в ядре Linux) вы можете найти некоторые операторы if, подобные следующим:
или аналогично:
И то,
likely()
и другоеunlikely()
на самом деле являются макросами, которые определяются с помощью чего-то вроде GCC,__builtin_expect
чтобы помочь компилятору вставлять код предсказания для поддержки условия с учетом информации, предоставленной пользователем. GCC поддерживает другие встроенные функции, которые могут изменить поведение работающей программы или выдавать низкоуровневые инструкции, такие как очистка кэша и т. Д. См. Эту документацию, в которой рассматриваются доступные встроенные функции GCC.Обычно такого рода оптимизации в основном встречаются в приложениях реального времени или встроенных системах, где время выполнения имеет значение, и оно критично. Например, если вы проверяете какое-либо условие ошибки, которое происходит только 1/10000000 раз, то почему бы не сообщить об этом компилятору? Таким образом, по умолчанию прогноз ветвления предполагает, что условие ложно.
источник
Часто используемые логические операции в C ++ создают много веток в скомпилированной программе. Если эти ветви находятся внутри циклов и их трудно предсказать, они могут значительно замедлить выполнение. Булевы переменные хранятся в виде 8-битных целых чисел со значением
0
дляfalse
и1
дляtrue
.Булевы переменные переопределены в том смысле, что все операторы, которые имеют булевы переменные в качестве входных данных, проверяют, имеют ли входы любое другое значение, кроме
0
или1
, но операторы, которые имеют логические переменные в качестве выходных данных, не могут производить никакого другого значения, кроме0
или1
. Это делает операции с булевыми переменными в качестве входных данных менее эффективными, чем необходимо. Рассмотрим пример:Обычно это реализуется компилятором следующим образом:
Этот код далеко не оптимален. Ветви могут занять много времени в случае неправильных прогнозов. Логические операции можно сделать намного более эффективными, если точно известно, что операнды не имеют других значений, кроме
0
и1
. Причина, по которой компилятор не делает такого предположения, состоит в том, что переменные могут иметь другие значения, если они неинициализированы или получены из неизвестных источников. Приведенный выше код может быть оптимизирован, еслиa
иb
был инициализирован для допустимых значений или если они получены от операторов, которые производят логический вывод. Оптимизированный код выглядит так:char
используется вместоbool
того, чтобы сделать возможным использование побитовых операторов (&
и|
) вместо логических операторов (&&
и||
). Побитовые операторы - это одиночные инструкции, которые занимают только один такт. Оператор OR (|
) работает, даже еслиa
иb
имеет другие значения, кроме0
или1
. Оператор AND (&
) и оператор EXCLUSIVE OR (^
) могут давать противоречивые результаты, если операнды имеют значения, отличные от0
и1
.~
не может быть использован для НЕ. Вместо этого вы можете сделать логическое НЕ для переменной, которая известна как0
или1
XOR'ом это с1
:можно оптимизировать для:
a && b
не может быть заменено выражениемa & b
ifb
, которое не должно оцениваться, еслиa
естьfalse
(&&
не будет оцениватьсяb
,&
будет). Аналогично,a || b
не может быть заменено выражениемa | b
ifb
, которое не должно оцениваться, еслиa
естьtrue
.Использование побитовых операторов более выгодно, если операнды являются переменными, чем если операнды являются сравнениями:
является оптимальным в большинстве случаев (если только вы не ожидаете, что
&&
выражение вызовет много ошибочных прогнозов).источник
Это точно!...
Предсказание ветвей делает логику медленной из-за переключения, которое происходит в вашем коде! Как будто вы идете по прямой улице или улице с большим количеством поворотов, наверняка прямая будет сделана быстрее! ...
Если массив отсортирован, ваше условие ложно на первом шаге:, а
data[c] >= 128
затем становится истинным значением для всего пути до конца улицы. Вот так вы быстрее доберетесь до конца логики. С другой стороны, при использовании несортированного массива вам нужно много поворотов и обработки, которые наверняка замедляют работу вашего кода ...Посмотрите на изображение, которое я создал для вас ниже. Какая улица будет закончена быстрее?
Таким образом, программно предсказание ветвлений приводит к замедлению процесса ...
Также, в конце, хорошо знать, что у нас есть два вида предсказаний ветвлений, каждый из которых по-разному повлияет на ваш код:
1. Статический
2. Динамический
источник
На этот вопрос уже много раз отвечали превосходно. Тем не менее, я хотел бы обратить внимание группы на еще один интересный анализ.
Недавно этот пример (измененный очень незначительно) также использовался для демонстрации того, как фрагмент кода может быть профилирован в самой программе в Windows. Попутно автор также показывает, как использовать результаты, чтобы определить, где код проводит большую часть своего времени как в отсортированном, так и в несортированном случае. Наконец, в этой части также показано, как использовать малоизвестную функцию HAL (Уровень аппаратной абстракции), чтобы определить, насколько часто происходит неправильное предсказание ветвления в несортированном случае.
Ссылка находится здесь: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm
источник
When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping.
автор пытается обсудить профилирование в контексте кода, размещенного здесь, и в процессе пытается объяснить, почему отсортированный случай намного быстрее.Как и то, что уже было упомянуто другими, то, что скрывается за тайной, - это предсказатель отрасли .
Я не пытаюсь что-то добавить, а объясняю концепцию по-другому. Существует краткое введение в вики, которое содержит текст и диаграмму. Мне нравится объяснение ниже, которое использует диаграмму для интуитивного развития предсказателя ветвлений.
На основе описанного сценария я написал демонстрационную анимацию, чтобы показать, как выполняются инструкции в конвейере в различных ситуациях.
Пример содержит три инструкции, и первая из них является инструкцией условного перехода. Последние две инструкции могут идти в конвейер, пока не будет выполнена команда условного перехода.
Для выполнения 3 инструкций потребуется 9 тактов.
Для выполнения 3 инструкций потребуется 7 тактов.
Для выполнения 3 инструкций потребуется 9 тактов.
Как видите, у нас нет причин не использовать Branch Predictor.
Это довольно простая демонстрация, которая разъясняет основную часть Branch Predictor. Если эти картинки раздражают, пожалуйста, удалите их из ответа, и посетители также могут получить живой исходный код демонстрации из BranchPredictorDemo.
источник
if()
блока может выполняться до того, как станет известно условие ветвления. Или для цикла поиска, подобногоstrlen
илиmemchr
, взаимодействия могут перекрываться. Если бы вам пришлось ждать, пока результат совпадения или не будет известен, прежде чем выполнять какую-либо следующую итерацию, вы бы столкнулись с узким местом в нагрузке на кэш + задержка ALU вместо пропускной способности.Выгода предсказания ветвлений!
Важно понимать, что неправильное прогнозирование веток не замедляет работу программ. Стоимость пропущенного прогноза такая же, как если бы прогноз ветвления не существовал, и вы подождали, пока оценка выражения решит, какой код запустить (дальнейшее объяснение в следующем параграфе).
Всякий раз, когда есть оператор
if-else
\switch
, выражение должно быть оценено, чтобы определить, какой блок должен быть выполнен. В коде сборки, сгенерированном компилятором, вставлены инструкции условного перехода .Инструкция ветвления может заставить компьютер начать выполнение другой последовательности команд и, таким образом, отклоняться от своего поведения по умолчанию при выполнении инструкций по порядку (то есть, если выражение ложно, программа пропускает код
if
блока) в зависимости от некоторого условия, которое является выражение выражения в нашем случае.При этом компилятор пытается предсказать результат до его фактической оценки. Он будет извлекать инструкции из
if
блока, и если выражение окажется истинным, тогда замечательно! Мы получили время, необходимое для его оценки, и добились прогресса в коде; если нет, то мы запускаем неправильный код, конвейер сбрасывается и запускается правильный блок.Визуализация:
Допустим, вам нужно выбрать маршрут 1 или маршрут 2. В ожидании вашего партнера, чтобы проверить карту, вы остановились на ## и ждали, или вы можете просто выбрать маршрут 1 и, если вам повезло (маршрут 1 - правильный маршрут), тогда здорово, что вам не нужно было ждать, пока ваш партнер проверит карту (вы сэкономили время, которое потребовалось бы ему, чтобы проверить карту), иначе вы просто вернетесь назад.
В то время как промывка трубопроводов очень быстрая, в наши дни эта игра стоит того. Прогнозировать отсортированные данные или данные, которые изменяются медленно, всегда проще и лучше, чем прогнозировать быстрые изменения.
источник
В ARM ветвление не требуется, поскольку каждая команда имеет 4-битное поле условия, которое проверяет (при нулевой стоимости) любое из 16 различных условий, которые могут возникнуть в регистре состояния процессора, и если условие в команде имеет вид false, инструкция пропущена. Это устраняет необходимость в коротких ветвлениях, и для этого алгоритма не будет никакого предсказания ветвлений. Поэтому отсортированная версия этого алгоритма будет работать медленнее, чем несортированная версия в ARM, из-за дополнительных затрат на сортировку.
Внутренний цикл для этого алгоритма будет выглядеть примерно так на языке ассемблера ARM:
Но на самом деле это часть общей картины:
CMP
коды операций всегда обновляют биты состояния в регистре состояния процессора (PSR), потому что это является их целью, но большинство других инструкций не затрагивают PSR, если вы не добавляете дополнительныйS
суффикс к инструкции, определяя, что PSR следует обновлять на основе результат инструкции. Точно так же, как 4-битный суффикс условия, возможность выполнять инструкции без влияния на PSR - это механизм, который уменьшает потребность в ветвях на ARM, а также облегчает неупорядоченную диспетчеризацию на аппаратном уровне , потому что после выполнения некоторой операции X, которая обновляет биты состояния, впоследствии (или параллельно) вы можете выполнять кучу других работ, которые явно не должны влиять на биты состояния, тогда вы можете проверить состояние битов состояния, установленных ранее X.Поле проверки состояния и необязательное поле «set status bit» можно комбинировать, например:
ADD R1, R2, R3
выполняетсяR1 = R2 + R3
без обновления каких-либо битов состояния.ADDGE R1, R2, R3
выполняет ту же операцию, только если предыдущая инструкция, которая затронула биты состояния, вызвала условие «Больше чем» или «Равно».ADDS R1, R2, R3
выполняет сложение и затем обновляетN
,Z
,C
иV
флаги в Processor регистра состояния на основе был ли результат отрицательный, нулевой, носимые (для знака сложения) или переполненного (для знакового дополнительно).ADDSGE R1, R2, R3
выполняет сложение только в том случае, еслиGE
проверка верна, а затем обновляет биты состояния на основе результата сложения.Большинство архитектур процессоров не имеют этой возможности указать, следует ли обновлять биты состояния для данной операции, что может потребовать написания дополнительного кода для сохранения и последующего восстановления битов состояния, или может потребовать дополнительных ветвей, или может ограничить выход процессора эффективности выполнения заказа: один из побочных эффектов большинства архитектур набора команд ЦП, принудительно обновляющих биты состояния после большинства команд, состоит в том, что гораздо сложнее отделить друг от друга, какие команды могут выполняться параллельно, не мешая друг другу. Обновление битов состояния имеет побочные эффекты, поэтому имеет линеаризующий эффект на код.Способность ARM смешивать и сопоставлять безусловное тестирование состояния для любой инструкции с возможностью либо обновлять, либо не обновлять биты состояния после любой инструкции является чрезвычайно мощной как для программистов на ассемблере, так и для компиляторов, и создает очень эффективный код.
Если вы когда-нибудь задумывались, почему ARM был настолько феноменально успешным, блестящая эффективность и взаимодействие этих двух механизмов являются большой частью истории, потому что они являются одним из величайших источников эффективности архитектуры ARM. Блеск оригинальных дизайнеров ARM ISA еще в 1983 году, Стив Фербер и Роджер (теперь Софи) Уилсон, невозможно переоценить.
источник
R2 = data + arraySize
, затем начните сR1 = -arraySize
. Нижняя часть цикла становитсяadds r1, r1, #1
/bnz inner_loop
. Компиляторы не используют эту оптимизацию по какой-то причине: / В любом случае, предикатное выполнение добавления в этом случае принципиально не отличается от того, что вы можете делать с кодом без ветвей на других ISA, таких как x86cmov
. Хотя это не так приятно: флаг оптимизации gcc -O3 делает код медленнее, чем -O2cmov
хранении, которые могут привести к сбою, в отличие от x86 с операндом источника памяти. Большинство ISA, включая AArch64, имеют только операции выбора ALU. Так что предопределение ARM может быть мощным и может использоваться более эффективно, чем код без ответвлений на большинстве ISA.)Это про предсказание ветвлений. Что это?
Предсказатель ветвей - это одна из древних технологий повышения производительности, которая все еще находит применение в современных архитектурах. Хотя простые методы прогнозирования обеспечивают быстрый поиск и эффективность энергопотребления, они страдают от высокой степени неверного прогнозирования.
С другой стороны, сложные предсказания ветвлений - либо нейронные, либо варианты двухуровневого предсказания ветвлений - обеспечивают лучшую точность предсказания, но они потребляют больше энергии, а сложность возрастает экспоненциально.
В дополнение к этому, в сложных методах прогнозирования время, затрачиваемое на прогнозирование ветвей, само по себе очень велико - в диапазоне от 2 до 5 циклов - что сопоставимо со временем выполнения фактических ветвей.
Прогнозирование ветвлений - это, по сути, проблема оптимизации (минимизации), в которой основное внимание уделяется достижению минимально возможного числа пропусков, низкого энергопотребления и низкой сложности при минимальных ресурсах.
На самом деле существует три вида ветвей:
Пересылка условных переходов - в зависимости от времени выполнения ПК (программный счетчик) изменяется, чтобы указывать на адрес пересылки в потоке команд.
Обратные условные ветви - ПК изменяется, чтобы указывать назад в потоке команд. Ветвление основано на некотором условии, таком как ветвление назад к началу цикла программы, когда тест в конце цикла указывает, что цикл должен быть выполнен снова.
Безусловные ветви - это включает переходы, вызовы процедур и возвраты, которые не имеют особых условий. Например, команда безусловного перехода может быть закодирована на языке ассемблера как просто «jmp», и поток команд должен быть немедленно направлен в целевое местоположение, на которое указывает инструкция перехода, тогда как условный переход, который может быть закодирован как «jmpne» будет перенаправлять поток команд только в том случае, если результат сравнения двух значений в предыдущих инструкциях «сравнения» показывает, что значения не равны. (Схема сегментированной адресации, используемая архитектурой x86, добавляет дополнительную сложность, поскольку переходы могут быть «ближними» (внутри сегмента) или «дальними» (вне сегмента). Каждый тип по-разному влияет на алгоритмы прогнозирования ветвлений.)
Статическое / динамическое предсказание ветвления : Статическое предсказание ветвления используется микропроцессором при первом обнаружении условного перехода, а динамическое предсказание ветвления используется для последующих выполнений условного кода ветвления.
Ссылки:
Предиктор ветвей
Демонстрация самопрофилирования
Обзор прогноза отрасли
Прогнозирование отрасли
источник
Помимо того, что предсказание ветвления может замедлить вас, у отсортированного массива есть еще одно преимущество:
Вы можете иметь условие остановки вместо простой проверки значения, таким образом вы только зацикливаетесь на соответствующих данных и игнорируете остальные.
Прогноз ветвления будет пропущен только один раз.
источник
Сортированные массивы обрабатываются быстрее, чем несортированный массив, из-за явления, называемого предсказанием ветвлений.
Предиктор ветвления - это цифровая схема (в компьютерной архитектуре), пытающаяся предсказать, каким образом пойдет ветвь, улучшая поток в конвейере команд. Схема / компьютер предсказывает следующий шаг и выполняет его.
Неправильный прогноз приводит к возврату к предыдущему шагу и выполнению с другим прогнозом. Если предположить, что прогноз верен, код перейдет к следующему шагу. Неправильный прогноз приводит к повторению одного и того же шага, пока не произойдет правильный прогноз.
Ответ на ваш вопрос очень прост.
В несортированном массиве компьютер делает несколько прогнозов, что повышает вероятность возникновения ошибок. Принимая во внимание, что в отсортированном массиве компьютер делает меньше прогнозов, снижая вероятность ошибок. Создание большего количества прогнозов требует больше времени.
Сортированный массив: Прямая дорога ____________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Несортированный массив: Кривая дорога
Прогноз ветвления: угадывание / предсказание, какая дорога прямая и следование по ней без проверки
Хотя обе дороги достигают одного и того же пункта назначения, прямая дорога короче, а другая длиннее. Если тогда вы выберете другой по ошибке, пути назад не будет, и поэтому вы потратите дополнительное время, если выберете более длинную дорогу. Это похоже на то, что происходит на компьютере, и я надеюсь, что это помогло вам лучше понять.
Также я хочу процитировать @Simon_Weaver из комментариев:
источник
Я попробовал тот же код с MATLAB 2011b на своем MacBook Pro (Intel i7, 64-разрядная, 2,4 ГГц) для следующего кода MATLAB:
Результаты для вышеуказанного кода MATLAB следующие:
Результаты кода C как в @GManNickG я получаю:
Исходя из этого, выглядит, что MATLAB почти в 175 раз медленнее, чем реализация C без сортировки, и в 350 раз медленнее с сортировкой. Другими словами, эффект (прогнозирования ветвлений) составляет 1,46x для реализации MATLAB и 2,7x для реализации C.
источник
Предположение другими ответами о том, что нужно сортировать данные, неверно.
Следующий код не сортирует весь массив, а только 200-элементные его сегменты, и, следовательно, работает быстрее всего.
Сортировка только k-элементных разделов завершает предварительную обработку за линейное время
O(n)
, а не заO(n.log(n))
время, необходимое для сортировки всего массива.Это также «доказывает», что это не имеет никакого отношения к каким-либо алгоритмическим проблемам, таким как порядок сортировки, и это действительно предсказание ветвлений.
источник
pcmpgtb
чтобы найти элементы с установленным старшим битом, а затем AND, чтобы обнулить меньшие элементы). Тратить любое время на сортировку кусков будет медленнее. Версия без ответвлений будет иметь независимую от данных производительность, что также будет свидетельствовать о том, что затраты обусловлены неправильным прогнозом переходов. Или просто используйте счетчики производительности, чтобы наблюдать это напрямую, как Skylakeint_misc.clear_resteer_cycles
илиint_misc.recovery_cycles
подсчитывать циклы простоя внешнего интерфейса от неправильных прогнозовОтвет Бьярна Страуструпа на этот вопрос:
Это звучит как вопрос интервью. Это правда? Как бы вы узнали? Плохо отвечать на вопросы об эффективности, не выполняя предварительных измерений, поэтому важно знать, как их измерять.
Итак, я попытался с вектором миллиона целых чисел и получил:
Я запускал это несколько раз, чтобы быть уверенным. Да, феномен настоящий. Мой код ключа был:
По крайней мере, такое явление реально с этим компилятором, стандартной библиотекой и настройками оптимизатора. Различные реализации могут и дают разные ответы. Фактически, кто-то провел более систематическое исследование (быстрый поиск в Интернете найдет его), и большинство реализаций показывают этот эффект.
Одной из причин является предсказание ветвления: ключевая операция в алгоритме сортировки является
“if(v[i] < pivot]) …”
или эквивалентной. Для отсортированной последовательности этот тест всегда верен, тогда как для случайной последовательности выбранная ветвь изменяется случайным образом.Другая причина в том, что когда вектор уже отсортирован, нам никогда не нужно перемещать элементы в правильное положение. Эффект этих маленьких деталей - фактор пяти или шести, который мы видели.
Быстрая сортировка (и сортировка в целом) - сложное исследование, которое привлекло некоторые из величайших умов информатики. Хорошая функция сортировки - это результат выбора хорошего алгоритма и внимания к производительности оборудования при его реализации.
Если вы хотите написать эффективный код, вам нужно немного узнать об архитектуре машины.
источник
Этот вопрос коренится в Модели прогнозирования ветвлений на процессорах. Я бы рекомендовал прочитать эту статью:
Увеличение скорости получения инструкций с помощью многократного предсказания ветвлений и кэша адресов ветвлений
Когда вы отсортировали элементы, IR не может беспокоиться о том, чтобы получать все инструкции процессора, снова и снова, он выбирает их из кэша.
источник
Один из способов избежать ошибок предсказания ветвлений - это создать справочную таблицу и проиндексировать ее, используя данные. Стефан де Брюин обсуждал это в своем ответе.
Но в этом случае мы знаем, что значения находятся в диапазоне [0, 255], и мы заботимся только о значениях> = 128. Это означает, что мы можем легко извлечь единственный бит, который скажет нам, хотим ли мы значение или нет: сдвигая данные справа 7 бит, у нас осталось 0 бит или 1 бит, и мы хотим добавить значение только тогда, когда у нас есть 1 бит. Давайте назовем этот бит «битом решения».
Используя значение 0/1 бита решения в качестве индекса массива, мы можем создать код, который будет одинаково быстрым, независимо от того, отсортированы данные или нет. Наш код всегда добавляет значение, но когда бит принятия решения равен 0, мы добавим значение туда, где нам все равно. Вот код:
// Тестовое задание
Этот код тратит впустую половину добавления, но никогда не имеет ошибки предсказания ветвления. Это значительно быстрее на случайных данных, чем версия с фактическим оператором if.
Но в моем тестировании явная таблица поиска была немного быстрее, чем эта, вероятно, потому что индексирование в таблицу поиска было немного быстрее, чем битовое смещение. Это показывает, как мой код устанавливает и использует таблицу поиска (в коде невообразимо называемую lut для таблицы поиска). Вот код C ++:
// Объявляем и затем заполняем таблицу поиска
В этом случае таблица поиска составляла всего 256 байт, поэтому она хорошо помещалась в кеш, и все было быстро. Этот метод не сработает, если данные будут 24-битными значениями, а нам нужна только половина из них ... таблица поиска была бы слишком большой, чтобы быть практичной. С другой стороны, мы можем объединить два метода, показанных выше: сначала сдвинуть биты, а затем проиндексировать таблицу поиска. Для 24-битного значения, для которого нам нужно только верхнее половинное значение, мы могли бы сдвинуть данные вправо на 12 бит и оставить 12-битное значение для индекса таблицы. 12-битный индекс таблицы подразумевает таблицу из 4096 значений, что может быть практичным.
Техника индексации в массив, вместо использования оператора if, может быть использована для решения, какой указатель использовать. Я увидел библиотеку, в которой реализованы двоичные деревья, и вместо двух именованных указателей (pLeft и pRight и т. Д.) Имел массив указателей длины 2 и использовал технику «бит решения», чтобы решить, какой из них следовать. Например, вместо:
это хорошее решение, может быть, оно будет работать
источник
mask = tmp < 128 : 0 : -1UL;
/total += tmp & mask;