标签 GNU 下的文章

GLIBC strlen源代码分析

直接操作C标准库提供的字符串操作函数是有一定风险的,稍有不慎就会导致内存问题。这周用业余时间写了一个小型的安全字符串操作库,但是测试之后才发现自己的实现有很大的性能缺陷。

在Solaris上初步做了一个简单的性能比对,以下是得到的性能数据(以strlen的数据为例):
当传入的字符串长度为10时,执行100w次:
strlen 执行时间是:32762毫秒
my_strlen执行时间是:491836毫秒

当传入的字符串长度为20时,执行100w次:
strlen 执行时间是:35075毫秒
my_strlen执行时间是:770397毫秒

很显然,标准库中strlen的消耗仅是my_strlen的十分之一不到,且其性能消耗随着字符串长度的增加并未有近线性的增加,而my_strlen则是变化明显。想必大家这时也能猜到my_strlen采用了传统的实现的方式,即采用逐个字节判断是否为''方式,这也与测试出的现象相符。本着刨根问底的精神,我在网上找到了GNU提供的C标准库中strlen实现的源码,要看看GLIBC中strlen究竟采用何种技巧才达到了那么高的性能。说实话在性能优化这方面自己一直还处于比较初级的位置,这也将是自己将来努力的一个方向。

下载了全部GLIBC的代码包,这个包还真不小。在string子目录下找到strlen.c,这就是大多数UNIX平台、Linux平台以及绝大多数GNU软件使用的strlen的实现源码了。这份代码由Torbjorn Granlund(还实现了memcpy)编写,Jim Blandy和Dan Sahlin提供了帮助和注释。包括注释在内,GLIBC的strlen的代码足足有近130行,大致浏览一下, 没有怎么看懂,可耐下心来细致阅读,还是有些心得的。下面是strlen源码摘要版,后面我将针对这段代码写一些我的理解:
 
  1 /* Return the length of the null-terminated string STR.  Scan for
  2    the null terminator quickly by testing four bytes at a time.  */
  3 size_t strlen (str)  const char *str;
  4 {
  5         const char *char_ptr;
  6         const unsigned long int *longword_ptr;
  7         unsigned long int longword, magic_bits, himagic, lomagic;
  8
  9         /* Handle the first few characters by reading one character at a time.
 10            Do this until CHAR_PTR is aligned on a longword boundary.  */
 11
 12         for (char_ptr = str; ((unsigned long int) char_ptr
 13              & (sizeof (longword) – 1)) != 0;
 14              ++char_ptr)
 15                 if (*char_ptr == '')
 16                         return char_ptr – str;
 17
 18         /* All these elucidatory comments refer to 4-byte longwords,
 19            but the theory applies equally well to 8-byte longwords.  */
 20
 21         longword_ptr = (unsigned long int *) char_ptr;
 22
 23         himagic = 0x80808080L;
 24         lomagic = 0x01010101L;
 25
 26         if (sizeof (longword) > 8)
 27                 abort ();
 28
 29         /* Instead of the traditional loop which tests each character,
 30            we will test a longword at a time.  The tricky part is testing
 31            if *any of the four* bytes in the longword in question are zero.  */
 32
 33         for (;;)     
 34         {                        
 35                 longword = *longword_ptr++;    
 36
 37                 if ( ((longword – lomagic) & himagic) != 0)
 38                 {
 39                         /* Which of the bytes was the zero?  If none of them were, it was
 40                            a misfire; continue the search.  */
 41
 42                         const char *cp = (const char *) (longword_ptr – 1);
 43
 44                         if (cp[0] == 0)
 45                                 return cp – str;
 46                         if (cp[1] == 0)
 47                                 return cp – str + 1;
 48                         if (cp[2] == 0)
 49                                 return cp – str + 2;
 50                         if (cp[3] == 0)
 51                                 return cp – str + 3;
 52                         if (sizeof (longword) > 4)
 53                         {
 54                                 if (cp[4] == 0)
 55                                         return cp – str + 4;
 56                                 if (cp[5] == 0)
 57                                         return cp – str + 5;
 58                                 if (cp[6] == 0)
 59                                         return cp – str + 6;
 60                                 if (cp[7] == 0)
 61                                         return cp – str + 7;
 62                         }
 63                 }
 64         }
 65 }

