Ответ 1
Одиночный вызов
Обратите внимание, что вы можете записать номер k
в системе позиционных обозначений с mixed radix, чтобы каждая цифра в этом представлении определите индексы обмениваемых элементов для нескольких последовательных значений j
.
Например, для строк длиной 12 вы можете написать любой k
как трехзначное число с основами:
720 = 1*2*3*4*5*6 (0-th digit, lowest value)
504 = 7*8*9 (1-th digit)
1320 = 10*11*12 (2-th digit, highest value)
Теперь вы можете предварительно скопировать для каждой позиции и для каждого значения цифры в этой позиции кумулятивную перестановку всех ваших элементов и сохранить ее в таблице поиска. Затем вы сможете сделать несколько свопов по одной инструкции.
Вот пример (предварительная компиляция будет самой сложной):
//to be precomputed:
__m128i mask0[ 720];
__m128i mask1[ 504];
__m128i mask2[1320];
__m128i permutation(int k, __m128i s) {
s = _mm_shuffle_epi8(s, mask0[k % 720]); k /= 720; //j = [1..5]
s = _mm_shuffle_epi8(s, mask1[k % 504]); k /= 504; //j = [6..8]
s = _mm_shuffle_epi8(s, mask2[k ]); //j = [9..11]
return s;
}
Вы можете изменить разложение на базы, чтобы сбалансировать количество шагов и размер таблицы поиска.
Примечание: операция разделения выполняется очень медленно. Используйте только деления по константам времени компиляции, тогда оптимизатор преобразует их в умножения. Проверьте сгенерированную сборку, чтобы убедиться, что нет инструкций разделения.
Многие вызовы
К сожалению, индексные вычисления по-прежнему будут потреблять большую часть времени с предлагаемым решением, см. сгенерированная сборка. Эти служебные данные могут быть значительно уменьшены, если вы обрабатываете сразу несколько последовательных значений k
.
Простейшим подходом к оптимизации решения будет: итерация над цифрами k
отдельно, а не выполнение одиночного цикла над k
. Тогда расчеты индекса становятся ненужными. Кроме того, вы можете повторно использовать частично вычисленные результаты.
__m128i s;// = ???
for (int k0 = 0; k0 < 720; k0++) {
__m128i s0 = _mm_shuffle_epi8(s, mask0[k0]);
for (int k1 = 0; k1 < 504; k1++) {
__m128i s1 = _mm_shuffle_epi8(s0, mask1[k1]);
for (int k2 = 0; k2 < 1320; k2+=4) {
//for k = (((k2+0) * BASE1) + k1) * BASE0 + k0:
__m128i sx0 = _mm_shuffle_epi8(s1, mask2[k2+0]);
//for k = (((k2+1) * BASE1) + k1) * BASE0 + k0:
__m128i sx1 = _mm_shuffle_epi8(s1, mask2[k2+1]);
//for k = (((k2+2) * BASE1) + k1) * BASE0 + k0:
__m128i sx2 = _mm_shuffle_epi8(s1, mask2[k2+2]);
//for k = (((k2+3) * BASE1) + k1) * BASE0 + k0:
__m128i sx3 = _mm_shuffle_epi8(s1, mask2[k2+3]);
// ... check four strings: sx0, sx1, sx2, sx3
}
}
}
Таким образом, вам нужно сделать одну перетасовку для каждой перестановки в среднем (см. сборка), которая кажется близкой к совершенству.
Код и результаты
Вот полный рабочий код всех решений.
Обратите внимание, что генерация таблиц поиска не является тривиальной, чтобы полностью объяснить, а соответствующий код довольно большой (и заполнен неприятными деталями).
Тест производительности Intel Core 2 Duo E4700 Allendale (2600 МГц) дает результаты:
2.605 s: original code (k < 12739451)
0.125 s: single-call fast code (k < 12739451)
4.822 s: single-call fast code (k < 479001600)
0.749 s: many-call fast code (k < 479001600)
Таким образом, версия с одним вызовом примерно 20 раз быстрее, чем исходный код, а версия с многочисленными вызовами - 6.5 раз быстрее, чем версия с одним вызовом.