优化逐块位操作:base-4数字

时间:2015-08-28 13:33:17

标签: c++ optimization x86 bit-manipulation sse

这应该是一个有趣的问题,至少对我而言。

我的目的是操纵以无符号整数编码的 base-4数字。然后,每个两位块表示一个基数为4的数字,从最低有效位开始:

01 00 11 = base4(301)

我想使用SSE说明优化我的代码 ,因为我不确定我在这里得分,可能很差。

代码从字符串开始(并使用它们来检查正确性),并实现:

  • 将字符串转换为二进制
  • 将二进制转换为字符串
  • 反转数字

任何提示都非常受欢迎!

uint32_t tobin(std::string s)
{
    uint32_t v, bin = 0;

    // Convert to binary
    for (int i = 0; i < s.size(); i++)
    {
        switch (s[i])
        {
            case '0':
                v = 0;
                break;

            case '3':
                v = 3;
                break;

            case '1':
                v = 1;
                break;

            case '2':
                v = 2;
                break;

            default:
                throw "UNKOWN!";
        }

        bin = bin | (v << (i << 1));
    }

    return bin;
}

std::string tostr(int size, const uint32_t v)
{
    std::string b;

    // Convert to binary
    for (int i = 0; i < size; i++)
    {
        uint32_t shl = 0, shr = 0, q;

        shl = (3 << (i << 1));
        shr = i << 1;
        q   = v & shl;
        q   = q >> shr;

        unsigned char c = static_cast<char>(q);

        switch (c)
        {
            case 0:
                b += '0';
                break;

            case 3:
                b += '3';
                break;

            case 1:
                b += '1';
                break;

            case 2:
                b += '2';
                break;

            default:
                throw "UNKOWN!";
        }
    }

    return b;
}

uint32_t revrs(int size, const uint32_t v)
{
    uint32_t bin = 0;

    // Convert to binary
    for (int i = 0; i < size; i++)
    {
        uint32_t shl = 0, shr = 0, q;

        shl = (3 << (i << 1));
        shr = i << 1;
        q   = v & shl;
        q   = q >> shr;

        unsigned char c = static_cast<char>(q);

        shl = (size - i - 1) << 1;

        bin = bin | (c << shl);
    }

    return bin;
}

bool ckrev(std::string s1, std::string s2)
{
    std::reverse(s1.begin(), s1.end());

    return s1 == s2;
}

int main(int argc, char* argv[])
{
    // Binary representation of base-4 number
    uint32_t binr;

    std::vector<std::string> chk { "123", "2230131" };

    for (const auto &s : chk)
    {
        std::string b, r;
        uint32_t    c;

        binr = tobin(s);
        b    = tostr(s.size(), binr);
        c    = revrs(s.size(), binr);
        r    = tostr(s.size(), c);

        std::cout << "orig " << s << std::endl;
        std::cout << "binr " << std::hex << binr << " string " << b << std::endl;
        std::cout << "revs " << std::hex << c    << " string " << r << std::endl;
        std::cout << ">>> CHK  " << (s == b) << " " << ckrev(r, b) << std::endl;
    }

    return 0;
}

2 个答案:

答案 0 :(得分:2)

这对SSE来说有点挑战性,因为几乎没有提供比特打包(你想从每个字符中取两个比特并将它们连续打包)。无论如何,特殊指令_mm_movemask_epi8可以帮助你。

对于字符串到二进制的转换,您可以按以下步骤操作:

  • 加载16个字符的字符串(如果需要,在填充后填充零或清除);

  • 按字节顺序减去ASCII零。

  • 将字节顺序'unsigned greater than'与16'3'字节的字符串进行比较;这将在存在无效字符的地方设置字节0xFF

  • 使用_mm_movemask_epi8检测打包的短值中的此类字符

如果一切正常,您现在需要打包位对。为此你需要

  • 复制16个字节

  • 将权重1和2的位移位7或6位,使它们最显着(_mm_sll_epi16。没有epi8版本,但是来自一个元素的位在另一个元素的低位中变为垃圾对此并不重要。)

  • 交错(_mm_unpack ... _ epi8,一次用lo,一次用hi)

  • 使用_mm_movemask_epi8将这两个向量的高位存储为短路。

对于二进制到字符串的转换,我想不出有意义的SSE实现,因为没有_mm_movemask_epi8的对应物可以让你有效地解压缩。

答案 1 :(得分:2)

我将解决在SSE上将32位整数转换为base4字符串的问题。 不考虑删除前导零的问题,即base4字符串的长度始终为16。

一般通过

