Может ли умножение символов/цифр быть более результативным?
У меня есть следующий код, в котором вычисляется сумма, основанная на очень большой серии.
Серия char *a
представляет собой массив char, который содержит только цифры (0..9).
Я хотел спросить, есть ли возможность сделать код быстрее. В настоящее время это горло бутылки в распределенном вычислительном приложении.
Небольшой код воспроизведения. Не настоящий код и более упрощенный.
int top = 999999999;
char *a;
a = (char*) calloc(top+1, sizeof(char));
// ... fill a with initial values ...
for (int i=0; i<10; ++i) {
unsigned long long int sum = 0;
for (m = 1, k = top; m < k; ++m, --k) {
// Here is the bottle neck!!
sum += a[m]*a[k];
}
printf("%d\n", sum);
// ... Add something at the end of a, and increase top ...
}
Я уже пробовал следующее:
-
Оптимизация кода с помощью -O3
(gcc-компилятор). Теперь строка компилятора:
gcc -c -Wall -fopenmp -Wno-unused-function -O3 -std=c99 -g0 -march=native -pipe -D_FILE_OFFSET_BITS=64 -m64 -fwhole-program -fprefetch-loop-arrays -funsafe-loop-optimizations -Wunsafe-loop-optimizations -fselective-scheduling -fselective-scheduling2 -fsel-sched-pipelining -fsel-sched-pipelining-outer-loops -fgcse-sm -fgcse-lm -fgcse-las -fmodulo-sched -fgcse-after-reload -fsee -DLIBDIVIDE_USE_SSE2 -DLIBDIVIDE_USE_SSE4_1 xxx.c -o xxx.o
-
Использование GNU openMP для разделения for-loop на несколько ядер
unsigned long long int halfway = (top>>1) + 1; // = top/2 + 1
// digits is defined as top+1
#pragma omp parallel // firstprivate/*shared*/(a, digits, halfway)
for (unsigned long long int m = 1; m < halfway; ++m) {
sum += a[m] * a[digits-m];
}
Результат: намного, намного быстрее, но требуется больше ядер, и я все равно хочу сделать это быстрее.
-
Отбрасывание a[m]
до unsigned long long int
перед умножением
sum += (unsigned long long int)a[m] * a[k];
Результат: небольшое повышение производительности.
-
Использование таблицы поиска умножения, поскольку поиск массива выполняется быстрее, чем фактическое умножение.
sum += multiply_lookup[a[m]][a[k]]; // a[m]*a[k];
Результат: небольшое повышение производительности.
-
Я попытался найти математическое решение для сокращения операций, но кажется, что ничто не может быть оптимизировано математически.
У меня есть следующая идея для оптимизации:
I прочитали, что умножение поплавков (asm fmul
) намного быстрее, чем умножение целых чисел (asm mul
). Просто изменить int
на float
не поможет - но я думаю, что код может стать намного более эффективным, если работа выполняется с использованием наборов команд MMX или SSE или если работа выполняется FPU. Хотя у меня есть некоторые знания ассемблера, я не знаю об этих темах.
Однако, если у вас есть дополнительные идеи, как их оптимизировать, я рад их услышать.
Обновить. Дополнительная информация:
- После каждого цикла серия увеличивается на 1 элемент.
- Пока серия растет, увеличивается
top
.
- Когда
top
достигает предела массива, a
будет увеличиваться на 100000 байт, используя realloc()
.
- Платформа: Debian Linux Jessie x64, на процессоре Intel (R) Xeon (R) X3440 @2.53 ГГц
Дополнительный вопрос вне темы: Знаете ли вы математическое имя этой суммы, где пары элементов серии умножаются извне внутрь?
Ответы
Ответ 1
Для этого вы можете использовать малоизвестные PMADDUBSW (Multiply и Add Packed Signed and Unsigned Bytes). Подписанный/неподписанный бизнес здесь не имеет значения, все равно в промежутке [0.. 9]. Добавка насыщает, но это не имеет значения, потому что 9 * 9 - всего 81. С внутренними свойствами _mm_maddubs_epi16
. Поскольку индекс k
понижается, вам нужно поменять его байтом, что вы можете сделать с помощью PSHUFB (_mm_shuffle_epi8
). Досадная вещь случается, когда индексы "встречаются" посередине, вы можете делать эту часть один за другим.
Здесь попытка, только слегка протестированная:
__m128i sum = _mm_setzero_si128();
int m, k;
for (m = 1, k = top - 15; m + 15 < k; m += 16, k -= 16) {
__m128i am = _mm_loadu_si128((__m128i*)(a + m));
__m128i ak = _mm_loadu_si128((__m128i*)(a + k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ,15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
// could use phaddw, but I do this the long way to avoid overflow slightly longer
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
int s = _mm_cvtsi128_si32(sum);
// this is for the "tail"
k += 15;
for (; m < k; ++m, --k)
s += a[m] * a[k];
Также игнорирую переполнение. Вы можете сделать это для (2 16 -1)/(2 * 81) = 404 итераций и по-прежнему не имеют переполнения. Если вам нужно больше, периодически добавляйте это к 32-битовому результату.
В быстром бенчмарке это примерно в 7 раз быстрее, чем простой способ (тестируется с 2 Кбайт случайных данных на 4770 КБ, беря максимум из ста пробегов для каждого).
Использование указателей, предложенных другим ответом, улучшает его дальше, примерно в 9 раз быстрее, чем простой способ. С индексами произошло некоторое странное расширение знака.
int foobar(char* a, int top)
{
__m128i sum = _mm_setzero_si128();
char *m, *k;
for (m = a + 1, k = a + top - 15; m + 15 < k; m += 16, k -= 16) {
__m128i am = _mm_loadu_si128((__m128i*)(m));
__m128i ak = _mm_loadu_si128((__m128i*)(k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
int s = _mm_cvtsi128_si32(sum);
k += 15;
for (; m < k; ++m, --k)
s += *m * *k;
return s;
}
Разделить по частям, все еще примерно в 9 раз быстрее оригинала, несмотря на дополнительную логику:
int foobar(char* a, int top)
{
int s = 0;
char *m, *k;
for (m = a + 1, k = a + top - 15; m + 15 < k;) {
__m128i sum = _mm_setzero_si128();
for (int i = 0; i < 404 && m + 15 < k; m += 16, k -= 16, ++i) {
__m128i am = _mm_loadu_si128((__m128i*)(m));
__m128i ak = _mm_loadu_si128((__m128i*)(k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ,15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
s += _mm_cvtsi128_si32(sum);
}
k += 15;
for (; m < k; ++m, --k)
s += *m * *k;
return s;
}
Ответ 2
Этот цикл,
for (m = 1, k = top; m < k; ++m, --k) {
// Here is the bottle neck!!
sum += a[m]*a[k];
}
может получить выгоду от перехода на:
char *b = a + top;
a++;
for (; a < b; )
{
sum += ( *a++ ) * ( *b--);
}
Удалив [], вы сохраняете много арифметики для каждого доступа к массиву. Это уменьшает теоретическое число адресных вычислений:
4 с ++m --k
и a[m] a[k]
до
2 с *a++ *b--
Приращение простого указателя дешевле и обычно быстрее общего, поскольку доступ к массиву не всегда оптимизирован с помощью [].
Надеюсь, что это поможет
Ответ 3
Операция, которую вы хотите выполнить, называется дискретной сверткой, она появляется при умножении больших чисел. Наивный алгоритм, который вы используете, имеет сложность O (n & thinsp; 2), но решение O (n log n) получается с помощью дискретное преобразование Фурье.
Дискретные конволюции
Дискретная свертка c = a & lowast; b двух последовательностей a = a 0, a 1, & hellip;, a n & minus; 1 и b = b 0, b 1, & hellip;, b n & minus; 1 с n элементами каждая представляет собой последовательность из 2 n & minus; 1 элементов, определенных для каждого k в виде суммы:
c i= & sum; max (0, я & minus; n + 1) & le; j < min (n, я + 1) a j & thinsp; b я & minus; Jсуб >
Если предположить, что a i= b i= 0 для я & notin; {0, & hellip;, n & minus; 1}, то мы можем упростить это и суммировать по целому числу i:
c i= & sum; j a j & thinsp; b я & minus; Jсуб >
Обратите внимание, как это операция, которую вы хотите выполнить: a = b = a[]
, а i
-я итерация вашего цикла, sum
- это просто c i.
Дискретная свертка хорошо изучена и появляется в ряде математических задач, связанных с обработкой сигналов, комбинаторика и статистикой. К счастью, его можно вычислить в сверхлинейном времени O (n log n) вместо наивного O (n 2).
Циклические дискретные конволюции
Мы можем разложить b на b N такие, что
b k= b k mod n, & emsp; или
b N= b 0, b 1, & hellip;, b n & minus; 1, b 0, & hellip;, b n & minus; 1, b 0, & hellip;
Это называется циклическим расширением b до b N. Дискретная свертка a & lowast; b N называется дискретной циклической сверткой a и b.
Заметим, что нециклическая дискретная свертка a и b может быть вычислена из циклической свертки путем добавления достаточно большого числа нулей к a и b до свертывания, так что циклическое разложение b не изменяет результат. Подробнее см. в этой статье.
Дискретное преобразование Фурье
Дискретное преобразование Фурье (DFT) преобразует последовательность выборок a в комплексный частотный спектр ℱ (a) дискретизированного сигнала. Преобразование Фурье обратимо и вычислимо в суперлинейном времени O (n log n) с использованием различных алгоритмов быстрого преобразования Фурье (FFT). Это преобразование имеет много применений в обработке сигналов. Он имеет полезное свойство, что
a & lowast; bN= ℱ & minus; 1 (ℱ (a) & times; ℱ (b))
То есть циклическая свертка двух последовательностей a и b является умножением по элементам при преобразовании Фурье. Так как элементарное умножение может быть выполнено в линейном времени, это позволяет вычислить результат, который вам нужен в общем времени O (n log n).
Реализация
Проект FFTW обеспечивает высоко оптимизированные реализации быстрого преобразования Фурье.
Проблемы
Преобразования Фурье работают над комплексными числами и требуют, чтобы блок с плавающей точкой был быстрым. Результат, скорее всего, будет немного неточным. Насколько мне известно, точные методы возможны с помощью некоторой модульной магии (это то, что используется в алгоритме Шенхаге-Штрассена для многоцелевого умножения), но я не уверен в деталях.