优化检查一个比特向量是否是另一个的适当子集

Optimize check for a bit-vector being a proper subset of another?

本文关键字:另一个 是否是 向量 子集 检查 一个 优化      更新时间:2023-10-16

我想要一些帮助来优化我的程序中计算量最大的函数。目前,我发现基本(非SSE)版本明显更快(高达3倍)。因此,我请求你帮助纠正这一点。

该函数在无符号整数向量中查找子集,并报告它们是否存在。为了方便起见,我只包含了相关的代码片段。

首先是基本变体。它检查blocks_是否是x.blocks_的适当子集。(不完全相等。)这些是位图,也称为位向量或位集。

//Check for self comparison
    if (this == &x)
            return false;
//A subset is equal to or smaller.
    if (no_bits_ > x.no_bits_)
            return false;
    int i;
    bool equal = false;
//Pointers should not change.  
    const unsigned int *tptr = blocks_;
    const unsigned int *xptr = x.blocks_;
    
    for (i = 0; i < no_blocks_; i++, tptr++, xptr++) {
            if ((*tptr & *xptr) != *tptr)
                    return false;
            if (*tptr != *xptr)
                    equal = true;
    }
    return equal;

然后是SSE变体,遗憾的是,它的性能并没有达到我的预期。这两个片段应该寻找相同的东西。

    //starting pointers.        
    const __m128i* start = (__m128i*)&blocks_;
    const __m128i* xstart = (__m128i*)&x.blocks_;
    
    __m128i block;
    __m128i xblock;
    //Unsigned ints are 32 bits, meaning 4 can fit in a register.
    for (i = 0; i < no_blocks_; i+=4) {
            block = _mm_load_si128(start + i);
            xblock = _mm_load_si128(xstart + i);
                    //Equivalent to  (block & xblock) != block
                    if (_mm_movemask_epi8(_mm_cmpeq_epi32(_mm_and_si128(block, xblock), block)) != 0xffff)
                            return false;
                    //Equivalent to block != xblock
                    if (_mm_movemask_epi8(_mm_cmpeq_epi32(block, xblock)) != 0xffff)
                            equal = true;
    }
    return equal;

你对我如何提高SSE版本的性能有什么建议吗?我做错什么了吗?还是在这种情况下,应该在其他地方进行优化?

我还没有添加no_blocks_ % 4 != 0的剩余计算,但在性能提高之前,这样做几乎没有什么意义,而且在这一点上只会打乱代码。

我在这里看到了三种可能性。

首先,您的数据可能不适合广泛比较。如果(*tptr & *xptr) != *tptr在前几个块中的可能性很高,那么纯C++版本几乎肯定总是更快。在这种情况下,SSE将运行更多的代码&数据来完成同样的事情。

其次,您的SSE代码可能不正确。这里还不完全清楚。如果no_blocks_在两个样本之间是相同的,那么start + i可能具有索引到128位元素的不希望的行为,而不是作为第一个样本的32位元素。

第三,SSE真的很喜欢指令可以流水线传输,这是一个很短的循环,你可能不会得到。通过一次处理多个SSE块,可以显著减少分支。

以下是一个同时处理2个SSE块的未经测试的快速快照。注意,我已经完全删除了block != xblock分支,将状态保持在循环之外,并且只在最后进行测试。总的来说,这会将每个int的1.3个分支移动到0.25个。

bool equal(unsigned const *a, unsigned const *b, unsigned count)
{
    __m128i eq1 = _mm_setzero_si128();
    __m128i eq2 = _mm_setzero_si128();
    for (unsigned i = 0; i != count; i += 8)
    {
        __m128i xa1 = _mm_load_si128((__m128i const*)(a + i));
        __m128i xb1 = _mm_load_si128((__m128i const*)(b + i));
        eq1 = _mm_or_si128(eq1, _mm_xor_si128(xa1, xb1));
        xa1 = _mm_cmpeq_epi32(xa1, _mm_and_si128(xa1, xb1));
        __m128i xa2 = _mm_load_si128((__m128i const*)(a + i + 4));
        __m128i xb2 = _mm_load_si128((__m128i const*)(b + i + 4));
        eq2 = _mm_or_si128(eq2, _mm_xor_si128(xa2, xb2));
        xa2 = _mm_cmpeq_epi32(xa2, _mm_and_si128(xa2, xb2));
        if (_mm_movemask_epi8(_mm_packs_epi32(xa1, xa2)) != 0xFFFF)
            return false;
    }
    return _mm_movemask_epi8(_mm_or_si128(eq1, eq2)) != 0;
}

如果你有足够的数据,并且在最初的几个SSE块中失败的概率很低,那么这样的事情至少应该比你的SSE快一些。

我认为您的问题是内存带宽受限的问题:渐近你需要大约2个运算来处理扫描内存中的一对整数。没有足够的算术复杂性来利用CPU SSE指令的更多算术吞吐量。事实上,您的CPU会花费大量时间等待数据传输。但是,在您的情况下使用SSE指令会导致整个指令,编译器并没有很好地优化生成的代码。

有一些可供选择的策略可以提高带宽受限问题的性能:

  • 基于并发算法的多线程隐藏访问内存超线程上下文中的操作
  • 每次对数据负载大小进行微调可以提高内存带宽
  • 通过在循环中添加补充的独立操作来提高管线的连续性(在"for"循环的每个步骤扫描两组不同的数据)
  • 在缓存或寄存器中保留更多数据(代码的某些迭代可能多次需要相同的数据集)
相关文章: