¿Cuál es la secuencia de instrucciones de recopilación stride-3 más rápida?

La pregunta:

¿Cuál es la secuencia más eficiente para generar una reunión stride-3 de elementos de 32 bits de la memoria? Si la memoria está organizada como:

MEM = R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 ...

Queremos obtener tres registros YMM donde:

YMM0 = R0 R1 R2 R3 R4 R5 R6 R7
YMM1 = G0 G1 G2 G3 G4 G5 G6 G7
YMM2 = B0 B1 B2 B3 B4 B5 B6 B7
Motivación y discusión.

El código escalar C es algo así como

template <typename T>
T Process(const T* Input) {
  T Result = 0;
  for (int i=0; i < 4096; ++i) {
    T R = Input[3*i];
    T G = Input[3*i+1];
    T B = Input[3*i+2];
    Result += some_parallelizable_algorithm<T>(R, G, B);  
  }
  return Result;
}

Digamos quealgún_algoritmo_paralelizable fue escrito en intrínseco y se ajustó a la implementación más rápida posible:

template <typename T>
__m256i some_parallelizable_algorithm(__m256i R, __m256i G, __m256i B);

Entonces, la implementación del vector para T = int32_t puede ser algo como:

    template <>
    int32_t Process<int32_t>(const int32_t* Input) {
     __m256i Step = _mm256_set_epi32(0, 1, 2, 3, 4, 5, 6, 7);
     __m256i Result = _mm256_setzero_si256(); 
     for (int i=0; i < 4096; i+=8) {
       // R = R0 R1 R2 R3 R4 R5 R6 R7
       __m256i R = _mm256_i32gather_epi32 (Input+3*i, Step, 3);
       // G = G0 G1 G2 G3 G4 G5 G6 G7
       __m256i G = _mm256_i32gather_epi32 (Input+3*i+1, Step, 3);
       // B = B0 B1 B2 B3 B4 B5 B6 B7
       __m256i B = _mm256_i32gather_epi32 (Input+3*i+2, Step, 3);
       Result = _mm256_add_epi32 (Result, 
                                  some_parallelizable_algorithm<int32_t>(R, G, B));
     }
   // Here should be the less interesting part:
   // Perform a reduction on Result and return the result
}

Primero, esto se puede hacer porque hay instrucciones de recopilación para elementos de 32 bits, pero no hay ninguna para elementos de 16 bits o elementos de 8 bits. En segundo lugar, y más importante, la instrucción de recopilación anterior debe evitarse por completo por razones de rendimiento. Probablemente sea más eficiente usar cargas anchas contiguas y barajar los valores cargados para obtener los vectores R, G y B.

    template <>
    int32_t Process<int32_t>(const int32_t* Input) {
     __m256i Result = _mm256_setzero_si256(); 
     for (int i=0; i < 4096; i+=3) {
       __m256i Ld0 = _mm256_lddqu_si256((__m256i*)Input+3*i));
       __m256i Ld1 = _mm256_lddqu_si256((__m256i*)Input+3*i+1));
       __m256i Ld2 = _mm256_lddqu_si256((__m256i*)Input+3*i+2));
       __m256i R = ???
       __m256i G = ???
       __m256i B = ???
       Result = _mm256_add_epi32 (Result, 
                                  some_parallelizable_algorithm<int32_t>(R, G, B));
     }
   // Here should be the less interesting part:
   // Perform a reduction on Result and return the result
}

Parece que para los pasos de potencia 2 (2, 4, ...) existen métodos conocidos que utilizan UNKPCKL / UNKPCKH, pero para los accesos de paso 3 no pude encontrar ninguna referencia.

Estoy interesado en resolver esto para T = int32_t, T = int16_t y T = int8_t, pero para mantener la concentración solo analicemos el primer caso.

Respuestas a la pregunta(2)

Su respuesta a la pregunta