我在内存中有一个字节数组。查看数组中所有字节是否为零的最快方法是什么?
答案 0 :(得分:26)
如今,没有使用SIMD扩展程序(例如x86处理器上的SSE),您可能遍历数组并且将每个值与0进行比较。
在遥远的过去,对数组中的每个元素执行比较和条件分支(除了循环分支本身)本来会被认为是昂贵的,并且取决于多久(或早期)你可以期望一个非零元素出现在数组中,你可能已经选择完全没有条件在循环中,只使用按位 - 或检测任何设置位并推迟实际检查直到循环完成后:
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");
}
然而,今天的流水线超标量处理器设计与branch prediction一起完成,所有非SSE方法在循环中都是无法区分的。如果有的话,将每个元素与零进行比较并尽早摆脱循环(一旦遇到第一个非零元素),从长远来看,可能比sum |= array[i]
方法更有效(总是遍历)整个数组)除非,也就是说,你希望你的数组几乎总是由零组成(在这种情况下,通过使用GCC sum |= array[i]
使-funroll-loops
方法真正无分支可以给你更好的数字 - 请参阅下面的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
答案 1 :(得分:12)
如果你可以使用内联汇编,这是一个简短的快速解决方案。
#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的回答。在两种情况下都消除了函数调用。)
答案 2 :(得分:4)
如果你想在32位C中执行此操作,可能只需将数组作为32位整数数组循环并将其与0进行比较,然后确保最后的内容也为0。
答案 3 :(得分:3)
如果阵列大小合适,那么现代CPU的限制因素就是访问内存。
确保使用缓存预取(例如1-2K)与__dcbt或prefetchnta(或prefetch0,如果您将很快再次使用缓冲区)。
您还希望一次执行SIMD或SWAR或多个字节。即使使用32位字,它的操作也比每个字符版少4倍。我建议展开或者让它们进入“树”中。您可以在我的代码示例中看到我的意思 - 这利用了超标量功能,通过使用没有那么多中间数据依赖性的操作来并行执行两个整数操作(或者)。我使用的树大小为8(4x4,然后是2x2,然后是1x1),但您可以根据CPU架构中有多少个空闲寄存器将其扩展为更大的数字。
以下内部循环的伪代码示例(无prolog / epilog)使用32位整数但您可以使用MMX / SSE或任何可用的内容执行64/128位。如果您已将块预取到缓存中,则此速度相当快。如果你的缓冲区不是4字节对齐的话,你可能需要做一个未对齐的检查,如果你的缓冲区(对齐后)不是32字节长的倍数,你可能需要做。
const 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***
我实际上建议将值的“行”比较封装到单个函数中,然后用缓存预取几次展开。
答案 4 :(得分:3)
将已检查的内存分成两半,并将第一部分与第二部分进行比较
一个。如果有任何差异,则不能完全相同。
湾如果上半场没有重复差异。
最坏情况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;
}
答案 5 :(得分:0)
Rusty Russel memeqzero
非常快。它重用memcmp
来完成繁重的工作:
https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92