【问题标题】:How to do _mm256_maskstore_epi8() in C/C++?如何在 C/C++ 中执行 _mm256_maskstore_epi8()?
【发布时间】:2017-03-10 02:50:46
【问题描述】:

问题

我想要做的是,如果我的向量为 27(不是 32!)int8_t:

x = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26}

我想首先将它循环向右移动 n(不是常数),例如如果 n=1:

x2 = {26,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25}

然后这个向量用于执行一些非常复杂的计算,但是为了简单起见,我们假设下一步只是将它循环左移n,并将其存储到内存中。所以我应该有一个新的向量 27 int8_t:

y = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26}

所以有成千上万个这样的向量,性能在这里非常关键。我们使用的 CPU 支持 AVX2,因此我们希望使用它来加快速度。

我目前的解决方案

要获得x2,我使用两个_mm256_loadu_si256() 和一个_mm256_blendv_epi8()

int8_t x[31+27+31];
for(int i=0; i<27; i++){
    x[31+i] = i;
}
__m256i mask = _mm256_set_epi32 (0x0, 0x00800000, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0);
__m256i x_second_part = _mm256_loadu_si256((__m256i*)(x+31+1));  //{1,2,...,26}
__m256i x_first_part  = _mm256_loadu_si256((__m256i*)(x+31-26)); //{0}
__m256i x2            = _mm256_blendv_epi8(x_second_part, x_first_part, mask); //{1,2,...,26, 0}
int8_t y[31+27+31];
_mm256_storeu_si256((__m256i*)(y+31-26), x2);
_mm256_storeu_si256((__m256i*)(y+31+1), x2);

xy 被声明为大小为 [31+27+31] 的原因是在这种情况下 _mm256_loadu_si256()_mm256_storeu_si256() 不会导致段错误。

我可以通过以下方式获取y 的值:

for(int i=0; i<27; i++){
    cout << (int)y[31+i] << ' ';
}

新问题

不幸的是,所有向量在内存中必须是连续的,例如,如果总共有两个向量需要处理:

x = {[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26];
     [27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53]}; 

那么我不能只使用_mm256_storeu_si256()y 的值放回内存,因为当第二个向量的值写入内存时,它会覆盖第一个向量的一些值:

int8_t x[31+27+27+31];
int8_t y[31+27+27+31];
for(int i=0; i<27*2; i++){
    x[31+i] = i;
}
for(int i=0; i<2; i++){
    __m256i mask = _mm256_set_epi32 (0x0, 0x00800000, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0);
    __m256i x_second_part = _mm256_loadu_si256((__m256i*)(x+31+27*i+1));  //{1,2,...,26}
    __m256i x_first_part  = _mm256_loadu_si256((__m256i*)(x+31+27*i-26)); //{0}
    __m256i x2            = _mm256_blendv_epi8(x_second_part, x_first_part, mask); //{1,2,...,26, 0}
    _mm256_storeu_si256((__m256i*)(y+31+27*i-26), x2);
    _mm256_storeu_si256((__m256i*)(y+31+27*i+1), x2);
}
for(int i=0; i<27; i++){
    cout << (int)y[31+i] << ' ';
}cout << endl;
for(int i=0; i<27; i++){
    cout << (int)y[31+27+i] << ' ';
}cout << endl;

会输出

0 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 

而不是

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 

所以我在考虑使用 maskstore。但是在Intel Intrinsic Guide 中我找不到_mm256_maskstore_epi8。这让我回到主题:

如何在C/C++中做_mm256_maskstore_epi8()?

