最近在优化软件运行速度,翻查C++代码的时候,发现了这么一处函数。功能很简单,就是比较内存值是否相同。
函数如下:
int cfunc_memcmp(char* src, char* dest, int n)
{
while (n--)
if (*src++ != *dest++)
return 1;
return 0;
}
看起来,这是一个很简单的函数,程序逻辑也很简短,似乎并没有什么可以优化的地方。
然而,对于这样一个简单的功能,在C Runtime库里,却有一个函数可以直接调用,效果完全相同。
代码如下:
#include <memory.h>
int cruntime_memcmp(void *src, void *dest, int n)
{
return memcmp(src, dest, n);
}
如果将函数cfunc_memcmp替换成cruntime_memcmp,速度会变化多少呢?
我们写一个简单的计时程序测试一下。
代码如下:
#include <stdio.h>
#include <memory.h>
#include <time.h>
int cruntime_memcmp(void *src, void *dest, int n)
{
return memcmp(src, dest, n);
}
int cfunc_memcmp(char* src, char* dest, int n)
{
while (n--)
if (*src++ != *dest++)
return 1;
return 0;
}
int main()
{
#define SIZE_BUF 1000000000
char *buf1 = new char[SIZE_BUF];
char *buf2 = new char[SIZE_BUF];
memset(buf1, 123, SIZE_BUF);
memset(buf2, 123, SIZE_BUF);
buf2[SIZE_BUF - 1] = (char)321;
clock_t start, end;
int nRet;
start = clock();
nRet = cfunc_memcmp(buf1, buf2, SIZE_BUF);
end = clock();
printf("The time clock was : %d \n", (end - start));
start = clock();
nRet = cruntime_memcmp(buf1, buf2, SIZE_BUF);
end = clock();
printf("The time clock was :%d \n", (end - start));
delete buf1;
delete buf2;
return 0;
}
输出结果为:
The time clock was : 435
The time clock was : 136
可以看到,两者之间速度相差有3倍之多。如果在大循环的内部,调用了这个函数,那么程序总体耗时就有明显的差异了。
为了充分研究memcmp为什么这么高效的原因,我们尝试直接编写汇编代码,去比较一下速度。
第一个版本,使用repz cmpsb指令,可以按字节比较内存
代码如下:
.code
asm_memcmp proc src:QWORD, dest:QWORD, n:DWORD
push rsi
push rdi
mov rsi, rcx
mov rdi, rdx
mov rcx, r8
repz cmpsb
jnz no_equal
equal:
mov rax, 0
jmp done
no_equal:
mov rax, 1
done:
pop rdi
pop rsi
ret
asm_memcmp endp
END
然后在C++代码修改如下:
extern "C" {
int asm_memcmp(void *src, void *dest, int n);
}
int main()
{
//...
start = clock();
nRet = asm_memcmp(buf1, buf2, SIZE_BUF);
end = clock();
printf("The time clock was : %d \n", (end - start));
//...
}
输出结果如下:
The time clock was : 568
这个汇编级别的repz cmpsb指令,居然比C++里的字节比较函数cfunc_memcmp还要慢。让人不可思议。
接下来,我们不使用repz cmpsb指令,直接按地址循环取值,每次以8 bytes为单位比较。
代码如下:
.code
asm_memcmp proc src:QWORD, dest:QWORD, n:DWORD
push rsi
push rdi
mov rsi, rcx
mov rdi, rdx
mov rcx, r8
start:
test rcx, rcx
jz equal
mov rax, [rsi]
mov rdx, [rdi]
cmp rax, rdx
jnz no_equal
add rsi, 8
add rdi, 8
sub rcx, 8
jmp start
equal:
mov rax, 0
jmp done
no_equal:
mov rax, 1
done:
pop rdi
pop rsi
ret
asm_memcmp endp
END
输出结果如下:
The time clock was : 141
这个结果也出乎意料。这种直接编写的汇编代码,竟然和C Runtime库的效率差不多,甚至还要慢一些。
至此,我们对库函数memcmp到底是如何处理的,产生了浓厚的兴趣。
于是,我们通过调试器进入了memcmp的反汇编代码,跟踪进去查看。
代码如下:
00007FFEEA64C286 nop word ptr [rax+rax]
00007FFEEA64C290 sub rdx,rcx
00007FFEEA64C293 cmp r8,8
00007FFEEA64C297 jb 00007FFEEA64C2BB
00007FFEEA64C299 test cl,7
00007FFEEA64C29C je 00007FFEEA64C2B2
00007FFEEA64C29E xchg ax,ax
00007FFEEA64C2A0 mov al,byte ptr [rcx]
00007FFEEA64C2A2 cmp al,byte ptr [rdx+rcx]
00007FFEEA64C2A5 jne 00007FFEEA64C2D3
00007FFEEA64C2A7 inc rcx
00007FFEEA64C2AA dec r8
00007FFEEA64C2AD test cl,7
00007FFEEA64C2B0 jne 00007FFEEA64C2A0
00007FFEEA64C2B2 mov r9,r8
00007FFEEA64C2B5 shr r9,3
00007FFEEA64C2B9 jne 00007FFEEA64C2DA
00007FFEEA64C2BB test r8,r8
00007FFEEA64C2BE je 00007FFEEA64C2CF
00007FFEEA64C2C0 mov al,byte ptr [rcx]
00007FFEEA64C2C2 cmp al,byte ptr [rdx+rcx]
00007FFEEA64C2C5 jne 00007FFEEA64C2D3
00007FFEEA64C2C7 inc rcx
00007FFEEA64C2CA dec r8
00007FFEEA64C2CD jne 00007FFEEA64C2C0
00007FFEEA64C2CF xor rax,rax
00007FFEEA64C2D2 ret
00007FFEEA64C2D3 sbb eax,eax
00007FFEEA64C2D5 sbb eax,0FFFFFFFFh
00007FFEEA64C2D8 ret
00007FFEEA64C2D9 nop
00007FFEEA64C2DA shr r9,2
00007FFEEA64C2DE je 00007FFEEA64C317
00007FFEEA64C2E0 mov rax,qword ptr [rcx]
00007FFEEA64C2E3 cmp rax,qword ptr [rdx+rcx]
00007FFEEA64C2E7 jne 00007FFEEA64C344
00007FFEEA64C2E9 mov rax,qword ptr [rcx+8]
00007FFEEA64C2ED cmp rax,qword ptr [rdx+rcx+8]
00007FFEEA64C2F2 jne 00007FFEEA64C340
00007FFEEA64C2F4 mov rax,qword ptr [rcx+10h]
00007FFEEA64C2F8 cmp rax,qword ptr [rdx+rcx+10h]
00007FFEEA64C2FD jne 00007FFEEA64C33C
00007FFEEA64C2FF mov rax,qword ptr [rcx+18h]
00007FFEEA64C303 cmp rax,qword ptr [rdx+rcx+18h]
00007FFEEA64C308 jne 00007FFEEA64C338
00007FFEEA64C30A add rcx,20h
00007FFEEA64C30E dec r9
00007FFEEA64C311 jne 00007FFEEA64C2E0
00007FFEEA64C313 and r8,1Fh
00007FFEEA64C317 mov r9,r8
00007FFEEA64C31A shr r9,3
00007FFEEA64C31E je 00007FFEEA64C2BB
00007FFEEA64C320 mov rax,qword ptr [rcx]
00007FFEEA64C323 cmp rax,qword ptr [rdx+rcx]
00007FFEEA64C327 jne 00007FFEEA64C344
00007FFEEA64C329 add rcx,8
00007FFEEA64C32D dec r9
00007FFEEA64C330 jne 00007FFEEA64C320
00007FFEEA64C332 and r8,7
00007FFEEA64C336 jmp 00007FFEEA64C2BB
00007FFEEA64C338 add rcx,8
00007FFEEA64C33C add rcx,8
00007FFEEA64C340 add rcx,8
00007FFEEA64C344 mov rcx,qword ptr [rcx+rdx]
00007FFEEA64C348 bswap rax
00007FFEEA64C34B bswap rcx
00007FFEEA64C34E cmp rax,rcx
00007FFEEA64C351 sbb eax,eax
00007FFEEA64C353 sbb eax,0FFFFFFFFh
00007FFEEA64C356 ret
可以看到,汇编代码做了8字节对齐,并利用了冗余指令减少寄存器运算,把一个10行左右的指令,写成了70行。
我们简要分析一下这些代码的逻辑。
首先,把地址做8字节对齐,通过test cl,7指令判断。不对齐的部分,单字节(byte)比较。
00007FFEEA64C299 test cl,7
00007FFEEA64C29C je 00007FFEEA64C2B2
00007FFEEA64C29E xchg ax,ax
00007FFEEA64C2A0 mov al,byte ptr [rcx]
00007FFEEA64C2A2 cmp al,byte ptr [rdx+rcx]
00007FFEEA64C2A5 jne 00007FFEEA64C2D3
00007FFEEA64C2A7 inc rcx
00007FFEEA64C2AA dec r8
00007FFEEA64C2AD test cl,7
00007FFEEA64C2B0 jne 00007FFEEA64C2A0
然后,以32个字节为单位,通过冗余指令,做4次8字节(qword)比较
00007FFEEA64C2DA shr r9,2
00007FFEEA64C2DE je 00007FFEEA64C317
00007FFEEA64C2E0 mov rax,qword ptr [rcx]
00007FFEEA64C2E3 cmp rax,qword ptr [rdx+rcx]
00007FFEEA64C2E7 jne 00007FFEEA64C344
00007FFEEA64C2E9 mov rax,qword ptr [rcx+8]
00007FFEEA64C2ED cmp rax,qword ptr [rdx+rcx+8]
00007FFEEA64C2F2 jne 00007FFEEA64C340
00007FFEEA64C2F4 mov rax,qword ptr [rcx+10h]
00007FFEEA64C2F8 cmp rax,qword ptr [rdx+rcx+10h]
00007FFEEA64C2FD jne 00007FFEEA64C33C
00007FFEEA64C2FF mov rax,qword ptr [rcx+18h]
00007FFEEA64C303 cmp rax,qword ptr [rdx+rcx+18h]
00007FFEEA64C308 jne 00007FFEEA64C338
00007FFEEA64C30A add rcx,20h
00007FFEEA64C30E dec r9
00007FFEEA64C311 jne 00007FFEEA64C2E0
再然后,不足32个字节的部分,以8个字节为单位,做8字节(qword)比较
00007FFEEA64C313 and r8,1Fh
00007FFEEA64C317 mov r9,r8
00007FFEEA64C31A shr r9,3
00007FFEEA64C31E je 00007FFEEA64C2BB
00007FFEEA64C320 mov rax,qword ptr [rcx]
00007FFEEA64C323 cmp rax,qword ptr [rdx+rcx]
00007FFEEA64C327 jne 00007FFEEA64C344
00007FFEEA64C329 add rcx,8
00007FFEEA64C32D dec r9
00007FFEEA64C330 jne 00007FFEEA64C320
最后,剩余不足8个字节的,做单字节(byte)比较
00007FFEEA64C2BB test r8,r8
00007FFEEA64C2BE je 00007FFEEA64C2CF
00007FFEEA64C2C0 mov al,byte ptr [rcx]
00007FFEEA64C2C2 cmp al,byte ptr [rdx+rcx]
00007FFEEA64C2C5 jne 00007FFEEA64C2D3
00007FFEEA64C2C7 inc rcx
00007FFEEA64C2CA dec r8
00007FFEEA64C2CD jne 00007FFEEA64C2C0
应该说,这一系列的指令序列非常繁琐,但是却是非常符合x64指令系统内部结构的。所以,在效率上,确实更胜一筹。
最后,我们把各种代码方案,做一个速度的对比,按照耗时排列。
名次 | 方案 | 耗时 |
---|---|---|
1 | cruntime_memcmp | 136 |
2 | asm_memcmp | 141 |
3 | cfunc_memcmp | 435 |
4 | asm_memcmp (repz cmpsb) | 568 |
总结,现代软件工程已经不需要我们详细深入汇编层编写程序了。而且,即便我们自己编写汇编代码,往往效率还远不如编译器优化后的指令序列。
所以,我们可以尽量使用现有C Runtime库,或者STL库。既可以简化我们的开发过程,还可以加快程序的运行效率。这是一个又快又好的结果。