上过大学的计算机系的莘莘学子们,遗憾啊,在国产的教科书的恶臭的熏陶下,四年的青春流逝,悲哀啊!很幸运在大学中有很多研究GNU的人,这些人是幸运的,在学会了hello world之后就和国产教科书分道扬镳了。
前面的文章分析了strstr的各种实现,本文分析两个更加普遍的函数,这就是strcpy以及memcpy,strcpy是各大计算机类型考试或者面试中几乎必不可少的考点,国产教科书或者微软官方几乎在某方面达成了一致,代码的艺术性仅仅停留在可读性这种展现给人的形式上,于是很多的越来越短的代码产生了,将while换成for就可以节省最少一行代码,然而编译之后的二进制形式中,机器却没有得到任何好处,诸如*dest++ = *src++之类的代码太泛滥了,很多面试官也会因为应聘者写出这类代码而得出该应聘者基本符合要求的结论,这些人都走进了一个误区,代码最终其实是让机器执行的,越高效越好。我最近应聘时也面临了这个考题,令我十分欣慰的是,在我没有看过GNU的strcpy代码的前提下我竟然写出了与之类似的代码,当面试官问我为何这么写的时候,我说出了这么写可以每次循环最少减少一条指令的执行,这也许和我多年来研究linux和BSD等代码有关,微软似乎十分害怕开发者得了甚解,于是要么用可读性很差的汇编写库函数(对应的C代码也不是很高效),要么就用很多宏把你绕来绕去(也许是我的误解,实际上windows宏很多时候是为了兼容性而设置的),微软希望人们对它提供的接口之上的世界熟悉到不能再熟悉,而对接口之下的世界一无所知,然而我们并不买账!我在面试时写的strcpy我就不写了,这里直接写出GNU的代码:
char * strcpy (char *dest, const char *src)
{
reg_char c;
char *__unbounded s = (char *__unbounded) CHECK_BOUNDS_LOW (src);
const ptrdiff_t off = CHECK_BOUNDS_LOW (dest) - s - 1;
size_t n;
do //注意此算法利用了进程平坦的内存模型,虚拟内存平坦铺开,于是任意两个指针的差就是二者之间的距离。
{ //得到地址的相对距离off就不用绝对地址来寻址了,这样在循环中可以少一次dest++操作,而多出来的相对地址定位操作则完全可以用寄存器高效完成
c = *s++;
s[off] = c;
}
while (c != '/0');
n = s - src;
(void) CHECK_BOUNDS_HIGH (src + n);
(void) CHECK_BOUNDS_HIGH (dest + n);
return dest;
}
另外可以用GNU的strlen中的算法来实现,一次一个word的copy而不是一个char,该算法在字符串很长的时候是很高效的,一会我会分析GNU的memcpy的实现,它就用了类似的算法,将内存进行分割,按照机器支持的page,word,byte的顺序从大到小的对齐粒度copy,在讨论memcpy之前先看看以上的strcpy中的另一个技巧,看一下CHECK_BOUNDS_LOW,这个机制是编译器相关的,其实就是判断数组越界的,当检测到数组下标越界的时候就会触发一个5号异常,由操作系统负责处理,该宏的一般写法是:
#define BOUNDS_VIOLATED int $5
#define CHECK_BOUNDS_LOW(VAL_REG, BP_MEM)/
cmpq 8+BP_MEM, VAL_REG; /
jae 0f; /
BOUNDS_VIOLATED; /
0:
然而看看GNU的写法,巧妙了利用了&&操作符,只有在前面为真的情况下才执行后面的,而不是将两端的值都计算完毕之后再相与操作:
#define BOUNDS_VIOLATED (__builtin_trap (), 0)
#define CHECK_BOUNDS_LOW(ARG) /
(((__ptrvalue (ARG) < __ptrlow (ARG)) && BOUNDS_VIOLATED), /
__ptrvalue (ARG))
下面看看GNU的memcpy的操作,首先先看看几个预定义的宏:
#define op_t unsigned long int
#define OPSIZ (sizeof(op_t))
#define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left, nbytes) /
do /
{ /
if ((nbytes) >= PAGE_COPY_THRESHOLD && /
PAGE_OFFSET ((dstp) - (srcp)) == 0) /
{
//注意这里不是将dstp取反,而是直接加上一个符号,这个操作可以求出该dstp向下离最近的页面对齐的地址还有多远。
size_t nbytes_before = PAGE_OFFSET (-(dstp)); /
if (nbytes_before != 0) /
{ //首先拷贝这些页面不对齐的离散数据 /
WORD_COPY_FWD (dstp, srcp, nbytes_left, nbytes_before); /
assert (nbytes_left == 0); /
nbytes -= nbytes_before; /
}
//拷贝整页的数据 /
PAGE_COPY_FWD (dstp, srcp, nbytes_left, nbytes); /
} /
} while (0)
#define PAGE_OFFSET(n) ((n) & (PAGE_SIZE - 1)) //页面对齐
由于对齐操作可以有效利用硬件cache line,于是对齐后的操作将是十分高效的,但是如果一个数据段的大小还不足一个对齐数据段的大小,比如对不足一个页面的数据进行页面对齐就没有意义净增加了管理开销,,于是经过权衡之后,代码可能并不是很直观,但是无论如何,思想还是很重要的,下面请看GNU的memcpy:
#define OP_T_THRES 8
void * memcpy (void *dstpp, const void *srcpp, size_t len)
{
unsigned long int dstp = (long int) dstpp; //将内存尽可能转为大的数据类型,可以用机器内置的操作指令进行操作,比如movl,movb
unsigned long int srcp = (long int) srcpp;
if (len >= OP_T_THRES) //只有在数据长度大于一个阀值时才值得这么做
{
len -= (-dstp) % OPSIZ; //减去将要拷贝的数据长度
BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ); //dstp前直接加负号然后与OPSIZ取模得到了到下一个word对齐的数据段的偏移,首先要将此段数据拷贝之后才可以按照word对齐来拷贝数据
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
WORD_COPY_FWD (dstp, srcp, len, len);
}
BYTE_COPY_FWD (dstp, srcp, len); //收尾拷贝操作
return dstpp;
}
实际上在很多机器上根本就没有页面拷贝的指令,很多机器上最大的操作指令就是word操作指令,比如在x86机器上的页面拷贝宏实际上就是一个空操作,很多的操作都是通过word操作来完成的。
GNU的代码是很难懂的,但是处处充满了玄机,很有意思,如果有机会调试一下是很有趣的,如果论可读性而言,BSD的libc的代码是很好的,然而GNU的代码从来就不是让读的,它的高效性似乎已经无库可及了。实际上string的库函数都可以用类似strlen的算法来实现,char在32位系统上就一个字节,操作一个字节实际上是很低效的,于是可以转化为操 作多个字节来实现string的库函数,这种算法有个前提就是string字符串在内存中是连续的,人可以将之看做一个字节一个字节的字符组成的串,但是 既然是连续的,那么机器完全可以将之看做一个word一个word的word组成的串,需要做的仅仅就是在操作word的时候判断是否到达串尾,而到达串 尾这个判断很简单,只要一个word中的某个字节全部为0就可以了,这个判断的实现在strlen中是很清晰的,于是任意操作小数据类型的操作都可以转换 为操作大数据类型的操作,这样会使机器少执行几轮循环,优化的前提是机器本身对转换后的大数据类型提供原生支持而不是模拟支持,比如word就被x86所 支持,int,long long等都被支持,而更大的页面大小的数据类型就不被指令直接支持。