最快的字节级比较方法memcmp.c反汇编分析

来源:互联网 发布:端口波特率修改工具 编辑:程序博客网 时间:2024/06/05 18:05

在stackoverflow上有关于在C#中最快的位判断的方法的讨论。
https://stackoverflow.com/questions/43289/comparing-two-byte-arrays-in-net
据讨论的网友统计,最快的方法是通过[Dllimport]第三方调用的memcmp.c。

源码:

/* * memcmp.c -- * *  Source code for the "memcmp" library routine. * * Copyright (c) 1998 Sun Microsystems, Inc. * * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * * SCCS: @(#) memcmp.c 1.2 98/01/19 10:48:58 */#include "tcl.h"#include "tclPort.h"/* * Here is the prototype just in case it is not included * in tclPort.h. */int     memcmp _ANSI_ARGS_((CONST VOID *s1,                            CONST VOID *s2, size_t n));/* *---------------------------------------------------------------------- * * memcmp -- * *  Compares two bytes sequences. * * Results: *     compares  its  arguments, looking at the first n *     bytes (each interpreted as an unsigned char), and  returns *     an integer less than, equal to, or greater than 0, accord- *     ing as s1 is less  than,  equal  to,  or *     greater than s2 when taken to be unsigned 8 bit numbers. * * Side effects: *  None. * *---------------------------------------------------------------------- */intmemcmp(s1, s2, n)CONST VOID *s1;         /* First string. */CONST VOID *s2;         /* Second string. */size_t      n;                      /* Length to compare. */{    unsigned char u1, u2;    for ( ; n-- ; s1++, s2++) {        u1 = * (unsigned char *) s1;        u2 = * (unsigned char *) s2;        if ( u1 != u2) {            return (u1-u2);        }    }    return 0;}

看到源码后最好奇的还是return (u1-u2)这里,如果说此方法的目的是进行最快的字节级相等比较,那么return 1应该是更好的选择,为什么要返回一个没有太大意义的u1-u2值。出于好奇,将memcmp.c进行了编译,并进行了反汇编分析。

首先看下Xcode Assembler对memcmp直接翻译成未优化的汇编代码版本:

...Ltmp20:    movzbl  -33(%rbp), %edx//取一个指针    .loc    1 86 18 is_stmt 0       ## /Users/.../Desktop/test3/test3/main.c:86:18    movzbl  -34(%rbp), %esi//再取一个指针    .loc    1 86 16                 ## /Users/.../Desktop/test3/test3/main.c:86:16    cmpl    %esi, %edx//比较:u1!=u2Ltmp21:    .loc    1 86 14                 ## /Users/.../Desktop/test3/test3/main.c:86:14    je  LBB1_4//如果 u1等于u2, 跳到LBB1-4,重新开始循环## BB#3:    .loc    1 87 21 is_stmt 1       ## /Users/.../Desktop/test3/test3/main.c:87:21Ltmp22:    movzbl  -33(%rbp), %eax//否则进行u1-u2计算。先取u1。    .loc    1 87 24 is_stmt 0       ## /Users/.../Desktop/test3/test3/main.c:87:24    movzbl  -34(%rbp), %ecx//再取u2。    .loc    1 87 23                 ## /Users/.../Desktop/test3/test3/main.c:87:23    subl    %ecx, %eax//u1-u2。subl S,D等于源码里的 D-S,顺序是反的,计算结果会存到D处。    .loc    1 87 13                 ## /Users/.../Desktop/test3/test3/main.c:87:13    movl    %eax, -4(%rbp)//准备将u1-u2的值出栈    jmp LBB1_7Ltmp23:LBB1_4:                                 ##   in Loop: Header=BB1_1 Depth=1    .loc    1 89 5 is_stmt 1        ## /Users/.../Desktop/test3/test3/main.c:89:5    jmp LBB1_5...

