#x86-64 #sse #simd #intrinsics #lerp
#x86-64 #sse #simd #встроенные #lerp
Вопрос:
Я пытался найти наилучший способ использования инструкций AMD64 SIMD для реализации lerp для использования с большими наборами значений u8, но, похоже, я не могу найти правильные инструкции, не требуя всех расширений SIMD.
Формула, с которой я сейчас работаю, такова
u8* a;
u8* b;
u8* resu<
size_t count;
u16 total;
u16 progress;
u32 invertedProgress = total - progress;
for(size_t i = 0; i < count; i ){
result[i] = (u8)((b[i] * progress a[i] * invertedProgress) / total);
}
Я думаю, это будет выглядеть примерно так:
u8* a;
u8* b;
u8* resu<
size_t count;
u16 total;
u16 progress;
__m128i mmxZero;
__m128i mmxProgress;
__m128i mmxInvertedProgress;
__m128i mmxProductA;
__m128i mmxProductB;
mmxZero = _mm_xor_ps(zero, zero); // Is there a clear?
mmxProgress = Fill with progress;
mmxTotal = Fill with total;
mmxInvertedProgress = mmxTotal;
mmxInvertedProgress = _mm_unpacklo_epi8(mmxInvertedProgres, mmxZero);
mmxInvertedProgress = _mm_sub_epi8(mmxTotal, progress);
for(size_t i = 0; i < count; i = 8){
mmxProductA = load A;
// u8 -> u16
mmxProductA = _mm_unpacklo_epi8(mmxProductA, mmxZero);
mmxProductB = load B;
// u8 -> u16
mmxProductB = _mm_unpacklo_epi8(mmxProductB, mmxZero);
// a * (total - progress)
mmxProductA = _mm_mullo_epi16(mmxProductA, mmxInvertedProgress);
// b * progress
mmxProductB = _mm_mullo_epi16(mmxProductB, mmxProgress);
// a * (total - progress) b * progress
mmxProductA = _mm_add_epi16(mmxProductA, mmxProductB);
// (a * (total - progress) b * progress) / total
mmxProductA = _mm_div_epi16(mmxProductA, mmxTotal);
mmxProductA = saturated u16 -> u8;
store result = maxProductA;
}
Здесь есть пара вещей, которые я просто не смог найти в руководстве, в основном связанных с загрузкой и сохранением значений.
Я знаю, что есть несколько более новых инструкций, которые могут выполнять большие объемы одновременно, предполагается, что эта первоначальная реализация будет работать на старых чипах.
В этом примере я также игнорирую выравнивание и возможность переполнения буфера, я подумал, что это немного выходит за рамки вопроса.
Комментарии:
1.
_mm_div_epi16
является библиотечной функцией, а не встроенной для аппаратной инструкции. Даже в AVX512 нет целочисленного деления SIMD, только деление с плавающей запятой. В руководстве Intel по встроенным функциям сбивчиво перечислены их функции библиотеки SVML вместе с встроенными инструкциями для реальных инструкций, возможно, чтобы соблазнить людей покупать их библиотеки. Вы можете отфильтровать это с помощью флажков с левой стороны.2. Кроме того, MMX предшествует SSE / SSE2 и имеет только 64-разрядные целочисленные векторы. Я бы предложил имена переменных, такие как
__m128i vzero = _mm_setzero_si128();
или_mm_set1_epi32(0)
. Не пытайтесь использовать xor-ноль в вашем исходном коде C, пусть компиляторы сделают эту оптимизацию за вас.3. Я, честно говоря, не нашел встроенного набора, я просто сделал это, потому что это единственный способ, которым я нашел ноль. На этой странице так много встроенных функций!
Ответ №1:
Хороший вопрос. Как вы выяснили, в SSE нет инструкции целочисленного деления, и (в отличие от ARM NEON) в нем нет умножения или FMA для байтов.
Вот что я обычно делаю вместо этого. Приведенный ниже код разбивает векторы на четные / нечетные байты, использует 16-разрядные инструкции умножения для раздельного масштабирования, а затем объединяет их обратно в байты.
// Linear interpolation is based on the following formula: x*(1-s) y*s which can equivalently be written as x s(y-x).
class LerpBytes
{
// Multipliers are fixed point numbers in 16-bit lanes of these vectors, in 1.8 format
__m128i mulX, mulY;
public:
LerpBytes( uint16_t progress, uint16_t total )
{
// The source and result are bytes.
// Multipliers only need 1.8 fixed point format, anything above that is wasteful.
assert( total > 0 );
assert( progress >= 0 );
assert( progress <= total );
const uint32_t fp = (uint32_t)progress * 0x100 / total;
mulY = _mm_set1_epi16( (short)fp );
mulX = _mm_set1_epi16( (short)( 0x100 - fp ) );
}
__m128i lerp( __m128i x, __m128i y ) const
{
const __m128i lowMask = _mm_set1_epi16( 0xFF );
// Split both vectors into even/odd bytes in 16-bit lanes
__m128i lowX = _mm_and_si128( x, lowMask );
__m128i highX = _mm_srli_epi16( x, 8 );
__m128i lowY = _mm_and_si128( y, lowMask );
__m128i highY = _mm_srli_epi16( y, 8 );
// That multiply instruction has relatively high latency, 3-5 cycles.
// We're lucky to have 4 vectors to handle.
lowX = _mm_mullo_epi16( lowX, mulX );
lowY = _mm_mullo_epi16( lowY, mulY );
highX = _mm_mullo_epi16( highX, mulX );
highY = _mm_mullo_epi16( highY, mulY );
// Add the products
__m128i low = _mm_adds_epu16( lowX, lowY );
__m128i high = _mm_adds_epu16( highX, highY );
// Pack them back into bytes.
// The multiplier was 1.8 fixed point, trimming the lowest byte off both vectors.
low = _mm_srli_epi16( low, 8 );
high = _mm_andnot_si128( lowMask, high );
return _mm_or_si128( low, high );
}
};
static void print( const char* what, __m128i v )
{
printf( "%s:t", what );
alignas( 16 ) std::array<uint8_t, 16> arr;
_mm_store_si128( ( __m128i * )arr.data(), v );
for( uint8_t b : arr )
printf( " X", (int)b );
printf( "n" );
}
int main()
{
const __m128i x = _mm_setr_epi32( 0x33221100, 0x77665544, 0xBBAA9988, 0xFFEEDDCC );
const __m128i y = _mm_setr_epi32( 0xCCDDEEFF, 0x8899AABB, 0x44556677, 0x00112233 );
LerpBytes test( 0, 1 );
print( "zero", test.lerp( x, y ) );
test = LerpBytes( 1, 1 );
print( "one", test.lerp( x, y ) );
test = LerpBytes( 1, 2 );
print( "half", test.lerp( x, y ) );
test = LerpBytes( 1, 3 );
print( "1/3", test.lerp( x, y ) );
test = LerpBytes( 1, 4 );
print( "1/4", test.lerp( x, y ) );
return 0;
}
Комментарии:
1. Это выглядит очень многообещающе, я попробую это, когда у меня будет возможность, и отчитаюсь.
2. Есть ли способ загрузить значения в регистр из указателя?
3. Я также, должно быть, упускаю что-то глупое, потому что я не вижу способа отменить _mm_setr_epi32.
4. @gudenau Смотрите
_mm_load_si128
и_mm_loadu_si128
, их больше, но эти два являются самыми популярными для 16-байтовых целых векторов.