【问题标题】:Strlen of MAX 16 chars string using bitwise operators使用按位运算符的 MAX 16 个字符字符串的 Strlen
【发布时间】:2011-02-09 18:48:02
【问题描述】:

挑战是找到在 C/C++ 中使用 C 中的按位运算确定 c 字符串长度的最快方法。

char thestring[16];

c 字符串的最大大小为 16 个字符,并且位于缓冲区内 如果字符串等于 16 个字符,则末尾没有空字节。

我确信可以完成,但还没有做对。

我目前正在处理这个问题,但假设字符串是在 zero-filled 缓冲区上存储的。

len =   buff[0] != 0x0 +
            buff[1] != 0x0 +
            buff[2] != 0x0 +
            buff[3] != 0x0 +
            buff[4] != 0x0 +
            buff[5] != 0x0 +
            buff[6] != 0x0 +
            buff[7] != 0x0 +
            buff[8] != 0x0 +
            buff[9] != 0x0 +
            buff[10] != 0x0 +
            buff[11] != 0x0 +
            buff[12] != 0x0 +
            buff[13] != 0x0 +
            buff[14] != 0x0 +
            buff[15] != 0x0;

注意: 缓冲区零填充“\0123456789abcde”不可能发生。

【问题讨论】:

  • 位运算符对某些人有什么奇怪的魅力?
  • @Neil:Speeeed,适用于 GPU 内核,内核上少一条指令 == 设备上少一千条指令
  • @GMan - Nvidia 的一个,从 1.0 到 2.0 的所有计算能力
  • @fabrizio 您似乎假设使用 C 或 C++ 位运算符会转换为单个 CPU/GPU 操作。
  • @fabrizioM:简单地编写低级代码保证“速度”。通常,它会为您提供 1) 错误,以及 2) 如果您只是在高层次上表达了您的意图,编译器可以提供的代码

标签: c++ c algorithm string


【解决方案1】:

假设 64 位长和小端系统:

long a = ((long *)string)[0];
long b = ((long *)string)[1];

a = (a - 0x0101010101010101UL) & ~a & 0x8080808080808080UL;
b = (b - 0x0101010101010101UL) & ~b & 0x8080808080808080UL;

return a ? count_trailing_zeros( a ) / 8 : b ? 8 + count_trailing_zeros( b ) / 8 : 16;

对于大端计数前导零。任何系统 strlen 实现都将使用它。

