Быстрая генерация "последовательности треугольника": избегая неправильных прогнозов
Я заинтересован в расчете последовательности треугольника1, которая является последовательностью пар (i, j): (0, 0), (1, 0), (1, 1), (2, 0), (2, 1) ...
который перебирает все пары (i, j)
с ограничением, что i >= j
, Та же самая последовательность с, но с ограничением i > j также интересна.
Эти последовательности представляют, помимо прочего, все способы выбора 2 (возможно, идентичных) элементов из набора из n элементов (для последовательности до (n, n)
2) или индексы нижних триагулярных элементов матрицы3. Последовательность значений для i
только A003056 в OEIS, в то время как j
один A002262. Последовательность часто возникает в комбинаторных алгоритмах, где их производительность может быть критической.
Простой, но ветвистый способ генерировать следующее значение в последовательности:
if (i == j) {
j = 0;
i++;
} else {
j++;
}
}
Тем не менее, это страдает от многих ошибочных прогнозов при расчете начальных элементов последовательности, при проверке условия (i == j)
- как правило, один неверный прогноз каждый раз i
увеличивается Поскольку последовательность увеличивается, число ошибочных прогнозов становится меньше, так как i
увеличивается с пониженной частотой, поэтому j++
Отрасль доминирует и хорошо предсказуема. Тем не менее, некоторые типы комбинаторного поиска многократно перебирают небольшие термины в последовательности, поэтому я ищу подход без ветвей или другой подход, который подвергается меньшему количеству ошибочных прогнозов.
Для многих случаев порядок последовательностей не так важен, поэтому допустимо генерирование значений в отличающемся от приведенного выше порядке, если оно приводит к лучшему решению. Например, j
может считать, а не вверх: (0, 0), (1, 1), (1, 0), (2, 2), (2, 1), ...
,
1 Мне также интересно узнать, как правильно называть эту последовательность (возможно, поэтому я сделаю лучший заголовок для этого вопроса). Я просто изобразил "последовательность треугольников".
2 Здесь i >= j
версия представляет суб-мультимножества (повторение разрешено), в то время как i > j
Вариант представляет нормальные подмножества (без повторений).
3 Здесь i >= j
версия включает в себя основную диагональ, а i > j
вариант исключает это.
3 ответа
Вот два подхода без ответвлений, которые не используют дорогостоящие вычисления. Первый использует сравнение и логическое И:
const bool eq = i == j;
i += eq;
j = (j + 1) & (eq - 1);
Второй использует сравнение и умножение:
const bool eq = i == j;
i += eq;
j = (j + 1) * (1 - eq);
В теории вариант "умножения" должен быть медленнее, чем "логический", но измерения показывают очень небольшую разницу.
Оба подхода приводят к созданию кода без ветвей только для процессоров, которые позволяют проводить сравнения без ветвей (например, x86). Также предполагается, что эти подходы будут реализованы с использованием языка, где результаты условных выражений могут быть легко преобразованы в целые числа (например, C/C++, где "ложные" сравнения преобразуются в нулевые целые числа, а "истинные" - в целые числа, равные "1").
Единственная проблема с этими подходами - производительность. Теоретически они могут превзойти разветвленный код, но только в том случае, если неправильные прогнозы действительно часты. Простой тест, в котором нет никакой другой работы, кроме генерации "последовательности треугольников" (см. Его на ideone), показывает жалкую скорость неверного прогнозирования и, следовательно, оба метода без ветвления примерно в 3 раза медленнее, чем метод с ветвлением. Объяснение простое: не должно быть много неправильных прогнозов для более длинных последовательностей; Что касается более коротких, современные процессоры имеют очень хорошие предикторы ветвления, которые почти никогда не выходят из строя в случае коротких паттернов ветвления; поэтому у нас не так много неправильных прогнозов, код ветвления почти всегда выполняет только 2 инструкции (сравнение, приращение), в то время как код без ветвлений выполняет как активные, так и относительные "ветки" плюс некоторые инструкции, характерные для подхода без ветвей.
В случае, если вы хотите repeatedly iterate over the small terms in the sequence
Возможно, другой подход был бы предпочтительнее. Вы вычисляете последовательность только один раз, а затем повторно считываете ее из памяти.
В Python мы можем выразить это как:
i, j = i + (i == j), (j + 1) * (i != j)
но получается, что примерно на миллион итераций на моем компьютере следующий, более длинный и ленивый код оценки работает примерно на 20% быстрее:
from itertools import count, repeat
def gen_i():
""" A003056 """
for x in count(0): # infinitely counts up
yield from repeat(x, x + 1) # replication
def gen_j():
""" A002262 """
for x in count(0): # infinitely counts up
yield from range(x + 1) # count up to (including) x
sequence = zip(gen_i(), gen_j())
for _ in range(1000000):
i, j = next(sequence)
В приведенном выше коде, gen_i()
, gen_j()
, count()
, repeat()
, а также zip()
все генераторы (и range()
это итератор) так sequence
продолжает вызывать код по требованию, так как требуются новые (i, j) пары. Я предполагаю, что реализация range()
а также repeat()
прекратить с ошибочным прогнозом.
Простое не обязательно также быстро (т.е. рассмотрите все ненужные добавления нуля и умножения на единицу в компактной форме).
Итак, что важнее: быстро создать последовательность или избежать неправильных прогнозов?
Вы можете вывести j из i:
...set val...
old_j = j;
j = (j + 1) % (i + 1);
if (i == old_j) {
i++;
}
...loop if more...
И, кроме того, я получаю приращение от j до текущего i:
...set val...
old_j = j;
j = (j + 1) % (i + 1);
i = i + (i / old_j);
...loop if more...
(Не могу проверить это в данный момент... Пожалуйста, просмотрите)