如何让 gcc 生成合适的代码来检查缓冲区是否充满 NUL 字节?

How to get gcc to generate decent code that checks if a buffer is full of NUL bytes?

我正在实施一个解析磁带档案的程序。部分解析器逻辑正在检查归档结束标记,这是一个充满 NUL 字节的 512 字节块。为此我写了如下代码,希望gcc能把这个优化好:

int is_eof_block(const char usth[static 512])
{
    size_t i;

    for (i = 0; i < 512; i++)
        if (usth[i] != '[=10=]')
            return 0;

    return 1;
}

但令我惊讶的是,gcc 仍然会为此生成糟糕的代码,即使我明确允许它访问缓冲区中的全部 512 个字节:

is_eof_block:
    leaq    512(%rdi), %rax
    jmp .L239
    .p2align 4,,10
.L243:
    addq    , %rdi
    cmpq    %rax, %rdi
    je  .L242
.L239:
    cmpb    [=11=], (%rdi)
    je  .L243
    xorl    %eax, %eax
    ret
    .p2align 4,,10
.L242:
    movl    , %eax
    ret

我希望 gcc 生成类似这样的东西甚至 SIMD 代码:

is_eof_block:
    mov ,%ecx
    xor %eax,%eax
    repz scasq
    setz %al
    ret

我如何重写代码以使其仍然可移植(例如:不使用非 C99 语言扩展并在不支持未对齐内存访问的体系结构上工作)但在常见体系结构上编译为更好的机器代码例如 amd64 和 AArch32?

基准

我编写了以下微基准测试来演示时差。您可以将 MISALIGNED 定义为正整数以使用未对齐的缓冲区进行测试。

benchmark.c

#include <stdio.h>
#include <time.h>

#define TESTS 10000000
#ifndef MISALIGNED
# define MISALIGNED 0
#endif

char testarray[512 + MISALIGNED];

extern int is_eof_block(const char[static 512]);

int main()
{
    size_t i, j;
    clock_t begin, end;

    fprintf(stderr, "testing %d times\n", TESTS);
    fprintf(stderr, "no byte set to 1... ");
    begin = clock();

    for (i = 0; i < TESTS; i++)
        if (!is_eof_block(testarray + MISALIGNED)) {
            fprintf(stderr, "\nWrong test result in iteration %zu!\n", i);
            return EXIT_FAILURE;
        }

    end = clock();
    fprintf(stderr, "%fs\n", (end - begin) / (double)CLOCKS_PER_SEC);

    fprintf(stderr, "with non-null byte... ");
    begin = clock();

    for (i = j = 0; i < TESTS; i++) {
        testarray[MISALIGNED + j] = '[=13=]';
        j = (j + 47) & 511;
        testarray[MISALIGNED + j] = '1';

        if (is_eof_block(testarray + MISALIGNED)) {
            fprintf(stderr, "\nWrong test result in iteration %zu!\n", i);
            return EXIT_FAILURE;
        }       
    }

    end = clock();
    fprintf(stderr, "%fs\n", (end - begin) / (double)CLOCKS_PER_SEC);

    return EXIT_SUCCESS;
}

is_eof_block_c.c

#include <stddef.h>

int is_eof_block(const char test[static 512])
{
    size_t i;

    for (i = 0; i < 512; i++)
        if (test[i] != '[=14=]')
            return 0;

    return 1;
}

is_eof_block_asm.s

    .text
    .globl is_eof_block
    .type is_eof_block,@function

    .align 16
is_eof_block:
    mov ,%ecx
    xor %eax,%eax
    repz scasq
    setz %al
    ret
    .size is_eof_block,.-is_eof_block

这是 is_eof_block 的 C 实现链接的输出:

testing 10000000 times
no byte set to 1... 2.281250s
with non-null byte... 1.195312s

这里是汇编版本:

testing 10000000 times
no byte set to 1... 0.476562s
with non-null byte... 0.320312s

两者都是用 gcc 5 编译的,唯一的优化选项是 -O3。传递各种 -march=... 标志并没有改变代码。差异大约是四分之一。使用未对齐的缓冲区,汇编实现大约慢 3%,而与 C 实现没有区别。

由于块已知为 512 字节,因此将每个 16 字节组提取到 UInt64 中,然后针对零进行测试。那应该减少循环开销。

您的对齐问题的可能解决方法是将缓冲区复制到本地结构中。

struct x
{
    unsigned long long :0;

    char buffer[512];
};

这将为您提供一个对齐的缓冲区。

这是一个触及每个字节的版本,似乎比您的测试工具中的原始函数快 2-3 倍(我不相信它准确反映了现实):

int
is_eof_block1(const char usth[static 512])
{
        unsigned int i;
        int res = 0;
        for (i = 0; i < 512; i++)
                res |= usth[i];
        return res == 0;
}

这是一个针对可读性进行了优化的版本,不会浪费人们的时间,也不会试图比编写您的 compiler/libc 的人更聪明(它比您的汇编程序快得多,至少在我的机器上是这样):

int
is_eof_block2(const char usth[static 512])
{
        const static char foo[512];
        return !memcmp(usth, foo, sizeof(foo));
}

由于对问题的真正有用的评论,我决定使用原始 C 代码。感谢大家的帮助!

这是一个版本,它(天真地)相信如果你给它一个 stdint.h _fast 类型,编译器会做最好的工作:

#include <stdint.h>
#include <stdio.h>

typedef uint_fast16_t fast_t; // 16 since 512 can't fit in 8 bits

#define FAST_SIZE (512/sizeof(fast_t))

typedef union  // union to guarantee there's no aliasing mishaps
{
  char   usth [512];
  fast_t fast [FAST_SIZE];

} block_t;


// misc sanity checks:
_Static_assert(512%sizeof(fast_t) == 0, "This should never happen");
_Static_assert(sizeof(block_t) == 512,  "Padding gone crazy");


int is_eof_block(const block_t* block)
{
  for(const fast_t* i=&block->fast[0]; i<block->fast+FAST_SIZE; i++)
  {
    if(*i != 0)
      return 0;
  }

  return 1;
}


int main (void)
{
  block_t block = {0};

  printf("%d", is_eof_block(&block));
}

循环可以用数组+迭代器代替指针运算。可能会更快或更慢,我还没有对它进行基准测试。

编辑:

数组+迭代器版本。这就是我使用 uint_fast16_t 的原因——我希望“fast_t”会比 size_t 做得更好,然后它必须至少足够大以包含值 512。

int is_eof_block(const block_t* block)
{
  for(fast_t i=0; i<FAST_SIZE; i++)
  {
    if(block->fast[i] != 0)
      return 0;
  }

  return 1;
}