从这段代码开头作者的注释我们大致可以了解到该strlen实现的原理:就是通过每次测试四个字节来代替传统实现中每次测试一个字节的方法。知道这个原理了,那么还需要解决两个难题:
1) C标准库要求有很好的移植性,在绝大部分系统体系结构下都应该能正确运行。那么每次拿出4个字节比较(unsigned long int),就需要考虑内存对齐问题,传入的字符串的首字符地址可不一定在4对齐的地址上;
2) 如何对四个字节进行测试,找出其中某个字节为全0,这是个技巧问题。

12~21行的代码解决的就是第一个问题:
      for (char_ptr = str; ((unsigned long int) char_ptr
             & (sizeof (longword) – 1)) != 0;
             ++char_ptr)
                if (*char_ptr == '')
                        return char_ptr – str;

        /* All these elucidatory comments refer to 4-byte longwords,
           but the theory applies equally well to 8-byte longwords.  */

        longword_ptr = (unsigned long int *) char_ptr;
作者通过一个for-loop找到传入字符串中第一个地址对齐到4的字符的地址,由于该地址已经对齐到4,所以最后一行那个强制转型是安全的。虽然可以通过圆整算式直接得到该对齐地址,但是考虑到这个区间可能存在的'',一个字符一个字符比对也是不可避免的。在很多严格对齐的架构上(比如SUN的SPARC平台),编译器一般会将字符串地址在编译器就放到对齐的地址上,这样一来,实际执行strlen时for-loop很少能执行一步。

第二个问题作者则是通过一个"带前提"的技巧来解决的。作者设定了两个掩码变量:
 himagic = 0x80808080L;
 lomagic = 0x01010101L;
并通过一个conditional expression完成了对四字节中全0字节的检测:((longword – lomagic) & himagic) != 0
我们将himagic和lomagic按bit展开:
himagic   1000 0000 1000 0000 1000 0000 1000 0000
lomagic   0000 0001 0000 0001 0000 0001 0000 0001

对于这样的代码,似乎没有什么理论可以遵循,需要在实践中去理解。起初我构造了一个不含全0字节的longword,比如:
longword  1000 0001 1000 0001 1000 0001 1000 0001,然后按照那个条件表达式计算后,居然也满足!=0的条件,是不是作者的逻辑有问题呢?后来转念一想,这种逻辑是有“前提条件”的。回顾一下strlen是做什么的,其输入参数是任意的么?当然不是。输入的字符串中每个字符的值都在[0, 127]的ascii码范围内,也就是说每个字节最高位的bit都是0,这样longword就应该是如下这个样子了:
longword  0xxx xxxx 0xxx xxxx 0xxx xxxx 0xxx xxxx

基于这样的前提我们考虑两种情况:
当longword中没有全0字节时,比如:
longword 0000 0001 0000 0001 0000 0001 0000 0001
这样在做完计算后,值为0,不满足条件。

当longword中有全零字节时,比如:
longword 0000 0000 0000 0001 0000 0001 0000 0001
这样在做完计算后,最高字节最高bit的值肯定为1,满足!=0条件,全0字节被检测出来。也就是说一旦有全0字节,在减去lomagic时势必会产生借位,全0的那个字节在减去lomagic后最高位bit肯定由0变1,这样与himagic一与,肯定不为0,就是这么检测出来的。

这一方法在64位平台依然适用,上面的代码摘要中省略了对64bit平台的特殊处理,为的是使代码逻辑更清晰,更易读。

使用Scons改造现有项目

今天是冬至,也是入冬以来感觉最冷的一天,毫不夸张的说:你一张嘴,牙就冻上了。上午LP在家收拾卫生,我继续用Scons改造现有的项目。下午出去理发,头发长长了后,似乎会造成思维迟钝^_^。

