【问题标题】:fast way to check if an array of chars is zero [duplicate]检查字符数组是否为零的快速方法[重复]
【发布时间】:2011-02-05 02:14:21
【问题描述】:

我在内存中有一个字节数组。查看数组中所有字节是否为零的最快方法是什么?

【问题讨论】:

标签: c optimization memory performance 32-bit


【解决方案1】:

现在,没有使用 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 的 -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

【讨论】:

  • 线程怎么了?它会变得更快吗?
  • 线程很繁重,除非它是一个非常大的数组,否则不值得(cf stackoverflow.com/questions/3929774/…
  • 甚至没有提到如果你没有在 NUMA 部分中分配你的数组,它将序列化访问。如果它在 L3 虽然你有机会。
【解决方案2】:

如果您可以使用内联汇编,这里有一个简短、快速的解决方案。

#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 的回答。在这两种情况下都消除了函数调用。)

【讨论】:

  • 如果我们采用这种bitmagic方法并与线程结合会怎样?你能把这个任务交给线程池吗?
【解决方案3】:

如果您想在 32 位 C 中执行此操作,可能只需将数组作为 32 位整数数组循环并将其与 0 进行比较,然后确保最后的内容也是 0。

【讨论】:

  • 请注意,这在技术上 依赖于平台,尽管我想不出一个不能工作的平台。 +1
  • Billy - 我同意,但我猜没关系,因为它被标记为 32 位。
  • 事实上,只需在 char 上使用一个普通的 for 循环并使用 -funroll-loops 进行编译,编译器就会为您做正确的事情。
  • @Billy ONeal:如果“integer”表示int,那么它不适用于任何使用符号整数的平台,因为 0 和 -0 的位模式不能 两者都是零,但它们比较相等。所以你会得到误报。不过,我无法在脑海中说出这样一个平台的名字,而且我真的不希望使用一个。您可以通过加载 unsigned int 或更好的 uint32_t 来解决该特定问题,因为不允许有填充位。
  • @J-16:这个问题需要一个快速版本。作为一个花了很多年时间优化代码的专业游戏程序员,我可以告诉你,天真地编写代码并使用像“-funroll-loops”这样的编译器标志只会产生大约 1% 的时间。大多数时候你必须帮助编译器。
【解决方案4】:

如果数组大小合适,那么现代 CPU 的限制因素将是对内存的访问。

确保使用 __dcbt 或 prefetchnta (或 prefetch0,如果您打算很快再次使用缓冲区)等适当的距离(即 1-2K)使用缓存预取。

您还需要一次对多个字节执行 SIMD 或 SWAR 之类的操作。即使使用 32 位字,它的运算量也比每个字符版本少 4 倍。我建议展开 or 并将它们放入 or 的“树”中。您可以在我的代码示例中看到我的意思——这利用了超标量功能,通过使用没有那么多中间数据依赖关系的操作来并行执行两个整数操作(或)。我使用的树大小为 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***

我实际上建议将“行”值的比较封装到单个函数中,然后通过缓存预取将其展开几次。

【讨论】:

    【解决方案5】:

    将检查的内存分成一半,并将第一部分与第二部分进行比较。
    一种。如果有任何区别,就不可能完全一样。
    湾。如果没有差异,则重复上半场。

    最坏情况 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;
    }
    

    【讨论】:

    • 您还应该检查第一个元素是否为 0,否则对于每个字节相同的任何内容,它都会返回 true,不是吗?
    • 它也有n + n/2 + n/4 + ...操作,最多只能是2n,所以我认为它仍然是O(n)...
    • 抱歉,进行了一些修改。现在它是最终的。 Clau,第一个字符被检查。 “返回 *p == chr;”。你对 O(N) 的看法是正确的。
    • 啊,我没看到,我在寻找 '0' 文字,但这会检查数组是否是所有给定字符
    • 此算法比较每个字节并执行许多无序的内存加载。因为它是O(2n-1)=O(n)+O(n/2)+O(n/4)+...,所以将每个字节(或字/双字等)与寄存器进行比较会更快。任何算法都会受到内存限制(对于肯定的情况),因此最小化内存周期将带来最大的收益。 memcmp() 试图隐藏复杂性;它本身是 O(n) 用于内存访问。
    【解决方案6】:

    在 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 的第二种算法表现更好。否则,早点回归显然是表现出色的策略。

    【讨论】:

      【解决方案7】:

      Rusty Russel 的 memeqzero 速度非常快。它重用memcmp 来完成繁重的工作: https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92.

      【讨论】:

        猜你喜欢
        • 2014-07-12
        • 1970-01-01
        • 2018-05-04
        • 1970-01-01
        • 2013-10-28
        • 2019-02-11
        • 2021-04-27
        • 1970-01-01
        相关资源
        最近更新 更多