Rob*_*Rob 16 c optimization performance buffer
我正在寻找一种更快的方法来完成这个:
int is_empty(char * buf, int size)
{
int i;
for(i = 0; i < size; i++) {
if(buf[i] != 0) return 0;
}
return 1;
}
Run Code Online (Sandbox Code Playgroud)
我意识到我正在寻找一种微观优化,除非在极端情况下,但我知道存在更快的方法,我很好奇它是什么.
der*_*ert 27
在许多体系结构中,比较1个字节花费的时间与4或8相同,有时甚至是16个.4个字节通常很容易(int或long),8个也是(long或long long).16或更高版本可能需要内联组装,例如,使用矢量单元.
此外,分支误预测真的很痛,它可能有助于消除分支机构.例如,如果缓冲区几乎总是为空,而不是针对0测试每个块,而是将它们或它们放在一起并测试最终结果.
在便携式C中表达这一点很难:强制转换char*
为long*
违反严格的别名.但幸运的是,您可以使用memcpy
可移植的方式表达可以对任何内容进行别名的未对齐多字节加载.编译器会根据您的需要优化它.
例如,Godbolt编译器资源管理器中的这项正在进行的实现(https://godbolt.org/z/3hXQe7)表明,您可以通过加载两个连续的uint_fast32_t
变量来获得良好的内部循环(带有一些启动开销)64位)用memcpy然后检查tmp1 | tmp2
,因为许多CPU会根据OR结果设置标志,所以这可以让你检查两个单词的价格.
如果没有高效的未对齐负载,有效地为目标进行编译需要在启动代码中进行一些手动对齐,即使gcc可能也没有内联memcpy
无法证明对齐的负载.
Chr*_*utz 13
一种可能的方式,受到Kieveli被驳斥的想法的启发:
int is_empty(char *buf, size_t size)
{
static const char zero[999] = { 0 };
return !memcmp(zero, buf, size > 999 ? 999 : size);
}
Run Code Online (Sandbox Code Playgroud)
请注意,您无法使此解决方案适用于任意大小.你可以这样做:
int is_empty(char *buf, size_t size)
{
char *zero = calloc(size);
int i = memcmp(zero, buf, size);
free(zero);
return i;
}
Run Code Online (Sandbox Code Playgroud)
但是任何动态内存分配都会比你的速度慢.第一个解决方案更快的唯一原因是因为它可以使用memcmp()
,它将由库编写者在汇编语言中进行手工优化,并且比你在C中编码的任何东西都要快得多.
编辑:没有其他人提到过的优化,基于先前关于缓冲区处于状态X的"可能性"的观察:如果缓冲区不为空,它在开始还是结束时更可能不是空的?如果它最终可能会有瑕疵,你可以在结束时开始检查,并且可能会看到一个不错的性能提升.
编辑2:感谢Accipitridae在评论中:
int is_empty(char *buf, size_t size)
{
return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
}
Run Code Online (Sandbox Code Playgroud)
这基本上将缓冲区与自身进行比较,并初步检查第一个元素是否为零.这样,任何非零元素都会导致memcmp()
失败.我不知道这与使用另一个版本相比如何,但我知道如果第一个元素非零,它将很快失败(在我们循环之前).如果你更可能在年底有多余的内容更改buf[0]
,以buf[size]
获得相同的效果.
sam*_*wry 12
使用简单基准测试来测试缓冲区的zeroness的四个函数:
#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 );
}
Run Code Online (Sandbox Code Playgroud)
结果在我的旧电脑上:
func1: zero 108668
func2: zero 38680
func3: zero 8504
func4: zero 24768
Run Code Online (Sandbox Code Playgroud)
Tom*_*mas 10
如果您的程序仅限x86或仅x64,则可以使用内联assambler轻松优化.REPE SCASD指令将扫描缓冲区,直到找到非EAX双字.
由于没有等效的标准库函数,因此编译器/优化器可能无法使用这些指令(由Sufian的代码确认).
从头部开始,如果缓冲区长度为4字节对齐(MASM语法),则会发生类似这样的事情:
_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
}
Run Code Online (Sandbox Code Playgroud)
托马斯
编辑:更新Tony D的修复
小智 10
上面给出的基准(/sf/answers/104614961/)并不准确.他们暗示func3比其他选项快得多.
但是,如果改变测试的顺序,使func3在func2之前出现,你会发现func2要快得多.
在单次执行中运行组合基准时要小心......副作用很大,尤其是在重复使用相同的变量时.最好运行隔离的测试!
例如,将其更改为:
int main(){
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
MEASURE( func3 );
}
Run Code Online (Sandbox Code Playgroud)
给我:
func3: zero 14243
func3: zero 1142
func3: zero 885
func3: zero 848
func3: zero 870
Run Code Online (Sandbox Code Playgroud)
这真是让我烦恼,因为我无法看到func3如何比func2快得多.
(为答案道歉,而不是评论,没有声誉)
对于这么简单的事情,您需要查看编译器生成的代码.
$ gcc -S -O3 -o empty.s empty.c
Run Code Online (Sandbox Code Playgroud)
和汇编的内容:
.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
Run Code Online (Sandbox Code Playgroud)
这是非常优化的.循环做三件事:
通过逐字逐句比较可以略微优化它,但是你需要担心对齐等等.
当所有其他方法都失败时,先测量,不要猜测.
尝试尽可能使用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;
Run Code Online (Sandbox Code Playgroud)
使用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;
}
Run Code Online (Sandbox Code Playgroud)
循环展开可能会进一步改善这一点.
在具有AVX的现代x86 CPU上,您甚至可以使用256位SIMD并一次测试32个字节.