I have the following method for multiplying two polynomials in GF(232):
#include <iostream>
#include <cassert>
#include <ctime>
#include <random>
#include <wmmintrin.h>
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
unsigned get_high16(unsigned x)
{
return (x >> 16) & 0xffff;
}
unsigned get_low16(unsigned x)
{
return x & 0xffff;
}
uint32_t mul_reduce_32(uint32_t x, uint32_t y)
{
const __m128i a = _mm_set_epi32(0, 0, 0, x);
const __m128i b = _mm_set_epi32(0, 0, 0, y);
const __m128i c =_mm_clmulepi64_si128(a, b, 0);
uint16_t D = _mm_extract_epi16(c, 2);
D ^= _mm_extract_epi32(c, 1) >> 31;
D ^= _mm_extract_epi32(c, 1) >> 30;
D ^= _mm_extract_epi32(c, 1) >> 29;
D ^= _mm_extract_epi32(c, 1) >> 27;
D ^= _mm_extract_epi32(c, 1) >> 25;
const uint16_t X3 = _mm_extract_epi16(c, 3);
const __m128i X3D = _mm_set_epi16(0, 0, 0, 0, 0, 0, X3, D);
const uint32_t E = _mm_extract_epi32(X3D, 0) << 7;
const uint32_t F = _mm_extract_epi32(X3D, 0) << 5;
const uint32_t G = _mm_extract_epi32(X3D, 0) << 3;
const uint32_t I = _mm_extract_epi32(X3D, 0) << 2;
const uint32_t J = _mm_extract_epi32(X3D, 0) << 1;
const uint16_t H1 = X3 ^ get_high16(E) ^ get_high16(F) ^ get_high16(G) ^ get_high16(I) ^ get_high16(J);
const uint16_t H0 = D ^ get_low16(E) ^ get_low16(F) ^ get_low16(G) ^ get_low16(I) ^ get_low16(J);
const uint16_t R1 = _mm_extract_epi16(c, 1) ^ H1;
const uint16_t R0 = _mm_extract_epi16(c, 0) ^ H0;
return _mm_extract_epi32(_mm_set_epi16(0, 0, 0, 0, 0, 0, R1, R0), 0);
}
The following is a small test program and benchmark for the function.
int main()
{
assert(mul_reduce_32(1, 1) == 1);
assert(mul_reduce_32(23523, 34651) == 744709325);
assert(mul_reduce_32(34652346, 5534651) == 1329462203);
// A little benchmark
std::mt19937 rng(std::time(0));
std::uniform_int_distribution<uint32_t> uint_dist10(0, std::numeric_limits<uint32_t>::max());
const int N = 100000000;
std::vector<uint32_t> vec1(N, 0);
std::cout << "mul_reduce_32\n";
clock_t startTime = clock();
for(int i = 0; i < N; ++i)
{
vec1[i] = mul_reduce_32(rng(), rng());
}
std::cout << double( clock() - startTime ) / (double)CLOCKS_PER_SEC << " seconds.\n";
}
I'm interested in getting more out of mul_reduce_32
in terms of performance. Here are my main concerns:
I'm setting
a
andb
using_mm_set_epi32
. Is this even a good idea? I suppose I could have used_mm_load_si128
or_mm_loadu_si128
, but the problem seems to be that it doesn't touch the 98 higher bits, and makes those high bits garbage.Currently, I am using many temporary variables. I could try to help the compiler by removing many of them, but this way the code should be more readable and easier for someone to modify.
I'm using variables of different size -- is this a good idea? I might remember some old wisdom that says I should always just stick to the "most native" unsigned type. Concretely, should I replace say each
uint16_t
withuint32_t
(or even a 64-bit type)?Should I be mixing
__m128i
types with unsigned types to begin with? As I understand it, the__m128i
types are guaranteed to be mapped to a dedicated register, whereas this is not the case for auint32_t
.
Can you suggest additional optimizations? I'm knowledgeable in C++, but a total newbie with SSE intrinsics.