试验性的用Scons改造现有的project,过程中对Scons了解又多了一些。上篇文章对Scons的性能没有给出定论,经过对Scons的深入后,发现Scons在执行初始时的性能的确不够快,这是因为Scons启动后,会对全部SConstruct以及下面子目录中的SConscript进行分析,子目录越多Sconscript文件个数越多,性能也就越差。但是这种分析也有一个优点,就是能帮你提前发现你SConscript中的一些“语义”错误,比如如果你在编译两个基础库,一个叫add,一个叫sub,这个基础库源码分别分布在两个目录add和sub中,编译后将分别生成libadd.a和libsub.a的库文件,但是如果你马虎了,在编写SConscript时将target都写成了'add'或都写成了'sub',则Scons会在执行gcc之前就帮你找出这个"语义"错误,提示如下:
/export/home1/tony_bai/xxlib>scons -f SC*t
scons: Reading SConscript files …
scons: *** Multiple ways to build the same target were specified for: /export/home1/tony_bai/xxlib/lib/libsub.a  (from ['/export/home1/tony_bai/xxlib/add/libsub.a'] and from ['libsub.a'])
File "/export/home1/tony_bai/xxlib/sub/SConscript", line 3, in

Scons脚本基本写的差不多了,编译也ok了,但是编译出来的可执行程序在执行时却出现了问题:提示找不到某.so文件。而用项目"原配"的Makefile编译出来的可执行程序却执行的很好,没有类似问题,百思不得其解。将.so文件所在目录放到"LD_LIBRARY_PATH"中,问题得以解决,但这更加深了对这一现象的质疑。起初我一直以为是Scons在编译选项上不规范造成的,而Scons使用gcc -G -o xx.so xx.o来编译也的确有值得的怀疑点,-G选项是我从未见过的gcc编译选项,查了半天手册也没有对该参数的说明,遂放弃。上工具吧!先用ldd对编译出来的可执行文件进行分析,我们先来假设用Scons编译出来的可执行程序名字为Bin-scons,用"原配"Make编译出来的可执行程序名字为Bin-make。ldd将列出可执行文件中动态依赖的库的名字,并在本机定位出各个动态库的位置。对Bin-scons和Bin-make分别ldd的结果却让我大吃一惊,Bin-scons的ldd结果很正常,xx.so出现在list中,并且其位置为我刚刚加入到LD_LIBRARY_PATH中的那个目录;但是Bin-make的ldd结果中却不见了xx.so的踪影,这是怎么回事呢?回头翻看Makefile,并且又执行了多遍Make,项目的Makefile明明是构造了xx.so,在生成Bin-make时链接了xx.so,并且Bin-make中使用了xx.so中提供的接口。再次仔细对比Make和Scons编译.so时的差别,这回发现了些许不同的地方,"原配"的make在编译.so时,除了用了-shared -fPIC之外,还用了"-c"选项,而从Scons日志中只能看到gcc -G -o libxx.so xx.pic.o,显然Scons先控制gcc将xx.c编译为xx.pic.o,再由xx.pic.o构成libxx.so,而且我发现用Scons和Make编译出的.so文件大小居然不同。显然"-c"对两个编译过程带来了影响。一般来说,我们在编译一个动态库时是不会使用"-c"的,这里先不论项目Makefile写的是否ok,单说"-c"会给编译过程带来什么吧。打开gcc的"–verbose"开关,我们来试试使用和不使用"-c"gcc都做了些什么。还是以add.c为例,将add.c编译为libadd.so。

gcc -o libadd.so -shared -fPIC -c add.c –verbose
执行结果:
Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/specs
Configured with: ../configure –with-as=/usr/ccs/bin/as –with-ld=/usr/ccs/bin/ld –disable-nls
Thread model: posix
gcc version 3.2
 /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/cc1 -lang-c -v -D__GNUC__=3 -D__GNUC_MINOR__=2 -D__GNUC_PATCHLEVEL__=0 -D__GXX_ABI_VERSION=102 -Dsparc -Dsun -Dunix -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc__ -D__sun__ -D__unix__ -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc -D__sun -D__unix -Asystem=unix -Asystem=svr4 -D__NO_INLINE__ -D__STDC_HOSTED__=1 -D__SIZE_TYPE__=unsigned int -D__PTRDIFF_TYPE__=int -D__WCHAR_TYPE__=long int -D__WINT_TYPE__=long int -D__GCC_NEW_VARARGS__ -Acpu=sparc -Amachine=sparc add.c -quiet -dumpbase add.c -version -fPIC -o /var/tmp//cca0mHxn.s
