如何让 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;
}
我正在实施一个解析磁带档案的程序。部分解析器逻辑正在检查归档结束标记,这是一个充满 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;
}