在这个版本里可以看到u1!=u2在汇编层面是使用cmpl %esi, %edx。cmp的实现是基于减法。cmp(比较)与sub(减法)指令在指令的执行阶段都会进入CPU ALU逻辑单元进行u1-u2的计算,然后根据结果更改条件码,唯一不同的是在指令的写回阶段,sub指令将会把计算结果存入到寄存器,而cmp指令不保存计算结果(https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-23/index.html)。这也就导致了在源码层return (u1-u2)处,汇编层还要进行一次减法运算(subl %ecx, %eax)。

再看下在OSX终端用gcc编译器进行优化编译后的反汇编代码版本:

00000000000000b0    pushq   %rbp00000000000000b1    movq    %rsp, %rbp00000000000000b4    jmp 0xc900000000000000b6    nopw    %cs:_main(%rax,%rax)00000000000000c0    decq    %rdx00000000000000c3    incq    %rdi00000000000000c6    incq    %rsi00000000000000c9    testq   %rdx, %rdx//检查n!=000000000000000cc    je  0xda//如果n==0,跳到0xda处,直接返回000000000000000ce    movzbl  _main(%rdi), %eax//取u100000000000000d1    movzbl  _main(%rsi), %ecx//取u200000000000000d4    subl    %ecx, %eax//u1-u2,结果存入%eax00000000000000d6    je  0xc0//如果u1-u2=0,也既是u1==u2,跳到0xc0再次开始循环00000000000000d8    jmp 0xdc//否则,既是u1!=u2,跳到0xdc,将%eax出栈00000000000000da    xorl    %eax, %eax//循环结束,返回000000000000000dc    popq    %rbp00000000000000dd    retq

此版本有两处巧妙的优化,第一是将源码层if ( u1!=u2)与return (u1-u2)在汇编层合并成了一次subl %ecx, %eax。上面提过sub指令在指令的执行阶段会根据ALU的计算结果更改条件码,所以subl %ecx, %eax在指令的执行阶段既完成了u1!=u2的判断,并在指令的写回阶段完成了u1-u2。第二处优化,是由于sub指令在写回阶段会将计算结果存入D(subl S,D),同时一个方法的返回值需要存在%eax寄存器内,所以subl %ecx, %eax相当于把u1-u2的结果放到了一个随时可以出栈(返回)的状态。因此,如果是return 1,还需要增加一条将一个立即数传送到eax%的汇编指令,return (u1-u2)比return 1还要快。

将return (u1-u2)改为return 1 后的版本:

00000000000000b0    pushq   %rbp00000000000000b1    movq    %rsp, %rbp00000000000000b4    jmp 0xc900000000000000b6    nopw    %cs:_main(%rax,%rax)00000000000000c0    decq    %rdx00000000000000c3    incq    %rdi00000000000000c6    incq    %rsi00000000000000c9    testq   %rdx, %rdx00000000000000cc    je  0xe100000000000000ce    movl    $0x1, %eax//此处多了一个立即数传送,并且还是在循环之内00000000000000d3    movzbl  _main(%rsi), %r8d00000000000000d7    movzbl  _main(%rdi), %ecx00000000000000da    cmpl    %r8d, %ecx//此处由sub变为了cmp比较00000000000000dd    je  0xc000000000000000df    jmp 0xe300000000000000e1    xorl    %eax, %eax00000000000000e3    popq    %rbp00000000000000e4    retq

总结, if ( u1 != u2) return (u1-u2) 这种判断与返回形式通过利用了sub指令在指令的执行阶段进行的判断(修改条件码)和在写回阶段将计算结果直接存入栈返回默认寄存器(%eax)的特点,从而达到了在一个指令序列中就完成了判断与计算的高效优化。当然除了return处,其他地方,例如u1 = * (unsigned char *) s1;在汇编层变为movzbl _main(%rdi), %eax这种高效转化也值得研究。
————————————————————————————
参考:
https://stackoverflow.com/questions/43289/comparing-two-byte-arrays-in-net
https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-23/index.html –Oracle
深入理解计算机系统 –R.E.Bryant,D.R.O’Hallaron

————————————————————————————
日志:
2017-7-4: 修改了标题
2017-8-22:将“..最快的方法是memcmp.c。”改为“..最快的方法是通过[Dllimport]第三方调用的memcmp.c。”

阅读全文
0 0
原创粉丝点击