I have an array of bytes, in memory. What's the fastest way to see if all the bytes in the array are zero?
Nowadays, short of using SIMD extensions (such as SSE on x86 processors), you might as well iterate over the array and compare each value to 0.
In the distant past, performing a comparison and conditional branch for each element in the array (in addition to the loop branch itself) would have been deemed expensive and, depending on how often (or early) you could expect a non-zero element to appear in the array, you might have elected to completely do without conditionals inside the loop, using solely bitwise-or to detect any set bits and deferring the actual check until after the loop completes:
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"); }
However, with today's pipelined super-scalar processor designs complete with branch prediction, all non-SSE approaches are virtualy indistinguishable within a loop. If anything, comparing each element to zero and breaking out of the loop early (as soon as the first non-zero element is encountered) could be, in the long run, more efficient than the sum |= array[i]
approach (which always traverses the entire array) unless, that is, you expect your array to be almost always made up exclusively of zeroes (in which case making the sum |= array[i]
approach truly branchless by using GCC's -funroll-loops
could give you the better numbers -- see the numbers below for an Athlon processor, results may vary with processor model and manufacturer.)
#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
Here's a short, quick solution, if you're okay with using inline assembly.
#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; }
In case you're unfamiliar with assembly, I'll explain what we do here: we store the length of the string in a register, and ask the processor to scan the string for a zero (we specify this by setting the lower 8 bits of the accumulator, namely %%al
, to zero), reducing the value of said register on each iteration, until a non-zero byte is encountered. Now, if the string was all zeroes, the register, too, will be zero, since it was decremented length
number of times. However, if a non-zero value was encountered, the "loop" that checked for zeroes terminated prematurely, and hence the register will not be zero. We then obtain the value of that register, and return its boolean negation.
Profiling this yielded the following results:
$ time or.exe real 0m37.274s user 0m0.015s sys 0m0.000s $ time scasb.exe real 0m15.951s user 0m0.000s sys 0m0.046s
(Both test cases ran 100000 times on arrays of size 100000. The or.exe
code comes from Vlad's answer. Function calls were eliminated in both cases.)
If you want to do this in 32-bit C, probably just loop over the array as a 32-bit integer array and compare it to 0, then make sure the stuff at the end is also 0.
If the array is of any decent size, your limiting factor on a modern CPU is going to be access to the memory.
Make sure to use cache prefetching for a decent distance ahead (i.e. 1-2K) with something like __dcbt or prefetchnta (or prefetch0 if you are going to use the buffer again soon).
You will also want to do something like SIMD or SWAR to or multiple bytes at a time. Even with 32-bit words, it will be 4X less operations than a per character version. I'd recommend unrolling the or's and making them feed into a "tree" of or's. You can see what I mean in my code example - this takes advantage of superscalar capability to do two integer ops (the or's) in parallel by making use of ops that do not have as many intermediate data dependencies. I use a tree size of 8 (4x4, then 2x2, then 1x1) but you can expand that to a larger number depending on how many free registers you have in your CPU architecture.
The following pseudo-code example for the inner loop (no prolog/epilog) uses 32-bit ints but you could do 64/128-bit with MMX/SSE or whatever is available to you. This will be fairly fast if you have prefetched the block into the cache. Also you will possibly need to do unaligned check before if your buffer is not 4-byte aligned and after if your buffer (after alignment) is not a multiple of 32-bytes in length.
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***
I would actually suggest encapsulating the compare of a "line" of values into a single function and then unrolling that a couple times with the cache prefetching.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With