GNU CPP version 3.2 (cpplib) (sparc ELF)
GNU C version 3.2 (sparc-sun-solaris2.9)
        compiled by GNU C version 3.2.
ignoring nonexistent directory "NONE/include"
ignoring nonexistent directory "/usr/local/sparc-sun-solaris2.9/include"
#include "…" search starts here:
#include search starts here:
 /usr/local/include
 /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/include
 /usr/include
End of search list.
 /usr/ccs/bin/as -V -Qy -s -K PIC -o libadd.so /var/tmp//cca0mHxn.s
/usr/ccs/bin/as: Sun WorkShop 6 update 2 Compiler Common 6.2 Solaris_9_CBE 2001/04/02

gcc -o libadd.so -shared -fPIC add.c –verbose
执行结果:
Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/specs
Configured with: ../configure –with-as=/usr/ccs/bin/as –with-ld=/usr/ccs/bin/ld –disable-nls
Thread model: posix
gcc version 3.2
 /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/cc1 -lang-c -v -D__GNUC__=3 -D__GNUC_MINOR__=2 -D__GNUC_PATCHLEVEL__=0 -D__GXX_ABI_VERSION=102 -Dsparc -Dsun -Dunix -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc__ -D__sun__ -D__unix__ -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc -D__sun -D__unix -Asystem=unix -Asystem=svr4 -D__NO_INLINE__ -D__STDC_HOSTED__=1 -D__SIZE_TYPE__=unsigned int -D__PTRDIFF_TYPE__=int -D__WCHAR_TYPE__=long int -D__WINT_TYPE__=long int -D__GCC_NEW_VARARGS__ -Acpu=sparc -Amachine=sparc add.c -quiet -dumpbase add.c -version -fPIC -o /var/tmp//ccz128Nl.s
GNU CPP version 3.2 (cpplib) (sparc ELF)
GNU C version 3.2 (sparc-sun-solaris2.9)
        compiled by GNU C version 3.2.
ignoring nonexistent directory "NONE/include"
ignoring nonexistent directory "/usr/local/sparc-sun-solaris2.9/include"
#include "…" search starts here:
#include search starts here:
 /usr/local/include
 /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/include
 /usr/include
End of search list.

 /usr/ccs/bin/as -V -Qy -s -K PIC -o /var/tmp//ccoU5RTD.o /var/tmp//ccz128Nl.s
/usr/ccs/bin/as: Sun WorkShop 6 update 2 Compiler Common 6.2 Solaris_9_CBE 2001/04/02
 /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/collect2 -V -G -dy -z text -Y P,/usr/ccs/lib:/usr/lib -Qy -o libadd.so /usr/local/lib/gcc-lib/sparc-sun-
solaris2.9/3.2/crti.o /usr/ccs/lib/values-Xa.o /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtbegin.o -L/usr/local/lib/gcc-lib/sparc-sun-
solaris2.9/3.2 -L/usr/ccs/bin -L/usr/ccs/lib -L/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/../../.. /var/tmp//ccoU5RTD.o -lgcc_s -lgcc_s
/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtend.o /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtn.o
ld: Software Generation Utilities – Solaris Link Editors: 5.9-1.276

对比这两次的执行结果,我们可以发现,使用了-c的编译过程实际上不是一个完整的共享库(动态库.so)的构建过程,而只是一个带有"-shared, -fPIC"的目标文件(.o)的编译过程,缺少gcc crt目标文件的链接过程,只是目标文件被命名为libadd.so了。这恰恰能解释我们前面提到了两点疑问了。为什么ldd Bin-make时没有发现其依赖xx.so以及Bin-make执行时一切ok,没有报“找不到xx.so”,这一切都是因为xx.so实际上是以.o形式存在的一个文件,在构建Bin-make链接xx.so时,实际上做到是静态链接而不是动态链接,xx.so中的接口代码都已经存在于Bin-make中了,所以ldd无法找到对xx.so的依赖,Bin-make执行时也无需找到xx.so了。看来这是项目Makefile中的一个问题了,只是这个"问题"隐藏太久而未能被发现罢了。

从收音机中得知"冬至"这天应该吃饺子,晚上和LP煮了两包水饺,热腾腾的,吃得直打饱嗝^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats