2010-06-28 16 views
7

Soy muy nuevo en SIMD/SSE y estoy tratando de hacer un simple filtrado de imagen (desenfoque). El siguiente código filtra cada píxel de un mapa de bits gris de 8 bits con una simple [1 2 1] ponderación en dirección horizontal. Estoy creando sumas de 16 píxeles a la vez.SIMD/SSE novato: filtro de imagen simple

Lo que parece muy malo acerca de este código, al menos para mí, es que hay una gran cantidad de inserción/extracto en él, que no es muy elegante y, probablemente, ralentiza todo también. ¿Hay una mejor manera de ajustar los datos de un registro a otro cuando se cambia?

buf es la información de imagen, 16 bytes alineados. w/h son anchura y altura, múltiplos de 16.

__m128i *p = (__m128i *) buf; 
__m128i cur1, cur2, sum1, sum2, zeros, tmp1, tmp2, saved; 
zeros = _mm_setzero_si128(); 
short shifted, last = 0, next; 

// preload first row 
cur1 = _mm_load_si128(p); 
for (x = 1; x < (w * h)/16; x++) { 
    // unpack 
    sum1 = sum2 = saved = cur1; 
    sum1 = _mm_unpacklo_epi8(sum1, zeros); 
    sum2 = _mm_unpackhi_epi8(sum2, zeros); 
    cur1 = tmp1 = sum1; 
    cur2 = tmp2 = sum2; 
    // "middle" pixel 
    sum1 = _mm_add_epi16(sum1, sum1); 
    sum2 = _mm_add_epi16(sum2, sum2); 
    // left pixel 
    cur2 = _mm_slli_si128(cur2, 2); 
    shifted = _mm_extract_epi16(cur1, 7); 
    cur2 = _mm_insert_epi16(cur2, shifted, 0); 
    cur1 = _mm_slli_si128(cur1, 2); 
    cur1 = _mm_insert_epi16(cur1, last, 0); 
    sum1 = _mm_add_epi16(sum1, cur1); 
    sum2 = _mm_add_epi16(sum2, cur2); 
    // right pixel 
    tmp1 = _mm_srli_si128(tmp1, 2); 
    shifted = _mm_extract_epi16(tmp2, 0); 
    tmp1 = _mm_insert_epi16(tmp1, shifted, 7); 
    tmp2 = _mm_srli_si128(tmp2, 2); 
    // preload next row 
    cur1 = _mm_load_si128(p + x); 
    // we need the first pixel of the next row for the "right" pixel 
    next = _mm_extract_epi16(cur1, 0) & 0xff; 
    tmp2 = _mm_insert_epi16(tmp2, next, 7); 
    // and the last pixel of last row for the next "left" pixel 
    last = ((uint16_t) _mm_extract_epi16(saved, 7)) >> 8; 
    sum1 = _mm_add_epi16(sum1, tmp1); 
    sum2 = _mm_add_epi16(sum2, tmp2); 
    // divide 
    sum1 = _mm_srli_epi16(sum1, 2); 
    sum2 = _mm_srli_epi16(sum2, 2); 
    sum1 = _mm_packus_epi16(sum1, sum2); 
    mm_store_si128(p + x - 1, sum1); 
} 
+0

También apreciaría comentarios generales acerca de posibles mejoras para el código. Gracias! – dietr

Respuesta

2

Sugiero mantener los píxeles vecinos en el registro SSE. Es decir, conserve el resultado de _mm_slli_si128/_mm_srli_si128 en una variable SSE y elimine todo el inserto y extracto. Mi razonamiento es que en las CPU antiguas, las instrucciones de inserción/extracción requieren la comunicación entre las unidades SSE y las unidades de propósito general, que es mucho más lenta que mantener el cálculo dentro de SSE, incluso si se extiende a la caché L1.

Cuando se hace esto, debe haber solo cuatro cambios de 16 bits (_mm_slli_si128, _mm_srli_si128, sin contar el cambio de división). Mi sugerencia es hacer un punto de referencia con su código, porque para ese momento su código ya puede haber alcanzado el límite de ancho de banda de la memoria ... lo que significa que ya no puede optimizar.

Si la imagen es grande (más grande que el tamaño L2) y la salida no se volverá a leer pronto, intente usar MOVNTDQ (_mm_stream_si128) para volver a escribir. Según varios sitios web, está en SSE2, aunque es posible que desee verificarlo dos veces.

SIMD tutorial:

Algunos sitios web gurú SIMD:

2

Este tipo de operación barrio era siempre un dolor con SSE, hasta SSE3.5 (aka SSSE3) llegó, y se introdujo PALIGNR (_mm_alignr_epi8) .

Si necesita compatibilidad con versiones anteriores de SSE2/SSE3, puede escribir una macro equivalente o una función en línea que simule _mm_alignr_epi8 para SSE2/SSE3 y que pase a _mm_alignr_epi8 cuando apunte a SSE3.5/SSE4.

Otro enfoque es usar cargas desalineadas para obtener los datos desplazados - esto es relativamente costoso en CPUs antiguas (aproximadamente el doble de la latencia y la mitad del rendimiento de cargas alineadas) pero esto puede ser aceptable dependiendo de mucho cómputo. haciendo por carga. También tiene la ventaja de que en las actuales CPUs de Intel (Core i7) las cargas desalineadas no tienen penalización en comparación con las cargas alineadas, por lo que su código será bastante eficiente en Core i7 y otros.

+0

Ya he notado alignr, pero como sospechaba, quiero ser compatible con SSE2. Creo que SSE2 es un buen "mínimo común denominador" cuando se trata de SIMD en x86, y si SSE2 solo me dará una aceleración satisfactoria, no me molestaré en implementar nada avanzado. – dietr

Cuestiones relacionadas