【问题讨论】:

  • 它不存在。唯一的字节粒度掩码存储是 128b-only MASKMOVDQU,它具有 NT 语义(弱排序、绕过缓存、驱逐您使用它的数据)。你通常不希望这样。 VPMASKMOVD/Q (epi32/64) 是唯一可用的 256b 掩码存储大小(以及没有 NT 语义的掩码存储,因此即使解包到两个 128b 通道也无济于事。即使是 SSE 指令的 VEX 编码,VMASKMOVDQU 也具有 NT 语义)。
  • 非原子读取-修改-写入重叠的最后 5 个字节可以吗?如果是这样,加载它们并合并。 (还是前面的 5 个字节?)
  • 我认为使用 128 位向量是更好的解决方案,因为您可以在不退出 27 字节数组的情况下执行保存/加载。
  • 哦对了,做两个重叠的128b store!是的,那应该很好。使用 ALIGNR 设置重叠存储。
  • @PeterCordes。您的“两个重叠的 128b 商店”听起来像是要走的路。我会试一试。谢谢!

标签: c++ simd intrinsics avx avx2


【解决方案1】:

还有另一种使用 AVX2 实现 27 字节向量内的循环移位:

#include <iostream>
#include <immintrin.h>

const __m256i K0 = _mm256_setr_epi8(
    0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70,
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0);

const __m256i K1 = _mm256_setr_epi8(
    0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
    0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70);

inline const __m256i Shuffle(const __m256i & value, const __m256i & shuffle)
{
    return _mm256_or_si256(_mm256_shuffle_epi8(value, _mm256_add_epi8(shuffle, K0)),
        _mm256_shuffle_epi8(_mm256_permute4x64_epi64(value, 0x4E), _mm256_add_epi8(shuffle, K1)));
}

__m256i shuffles[27];

void Init()
{
    uint8_t * p = (uint8_t *)shuffles;
    for (int s = 0; s < 27; ++s)
        for (int i = 0; i < 32; ++i)
            p[s*32 + i] = i < 27 ? (27 + i - s)%27 : i;
}

void CyclicShift27(const uint8_t * src, size_t shift, uint8_t * dst)
{
    _mm256_storeu_si256((__m256i*)dst,  Shuffle(_mm256_loadu_si256((__m256i*)src), shuffles[shift]));
}

int main()
{
    Init();
    uint8_t src[32] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31 }, dst[32];
    for (int j = 0; j < 27; ++j)
    {
        CyclicShift27(src, j, dst);
        std::cout << "\t";
        for (int i = 0; i < 32; i++)
            std::cout << (int)dst[i] << ' ';
        std::cout << std::endl;
    }
    return 0;
}

输出:

    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
    26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 27 28 29 30 31
    25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 27 28 29 30 31
    24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 27 28 29 30 31
    23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 27 28 29 30 31
    22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 27 28 29 30 31
    21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 27 28 29 30 31
    20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 27 28 29 30 31
    19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 27 28 29 30 31
    18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 27 28 29 30 31
    17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 27 28 29 30 31
    16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 27 28 29 30 31
    15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 27 28 29 30 31
    14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 27 28 29 30 31
    13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 27 28 29 30 31
    12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 27 28 29 30 31
    11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 27 28 29 30 31
    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 27 28 29 30 31
    9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 27 28 29 30 31
    8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 27 28 29 30 31
    7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 27 28 29 30 31
    6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 27 28 29 30 31
    5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 27 28 29 30 31
    4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 27 28 29 30 31
    3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 27 28 29 30 31
    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 27 28 29 30 31
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 27 28 29 30 31

看起来比我之前的回答简单。

【讨论】:

  • 请注意,const __m256i K0 = _mm256_setr_epi8(...) 在全局范围内实际上比在你的函数中包含它更糟糕。出于某种原因,gcc 不会在编译时评估 _mm256_setr_epi8,因此它会生成一个类似构造函数的函数,以将该数据的只读常量版本复制到 BSS 中的全局变量。在每个需要它的函数中使用const __m256i K0 是我观察到的最好的方法,因为编译器对常量进行的合并与对相同的字符串文字所做的相同,因此可执行文件中只有一个副本。跨度>
  • 是否可以重叠不同的洗牌掩码,而不是拥有 27 * 32B 的向量常数?那是 864B 的 L1 缓存,跨越 14 个缓存行。这不是坏,但是花费一两条额外的指令来生成更小的常量掩码可能是值得的(如果可能的话,IDK),尤其是。如果这被用作也使用缓存的其他代码的一部分。
  • 您可以将 shuffle 掩码的整个可变部分存储在大约 52 个字节中,例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26,然后只需在字节粒度偏移处进行未对齐加载即可加载它。然后你需要为每个向量修复最后五个相同的字节 (27 28 29 30 31)。如果它们是 4 个字节,那可能会更容易(movd 或 dword blend 可以做到)。你可以pshufb 进去,但当然这里的端口 5 已经承受压力了......
【解决方案2】:

我已经使用 SSSE3 在 27 字节向量内实现了循环移位:

#include <iostream>
#include <tmmintrin.h>

union Shuffle
{
    uint8_t s[64];
    __m128i v[4];
};

Shuffle shuffles[27];

int Shift(int value)
{
    return (value >= 0 && value < 16) ? value : -1;
}

void Init()
{
    for (int s = 0; s < 27; ++s)
    {
        for (int i = 0; i < 16; ++i)
        {
            shuffles[s].s[0 + i] = s < 16 ? Shift(i - s) : Shift(i - s + 27);
            shuffles[s].s[16 + i] = Shift(16 + i - s);
            shuffles[s].s[32 + i] = Shift(11 + i - s);
            shuffles[s].s[48 + i] = s < 11 ? Shift(i - s) : Shift(i - s + 27);
        }
    }
}

void CyclicShift27(const uint8_t * src, size_t shift, uint8_t * dst)
{
    __m128i srcLo = _mm_loadu_si128((__m128i*)(src + 0));
    __m128i srcHi = _mm_loadu_si128((__m128i*)(src + 11));
    __m128i dstLo = _mm_or_si128(_mm_shuffle_epi8(srcLo, shuffles[shift].v[0]), _mm_shuffle_epi8(srcHi, shuffles[shift].v[1]));
    __m128i dstHi = _mm_or_si128(_mm_shuffle_epi8(srcLo, shuffles[shift].v[2]), _mm_shuffle_epi8(srcHi, shuffles[shift].v[3]));
    _mm_storeu_si128((__m128i*)(dst + 0), dstLo);
    _mm_storeu_si128((__m128i*)(dst + 11), dstHi);
}

int main()
{
    Init();
    uint8_t src[27] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26 }, dst[27];
    for (int j = 0; j < 27; ++j)
    {
        CyclicShift27(src, j, dst);
        for (int i = 0; i < 27; i++)
            std::cout << (int)dst[i] << ' ';
        std::cout << std::endl;
    }
    return 0;
}

输出:

    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
    26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
    25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
    24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
    23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
    22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
    21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
    20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
    19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
    18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
    17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
    16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
    14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13
    13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12
    12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11
    11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10
    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9
    9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8
    8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7
    7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6
    6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5
    5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4
    4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3
    3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2
    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0

希望对你有用。

【讨论】:

  • 那么,您实际上是在建议 OP 使用 128 位指令,因为 256 指令不可用?
  • 我想说的 27 字节向量内循环移位的优秀解决方案!但是将 SSE 与 AVX 混合可能会导致一些性能损失?也许我应该在决定走哪条路之前做一些分析。
  • IIRC SSE 指令在 AVX 中具有新的 VEX 编码等效项,因此只需使用它们即可。在通过 3-arg 版本的汇编中,通常,但对于内在函数,它有点复杂,并且取决于编译器开关或其他东西?
猜你喜欢
  • 1970-01-01
  • 2011-03-17
  • 2013-07-12
  • 2018-12-21
  • 1970-01-01
  • 2012-06-04
  • 1970-01-01
  • 1970-01-01
  • 2020-03-20
相关资源
最近更新 更多