memcmp的性能分析

最近在优化软件运行速度,翻查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指令系统内部结构的。所以,在效率上,确实更胜一筹。


最后,我们把各种代码方案,做一个速度的对比,按照耗时排列。

名次方案耗时
1cruntime_memcmp136
2asm_memcmp141
3cfunc_memcmp435
4asm_memcmp (repz cmpsb)568

总结,现代软件工程已经不需要我们详细深入汇编层编写程序了。而且,即便我们自己编写汇编代码,往往效率还远不如编译器优化后的指令序列。
所以,我们可以尽量使用现有C Runtime库,或者STL库。既可以简化我们的开发过程,还可以加快程序的运行效率。这是一个又快又好的结果。

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值