将两个向量<bool> 与 SSE 进行比较

Posted

技术标签:

【中文标题】将两个向量<bool> 与 SSE 进行比较【英文标题】:Comparing two vector<bool> with SSE 【发布时间】:2015-12-13 23:32:57 【问题描述】:

我有两个vector&lt;bool&gt; A 和 B。

我想比较它们并计算相等的元素数量:

例如:

A = 0,1,0,1
B = 0,0,1,1

结果将等于 2。

我可以使用_mm_cmpeq_epi8,但它只比较 16 个元素(即我应该将 0 和 1 转换为 char 然后进行比较)。 是否可以每次将 128 个元素与 SSE(或 SIMD 指令)进行比较?

【问题讨论】:

xor 并做一个popcount()? 请注意vector&lt;bool&gt; 具有特殊属性,与bool 对象数组不同。我不确定您是否考虑过这一点,因为我之前没有使用过这些说明。 这将取决于实现,因为 vector&lt;bool&gt; 的元素可能是字节、压缩位或实现者认为适合使用的任何其他内容。如果您想以便携方式执行此操作,请使用例如vector&lt;uint8_t&gt;,然后 SSE 的实现就相对简单了。 一定要使用vector&lt;bool&gt;吗?你想将每个布尔值存储为一个字节还是可以使用位? 如果您在编译时知道向量的大小,那么位集可能是解决方案,它会提供异或并计算所设置的位数。相当好的编译器应该使用可用的 SIMD 指令来实现这些。 【参考方案1】:

如果您可以假设 vector&lt;bool&gt; 正在使用连续的字节大小的元素进行存储,或者您可以考虑使用类似 vector&lt;uint8_t&gt; 的东西,那么这个示例应该可以为您提供一个很好的起点:

static size_t count_equal(const vector<uint8_t> &vec1, const vector<uint8_t> &vec2)

    assert(vec1.size() == vec2.size());         // vectors must be same size

    const size_t n = vec1.size();
    const size_t max_block_size = 255 * 16;     // max block size before possible overflow

    __m128i vcount = _mm_setzero_si128();
    size_t i, count = 0;

    for (i = 0; i + 16 <= n; )                  // for each block
    
        size_t m = std::min(n, i + max_block_size);

        for ( ; i + 16 <= m; i += 16)           // for each vector in block
        
            __m128i v1 = _mm_loadu_si128((__m128i *)&vec1[i]);
            __m128i v2 = _mm_loadu_si128((__m128i *)&vec2[i]);
            __m128i vcmp = _mm_cmpeq_epi8(v1, v2);
            vcount = _mm_sub_epi8(vcount, vcmp);
        
        vcount = _mm_sad_epu8(vcount, _mm_setzero_si128());
        count += _mm_extract_epi16(vcount, 0) + _mm_extract_epi16(vcount, 4);
        vcount = _mm_setzero_si128();           // update count from current block
    
    vcount = _mm_sad_epu8(vcount, _mm_setzero_si128());
    count += _mm_extract_epi16(vcount, 0) + _mm_extract_epi16(vcount, 4);
    for ( ; i < n; ++i)                         // deal with any remaining partial vector
    
        count += (vec1[i] == vec2[i]);
    
    return count;

请注意,这是使用vector&lt;uint8_t&gt;。如果您真的必须使用 vector&lt;bool&gt; 并且可以保证元素始终是连续的并且是字节大小的,那么您只需要以某种方式将 vector&lt;bool&gt; 强制转换为 const uint8_t * 或类似名称。

测试工具:

#include <cassert>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>

#include <emmintrin.h>    // SSE2

using std::vector;

static size_t count_equal_ref(const vector<uint8_t> &vec1, const vector<uint8_t> &vec2)

    assert(vec1.size() == vec2.size());

    const size_t n = vec1.size();
    size_t i, count = 0;

    for (i = 0 ; i < n; ++i)
    
        count += (vec1[i] == vec2[i]);
    
    return count;


static size_t count_equal(const vector<uint8_t> &vec1, const vector<uint8_t> &vec2)

    assert(vec1.size() == vec2.size());         // vectors must be same size

    const size_t n = vec1.size();
    const size_t max_block_size = 255 * 16;     // max block size before possible overflow

    __m128i vcount = _mm_setzero_si128();
    size_t i, count = 0;

    for (i = 0; i + 16 <= n; )                  // for each block
    
        size_t m = std::min(n, i + max_block_size);

        for ( ; i + 16 <= m; i += 16)           // for each vector in block
        
            __m128i v1 = _mm_loadu_si128((__m128i *)&vec1[i]);
            __m128i v2 = _mm_loadu_si128((__m128i *)&vec2[i]);
            __m128i vcmp = _mm_cmpeq_epi8(v1, v2);
            vcount = _mm_sub_epi8(vcount, vcmp);
        
        vcount = _mm_sad_epu8(vcount, _mm_setzero_si128());
        count += _mm_extract_epi16(vcount, 0) + _mm_extract_epi16(vcount, 4);
        vcount = _mm_setzero_si128();           // update count from current block
    
    vcount = _mm_sad_epu8(vcount, _mm_setzero_si128());
    count += _mm_extract_epi16(vcount, 0) + _mm_extract_epi16(vcount, 4);
    for ( ; i < n; ++i)                         // deal with any remaining partial vector
    
        count += (vec1[i] == vec2[i]);
    
    return count;


