2011-11-19 11 views
11

Quiero multiplicar con SSE4 un objeto __m128i con 16 enteros de 8 bits sin signo, pero solo pude encontrar un valor intrínseco para multiplicar números enteros de 16 bits. ¿No hay nada como _mm_mult_epi8?multiplicación SSE 16 x uint8_t

+1

¿Podría aclarar su pregunta un poco? ¿Desea multiplicar un entero de 128 bits con 16 enteros de 8 bits cada uno o 16 enteros de 8 bits con 16 enteros de 8 bits o los 16 enteros de 8 bits en un registro único entre sí? El primer caso sería un poco extraño. –

+0

Solo un pensamiento, pero ¿por qué no rellenar el 8bit a 16? y si quiere probar el desbordamiento, puede simplemente AND y AH y ver si hay coincidencia para verificar el desbordamiento. Un poco desordenado y solo una puñalada en la oscuridad. También me sorprendería si hubiera soporte directamente para mul de 8 bits ya que el conjunto de instrucciones para SIMD se escribió para procesadores posteriores de 8 bits –

+0

@Paul: los valores de 8 bits todavía se usan en gráficos. AltiVec tiene multiplicación de 8 bits, aunque solo 8 a la vez con resultados de 16 bits. – Potatoswatter

Respuesta

11

No hay multiplicación de 8 bits en MMX/SSE/AVX. Sin embargo, puede emular a 8 bits multiplicación intrínseca mediante la multiplicación de 16 bits de la siguiente manera:

inline __m128i _mm_mullo_epi8(__m128i a, __m128i b) 
{ 
    __m128i zero = _mm_setzero_si128(); 
    __m128i Alo = _mm_cvtepu8_epi16(a); 
    __m128i Ahi = _mm_unpackhi_epi8(a, zero); 
    __m128i Blo = _mm_cvtepu8_epi16(b); 
    __m128i Bhi = _mm_unpackhi_epi8(b, zero); 
    __m128i Clo = _mm_mullo_epi16(Alo, Blo); 
    __m128i Chi = _mm_mullo_epi16(Ahi, Bhi); 
    __m128i maskLo = _mm_set_epi8(0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 14, 12, 10, 8, 6, 4, 2, 0); 
    __m128i maskHi = _mm_set_epi8(14, 12, 10, 8, 6, 4, 2, 0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80); 
    __m128i C = _mm_or_si128(_mm_shuffle_epi8(Clo, maskLo), _mm_shuffle_epi8(Chi, maskHi)); 

    return C; 
} 
8

La única instrucción de multiplicar SSE de 8 bits es PMADDUBSW (SSSE3 y posterior, C/C++ intrínseca: _mm_maddubs_epi16). Esto multiplica los valores de 16 x 8 bit sin signo por 16 x 8 bit con valores firmados y luego suma pares adyacentes para obtener resultados firmados de 8 x 16 bits. Si no puede usar esta instrucción más bien especializada, necesitará desempaquetar pares de vectores de 16 bits y usar instrucciones regulares de multiplicar de 16 bits. Obviamente, esto implica al menos un golpe de 2x, así que usa la multiplicación de 8 bits si puedes.

12

Una forma (potencialmente) más rápido que la solución de Marat basado en Agner Fog's solution:

En lugar de dividir alta/baja, dividida impar/par. Esto tiene el beneficio adicional de que funciona con SSE2 puro en lugar de requerir SSE4.1 (sin uso para el OP, pero es una buena bonificación añadida para algunos). También agregué una optimización si tienes AVX2. Técnicamente, la optimización de AVX2 solo funciona con SSE2 intrínsecos, pero es más lenta que la solución de desplazamiento a la izquierda y luego a la derecha.

