我有一个字节数组,保存在内存中。如何最快地判断数组中所有的字节是否都为零?
我有一个字节数组,保存在内存中。如何最快地判断数组中所有的字节是否都为零?
int sum = 0;
for (i = 0; i < ARRAY_SIZE; ++i) {
sum |= array[i];
}
if (sum != 0) {
printf("At least one array element is non-zero\n");
}
sum |= array[i]
方法更有效(该方法总是遍历整个数组),除非您希望数组几乎总是由零组成(在这种情况下,通过使用GCC的-funroll-loops
使sum |= array[i]
方法真正无分支可能会给您更好的结果--请参见下面针对Athlon处理器的数字,结果可能因处理器型号和制造商而异)。#include <stdio.h>
int a[1024*1024];
/* Methods 1 & 2 are equivalent on x86 */
int main() {
int i, j, n;
# if defined METHOD3
int x;
# endif
for (i = 0; i < 100; ++i) {
# if defined METHOD3
x = 0;
# endif
for (j = 0, n = 0; j < sizeof(a)/sizeof(a[0]); ++j) {
# if defined METHOD1
if (a[j] != 0) { n = 1; }
# elif defined METHOD2
n |= (a[j] != 0);
# elif defined METHOD3
x |= a[j];
# endif
}
# if defined METHOD3
n = (x != 0);
# endif
printf("%d\n", n);
}
}
$ uname -mp
i686 athlon
$ gcc -g -O3 -DMETHOD1 test.c
$ time ./a.out
real 0m0.376s
user 0m0.373s
sys 0m0.003s
$ gcc -g -O3 -DMETHOD2 test.c
$ time ./a.out
real 0m0.377s
user 0m0.372s
sys 0m0.003s
$ gcc -g -O3 -DMETHOD3 test.c
$ time ./a.out
real 0m0.376s
user 0m0.373s
sys 0m0.003s
$ gcc -g -O3 -DMETHOD1 -funroll-loops test.c
$ time ./a.out
real 0m0.351s
user 0m0.348s
sys 0m0.003s
$ gcc -g -O3 -DMETHOD2 -funroll-loops test.c
$ time ./a.out
real 0m0.343s
user 0m0.340s
sys 0m0.003s
$ gcc -g -O3 -DMETHOD3 -funroll-loops test.c
$ time ./a.out
real 0m0.209s
user 0m0.206s
sys 0m0.003s
如果您愿意使用内联汇编,这里是一个简短快速的解决方案。
#include <stdio.h>
int main(void) {
int checkzero(char *string, int length);
char str1[] = "wow this is not zero!";
char str2[] = {0, 0, 0, 0, 0, 0, 0, 0};
printf("%d\n", checkzero(str1, sizeof(str1)));
printf("%d\n", checkzero(str2, sizeof(str2)));
}
int checkzero(char *string, int length) {
int is_zero;
__asm__ (
"cld\n"
"xorb %%al, %%al\n"
"repz scasb\n"
: "=c" (is_zero)
: "c" (length), "D" (string)
: "eax", "cc"
);
return !is_zero;
}
如果您不熟悉汇编语言,我将解释一下我们在这里做什么:我们将字符串的长度存储在一个寄存器中,并要求处理器扫描该字符串以查找零(我们通过将累加器的低8位设置为零来指定此操作,即%%al
),每次迭代减少该寄存器的值,直到遇到非零字节。现在,如果字符串全部为零,则该寄存器也将为零,因为它已经被递减了length
次。然而,如果遇到非零值,则检查零的"循环"会提前终止,因此该寄存器将不为零。然后,我们获取该寄存器的值,并返回其布尔否定。
对此进行分析得到以下结果:
$ time or.exe
real 0m37.274s
user 0m0.015s
sys 0m0.000s
$ time scasb.exe
real 0m15.951s
user 0m0.000s
sys 0m0.046s
这两个测试用例都在大小为100000的数组上运行了100000次。 or.exe
代码来自Vlad的答案。 在这两种情况下,函数调用均已被消除。
repz scasb
速度慢(每次1字节;快速字符串和ERMSB微码只能加速无条件的rep stos/movs,请参见https://agner.org/optimize/指令表)。更糟糕的是,**您的内联汇编约束存在错误/不安全**。它修改了RDI,因此您需要一个`"+D"`输出/输入约束。它还读取了不是`"m"`输入操作数的内存,并且您没有`"memory"`破坏。请参阅[如何指示内联ASM参数指向的内存可能被使用?](https://stackoverflow.com/q/56432259) - 一个虚拟的“m”约束可以避免内存破坏。 - undefinedint
,那么它在使用符号-幅度整数的任何平台上都无法工作,因为0和-0的位模式不能同时都是全零,但它们比较相等。所以你会得到错误的结果。我暂时想不起来这样的平台叫什么名字,而且我也不指望会用到这样的平台。你可以通过加载无符号整数来解决这个特定问题,或者更好的方法是使用uint32_t
,因为它不允许有填充位。 - Steve Jessop将检查的内存一分为二,并将第一部分与第二部分进行比较。
a.如果有任何不同,那么就不可能完全相同。
b.如果没有差异,则重复第一半。
最坏情况下需要2*N。内存效率高且基于memcmp。
不确定是否应该在实际生活中使用,但我喜欢自我比较的想法。
它适用于奇数长度。你明白为什么吗?:-)
bool memcheck(char* p, char chr, size_t size) {
// Check if first char differs from expected.
if (*p != chr)
return false;
int near_half, far_half;
while (size > 1) {
near_half = size/2;
far_half = size-near_half;
if (memcmp(p, p+far_half, near_half))
return false;
size = far_half;
}
return true;
}
n + n/2 + n/4 + ...
次操作,这最多只是 2n
,因此我认为它仍然是 O(n)
... - ClaudiuO(2n-1)
=O(n)+O(n/2)+O(n/4)+...
,因此只比较每个字节(或单词/双字等)与寄存器的东西会更快。任何算法都将受到内存限制(对于正面案例),因此最小化内存周期将带来最大的收益。memcmp()
试图隐藏复杂性;它本身对于内存访问是O(n)
。 - artless noiseconst UINT32 *pmem = ***aligned-buffer-pointer***;
UINT32 a0,a1,a2,a3;
while(bytesremain >= 32)
{
// Compare an aligned "line" of 32-bytes
a0 = pmem[0] | pmem[1];
a1 = pmem[2] | pmem[3];
a2 = pmem[4] | pmem[5];
a3 = pmem[6] | pmem[7];
a0 |= a1; a2 |= a3;
pmem += 8;
a0 |= a2;
bytesremain -= 32;
if(a0 != 0) break;
}
if(a0!=0) then ***buffer-is-not-all-zeros***
我建议将“一行”值的比较封装到一个函数中,然后使用缓存预取将其展开几次。
在ARM64上测试了两种实现方式,一种使用循环并在false时提前返回,另一种则将所有字节进行OR操作:
int is_empty1(unsigned char * buf, int size)
{
int i;
for(i = 0; i < size; i++) {
if(buf[i] != 0) return 0;
}
return 1;
}
int is_empty2(unsigned char * buf, int size)
{
int sum = 0;
for(int i = 0; i < size; i++) {
sum |= buf[i];
}
return sum == 0;
}
结果:
所有结果,单位为微秒:
is_empty1 is_empty2
MEDIAN 0.350 3.554
AVG 1.636 3.768
仅出现错误结果:
is_empty1 is_empty2
MEDIAN 0.003 3.560
AVG 0.382 3.777
仅返回真实结果:
is_empty1 is_empty2
MEDIAN 3.649 3,528
AVG 3.857 3.751
简介:仅在假结果概率非常小的数据集中,使用ORing算法的第二个算法由于省略了分支而表现更佳。否则,提前返回显然是优于的策略。
memeqzero
非常快。它重复使用memcmp
来进行重负载操作:https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92。