SSE оптимизированная эмуляция 64-битных целых чисел

Для хобби-проекта, над которым я работаю, мне нужно эмулировать определенные 64-битные целочисленные операции на процессоре x86, и он должен быть быстрым.

В настоящее время я делаю это с помощью инструкций MMX, но на самом деле это боль, с которой нужно работать, потому что мне нужно постоянно обновлять состояние регистра fp (и потому, что большинство команд MMX имеют дело со значными целыми числами, и мне нужно беззнаковое поведение).

Итак, мне интересно, смогут ли гуру SSE/оптимизации здесь на SO лучше реализовать с использованием SSE.

Операции, в которых я нуждаюсь, являются следующими (весьма специфическими):

uint64_t X, Y;

X = 0;
X = 1;
X << 1;
X != Y;
X + 1;
X & 0x1 // get lsb
X | 0x1 // set lsb
X > Y;

В частности, мне не нужна добавка или смена общего назначения, например, просто добавьте один и левый сдвиг. На самом деле, именно точные операции показаны здесь.

Кроме того, конечно, на x86, uint64_t эмулируется с использованием двух 32-битных скаляров, что является медленным (и, в моем случае, просто не работает, потому что мне нужно, чтобы грузы/хранилища были атомарными, которые они не будут при загрузке/хранении двух отдельных регистров).

Следовательно, мне нужно решение SIMD. Некоторые из этих операций тривиальны, уже поддерживаются SSE2. Другие (!= и <) требуют немного больше работы.

Предложения? SSE и SSE2 в порядке. Для разрешения SSE3 потребуется некоторое убеждение, и SSE4, вероятно, не может быть и речи (процессор, поддерживающий SSE4, вероятно, будет работать на 64-разрядном уровне, и поэтому мне не нужны эти обходные пути)

Ответ 1

SSE2 имеет прямую поддержку некоторых 64-битных целочисленных операций:

Установите оба элемента на 0:

__m128i z = _mm_setzero_si128();

Установите оба элемента в 1:

__m128i z = _mm_set1_epi64x(1);      // also works for variables.
__m128i z = _mm_set_epi64x(hi, lo);  // elements can be different

__m128i z = _mm_set_epi32(0,1,0,1);  // if any compilers refuse int64_t in 32-bit mode.  (None of the major ones do.)

Установить/загрузить младшие 64 бита с расширением нуля до __m128i

// supported even in 32-bit mode, and listed as an intrinsic for MOVQ
// so it should be atomic on aligned integers.
_mm_loadl_epi64((const __m128i*)p);     // movq or movsd 64-bit load

_mm_cvtsi64x_si128(a);      // only ICC, others refuse in 32-bit mode
_mm_loadl_epi64((const __m128i*)&a);  // portable for a value instead of pointer

Вещи, основанные на _mm_set_epi32 могут быть скомпилированы в беспорядок некоторыми компиляторами, поэтому _mm_loadl_epi64 кажется, лучшая ставка для MSVC и ICC, а также gcc/clang, и на самом деле должна быть безопасной для ваших требований атомарных 64-битных нагрузок в 32- битовый режим. Смотрите это в проводнике компилятора Godbolt

Вертикально складывать/вычитать каждое 64-битное целое число:

__m128i z = _mm_add_epi64(x,y)
__m128i z = _mm_sub_epi64(x,y)

http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse2_integer_arithmetic.htm#intref_sse2_integer_arithmetic

Сдвиг влево:

__m128i z = _mm_slli_epi64(x,i)   // i must be an immediate

http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse2_int_shift.htm

Битовые операторы:

__m128i z = _mm_and_si128(x,y)
__m128i z = _mm_or_si128(x,y)

http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse2_integer_logical.htm

SSE не имеет приращений, поэтому вам придется использовать константу с 1.


Сравнения сложнее, поскольку 64-битная поддержка отсутствует до SSE4.1 pcmpeqq и SSE4.2 pcmpgtq

Здесь один для равенства:

__m128i t = _mm_cmpeq_epi32(a,b);
__m128i z = _mm_and_si128(t,_mm_shuffle_epi32(t,177));

Это установит для каждого 64-битного элемента значение 0xffffffffffff (он же -1) если они равны. Если вы хотите, чтобы оно было 0 или 1 в int, вы можете вытащить его с помощью _mm_cvtsi32_si128() и добавить 1. (Но иногда вы можете сделать total -= cmp_result; вместо преобразования и добавления.)

И меньше, чем: (не полностью проверено)

a = _mm_xor_si128(a,_mm_set1_epi32(0x80000000));
b = _mm_xor_si128(b,_mm_set1_epi32(0x80000000));
__m128i t = _mm_cmplt_epi32(a,b);
__m128i u = _mm_cmpgt_epi32(a,b);
__m128i z = _mm_or_si128(t,_mm_shuffle_epi32(t,177));
z = _mm_andnot_si128(_mm_shuffle_epi32(u,245),z);

Это установит для каждого 64-битного элемента значение 0xffffffffffff если соответствующий элемент в a меньше, чем b.


Вот версии "равно" и "меньше чем", которые возвращают логическое значение. Они возвращают результат сравнения для нижнего 64-разрядного целого числа.

inline bool equals(__m128i a,__m128i b){
    __m128i t = _mm_cmpeq_epi32(a,b);
    __m128i z = _mm_and_si128(t,_mm_shuffle_epi32(t,177));
    return _mm_cvtsi128_si32(z) & 1;
}
inline bool lessthan(__m128i a,__m128i b){
    a = _mm_xor_si128(a,_mm_set1_epi32(0x80000000));
    b = _mm_xor_si128(b,_mm_set1_epi32(0x80000000));
    __m128i t = _mm_cmplt_epi32(a,b);
    __m128i u = _mm_cmpgt_epi32(a,b);
    __m128i z = _mm_or_si128(t,_mm_shuffle_epi32(t,177));
    z = _mm_andnot_si128(_mm_shuffle_epi32(u,245),z);
    return _mm_cvtsi128_si32(z) & 1;
}