Набор инструкций.
Какова наиболее эффективная последовательность для генерации набора из 3-х элементов из памяти? Если память устроена так:
MEM = R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 ...
Мы хотим получить три регистра YMM, где:
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
Мотивация и обсуждениеСкалярный код C - это что-то вроде
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;
}
Скажем такsome_parallelizable_algorithm был написан на внутреннем уровне и был настроен на максимально быструю реализацию:
template <typename T>
__m256i some_parallelizable_algorithm(__m256i R, __m256i G, __m256i B);
Таким образом, реализация вектора для T = int32_t может выглядеть примерно так:
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
}
Во-первых, это можно сделать, потому что есть инструкции по сбору для 32-битных элементов, но нет ни одного для 16-битных или 8-битных элементов. Во-вторых, и что более важно, вышеприведенная инструкция по сбору должна быть полностью исключена из соображений производительности. Вероятно, более эффективно использовать смежные широкие нагрузки и перемешивать загруженные значения для получения векторов R, G и 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
}
Кажется, что для шагов степени 2 (2, 4, ...) существуют известные методы, использующие UNKPCKL / UNKPCKH, но для доступа уровня 3 я не смог найти никаких ссылок.
Я заинтересован в решении этого для T = int32_t, T = int16_t и T = int8_t, но чтобы оставаться сосредоточенным, давайте обсудим только первый случай.