Техника определения, может ли целочисленная последовательность генерироваться без ветвей?
Если вы оптимизируете архитектуру, в которой ветвление стоит дорого (скажем, процессор ячеек PS3), может быть важно определить, можете ли вы выразить данный алгоритм без использования ветвей или, по крайней мере, с использованием меньшего количества ветвей. Один из примеров, который я часто вижу в неоптимизированном коде, - это набор if, используемый для подстройки индекса в некоторый массив (если размер массива нечетный, увеличьте индекс на 1, при некоторых других обстоятельствах умножьте на 2 и т. Д.). Поэтому было бы неплохо, если бы существовал способ, учитывая два списка чисел, определить, можно ли написать функцию без ответвлений, преобразующую один список в другой.
Например, недавно я хотел узнать, возможно ли написать функцию без ответвлений, которая преобразует: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
чтобы: 0, 2, 4, 6, 8, 9, 7, 5, 3, 1
(возрастание четное, затем убывание нечетное). Технически, я мог бы написать большую функцию switch/case, но, очевидно, меня интересует функция, которая будет следовать шаблону для произвольных размеров. Написание функции для выполнения этого преобразования является простым с ветвлением, но если есть неразветвленный способ сделать это, это не сразу очевидно.
Так есть ли общий подход к такой проблеме или какой-нибудь быстрый лакмусовый тест? Или вы должны придумывать доказательства в каждом конкретном случае? Я мог бы усерднее работать над такими проблемами, но это бессмысленно, если они буквально невозможны. Кажется, в какой-то момент я вспомнил, что читал, что есть формальное математическое слово для функций, которые используют только арифметику без ветвления, но я не могу вспомнить.
7 ответов
Преобразование: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 в: 0, 2, 4, 6, 8, 9, 7, 5, 3, 1 (возрастание четное, затем убывание нечетное),
Все просто: учитывая последовательность из N значений X от 0 до N-1, мы видим, что первая половина последовательности равна 2X. Вторая половина последовательности - (2N-1)-2X. Последовательность разбивается в X=(N+1)/2 с "целочисленной" математикой. В приведенном выше примере N == 10.
Итак, предположим, что 32-разрядные целые числа со знаком имеют арифметическое смещение вправо:
int Transform(int x)
{
const int seq1=x+x;
const int mask=(x-((N+1)>>1))>>31;
const int seq2=(N+N-1)-seq1;
return (mask&seq1)|((~mask)&seq2);
}
Обратите внимание, что шаблон маски, используемый здесь, быстрый, потому что PowerPC имеет ANDC (и с дополнением), который делает (~mask)
бесплатная операция.
Вы всегда можете написать полиномиальную формулу, используя, например, интерполяцию Лагранжа. Не очень (или особенно быстро), но у него не будет ветвей.
Если вы оптимизируете, в частности, для PS3, в Руководстве по написанию компиляторов Power PC в разделе 3.1.5 приведены методы для кода без ветвлений, а в приложении D. приведены последовательности супероптимизатора GNU для кода без ветвей.
Вы также можете быть заинтересованы в блоге Майка Актона "Cell Performance".
Если вы построите желаемые индексы по отношению к вашим входным индексам, вы получите функцию треугольной формы. Оказывается, для вашего n
=10, это
9.5 - abs(2 (x - 4.75))
Поэтому для общего n
, это было бы
n-0.5 - abs(2*(x - n/2-0.25))
Или в целочисленной форме,
(2*n-1 - abs(4*x - 2*n + 1)) / 2
Это совершенно без разветвлений в том смысле, что ваши выходные индексы генерируются с помощью одной математической функции. Я думаю, что общий подход состоит в том, чтобы составить график желаемых индексов и найти шаблон и способ представления его с помощью математических функций.
Очевидно, что если желаемые конечные индексы образуют прямую линию, преобразование будет простым. Если у вас есть излом в отображении, то вы хотите использовать функцию абсолютного значения, чтобы ввести изгиб, и вы можете настроить масштабирование, чтобы изменить угол изгиба. Вы можете наклонить излом, смещая его (например, abs(x)+x/2
). Если вам нужна прерывность перехода в вашей конечной индексной функции, используйте функцию sign (будем надеяться, встроенную или используйте abs(x)/x). Вы должны проявить творческий подход к тому, как использовать графики общих функций в ваших интересах здесь.
добавление
Если ваша функция индексации кусочно-линейная, существует простой алгоритм. Предположим, что желаемая индексная функция выражается в виде списка сегментов.
{(sx1,sy1)-(ex1,ey1), (sx2,sy2)-(ex2,ey2), ... , (sxN,syN)-(exN,eyN)}
segment 1 segment 2 segment N
где exK > sxK для всех K и sxK > sx(K-1) для всех K (поместите их слева направо).
k = 1
f(x) = Make affine model of segment k
g(x) = f(x)
Do:
k = k + 1
h(x) = Makeaffine model of segment k
If g(x) and h(x) intersect between ex(k-1) and ex(k)
f(x) = f(x) + [slope difference of g(x) and h(x)] * ramp(x)
Else
f(x) = f(x) + (h(ex(k-1)) - f(ex(k-1))) * step(x)
f(x) = f(x) + [slope difference of g(x) and h(x)] * ramp(x)
где ramp(x) = (abs(x)+x)/2
а также step(x) = (sign(x)+1)/2
, f(x) обозначает желаемую функцию, g(x)
является аффинной моделью последнего сегмента, и h(x)
аффинная модель текущего сегмента. Аффинная модель - это просто линия в форме смещения наклона: a*x+b
, а разница уклонов - это разница уклонов. Этот алгоритм просто работает слева направо, добавляя соответствующие части функций по мере продвижения. Функции, которые он добавляет, всегда равны нулю x <= 0
поэтому они не влияют на f(x)
это было создано до сих пор.
Конечно, могут быть некоторые ошибки / опечатки в выше. Мне действительно нужно попасть на встречу, поэтому я больше не могу писать.
Если скорость действительно важна, не могли бы вы выписать инструкции для списков до определенной длины? (Конечно, можно предварительно сгенерировать этот код).
так:
void algorithm1_Length6(int *srcList, int *destList)
{
*destList++ = *srcList;
*destList++ = srcList[2];
*destList++ = srcList[4];
*destList++ = srcList[5];
*destList++ = srcList[3];
*destList++ = srcList[1];
}
и все остальные вариации до определенной длины.
Технически любая серия операций может быть выполнена без "ветвления" с использованием конечного автомата, который использует логические операции. Концепция ветвления заключается в том, что большинство программ представляют собой последовательность команд, выполняемых счетчиком программ, которые могут идти тем или иным путем.
Даже если вы говорите о чисто функциональном подходе без сохранения состояния, для конечного набора дискретных значений вы всегда можете (за счет большого объема памяти) использовать справочную таблицу.
Для данного массива вы можете использовать такой метод:
void tranform(int[] src, int[] dest) {
//0, 2, 4, 6, 8, 9, 7, 5, 3, 1
dest[0] = src[0];
dest[1] = src[2];
dest[2] = src[4];
dest[3] = src[6];
dest[4] = src[8];
dest[5] = src[9];
dest[6] = src[7];
dest[7] = src[5];
dest[8] = src[3];
dest[9] = src[1];
}
Но в целом для больших массивов такие методы написать сложно, поэтому будет полезно, если вы напишите такой генератор:
static void createFunction(int[] src, int[] dest) {
System.out.println("void tranform(int[] src, int[] dest) {");
for (int i = 0; i < dest.length; i++) {
for (int j = 0; j < src.length; j++) {
if (dest[i] == src[j]) {
System.out.println("dest[" + i + "]=src[" + j + "];");
break;
}
}
}
System.out.println("}");
}
вызовите его со своими массивами:createFunction(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, new int[]{0, 2, 4, 6, 8, 9, 7, 5, 3, 1});
И вставьте вывод этого метода в вашу программу.