asp模版网站如何做优化科技论文发表网
asp模版网站如何做优化,科技论文发表网,职业生涯规划大赛ppt,如何优化网络以下有三个版本的memcpy#xff0c;对于版本3#xff0c;很多人都很熟悉#xff0c;它很经典#xff0c;很多人面试都写这个#xff0c;可是我不得不说一句#xff0c;对于类似的问题#xff0c;最好的回答有两个#xff1a;一是调用c库#xff0c;二是使用汇编。用这… 以下有三个版本的memcpy对于版本3很多人都很熟悉它很经典很多人面试都写这个可是我不得不说一句对于类似的问题最好的回答有两个一是调用c库二是使用汇编。用这一类的问题来考察应聘者的c语言能力真的很菜如果真的要考察c语言能力还不如给几个ifswitch-casefor语句呢。 版本1.linux内核中的实现其实glibc也是如此实现的省略了不少内容真正的实现很巧妙将n个字节分为了三部分(前导字节-对齐到页面页面后续字节-页面对齐后的游离字节)进行分块拷贝(linux内核是绝对不会卖弄c语言的基本的底层函数最终为了高效大多数都用汇编) char *memcpy1(char *to, char *from, size_t n) { long esi, edi; int ecx; esi (long)from; edi (long)to; asm volatile(rep ; movsl : c (ecx), D (edi), S (esi) : 0 (n / 4), 1 (edi), 2 (esi) : memory ); return to; } 版本2.memcpy2为经典的C面试者写法 char *memcpy2(char *dest, char *src, size_t n) { int i 0; while (i n) { dest[i] src[i]; i ; } return dest; } 版本3.glibc的写法 ... 仅仅这几个memcpy版本如何确定哪个更高效呢以下是一个测试的例子请看例子1。 例子1 #include stdlib.h #include stdio.h #include time.h #define COUNTS 6000000 long long getcyclenow() { unsigned int h, l; long long bs, cc; asm(rdtsc /n/t); asm(movl %%eax, %0/n/t:g(l)); asm(movl %%edx, %0/n/t:g(h)); bs h; cc (bs 32)|l; return cc; } int main(int argc, char **argv) { time_t st0, st1, st2, st3; long long l0, l1, l2, l3; int i 0; char *src (char *)calloc(1, 109); strcpy(src, iiiiabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz); char *dest0 (char *)calloc(1, 109); char *dest1 (char *)calloc(1, 109); char *dest2 (char *)calloc(1, 109); l0 getcyclenow(); dest0 memcpy(dest0, src, 108); l1 getcyclenow(); dest2 memcpy2(dest2, src, 108); l2 getcyclenow(); dest1 memcpy1(dest1, src, 108); l3 getcyclenow(); printf(%X/t%X/t%X/n, l1-l0, l2-l1, l3-l2); memset(dest0, 0, 109); memset(dest1, 0, 109); memset(dest2, 0, 109); st0 time(NULL); for (i 0; i COUNTS; i) dest0 memcpy(dest0, src, 108); st1 time(NULL); for (i 0; i COUNTS; i) dest2 memcpy2(dest2, src, 108); st2 time(NULL); for (i 0; i COUNTS; i) dest1 memcpy1(dest1, src, 108); st3 time(NULL); printf(t0:%f, t1:%f, %f/n, difftime(st1, st0), difftime(st2, st1), difftime(st3, st2)); /* memset(dest0, 0, 109); memset(dest1, 0, 109); memset(dest2, 0, 109); l0 getcyclenow(); dest0 memcpy(dest0, src, 108); l1 getcyclenow(); dest2 memcpy2(dest2, src, 108); l2 getcyclenow(); dest1 memcpy1(dest1, src, 108); l3 getcyclenow(); printf(%X/t%X/t%X/n, l1-l0, l2-l1, l3-l2); */ return 0; } 执行这个程序看第一个printf输出发现memcpy是最慢的而汇编写的memcpy1是最快的而经典的面试写法memcpy2位居中间可是看c库的memcpy源码或者其反汇编代码它也是使用了rep ; movsX指令那为什么它慢呢考虑到了数据缓存问题将memcpy和memcpy1以及memcpy2的调用互换了位置仍然是c库的memcpy最慢这就给了很多涉业不久的毕业生十足的理由继续往笔试卷上写memcpy2之类的。附带一句后面的for循环的COUNTS次测速为了得到一个统计值这次发现memcpy2最慢memcpy比memcpy1慢一点点但是都比memcpy2快很多这名显与通过rdtsc测试出来的结果不对应于是肯定哪里出了问题。 实际上这是cpu指令预取和指令cache以及缺页引起的而和c库以及c库实现的memcpy没有关系要知道c库大家都在用不会有如此明显的性能问题的再者说随便懂点cpu知识和汇编的人都知道memcpy2是最不可取的因为它里面有大量的条件判断和条件跳转这对于cpu的流水线是很不利的并且通过c语言语义上语句实现拷贝增加了指令数从而也就是增加了执行时间x86处理器猛就猛在其是从cisc演变过来的包含了大量的指令于是movsX加repeat前缀肯定是不二的选择啦。当第一次调用memcpy的时候cpu不得不从其所在的“地址”处将指令取到cpu然后就cache到那里了除非越来越多的别的指令将它冲掉(flush)它就一直在那里。 另外tlb也是一个重要的因素tlb缓存了虚拟地址和物理地址的映射在cpu用虚拟地址访存时需要通过mmu得到物理地址tlb可以缓存这个映射从而以后不再需要查询页表直接从tlb中得到物理地址。对于指令的寻址cpu也是按照虚拟地址来的第一次访问时要完全查找页表并且还可能缺页接下来从最慢的磁盘将数据读到次慢的内存然后将物理地址-虚拟地址的映射读入tlb指令进入icache因此接下来的调用就会很快了。有个问题需要解释c库的函数不是一直cache在内存中的吗是的但是第一它没有cache在处理器中第二它虽然缓存在了内存但是对于当前task却不一定建立了页表项而处理器dcache/icache(数据缓存/指令缓存)是通过物理地址寻址的(而tlb是虚拟地址寻址的)因此会导致两个不命中cpu cache(icache/dcache/tlb)不命中和页表项不命中然后发生缺页内核准备读磁盘之前首先要看一下它是否在内存中如果在的话直接建立页表项映射就可以了对于c库的函数一般是在内存的(前提是内存足够大)因此第一次调用c库函数只会有缺页-建立页表项的开销而大多数情况下不会有读磁盘的开销。 下面通过例子2分析问题到底出在哪里 例子2 #include stdlib.h #include stdio.h #define ALIGN 0X800 int test() { return 1; } void __attribute__ ((aligned (ALIGN))) stub() { return; } long long getcycle() { unsigned int h, l; long long bs, cc; asm(rdtsc /n/t); asm(movl %%eax, %0/n/t:g(l)); asm(movl %%edx, %0/n/t:g(h)); bs h; cc (bs 32)|l; return cc; } int main(int argc, char **argv) { long long t1, t2, t3, t4; printf(%p/t%p/t%p/n, test, stub, main); t1 getcycle(); test(); t2 getcycle(); test(); t3 getcycle(); test(); t4 getcycle(); printf(%X/t%X/t%X/n, t2-t1, t3-t2, t4-t3); printf(%p/t%p/t%p/n, getcycle, test, main); return 0; } stub函数设置了aligned这个attribute为了在test函数和main之间制造一点“距离”(用nop指令填充)这样的话第一cpu的指令预取(instruction prefetch)就取不到了毕竟cpu的指令缓存是有限的第二执行main之前main函数一定要映射进页表项由于mmu按页大小(4096字节)映射拉开一定距离就不会让test也映射进入在下一个例子中我们会调整这个距离来表明缺页对执行性能的影响。控制ALIGN的值使得test函数和main的距离变化当ALIGN很小的时候test和main离的很近(小于一个页面且在同一页面)cpu可能会根据局部性原理加载main的时候将test的指令取入cache并且由于test和main在一个页面test也会进入内存且建立页表映射从而tlb中也会有test的物理地址然而如果ALIGN很大比如是0x10000这样test和main很远cpu既取不到test的指令又不会建立页表映射在第一次执行test的时候会缺页会花费很多时间以下讨论不再考虑cpu预取仅考虑缺页和cpu的icache也不再考虑tlb因为在访问一组连续地址(页面)的第一个字节时地址映射信息就会进入tlb以下一个页面内的连续访问几乎都会命中1个字节带来的优势对比一个页面字节来讲效果不容易表现出来(测试不容易)。因此在ALIGN为0x10000的时候执行结果如下 b36 3f 38 0x450017 0x420006 0x450045 第一次和第二次调用test足有40多倍的延迟。由于指令已经cache了第二次和第三次执行就很快了。为了证明是缺页的的影响很大把getcycle的定义放到test之前 long long getcycle() { ... } int test() { return 1; } void __attribute__ ((aligned (ALIGN))) stub() { return; } 在执行test之前要先执行一个t1 getcycle();该调用会触发getcycle缺页由于getcycle和test在同一页面也就是说会建立test的页表映射执行结果如下 3f 38 3f 0x420000 0x420034 0x450017 虽然main和test离的仍然很远然而getcycle和test很近在调用getcycle的时候会建立页表项因此速度加快很多以后就从指令缓存icache中取指令为了再次证实这个事情且不影响getcycle的执行将getcycle放回原位然后在test之前放一个stub0然后在第一个调用getcycle之前调用stub0然后看结果三次执行test的时间仍然很接近。看一下linux的do_page_fault的代码的确很多这些都要算在t2-t1中怪不得缺页情形下多了那么多的时间。 因此前一个例子中c库的memcpy慢的原因就找到了将例子1中main函数最后的注释放开再次编译执行(-O0参数不优化前面的例子2也要这样编译)发现memcpy1仍然是最快的而c库的memcpy位居第二和memcpy1相差不多而面试版本的memcpy2最慢。接下来看例子3展示缺页的影响。 例子3 #include stdlib.h #include stdio.h #include string.h #define ALIGN_test 0X800 #define ALIGN_main 0X800 long long getcycle() { unsigned int h, l; long long bs, cc; asm(rdtsc /n/t); asm(movl %%eax, %0/n/t:g(l)); asm(movl %%edx, %0/n/t:g(h)); bs h; cc (bs 32)|l; return cc; } void __attribute__ ((aligned (0x10000))) stub0() //为了让test和main分布在两个页面中 { return; } int __attribute__ ((aligned (ALIGN_test))) test(char *buf) { //增加一些代码让它像真正的函数。要使用-O0编译 int i 2, j3; if (buf[0] i) j 3; for (j; j 10; i jbuf[i]) j buf[j]; if (buf[0] i) j 3; for (j; j 10; i jbuf[i]) j buf[j]; if (buf[0] i) j 3; for (j; j 10; i jbuf[i]) j buf[j]; return i*j; }; int __attribute__ ((aligned (ALIGN_main))) main(int argc, char **argv) { long long t1, t2, t3, t4; char b[25] {0}; t1 getcycle(); memcpy(b, ddddd, 5); t2 getcycle(); memcpy(b, ddddd, 5); t3 getcycle(); memcpy(b, ddddd, 5); t4 getcycle(); printf(%llx/t%llx/t%llx/n, t2-t1, t3-t2, t4-t3); t1 getcycle(); test(ddddd/n); t2 getcycle(); test(ddddd/n); t3 getcycle(); test(ddddd/n); t4 getcycle(); printf(%llx/t%llx/t%llx/n, t2-t1, t3-t2, t4-t3); printf(%p/t%p/t%p/n, getcycle, printf2, main); } ALIGN_main和ALIGN_test均为0x800的情况下上述的代码将把test和main分布在两个页面中间隔0x800执行main时并不会为test建立映射因此第一次执行test函数时将会慢很多如果将ALIGN_main和ALIGN_test分别调整为0x800和0x1000那么test和main就会分布在同一个页面第一次执行test明显会快很多这就是缺页带来的影响可见页面调入实在太耗时了。在结束rdtsc指令的第一个影响因素之前再看一个例子理解一下共享库的作用。 例子4 share.h: int test1(); share.c int test1() { return 3; } test1.c: #include stdlib.h #include stdio.h #include share.h long long getcycle() { unsigned int h, l; long long bs, cc; long long interval, start, end; asm(rdtsc /n/t); asm(movl %%eax, %0/n/t:g(l)); asm(movl %%edx, %0/n/t:g(h)); bs h; cc (bs 32)|l; return cc; } int main(int argc, char **argv) { time_t st0, st1, st2, st3; long long l0, l1, l2, l3; l0 getcycle(); test1(); l1 getcycle(); test1(); l2 getcycle(); test1(); l3 getcycle(); printf(%llx/t%llx/t%llx/n, l1-l0, l2-l1, l3-l2); } test2.c: #include stdlib.h #include stdio.h #include shar.h int main(int argc, char **argv) { while(1) { test1(); } } 编译 gcc -fPIC -shared share.c -o libshare.so -O0 gcc test1.c -o test1 -L. -lshare gcc test2.c -o test2 -L. -lshare 1.连续执行test1 28b 46 3f 2.执行sysctl -w vm.drop_caches3后执行一次test1 391 54 46 3.在另一tty上执行test2然后执行test1 29a 46 38 4.在另一tty上执行test2执行sysctl -w vm.drop_caches3后执行test1 2c8 46 38 测试1说明第一次执行test1函数时缺页建立页表映射消耗大量时间连续执行后即使test1退出linux也可能将so缓存在了内存不再需要读磁盘因此得到了一个大致0x28b的数值测试2由于刷新了文件缓存缺页时需要读磁盘时间消耗明显增加测试3和测试1不相上下测试4并没有重现测试2的噩梦因此test2在循环执行中即使刷新了磁盘缓存test2也会将libshare.so读入磁盘由于libshare.so是共享的因此test1在第一次执行test1函数时只需要建立页面映射即可不必再读磁盘了这就是共享库的好处。 知道了cache和缺页带来的影响rdtsc就可以无忧得使用了吗不是这样的如果getcycle包围的待测试代码本身执行时间过长或者导致了进程调度那么两个getcycle调用的差值并不是真正的执行时间比如 t1 getcycle(); test() //很长可能会sleep t2 getcycle(); 如此一来t2-t1可能会把其它进程执行的时间都算上因为rdtsc获得的是cpu全局的嘀嗒信息而我们需要测试的执行时间是基于本进程的统计时间。因此实际上我们是不能信任rdtsc测试出来的结果的。哪一次在test()执行过程中发生调度几乎是用户态决定不了的(除非设置实时进程优先级并且没有IO)。 总结 1.编写代码时主要的调用函数要尽量紧凑的放在一个页面中注意向物理页面起始地址对齐不要离的太远这样可以提高执行效率。 2.使用共享库虽然可能因此增加几条指令(比如got机制的代价)然而却省去了很多缺页时读磁盘的开销。 3.测量长函数执行一次的时间不要相信rdtsc因此中间如果发生调度别的进程时间也会计算进去rdtsc是全局的。 4.挖掘机器的特性在指令级别寻找办法不要太迷恋c语言。 转载于:https://blog.51cto.com/dog250/1271087
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/89166.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!