• strlen源码剖析(可查看glibc和VC的CRT源代码)


    学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效。恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简单的函数strlen研究了一下,并对各种实现作了简单的效率测试。

    strlen的函数原形如下:

          size_t strlen(const char *str);

    strlen返回str中字符的个数,其中str为一个以''结尾的字符串(a null-terminated string)。

    1. 简单实现
    如果不管效率,最简单的实现只需要4行代码:

    1 size_t strlen_a(const char * str) {
    2     size_t length = 0
     ;
    3     while (*str++
     )
    4         ++
     length;
    5     return
      length;
    6 }

    也许可以稍加改进如下:

    1 size_t strlen_b(const char * str) {
    2     const char *cp =
      str;
    3     while (*cp++
     )
             ;
    5     return (cp - str - 1
     );
    6 }

    2. 高效实现 
    很显然,标准库的实现肯定不会如此简单,上面的strlen_a以及strlen_b都是一次判断一个字符直到发现''为止,这是非常低效的。比较高效的实现如下(在这里WORD表示计算机中的一个字,不是WORD类型):
    (1) 一次判断一个字符直到内存对齐,如果在内存对齐之前就遇到''则直接return,否则到(2);
    (2) 一次读入并判断一个WORD,如果此WORD中没有为0的字节,则继续下一个WORD,否则到(3);
    (3) 到这里则说明WORD中至少有一个字节为0,剩下的就是找出第一个为0的字节的位置然后return。

    NOTE:
    数据对齐(data alignment),是指数据所在的内存地址必须是该数据长度的整数倍,这样CPU的存取速度最快。比如在32位的计算机中,一个WORD为4 byte,则WORD数据的起始地址能被4整除的时候CPU的存取效率比较高。CPU的优化规则大概如下:对于n字节(n = 2,4,8...)的元素,它的首地址能被n整除才能获得最好的性能。

    为了便于下面的讨论,这里假设所用的计算机为32位,即一个WORD为4个字节。下面给出在32位计算机上的C语言实现(假设unsigned long为4个字节):

     1 typedef unsigned long  ulong;
     2 
     
     3 size_t strlen_c(const char * str) {
     4 
     
     5     const char * char_ptr;
     6     const ulong *
     longword_ptr;
     7 
         register ulong longword, magic_bits;
     8 
     
     9     for (char_ptr =  str; ((ulong)char_ptr 
    10         & (sizeof(ulong) - 1)) != 0
     ;
    11         ++
     char_ptr) {
    12         if (*char_ptr == ''
     )
    13             return char_ptr -
      str;
    14 
         }
    15 
     
    16     longword_ptr = (ulong* )char_ptr;
    17 
     
    18     magic_bits = 0x7efefeffL ;
    19 
     
    20     while (1 ) {
    21 
     
    22         longword = *longword_ptr++ ;
    23 
     
    24         if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0 ) {
    25 
     
    26             const char *cp = (const char*)(longword_ptr - 1 );
    27 
                 
    28             if (cp[0] == 0
     )
    29                 return cp -
      str;
    30             if (cp[1] == 0
     )
    31                 return cp - str + 1
     ;
    32             if (cp[2] == 0
     )
    33                 return cp - str + 2
     ;
    34             if (cp[3] == 0
     )
    35                 return cp - str + 3
     ;
    36 
             }
    37 
         }
    38 }

    3. 源码剖析 
    上面给出的C语言实现虽然不算特别复杂,但也值得花点时间来弄清楚,先看9-14行:
    for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr) {
        if (*char_ptr == ''
    )
            return char_ptr -
     str;
    }

    上面的代码实现了数据对齐,如果在对齐之前就遇到''则可以直接return char_ptr - str;

    第16行将longword_ptr指向数据对齐后的首地址

    longword_ptr = (ulong*)char_ptr;

    第18行给magic_bits赋值(在后面会解释这个值的意义)
    magic_bits = 0x7efefeffL;

    第22行读入一个WORD到longword并将longword_ptr指向下一个WORD
    longword = *longword_ptr++;

    第24行的if语句是整个算法的核心,该语句判断22行读入的WORD中是否有为0的字节
    if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)
    if语句中的计算可以分为如下3步:
    (1) longword + magic_bits
    其中magic_bits的二进制表示如下:
                      b3      b2       b1       b0
                  31------------------------------->0

      magic_bits: 01111110 11111110 11111110 11111111
    magic_bits中的31,24,16,8这些bits都为0,我们把这几个bits称为holes,注意在每个byte的左边都有一个hole。

    检测0字节:
    如果longword 中有一个字节的所有bit都为0,则进行加法后,从这个字节的右边的字节传递来的进位都会落到这个字节的最低位所在的hole上,而从这个字节的最高位则永远不会产生向左边字节的hole的进位。则这个字节左边的hole在进行加法后不会改变,由此可以检测出0字节;相反,如果longword中所有字节都不为0,则每个字节中至少有1位为1,进行加法后所有的hole都会被改变。

    为了便于理解,请看下面的例子:
                      b3      b2       b1       b0
                  31------------------------------->0
      longword:   XXXXXXXX XXXXXXXX 00000000 XXXXXXXX
    + magic_bits: 01111110 11111110 11111110 11111111
    上面longword中的b1为0,X可能为0也可能为1。因为b1的所有bit都为0,而从b0传递过来的进位只可能是0或1,很显然b1永远也不会产生进位,所以加法后longword的第16 bit这个hole不会变。

    (2)  ^ ~longword
    这一步取出加法后longword中所有未改变的bit。

    (3)  & ~magic_bits
    最后取出longword中未改变的hole,如果有任何hole未改变则说明longword中有为0的字节。

    根据上面的描述,如果longword中有为0的字节,则if中的表达式结果为非0,否则为0。
    NOTE:
    如果b3为10000000,则进行加法后第31 bit这个hole不会变,这说明我们无法检测出b3为10000000的所有WORD。值得庆幸的是用于strlen的字符串都是ASCII标准字符,其值在0-127之间,这意味着每一个字节的第一个bit都为0。因此上面的算法是安全的。

    一旦检测出longword中有为0的字节,后面的代码只需要找到第一个为0的字节并返回相应的长度就OK:
    const char *cp = (const char*)(longword_ptr - 1);

    if (cp[0] == 0
    )
        return cp -
     str;
    if (cp[1] == 0
    )
        return cp - str + 1
    ;
    if (cp[2] == 0
    )
        return cp - str + 2
    ;
    if (cp[3] == 0
    )
        return cp - str + 3;

    4. 另一种实现
     1 size_t strlen_d(const char *str) {
     2 

     3     const char *char_ptr;
     4     const ulong *
    longword_ptr;
     5 
        register ulong longword, himagic, lomagic;
     6 

     7     for (char_ptr = str; ((ulong)char_ptr 
     8         & (sizeof(ulong) - 1)) != 0
    ;
     9         ++
    char_ptr) {
    10         if (*char_ptr == ''
    )
    11             return char_ptr -
     str;
    12 
        }
    13 

    14     longword_ptr = (ulong*)char_ptr;
    15 

    16     himagic = 0x80808080L;
    17     lomagic = 0x01010101L
    ;
    18 

    19     while (1) {
    20 

    21         longword = *longword_ptr++;
    22 

    23         if (((longword - lomagic) & himagic) != 0) {
    24 

    25             const char *cp = (const char*)(longword_ptr - 1);
    26 
                
    27             if (cp[0] == 0
    )
    28                 return cp -
     str;
    29             if (cp[1] == 0
    )
    30                 return cp - str + 1
    ;
    31             if (cp[2] == 0
    )
    32                 return cp - str + 2
    ;
    33             if (cp[3] == 0
    )
    34                 return cp - str + 3
    ;
    35 
            }
    36 
        }
    37 }
    上面的代码与strlen_c基本一样,不同的是:
    magic_bits换成了himagic和lomagic
    himagic = 0x80808080L;
    lomagic = 0x01010101L;
    以及 if语句变得比较简单了
    if (((longword - lomagic) & himagic) != 0)

    if语句中的计算可以分为如下2步:
    (1) longword - lomagic
    himagic和lomagic的二进制表示如下:
                    b3      b2       b1       b0
                31------------------------------->0

      himagic:  10000000 10000000 10000000 10000000
      lomagic:  00000001 00000001 00000001 00000001

    在这种方法中假设所有字符都是ASCII标准字符,其值在0-127之间,因此longword总是如下形式:
                    b3      b2       b1       b0
                31------------------------------->0

      longword: 0XXXXXXX 0XXXXXXX 0XXXXXXX 0XXXXXXX

    检测0字节:
    如果longword 中有一个字节的所有bit都为0,则进行减法后,这个字节的最高位一定会从0变为1;相反,如果longword中所有字节都不为0,则每个字节中至少有1位为1,进行减法后这个字节的最高位依然为0。

     (2)  & himagic
    这一步取出每个字节最高位的1,如果有任意字节最高位为1则说明longword中有为0的字节。

    根据上面的描述,如果longword中有为0的字节,则if中的表达式结果为非0,否则为0。

    5. 汇编实现
    VC CRT的汇编实现与前面strlen_c算法类似

      1         page    ,132
      2         title   strlen - return the length of a null-terminated string
      3 ;***

      4 ;strlen.asm - contains strlen() routine
      5 
    ;
      6 
    ;       Copyright (c) Microsoft Corporation. All rights reserved.
      7 
    ;
      8 
    ;Purpose:
      9 ;       strlen returns the length of a null-
    terminated string,
     10 ;       not including the null byte
     itself.
     11 
    ;
     12 ;*******************************************************************************

     13 
     14         .xlist
     15 
            include cruntime.inc
     16 
            .list
     17 

     18 page
     19 ;***

     20 ;strlen - return the length of a null-terminated string
     21 
    ;
     22 
    ;Purpose:
     23 
    ;       Finds the length in bytes of the given string, not including
     24 ;       the final null
     character.
     25 
    ;
     26 
    ;       Algorithm:
     27 ;       int strlen (const char *
     str)
     28 
    ;       {
     29 ;           int length = 0
    ;
     30 
    ;
     31 ;           while( *str++
     )
     32 ;                   ++
    length;
     33 
    ;
     34 ;           return
    ( length );
     35 
    ;       }
     36 
    ;
     37 
    ;Entry:
     38 ;       const char * str -
     string whose length is to be computed
     39 
    ;
     40 
    ;Exit:
     41 ;       EAX = length of the string "str", exclusive of the final null byte

     42 ;
     43 
    ;Uses:
     44 
    ;       EAX, ECX, EDX
     45 
    ;
     46 
    ;Exceptions:
     47 
    ;
     48 ;*******************************************************************************

     49 
     50         CODESEG
     51 

     52         public  strlen
     53 

     54 strlen  proc 
     55         buf:ptr byte

     56 
     57         OPTION PROLOGUE:NONE, EPILOGUE:NONE
     58 

     59         .FPO    ( 0, 1, 0, 0, 0, 0 )
     60 

     61 string  equ     [esp + 4]
     62 

     63         mov     ecx,string              ; ecx -> string
     64         test    ecx,3                   ; test if string is aligned on 32
     bits
     65         je      short
     main_loop
     66 

     67 str_misaligned:
     68         ; simple byte
     loop until string is aligned
     69         mov     al,byte
     ptr [ecx]
     70         add     ecx,1

     71         test    al,al
     72         je      short
     byte_3
     73         test    ecx,3

     74         jne     short str_misaligned
     75 

     76         add     eax,dword ptr 0         ; 5 byte nop to align label below
     77 

     78         align   16                      ; should be redundant
     79 

     80 main_loop:
     81         mov     eax,dword ptr [ecx]     ; read 4
     bytes
     82 
            mov     edx,7efefeffh
     83 
            add     edx,eax
     84         xor     eax,-1

     85         xor     eax,edx
     86         add     ecx,4

     87         test    eax,81010100h
     88         je      short
     main_loop
     89         ; found zero byte
     in the loop
     90         mov     eax,[ecx - 4
    ]
     91         test    al,al                   ; is it byte 0

     92         je      short byte_0
     93         test    ah,ah                   ; is it byte 1

     94         je      short byte_1
     95         test    eax,00ff0000h           ; is it byte 2

     96         je      short byte_2
     97         test    eax,0ff000000h          ; is it byte 3

     98         je      short byte_3
     99         jmp     short main_loop         ; taken if bits 24-30
     are clear and bit
    100                                         ; 31
     is set
    101 

    102 byte_3:
    103         lea     eax,[ecx - 1
    ]
    104 
            mov     ecx,string
    105 
            sub     eax,ecx
    106 
            ret
    107 
    byte_2:
    108         lea     eax,[ecx - 2
    ]
    109 
            mov     ecx,string
    110 
            sub     eax,ecx
    111 
            ret
    112 
    byte_1:
    113         lea     eax,[ecx - 3
    ]
    114 
            mov     ecx,string
    115 
            sub     eax,ecx
    116 
            ret
    117 
    byte_0:
    118         lea     eax,[ecx - 4
    ]
    119 
            mov     ecx,string
    120 
            sub     eax,ecx
    121 
            ret
    122 

    123 strlen  endp
    124 

    125         end

    6. 测试结果
    为了对上述各种实现的效率有一个大概的认识,我在VC8和GCC下分别进行了测试,测试时均采用默认优化方式。下面是在GCC下运行几百万次后的结果(在VC8下的运行结果与此相似):
    strlen_a
    --------------------------------------------------

           1:        515 ticks         0.515 seconds
           2:        375 ticks         0.375
     seconds
           3:        375 ticks         0.375
     seconds
           4:        375 ticks         0.375
     seconds
           5:        375 ticks         0.375
     seconds
       total:       2015 ticks         2.015
     seconds
     average:        403 ticks         0.403
     seconds
    --------------------------------------------------


    strlen_b
    --------------------------------------------------
           1:        360 ticks          0.36 seconds
           2:        390 ticks          0.39
     seconds
           3:        375 ticks         0.375
     seconds
           4:        360 ticks          0.36
     seconds
           5:        375 ticks         0.375
     seconds
       total:       1860 ticks          1.86
     seconds
     average:        372 ticks         0.372
     seconds
    --------------------------------------------------


    strlen_c
    --------------------------------------------------
           1:        187 ticks         0.187 seconds
           2:        172 ticks         0.172
     seconds
           3:        187 ticks         0.187
     seconds
           4:        187 ticks         0.187
     seconds
           5:        188 ticks         0.188
     seconds
       total:        921 ticks         0.921
     seconds
     average:        184 ticks        0.1842
     seconds
    --------------------------------------------------


    strlen_d
    --------------------------------------------------
           1:        172 ticks         0.172 seconds
           2:        187 ticks         0.187
     seconds
           3:        172 ticks         0.172
     seconds
           4:        187 ticks         0.187
     seconds
           5:        188 ticks         0.188
     seconds
       total:        906 ticks         0.906
     seconds
     average:        181 ticks        0.1812
     seconds
    --------------------------------------------------


    strlen
    --------------------------------------------------
           1:        187 ticks         0.187 seconds
           2:        172 ticks         0.172
     seconds
           3:        188 ticks         0.188
     seconds
           4:        172 ticks         0.172
     seconds
           5:        187 ticks         0.187
     seconds
       total:        906 ticks         0.906
     seconds
     average:        181 ticks        0.1812
     seconds
    --------------------------------------------------

    源代码:点击下载

    posted on 2007-10-12 13:19 蚂蚁终结者 阅读(26306) 评论(34)  编辑 收藏 引用 所属分类: The Annotated CRT Sources

    Feedback

    # re: strlen源码剖析 2007-09-26 19:29 msdn47

    果然很快,效率高啊  回复  更多评论   

    # re: strlen源码剖析 2007-09-26 21:29 hi

    (2) ^ ~longword
    这一步取出加法后longword中所有未改变的bit。

    ======================================

    这个什么意思?  回复  更多评论   

    # re: strlen源码剖析 2007-09-27 08:19 蚂蚁终结者

    可能我说的不够清楚,看下面的例子:
                       b3      b2       b1       b0 
                   31------------------------------->0 
         longword: 00001001 00011000 00000000 00001100 
    +  magic_bits: 01111110 11111110 11111110 11111111
              sum: 10001000 00010110 11111111 00001011
    ^   ~longword: 11110110 11100111 11111111 11110011
                a: 01111110 11110001 00000000 11111000 
    & ~magic_bits: 10000001 00000001 00000001 00000000
           result: 00000000 00000001 00000000 00000000
    sum = longword + magic_bits;
    a = sum ^ ~longword;
    即用sum与longword逐位比较,如果有某个位相同,就说这个位在加法后未改变,这样在a中为1的位就是未改变的。
     
    result = a & ~magic_bits;
    得到未改变的hole位,从上例可以看出第16 bit这个hole加法后未改变,这样就检测出了0字节。
     
      回复  更多评论   

    # re: strlen源码剖析 2007-09-27 08:28 wangmuy

    赞啊!以前不懂的今天终于看懂了!  回复  更多评论   

    # re: strlen源码剖析[未登录] 2007-09-27 08:42 漂舟

    楼主剖析得全面,顶 !  回复  更多评论   

    # re: strlen源码剖析 2007-09-27 08:52 Yong Sun

    只能检测0~127,是个问题,最好能兼容0~255,有部分制表符和扩展字符位于这个区域。  回复  更多评论   

    # re: strlen源码剖析 2007-09-27 09:06 蚂蚁终结者

    实际上0~255都能检测出来的:
    if (cp[0] == 0)
        return cp - str;
    if (cp[1] == 0)
        return cp - str + 1;
    if (cp[2] == 0)
        return cp - str + 2;
    if (cp[3] == 0)
        return cp - str + 3;
    如果上面的语句执行完还没有return,则会继续下一次循环,这样还是能检测到在if语句中漏掉的128~255,只不过效率上会有所损失。如果要检测0~255之间的字符,strlen_c比strlen_d要好。因为strlen_c只会漏掉这样的WORD:
        10000000 XXXXXXXX XXXXXXXX XXXXXXXX

    http://www.cppblog.com/ant/archive/2007/10/12/32886.aspx

    http://www.cnblogs.com/rollenholt/archive/2012/04/12/2443719.html

  • 相关阅读:
    单元测试-伪对象、桩对象、模拟对象
    单元测试那些事
    vscode 开发go版本1.14项目
    C#中的 async await
    一手遮天 Android
    一手遮天 Android
    一手遮天 Android
    一手遮天 Android
    一手遮天 Android
    一手遮天 Android
  • 原文地址:https://www.cnblogs.com/findumars/p/6375492.html
Copyright © 2020-2023  润新知