__m128i mullo_epi8(__m128i a, __m128i b) 
{ 
    // unpack and multiply 
    __m128i dst_even = _mm_mullo_epi16(a, b); 
    __m128i dst_odd = _mm_mullo_epi16(_mm_srli_epi16(a, 8),_mm_srli_epi16(b, 8)); 
    // repack 
#ifdef __AVX2__ 
    // only faster if have access to VPBROADCASTW 
    return _mm_or_si128(_mm_slli_epi16(dst_odd, 8), _mm_and_si128(dst_even, _mm_set1_epi16(0xFF))); 
#else 
    return _mm_or_si128(_mm_slli_epi16(dst_odd, 8), _mm_srli_epi16(_mm_slli_epi16(dst_even,8), 8)); 
#endif 
} 

Agner utiliza el blendv_epi8 intrínseca con el apoyo SSE4.1.

Editar:

Curiosamente, después de hacer un trabajo más desmontaje (con optimizado construye), al menos mis dos implementaciones se compilan a exactamente la misma cosa. Ejemplo de desensamblaje dirigido a "ivy-bridge" (AVX).

vpmullw xmm2,xmm0,xmm1 
vpsrlw xmm0,xmm0,0x8 
vpsrlw xmm1,xmm1,0x8 
vpmullw xmm0,xmm0,xmm1 
vpsllw xmm0,xmm0,0x8 
vpand xmm1,xmm2,XMMWORD PTR [rip+0x281] 
vpor xmm0,xmm0,xmm1 

Utiliza la versión "optimizada para AVX2" con una constante de 128 bits xmm precompilada. Compilar solo con soporte SSE2 produce resultados similares (aunque usando instrucciones SSE2). Sospecho que la solución original de Agner Fog podría optimizarse para lo mismo (sería una locura si no fuera así). No tengo idea de cómo se compara la solución original de Marat en una compilación optimizada, aunque para mí tener un solo método para todas las extensiones x86 simd más nuevas que SSE2 e inclusive es bastante agradable.

+2

Esto es realmente bueno. Aprovecha el hecho de que el firmado vs. el no firmado solo afecta la mitad alta de un N x N -> 2N multiplicación de bit, y [esa basura en los bits altos no afecta el resultado que desea en los bits bajos] (http://stackoverflow.com/questions/34377711/which-2s-complement-integer-operations-can-be-used-without-zeroing-high-bits-in).Si la caché falla cuando cargar la máscara es un problema, puede generarla sobre la marcha con 2 insns: 'pcmpeqw xmm7, xmm7' /' psrlw xmm7, 8'. (Ver http://stackoverflow.com/q/35085059/224132 para otras secuencias de generación constante). –

+1

Eso está bien, veo [clang optimiza el shift-left/shift-right a un 'vpand' con una máscara constante] (http://goo.gl/GmFc9H)! Probablemente sea mejor código, a menos que la máscara tienda a fallar en el caché. gcc no hace esa optimización. La elección entre turno y máscara no depende en absoluto de AVX2. En cambio, depende de si una gran constante de la memoria es lo que quiere. (Noté que sin avx, clang desperdicia una movdqa al final: podría haber usado 'pmullw xmm0, xmm1' para la 2nd pmul y construido el resultado final en' xmm0' (el registro de valor de retorno). –

+1

Su comentario sobre 'vpbroadcastw' es totalmente incorrecto: la mayoría de los compiladores no compilan' set1' en una emisión en tiempo de ejecución para las constantes, porque es caro. 'mov eax, 0xff' /' movd xmm0, eax'/vpbroadcastw xmm0, xmm0' es 3 uops en Haswell. 'Vpbroadcastw xmm0, [mem16]' también es 3 uops. Generar sobre la marcha es más barato que cualquiera (pero los compiladores tienden a simplemente echarlos en la memoria). Sin embargo, 'vpbroadcastd' de memoria es solo 1 uop, incluso sin fusible: solo necesita un puerto de carga, no ALU. Por lo tanto, no necesita desperdiciar 32B de memoria en una constante que se va a cargar fuera del lazo. –