我正在寻找更快的方法来完成这个任务:
int is_empty(char * buf, int size)
{
int i;
for(i = 0; i < size; i++) {
if(buf[i] != 0) return 0;
}
return 1;
}
我意识到我正在寻找微小的优化,除了在极端情况下不必要之外,但我知道存在一种更快的方法,我很好奇那是什么。
我正在寻找更快的方法来完成这个任务:
int is_empty(char * buf, int size)
{
int i;
for(i = 0; i < size; i++) {
if(buf[i] != 0) return 0;
}
return 1;
}
我意识到我正在寻找微小的优化,除了在极端情况下不必要之外,但我知道存在一种更快的方法,我很好奇那是什么。
char*
转换为long*
会违反严格的别名。但幸运的是,您可以使用memcpy
来表达一个不对齐的多字节加载,它可以别名任何东西。编译器将对其进行优化以得到所需的汇编代码。例如,这个正在进行的实现(https://godbolt.org/z/3hXQe7)在Godbolt编译器探索器上显示,您可以通过使用memcpy
来加载两个连续的uint_fast32_t
变量(通常为64位),并检查tmp1 | tmp2
来获得良好的内部循环(有一些启动开销),因为许多CPU将根据OR结果设置标志,因此这让您以一个单词的价格检查两个单词。为了使它在没有有效未对齐加载的目标上高效编译,需要在启动代码中进行一些手动对齐,即使如此,gcc也可能不会内联那些无法证明对齐的加载的memcpy
。灵感来自于Kieveli被否决的主意,有一个潜在的方法:
int is_empty(char *buf, size_t size)
{
static const char zero[999] = { 0 };
return !memcmp(zero, buf, size > 999 ? 999 : size);
}
请注意,您不能使此解决方案适用于任意大小。您可以这样做:
int is_empty(char *buf, size_t size)
{
char *zero = calloc(size);
int i = memcmp(zero, buf, size);
free(zero);
return i;
}
但任何动态内存分配都会比您所拥有的要慢。第一个解决方案之所以更快,只是因为它可以使用memcmp()
,这将由库编写者用汇编语言进行手动优化,并且比您在C中编写的任何内容都要快。
编辑:其他人没有提到的一种优化,基于早期对缓冲区处于状态X的“可能性”的观察:如果缓冲区不为空,则很可能在开头或结尾时不为空?如果在结尾处可能会有垃圾,您可以从结尾开始检查,并可能看到一些良好的性能提升。
编辑2:感谢评论区的Accipitridae:
int is_empty(char *buf, size_t size)
{
return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
}
这基本上是将缓冲区与其自身进行比较,首先检查第一个元素是否为零。这样,任何非零元素都会导致 memcmp()
失败。我不知道这与使用其他版本相比如何,但我知道如果第一个元素为非零值,它将很快失败(甚至在我们进入循环之前)。如果您更有可能在末尾产生无用数据,请将 buf[0]
更改为 buf[size]
以获得相同的效果。
memcmp()
会花费更长时间? - Chris Lutzmemcmp()
版本的速度几乎与OP版本相同,始终略微(如0.001秒)更快。 - Chris Lutzstatic
数组声明为const
。这个版本可以轻松地进行内联,这可能是另一个微小的优势。 - cafint main(){
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
}
给我:
func3: zero 14243
func3: zero 1142
func3: zero 885
func3: zero 848
func3: zero 870
对我来说,这真的很烦人,因为我看不出func3如何比func2快那么多。
(抱歉回答不是作为评论,没有声望)
四个用于测试缓冲区是否为零的函数,带有简单基准测试:
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <inttypes.h>
#define SIZE (8*1024)
char zero[SIZE] __attribute__(( aligned(8) ));
#define RDTSC(var) __asm__ __volatile__ ( "rdtsc" : "=A" (var));
#define MEASURE( func ) { \
uint64_t start, stop; \
RDTSC( start ); \
int ret = func( zero, SIZE ); \
RDTSC( stop ); \
printf( #func ": %s %12"PRIu64"\n", ret?"non zero": "zero", stop-start ); \
}
int func1( char *buff, size_t size ){
while(size--) if(*buff++) return 1;
return 0;
}
int func2( char *buff, size_t size ){
return *buff || memcmp(buff, buff+1, size-1);
}
int func3( char *buff, size_t size ){
return *(uint64_t*)buff || memcmp(buff, buff+sizeof(uint64_t), size-sizeof(uint64_t));
}
int func4( char *buff, size_t size ){
return *(wchar_t*)buff || wmemcmp((wchar_t*)buff, (wchar_t*)buff+1, size/sizeof(wchar_t)-1);
}
int main(){
MEASURE( func1 );
MEASURE( func2 );
MEASURE( func3 );
MEASURE( func4 );
}
我的旧电脑的结果:
func1: zero 108668
func2: zero 38680
func3: zero 8504
func4: zero 24768
*(uint64_t*)buff
违反了严格别名规则 - 您需要使用 memcpy
加载它或声明GNU C may_alias
版本的类型:为什么glibc的strlen需要如此复杂才能快速运行? - Peter Cordes_asm {
CLD ; search forward
XOR EAX, EAX ; search for non-zero
LEA EDI, [buf] ; search in buf
MOV ECX, [buflen] ; search buflen bytes
SHR ECX, 2 ; using dwords so len/=4
REPE SCASD ; perform scan
JCXZ bufferEmpty: ; completes? then buffer is 0
}
Tomas
编辑:已根据Tony D的修改更新
LEA EDI, ...
而不是 ESI,并且缺少 CLD 来保证方向标志被清除。 - Tony Delroyrepe scasd
不比普通循环快。在英特尔CPU中,字符串存储和复制指令(rep stos
和rep movs
)具有优化的微码实现,但repe scas
和repe cmps
则没有。此外,@TonyD:所有常规ABI都要求在函数进入时清除方向标志。cld
是一条指令的浪费。 - Peter Cordes对于如此简单的事情,您需要查看编译器正在生成的代码。
$ gcc -S -O3 -o empty.s empty.c
同时需要汇编的内容:
.text
.align 4,0x90
.globl _is_empty
_is_empty:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %edx ; edx = pointer to buffer
movl 8(%ebp), %ecx ; ecx = size
testl %edx, %edx
jle L3
xorl %eax, %eax
cmpb $0, (%ecx)
jne L5
.align 4,0x90
L6:
incl %eax ; real guts of the loop are in here
cmpl %eax, %edx
je L3
cmpb $0, (%ecx,%eax) ; compare byte-by-byte of buffer
je L6
L5:
leave
xorl %eax, %eax
ret
.align 4,0x90
L3:
leave
movl $1, %eax
ret
.subsections_via_symbols
这个非常优化。循环执行三件事情:
可以通过逐个字比较来进一步优化,但是需要考虑对齐等问题。
当其他方法都失败时,先进行测量,不要猜测。
repe scasd
一次检查多个字节。此汇编代码有点糟糕。它使用了两个寄存器的寻址模式,但仍然执行比较和分支,而不是使用inc
设置的标志位。更好的方法是获取指向数组末尾的指针,并计算一个负索引向零递增。此外,Sufian:一次处理一个字或双字会大大优化,而不仅仅是轻微的优化。对于大缓冲区,可以提高两倍(字)或四倍(双字)。此外,现代英特尔CPU 显然无法微融合2个寄存器的寻址模式。 - Peter Cordes尽可能使用int大小的变量来检查缓冲区(它应该对齐)。
以下是未编译、未经测试的代码(我随口说的,这里几乎肯定至少有一个错误。这只是给出了一般想法):
/* check the start of the buf byte by byte while it's unaligned */
while (size && !int_aligned( buf)) {
if (*buf != 0) {
return 0;
}
++buf;
--size;
}
/* check the bulk of the buf int by int while it's aligned */
size_t n_ints = size / sizeof( int);
size_t rem = size / sizeof( int);
int* pInts = (int*) buf;
while (n_ints) {
if (*pInt != 0) {
return 0;
}
++pInt;
--n_ints;
}
/* now wrap up the remaining unaligned part of the buf byte by byte */
buf = (char*) pInts;
while (rem) {
if (*buf != 0) {
return 0;
}
++buf;
--rem;
}
return 1;
使用x86架构,你可以使用SSE一次性测试16字节:
#include "smmintrin.h" // note: requires SSE 4.1
int is_empty(const char *buf, const size_t size)
{
size_t i;
for (i = 0; i + 16 <= size; i += 16)
{
__m128i v = _mm_loadu_si128((m128i *)&buf[i]);
if (!_mm_testz_si128(v, v))
return 0;
}
for ( ; i < size; ++i)
{
if (buf[i] != 0)
return 0;
}
return 1;
}
这个程序可能通过循环展开进一步优化。
在现代带有AVX的x86 CPU上,你甚至可以使用256位SIMD,每次测试32个字节。
ptest
。 (ptest
有2个微操作,不与分支进行宏融合)。 每次迭代使用多个“por”指令应该有助于饱和两个加载端口,因为有循环开销。 还要注意,如果大小> = 16,则清理循环可以是对以最后一个字节结尾的非对齐负载的单个ptest
。 这个负载与最后一个主循环迭代重叠是可以接受的,因为测试是否为零是幂等的。 - Peter Cordesint is_empty(char * buf, int size)
{
while(size --> 0) {
if(buf[size] != 0) return 0;
}
return 1;
}
必须注意的是,我们可能无法超越编译器,因此请在编译器中启用最激进的速度优化,并假设您不太可能变得更快。
或者使用指针处理所有内容(未经测试,但很可能表现良好):
int is_empty(char* buf, int size)
{
char* org = buf;
if (buf[size-1] == 1)
return 0;
buf[size-1] = 1;
while(! *buf++);
buf--;
return buf == org[size-1];
}
if (*buf++) return 0;
替换 if 语句。 - wovanosize == 0
时会出现越界访问,buf[size-1]
的原始内容没有恢复,最后一条语句缺少一个 &
。我也会避免对 buf[size-1]
进行不必要的地址计算(三次)。但我喜欢这种超越传统思维的方式,这种解决方案在某些场景下可能相对简单和高效 :) - wovano我看到很多人说对齐问题会阻止你进行字大小的访问,但这并不总是正确的。如果你想编写可移植的代码,那么这肯定是一个问题,然而x86实际上可以容忍未对齐的访问。例如,只有在EFLAGS中打开对齐检查(当然buf实际上没有对齐)时,这才会在x86上失败。
int is_empty(char * buf, int size) {
int i;
for(i = 0; i < size; i+= 4) {
if(*(int *)(buf + i) != 0) {
return 0;
}
}
for(; i < size; i++) {
if(buf[i] != 0)
return 0;
}
return 1;
}
无论编译器是否能够将原始循环转换为基于单词比较的循环以处理对齐问题,但在任何正常的优化级别下,它都不会这样做,因为缺乏信息。对于尺寸较小的情况,以这种方式展开循环会使代码变慢,并且编译器希望保守一些。
解决此问题的方法是利用基于配置文件的优化。如果您让GCC获取有关is_empty函数的配置文件信息,然后重新编译它,它将愿意将循环展开为基于单词的比较,并进行对齐检查。您还可以通过-funroll-all-loops强制执行此行为。
int is_empty(char *buf, int size) { memset(buf, 0, size); return 1; }
- Chris Lutzsize_t
而不是int
。 - Chris Lutz