显然,我们必须以矢量化形式提取位对。 为了做到这一点,我们可以执行一些字节操作和按位操作。 让我们看看我们可以用SSE做些什么:

  1. 单个内在_mm_shuffle_epi8(来自SSSE3)允许以任何您想要的方式随机播放16个字节。 显然,一些结构良好的混洗和寄存器混合可以通过SSE2的简单指令完成, 但重要的是要记住,任何注册时间的改组都可以用一条便宜的指令来完成。

  2. Shuffling无助于更改字节中的位索引。 为了移动大块的位,我们通常使用位移。 不幸的是,SSE中没有办法将不同数量的XMM寄存器的不同元素移位。 正如评论中提到的@PeterCorder,AVX2中有这样的指令(例如_mm_sllv_epi32),但它们至少以32位粒度运行。

  3. 从远古时代开始,我们不断地认为,位移很快,乘法很慢。今天算术加速了,不再如此。在SSE中,移位和乘法似乎具有相等的吞吐量,尽管乘法具有更多的延迟。

    1. 使用乘以2的幂,我们可以将单个XMM寄存器的不同元素向左移位不同的量。有许多指令,如_mm_mulhi_epi16,允许16位粒度。另外一条指令_mm_maddubs_epi16允许8位粒度的移位。 右移可以通过左移来完成,就像人们division via multiplication一样:向左移动 16-k ,然后向右移动两个字节(回想一下,任何字节改组都很便宜)。
    2. 我们实际上想做16个不同的位移。如果我们使用16位粒度的乘法,那么我们必须使用至少两个XMM寄存器进行移位,然后它们可以合并在一起。此外,我们可以尝试使用8位粒度的乘法来在单个寄存器中完成所有操作。

      16位粒度

      首先,我们必须将32位整数移动到XMM寄存器的低4字节。然后我们将字节混洗,以便XMM寄存器的每个16位部分包含一个输入字节:

      |abcd|0000|0000|0000|   before shuffle (little-endian)
      |a0a0|b0b0|c0c0|d0d0|   after shuffle (to low halves)
      |0a0a|0b0b|0c0c|0d0d|   after shuffle (to high halves)
      

      然后我们可以调用_mm_mulhi_epi16将每个部分右移 k = 1..16 。实际上,将输入字节放入16位元素的高半部分更方便,这样我们就可以向左移动 k = -8..7 。因此,我们希望看到XMM寄存器的一些字节,其中包含定义一些base4数字的位对(作为它们的低位)。之后,我们可以通过_mm_and_si128删除不必要的高位,并将有价值的字节拖放到适当的位置。

      由于只能以16位粒度一次完成8次移位,因此我们必须进行两次移位。然后我们将两个XMM寄存器合并为一个。

      下面你可以看到使用这个想法的代码。它有点优化:位移后没有字节混洗。

      __m128i reg = _mm_cvtsi32_si128(val);
      __m128i bytes = _mm_shuffle_epi8(reg, _mm_setr_epi8(-1, 0, -1, 0, -1, 1, -1, 1, -1, 2, -1, 2, -1, 3, -1, 3));
      __m128i even = _mm_mulhi_epu16(bytes, _mm_set1_epi32(0x00100100));  //epi16:  1<<8,  1<<4  x4 times
      __m128i odd  = _mm_mulhi_epu16(bytes, _mm_set1_epi32(0x04004000));  //epi16: 1<<14, 1<<10  x4 times
      even = _mm_and_si128(even, _mm_set1_epi16(0x0003));
      odd  = _mm_and_si128(odd , _mm_set1_epi16(0x0300));
      __m128i res = _mm_xor_si128(even, odd);
      res = _mm_add_epi8(res, _mm_set1_epi8('0'));
      _mm_storeu_si128((__m128i*)s, res);
      

      8位粒度

      首先,我们将32位整数移动到XMM寄存器中。然后我们将字节混洗,以便结果的每个字节等于包含该位置所需的两位的输入字节:

      |abcd|0000|0000|0000|   before shuffle (little-endian)
      |aaaa|bbbb|cccc|dddd|   after shuffle
      

      现在我们使用_mm_and_si128来过滤位:在每个字节处只需要保留所需的两位。之后,我们只需将每个字节向右移动0/2/4/6位。这应该通过内在_mm_maddubs_epi16来实现,它允许一次移动16个字节。不幸的是,我没有看到如何仅使用该指令正确地移位所有字节,但至少我们可以将每个奇数字节向右移位2位(偶数字节保持原样)。然后索引 4k + 2 4k + 3 的字节可以用单_mm_madd_epi16指令右移4位。

      以下是生成的代码:

      __m128i reg = _mm_cvtsi32_si128(val);
      __m128i bytes = _mm_shuffle_epi8(reg, _mm_setr_epi8(0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3));
      __m128i twobits = _mm_and_si128(bytes, _mm_set1_epi32(0xC0300C03));         //epi8: 3<<0, 3<<2, 3<<4, 3<<6  x4 times
      twobits = _mm_maddubs_epi16(twobits, _mm_set1_epi16(0x4001));               //epi8: 1<<0, 1<<6  x8 times
      __m128i res = _mm_madd_epi16(twobits, _mm_set1_epi32(0x10000001));          //epi16: 1<<0, 1<<12  x4 times
      res = _mm_add_epi8(res, _mm_set1_epi8('0'));
      _mm_storeu_si128((__m128i*)s, res);
      

      P.S。

      两种解决方案都使用大量编译时常量128位值。它们没有编码到x86指令中,因此处理器必须在每次使用时从内存(很可能是L1缓存)加载它们。但是,如果要在循环中运行许多转换,那么编译器会在循环之前将所有这些常量加载到寄存器中(我希望)。

      Here你可以找到完整的代码(没有时间安排),包括@YvesDaoust对str2bin解决方案的实现。