如何有效地对(大)打包位向量应用按位运算?

时间:2015-04-22 20:17:14

标签: c++ optimization boost vectorization bitwise-operators

我想实施

void bitwise_and(
    char*       __restrict__  result,
    const char* __restrict__  lhs,
    const char* __restrict__  rhs,
    size_t                    length);

或者可能是bitwise_or()bitwise_xor()或任何其他按位操作。显然,这不是算法,只是实现细节 - 对齐,从内存加载最大可能元素,缓存感知,使用SIMD指令等。

我确信这有(不止一个)现有的快速实现,但我猜大多数库实现都需要一些花哨的容器,例如: std::bitsetboost::dynamic_bit_set - 但我不想花时间构建其中一个。

我也是......从现有的库中复制粘贴?找到一个可以用一个漂亮的对象“包装”内存中的原始压缩位数组的库?无论如何,推动我自己的实施?

注意:

  • 我最感兴趣的是C ++代码,但我当然不介意简单的C方法。
  • 显然,制作输入数组的副本是不可能的 - 这可能会使执行时间增加近一倍。
  • 我故意没有对按位运算符进行模板化,以防对OR进行某些特定优化,或者对于AND等。
  • 用于一次讨论多个向量的操作的加分点,例如V_out = V_1按位 - 和V_2按位 - 和V_3等。
  • 我注意到this article比较了库实现,但它是从5年前开始的。我不知道要使用哪个库,因为这会违反SO政策我猜...
  • 如果它对你有帮助,假设它是uint64_t而不是char s(这并不重要 - 如果char数组未对齐,我们可以单独处理标题和尾随字符)

2 个答案:

答案 0 :(得分:2)

这个答案将假设你想要最快的方式,并且很乐意使用平台特定的东西。优化编译器可能能够从正常的C生成类似于下面的代码,但是我在几个编译器中的经验,特别是这个仍然是最好的手写。

显然,就像所有优化任务一样,永远不会假设任何东西更好/更差,并且衡量,衡量,衡量。

如果您可以使用至少SSE3将架构锁定到x86,则可以执行以下操作:

void bitwise_and(
    char*       result,
    const char* lhs,
    const char* rhs,
    size_t      length)
{
    while(length >= 16)
    {
        // Load in 16byte registers
        auto lhsReg = _mm_loadu_si128((__m128i*)lhs);
        auto rhsReg = _mm_loadu_si128((__m128i*)rhs);

        // do the op
        auto res = _mm_and_si128(lhsReg, rhsReg);

        // save off again
        _mm_storeu_si128((__m128i*)result, res);

        // book keeping
        length -= 16;
        result += 16;
        lhs += 16;
        rhs += 16;
    }

    // do the tail end. Assuming that the array is large the
    // most that the following code can be run is 15 times so I'm not
    // bothering to optimise. You could do it in 64 bit then 32 bit
    // then 16 bit then char chunks if you wanted...
    while (length)
    {
        *result = *lhs & *rhs;
        length -= 1;
        result += 1;
        lhs += 1;
        rhs += 1;
    }
}

这会编译为每16字节~10asm指令(+剩余更改和一点开销)。

这样做内在函数(手动滚动asm)的好处在于编译器仍然可以自由地在你编写的内容上进行额外的优化(例如循环展开)。它还处理寄存器分配。

如果您可以保证对齐的数据,则可以保存asm指令(使用_mm_load_si128代替,编译器将足够聪明以避免第二次加载并将其用作' pand的直接mem操作数。

如果你能保证AVX2 +那么你可以使用256位版本并且每32字节处理10asm指令。

手臂上有类似的NEON指令。

如果你想做多个操作,只需在中间添加相关的内在函数,它就会每16个字节添加1个asm指令。

我非常确定使用一个不错的处理器,你不需要任何额外的缓存控制。

答案 1 :(得分:0)

不要这样做。个人操作看起来很棒,时尚,性能良好......但是它们的组合将会非常糟糕。你不能做这个抽象,看起来不错。这些内核的算术强度几乎是最差的(唯一更糟的是执行 no 算术,例如直接复制),并且在高级别编写它们将保留那个可怕的属性。在每个使用前一个结果的操作序列中,结果会在很晚之后(在下一个内核中)再次写入和读取,即使可以转换高级别流程以使结果“下一个操作”需要就在那里。此外,如果相同的参数在表达式树中出现两次(而不是两个作为一个操作的操作数),它们将被流式传输两次,而不是重复使用数据进行两次操作。

它没有那种温暖的模糊感觉,“看看所有这些可爱的抽象”,但你应该做的是从高层次上找出你如何组合你的向量,然后尝试砍从表现的角度来看,这些都是有意义的。在某些情况下,这可能意味着制造大丑陋的杂乱循环,这将使人们在潜入之前获得额外的咖啡,那就太糟糕了。如果你想要表现,你经常要牺牲别的东西。通常它不是那么糟糕,它可能只是意味着你有一个循环,其中包含一个由内在函数组成的表达式,而不是每个单独都有一个循环的向量运算表达式。