int main(int argc, char * argv[])

    size_t n = 100;

    if (argc > 1)
    
        n = atoi(argv[1]);
    

    vector<uint8_t> vec1(n);
    vector<uint8_t> vec2(n);

    srand((unsigned int)time(NULL));

    for (size_t i = 0; i < n; ++i)
    
        vec1[i] = rand() & 1;
        vec2[i] = rand() & 1;
    

    size_t n_ref = count_equal_ref(vec1, vec2);
    size_t n_test = count_equal(vec1, vec2);

    if (n_ref == n_test)
    
        std::cout << "PASS" << std::endl;
    
    else
    
        std::cout << "FAIL: n_ref = " << n_ref << ", n_test = " << n_test << std::endl;
    

    return 0;

编译运行:

$ g++ -Wall -msse3 -O3 test.cpp && ./a.out
PASS

【讨论】:

使用n = 1000000 失败。这个 `assert(vec1.size() 我们使用 8 位向量元素来计算匹配,因此当向量中的元素超过 255*16 时可能会溢出。在问题中,您指定了 128 的向量大小,所以我没有费心处理大向量大小,但是我现在更新了上面的解决方案以支持任何长度的向量。 每 255 使用一个向量累加器将比两个 _mm_extract 更有效。 _mm_sad_epu8 将高位归零,因此您可以使用_mm_add_epi32(或_mm_add_epi64,如果您不关心它在 Intel pre-Nehalem/Atom/Silvermont 上的速度较慢)进行累加。然后你只需要在最后提取一次。 @PeterCordes:是的,好点 - 原始实现没有块循环,当 OP 抱怨它不适用于更长的向量时,我只是匆忙添加了它。话虽如此,内部循环通常会迭代 255 次,因此水平和的成本相对而言是微不足道的,除非是非常短的向量。【参考方案2】:

std::vector&lt;bool&gt;std::vector 类型 bool 的特化。尽管 C++ 标准没有指定,但在大多数实现中,std::vector&lt;bool&gt; 的空间效率很高,因此它的每个元素都是单个位而不是 bool

std::vector&lt;bool&gt; 的行为类似于它的主要模板对应物,除了:

    std::vector&lt;bool&gt; 不一定连续存储其元素。 为了公开它的元素(即各个位)std::vector&lt;bool&gt; 使用代理类(即std::vector&lt;bool&gt;::reference)。 class std::vector&lt;bool&gt;::reference 的对象由std::vector&lt;bool&gt; 下标运算符(即operator[])按值返回。

因此,我不认为使用 _mm_cmpeq_epi8 之类的函数是可移植的,因为 std::vector&lt;bool&gt; 的存储是实现定义的(即不保证连续)。

另一种可移植的方法是使用常规的 STL 工具,如下例所示:

std::vector<bool> A = 0,1,0,1;
std::vector<bool> B = 0,0,1,1;
std::vector<bool> C(A.size());
std::transform(A.begin(), A.end(), B.begin(), C.begin(), [](bool const &a, bool const &b)  return a == b;);
std::cout << std::count(C.begin(), C.end(), true) << std::endl;

Live Demo

【讨论】:

您可以使用 std::inner_product 计算相等的元素,而无需 STL 示例的临时向量。 Demo on ideone.com. 不知道是酷还是算法滥用。 为什么是a &amp;&amp; a==b? OP 想要计算它们相等的位置,所以 OP 想要计算 0==0 以及 1==1。希望编译器能够意识到它可以使用!(a ^ b) 来做到这一点,并最终生成实现64-popcnt(A[i] XOR B[i]) 的最佳asm 实现的代码(以64b 块形式)。或者,如果您不能假设硬件 popcnt 指令支持,则在向量寄存器中进行异或/popcnt,并且必须使用向量洗牌或向量元素中的 bithack 方法来做到这一点。

以上是关于将两个向量<bool> 与 SSE 进行比较的主要内容,如果未能解决你的问题,请参考以下文章

将包含位的字符串转换为向量<bool>

为啥两个向量的大小<bool> bVec = true,false,true,false,true;向量<char> cVec = 'a', 'b', 'c', 'd',

将向量归零的最快方法<vector<bool>>

使用 SSE2 优化 RGB565 到 RGB888 的转换

使用带有 STL 向量的 SSE 计算平均值

SSE 的整数/浮点值