【讨论】:

    【解决方案2】:

    从你所说的来看,我相信你想要做的是避免跳跃,所以这就是我正在努力的方向。

    我很确定您发布的代码看起来很漂亮,但在为许多处理器编译时实际上不会那么好,尽管它可能在您的处理器上。我所知道的大多数处理器实际上并没有一种简单的方法可以从比较中得到 1,因此这很可能最终成为以下形式的条件跳转或条件操作:

    set R1, 0
    test R2+0, 0
    cinc R1                   ; conditional increment
    test R2+1, 0
    cinc R1
    ...
    

    如果 GPU 可以进行条件增量并在八位字节大小的项目上运行良好,这可能对 GPU 很有效。

    如果编译器做得很好,在许多处理器上,这最终会是这样的:

    set R1, 0
    test R2+0, 0
    jz end  ; jump if zero
    inc R1
    test R2+1, 0
    jz end
    inc R1
    ...
    

    如果非跟随条件跳转不会对您造成太大伤害,这也是可以接受的,因为那时您只有一个跟随条件跳转(第一个找到 0 的条件跳转)。

    既然您说您的目标是 GPU,而且这些 GPU 往往对数学非常友好,您也许可以这样做:

    int acc = 0;
    acc += str[0]/str[0];
    acc += str[1]/str[1];
    ...
    

    如果您能够在不花费太多成本的情况下进行除以零的陷阱,并且只需处理陷阱中的混乱。不过,这最终可能会很昂贵。

    如果您的机器的寄存器可能保存超过一个八位字节的字符串,那么您可以尝试进行有限次数的跳转并一次测试 0 多个字节,然后检查最后一个非零字字节级别。

    您应该查看Bit Twiddling Hacks,了解一种很酷的方法来加速 strlen,它适用于大寄存器大小。

    您可能要考虑的其他事情是从字符串的末尾开始测量(您知道最大长度)。只要空终止字节后跟更多空值,这将起作用,并且如果您可能有更长的字符串,即使您确实在其中抛出了一个跳跃,这也可能是一个胜利。

    【讨论】:

    • +1 除法技巧实际上可能比 GPU 上的 !!str[0] (1 op 而不是 2)更快,寄存器是 32 位,我想知道这是否也适用于 32 。 .
    【解决方案3】:

    你可以随心所欲,但你可能不会打败这个:

    int fast1(const char *s)
    { 
        if (!*s++) return 0; 
        if (!*s++) return 1; 
        if (!*s++) return 2; 
        if (!*s++) return 3; 
        if (!*s++) return 4; 
        if (!*s++) return 5; 
        if (!*s++) return 6; 
        if (!*s++) return 7; 
        if (!*s++) return 8; 
        if (!*s++) return 9; 
        if (!*s++) return 10; 
        if (!*s++) return 11; 
        if (!*s++) return 12; 
        if (!*s++) return 13; 
        if (!*s++) return 14; 
        if (!*s++) return 15; 
    }
    

    或者,您可以这样做: (这是否更快取决于您的处理器和编译器)。

    int fast2(const char *s)
    { 
        if (!s[0]) return 0; 
        if (!s[1]) return 1; 
        if (!s[2]) return 2; 
        if (!s[3]) return 3; 
        if (!s[4]) return 4; 
        if (!s[5]) return 5; 
        if (!s[6]) return 6; 
        if (!s[7]) return 7; 
        if (!s[8]) return 8; 
        if (!s[9]) return 9; 
        if (!s[10]) return 10; 
        if (!s[11]) return 11; 
        if (!s[12]) return 12; 
        if (!s[13]) return 13; 
        if (!s[14]) return 14; 
        if (!s[15]) return 15; 
    }
    

    更新:

    我在 Core2Duo T7200 @ 2.0 GHz、Windows XP pro、Visual Studio 2008 上对这两个功能进行了分析,并关闭了优化。 (打开优化器会导致 VS 注意到我的计时循环中没有输出,因此它会完全删除它)。

    我在循环中调用了每个函数 222 次,然后取 8 次运行的平均值。

    fast1 每个函数调用大约需要 87.20 ns。

    fast2 每个函数调用大约需要 45.46 ns。

    所以在我的 CPU 上,数组索引版本的速度几乎是指针版本的两倍。

    我无法让此处发布的任何其他功能正常工作,因此无法进行比较。最接近的是原始海报的函数,它可以编译,但并不总是返回正确的值。执行此操作时,每个函数调用的执行时间约为 59 ns。

    更新 2

    这个函数也很快,每次调用大约 60 ns。我猜想指针取消引用是由地址单元执行的,并且是由整数单元执行的,所以这些操作是流水线操作的。在我的其他示例中,所有工作都由地址单元完成。

    int fast5(const char *s)
    {
        return  /* 0 * (s[0] == 0) + don't need to test 1st byte */
                1 * (s[1] == 0)  +
                2 * (s[2] == 0)  +
                3 * (s[3] == 0)  +
                4 * (s[4] == 0)  +
                5 * (s[5] == 0)  +
                6 * (s[6] == 0)  +
                7 * (s[7] == 0)  +
                8 * (s[8] == 0)  +
                9 * (s[9] == 0)  +
                10 * (s[10] == 0) +
                11 * (s[11] == 0) +
                12 * (s[12] == 0) +
                13 * (s[13] == 0) +
                14 * (s[14] == 0) +
                15 * (s[15] == 0);
    }
    

    【讨论】:

    • 你测过这个速度吗?
    • @KennyTM - 我对 GPU 了解不多,为什么多次退货很重要? @Peter - 我发布了一些结果
    • 所有这些内存引用和比较都无法胜过小题大做。
    【解决方案4】:

    这可以正常工作,因为buf 被初始化为零。您的解决方案有!=,它将使用跳转指令。如果 GPU 有多个 XOR 单元,则以下代码可以很好地流水线化。另一方面,JUMP 指令会导致流水线的刷新。

    len = !!buf[0] +
          !!buf[1] +
          //...
          !!buf[15]
    

    更新:上面的代码和 OP 的代码在 GCC 使用 -O3 标志编译时会产生相同的汇编代码。 (如果没有提供优化标志,则不同)

    【讨论】:

    • @Gman:我在考虑两种解决方案,在发帖时混淆了。谢谢:)
    • ++1 哇,我不知道这个!! (看看为什么我喜欢在 SO 上分享问题)
    • 如果 GPU 有多个 XOR 单元,上面的代码可以很好地流水线化。另一方面,JUMP 指令会导致流水线的刷新。
    • 只有在缓冲区初始化为零时才有效,否则只计算非nul字符的数量,包括第一个nul之后的垃圾。
    • @ergo:是的。 fabrizioM 假设缓冲区已被 memcpied 为 0。
    【解决方案5】:

    在假设的类 C++ 语言中,假设 2 的补码和小端序,

    int128_t v = *reinterpret_cast<int128_t*>(thestring);
    const int bit_count = 128;
    int eight = ((1 << 64) - 1 - v) >> (bit_count - 4) & 8;
    v >>>= 8 * eight;
    int four  = ((1 << 32) - 1 - v) >> (bit_count - 3) & 4;
    v >>>= 8 * four;
    int two   = ((1 << 16) - 1 - v) >> (bit_count - 2) & 2;
    v >>>= 8 * two;
    int one   = ((1 <<  8) - 1 - v) >> (bit_count - 1) & 1;
    return (one | two | four | eight) + !!v;
    

    (修改自http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog。)

    【讨论】:

    • @fab: &gt;&gt;&gt; 表示逻辑右移(即无符号移位)。嗯实际上算术右移也适用于&amp; x 的东西。
    • 只需将类型设为无符号或将其强制转换为无符号即可
    【解决方案6】:

    你可以开始

    template <typename T>
    bool containsANull(T n) {
       return (n  - ((T) -1)/255) & ((T) -1)/255*128) & ~n;
    }
    

    并构建一些东西。值得一提的是, T 可能应该是无符号的 64 位类型,但即便如此,也需要进行一些调整,这让我想知道您的缓冲区是否足够长,以使该技巧有用。

    它是如何工作的?

    (T)-1/255 是位模式 0x01010101 只要需要就重复

    (T)-1/255*128 因此是重复的位模式 0x80808080

    if n is                        0x0123456789ABCDEF
    n - 0x1111..1 is               0xF0123456789ABCDE
    (n-0x1111...1) & 0x8888...8 is 0x8000000008888888
    ~n is                          0xFEDCBA9876543210 
    so the result is               0x8000000000000000
    

    这里获取非空字节的唯一方法是从空字节开始。

    【讨论】:

    • 你能详细说明你的逻辑是什么吗?
    • +1 用于解释。我发现你的链接是@sparky 链接的通用版本!
    • 酷,但你不会用除法和乘法赢得“最快”的胜利。
    • 是的,(我很久以前从 Usenet 得到这个,我使用除法技巧来获得一个大小独立的版本。Mycroft 这个名字敲响了警钟,87 对我来说还为时过早;我的来源可能引用了他)。
    • @Seth,它们是常量表达式。如果您的编译器没有对此进行注释,只需使用调整为最长可用类型大小的正确位模式即可。
    【解决方案7】:

    这是我在 Hacker's Delight 中读到的一个小技巧,称为 SWAR (SIMD-within-a-register),假设每个字符 8 位:

    #define CHAR_BITS 8
    uint_fast_16_t all_character_bits[CHAR_BITS]= { 0 };
    
    for (int bit_index= 0; bit_index<CHAR_BITS; ++bit_index)
    {
        for (int character_index= 0; character_index<16; ++character_index)
        {
            all_character_bits[bit_index]|= ((buff[character_index] >> bit_index) & 1) << character_index;
        }
    }
    
    uint_fast_32_t zero_byte_character_mask= ~0;
    
    for (int bit_index= 0; bit_index<CHAR_BITS; ++bit_index)
    {
        zero_byte_character_mask&= (0xffff0000 | ~all_character_bits[bit_index]);
    }
    
    uint_fast_8_t first_null_byte= first_bit_set(zero_byte_character_mask);
    

    其中 first_bit_set 是在整数中查找第一个位集的任意数量的流行且快速的实现。

    这里的基本思想是将 16 个字符作为一个 8x16 位矩阵和AND 所有列的按位非。任何全为零的行都将在结果中设置该行的位。然后,我们只找到结果中设置的第一位,这就是字符串的长度。此特定实现确保在结果中设置第 16-31 位,以防所有字符都不为 NULL。实际的位转置也可能快得多(意味着没有分支)。

    【讨论】:

      【解决方案8】:

      请参考 Paul Hsieh 在 ...

      实现的 fstrlen()

      http://www.azillionmonkeys.com/qed/asmexample.html

      虽然不是您正在寻找的东西,但稍作调整它应该可以为您服务。

      该算法尝试使用一些位旋转来一次检查四个字节的字符串结尾字符。

      【讨论】:

        【解决方案9】:

        您的代码将无法正常工作。举个例子,考虑一个包含如下内容的缓冲区:

        "\0123456789abcde";
        

        根据您的代码,它的长度为 15,但实际上它的长度为 0,因为初始的“\0”。

        与并行计算一样好,简单的事实是,字符串的定义或多或少要求从头开始计算字符,直到遇到“\0”为止(或者,在你的情况下,达到 16)。

        【讨论】:

        • Strlen 不做任何其他事情......它也会返回零。
        • @imacake:我想你误解了:重点是 strlen 返回 0,但他的算法不会。看起来他已经编辑了这个问题,说我引用的问题不可能发生在他的输入中,但我不相信当时已经说明了这一点。
        【解决方案10】:

        按位运算...可能类似于:

        // TODO: optimize for 64-bit architectures
        uint32_t *a = (uint32_t*)thestring;
        
        for (int i = 0; i < 4; i++) // will be unwound
            for (int j = 0; j < 4; j++)
                if (a[i] & 0xff << j == 0)
                   return 4*i+j;
        return 16;
        

        【讨论】:

        • +1 它给了我一些关于如何一次使用 4 个字符的想法(寄存器是 32 位)
        • 只要确保这些 uint 正确对齐即可。如果不引起其他问题,错位可能代价高昂。
        猜你喜欢
        • 1970-01-01
        • 2019-04-21
        • 1970-01-01
        • 2014-04-08
        • 2019-11-29
        • 2015-07-05
        • 2019-11-17
        • 2013-05-23
        • 1970-01-01
        相关资源
        最近更新 更多