挑战在于找到使用C中的按位运算在C / C ++中确定c-string长度的最快方法。
char thestring[16];
c-string的最大大小为16个字符,位于缓冲区内 如果字符串等于16个字符,则末尾没有空字节。
我相信可以做到,但还没有做到。
我正在研究这个问题,但假设字符串是在零填充缓冲区上记忆的。
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”不会发生。
答案 0 :(得分:4)
这样可以正常工作,因为buf
初始化为零。您的解决方案有!=
,它将使用跳转指令。如果GPU具有多个XOR单元,则可以很好地流水线化以下代码。另一方面,JUMP指令会导致管道刷新。
len = !!buf[0] +
!!buf[1] +
//...
!!buf[15]
更新 :上面的代码和OP的代码在由GCC编译时使用-O3
生成相同的汇编代码标志。 (如果没有提供优化标志,则不同)
答案 1 :(得分:3)
您拥有的代码无法正常运行。例如,考虑一个包含以下内容的缓冲区:
"\0123456789abcde";
根据你的代码,它的长度为15,但实际上它的长度为0,因为最初的“\ 0”。
和并行执行计算一样好,简单的事实是字符串的定义或多或少要求从头开始并且只计算字符直到你遇到“\ 0”的点(或者,在你的情况下,到16)。
答案 2 :(得分:2)
这是我在Hacker's Delight中读到的一个小技巧,称为SWAR(SIMD-in-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
所有列的按位 - NOT组合在一起。任何全为零的行都将在结果中设置该行的位。然后,我们只找到结果中设置的第一个位,这是字符串的长度。此特定实现确保在结果中设置位16-31,以防所有字符都不为NULL。实际的位转换可以更快(意味着没有分支)。
答案 3 :(得分:1)
按位操作......可能类似于:
// 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;
答案 4 :(得分:1)
你可以从
开始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
这里获取非空字节的唯一方法是从空字节开始。
答案 5 :(得分:1)
请参考Paul Hsieh在......执行的fstrlen()。
http://www.azillionmonkeys.com/qed/asmexample.html
虽然不是你正在寻找的东西,但稍微调整就应该为你做好准备。
该算法尝试使用一些比特琐事一次检查四个字节的字符串结尾字符。
答案 6 :(得分:1)
你可以根据自己的想法捏捏,但你可能不会打败这个:
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专业版,Visual Studio 2008上分析了这两个功能。 (打开优化器会导致VS注意到我的计时循环中没有输出,因此它会完全删除它)。
我在循环2 22 次中调用每个函数,然后平均超过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);
}
答案 7 :(得分:1)
据你所说,我相信你要做的就是避免跳跃,这就是我正在努力的目标。
我很确定您发布的代码看起来很光滑,但在为许多处理器编译时实际上并不是那么好,尽管它可能在您的处理器上。我所知道的大多数处理器实际上并没有简单的方法来获得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而且那些数学上非常友好,你可以做到:
int acc = 0;
acc += str[0]/str[0];
acc += str[1]/str[1];
...
如果你能够在没有太多成本的情况下陷入零除,只需处理陷阱中的混乱。但这可能最终会变得昂贵。
如果您的机器的寄存器可能包含多个字符串的字符串,那么您可以尝试进行有限数量的跳转并一次测试0个以上的字节,然后检查最后一个非零字字节级别。
你应该查看Bit Twiddling Hacks一个很酷的方法来加速strlen,它适用于大寄存器大小。
您可能需要考虑的其他事情是从字符串末尾开始测量(您知道最大长度)。只要空终止字节后面跟着更多的空值,这就行了,如果你可能有更长的字符串,即使你在那里投掷了一个跳跃也可以获胜。
答案 8 :(得分:0)
在假设的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修改。)
答案 9 :(得分:0)
假设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;
对于big endian count前导零。任何系统strlen实现都将使用它。