上海网站建设搭建,用html做网站顺序,厦门网站建设68,淘客手机端网站建设影响内存访问速度的因素主要有#xff1a; 1.内存带宽#xff1a;每秒读写内存的数据量#xff0c;由硬件配置决定。 2.CACHE高速缓冲#xff1a;CPU与内存之间的缓冲器#xff0c;当命中率比较高时能大大提供内存平均访问速度。 3.TLB转换旁视缓冲#xff1a;系统虚拟地…影响内存访问速度的因素主要有 1.内存带宽每秒读写内存的数据量由硬件配置决定。 2.CACHE高速缓冲CPU与内存之间的缓冲器当命中率比较高时能大大提供内存平均访问速度。 3.TLB转换旁视缓冲系统虚拟地址向物理地址转换的高速查表机制转换速度比普通转换机制要快。
我们能够优化的只有第2点和第3点。由于CACHE的小容量与SMP的同步竞争如何最大限度的利用高速缓冲就是我们的明确优化突破口以常用的数据结构体为例 1.压缩结构体大小针对CACHE的小容量。 2.对结构体进行对齐针对内存地址读写特性与SMP上CACHE的同步竞争。 3.申请地址连续的内存空间针对TLB的小容量和CACHE命中。 4.其它优化综合考虑多种因素
具体优化方法 1.压缩结构体大小 系统CACHE是有限的并且容量很小充分压缩结构体大小使得CACHE能缓存更多的被访问数据无非是提高内存平均访问速度的有效方法之一。 压缩结构体大小除了需要我们对应用逻辑做好更合理的设计尽量去除不必要的字段还有一些额外针对结构体本身的压缩方法。
1.1.对结构体字段进行合理的排列 由于结构体自身对齐的特性具有同样字段的结构体不同的字段排列顺序会产生不同大小的结构体。 大小12字节 1 2 3 4 5 6 7 struct box_a { char a; short b; int c; char d; }; 大小8字节 1 2 3 4 5 6 7 struct box_b { char a; char d; short b; int c; }; 1.2.利用位域 实际中有些结构体字段并不需要那么大的存储空间比如表示真假标记的flag字段只取两个值之一0或1此时用1个bit位即可如果使用int类型的单一字段就大大的浪费了空间。 示例tcp.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct tcphdr { __be16 source; __be16 dest; __be32 seq; __be32 ack_seq; #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:4, cwr:1, ece:1, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error Adjust your asm/byteorder.h defines #endif __be16 window; __sum16 check; __be16 urg_ptr; }; 1.3.利用union union结构体也是压缩结构体大小的方法之一它允许我们在某些情况下能对结构体的多个字段进行合并或把小字节字段存放到大字节字段内。 示例skbuff.h 1 2 3 4 5 6 7 8 9 10 11 struct sk_buff { … union { __wsum csum; struct { __u16 csum_start; __u16 csum_offset; }; }; … }; 2.对结构体进行对齐 对结构体进行对齐有两层意思一是指对较小结构体进行机器字对齐二是指对较大结构体进行CACHE LINE对齐。
2.1.对较小结构体进行机器字对齐 我们知道对于现代计算机硬件来说内存只能通过特定的对齐地址比如按照机器字进行访问。举个例子来说比如在64位的机器上不管我们是要读取第0个字节还是要读取第1个字节在硬件上传输的信号都是一样的。因为它都会把地址0到地址7这8个字节全部读到CPU只是当我们是需要读取第0个字节时丢掉后面7个字节当我们是需要读取第1个字节丢掉第1个和后面6个字节。 当我们要读取的字节刚好落在两个机器字内时就出现两次访问内存的情况同时通过一些逻辑计算才能得到最终的结果。 因此为了更好的提升性能我们须尽量将结构体做到机器字或倍数对齐而结构体中一些频繁访问的字段也尽量安排在机器字对齐的位置。 大小12字节 1 2 3 4 5 6 7 8 struct box_c { char a; char d; short b; int c; int e; }; 大小16字节 1 2 3 4 5 6 7 8 9 struct box_d { char a; char d; short b; int c; int e; char padding[4]; }; 上面表格右边的box_d结构体通过增加一个填充字段padding将结构体大小增加到16字节从而与机器字倍数对齐这在我们申请连续的box_d结构体数组时仍能保证数组内的每一个结构体都与机器字倍数对齐。 通过填充字段padding使得结构体大小与机器字倍数对齐是一种常见的做法在Linux内核源码里随处可见。
2.2.对较大结构体进行CACHE LINE对齐 我们知道CACHE与内存交换的最小单位为CACHE LINE一个CACHE LINE大小以64字节为例。当我们的结构体大小没有与64字节对齐时一个结构体可能就要占用比原本需要更多的CACHE LINE。比如把一个内存中没有64字节长的结构体缓存到CACHE时即使该结构体本身长度或许没有还没有64字节但由于其前后搭占在两条CACHE LINE上那么对其进行淘汰时就会淘汰出去两条CACHE LINE。 这还不是最严重的问题非CACHE LINE对齐结构体在SMP机器上容易引发名为错误共享的CACHE问题。比如结构体T1和T2都没做CACHE LINE对齐如果它们T1后半部和T2前半部在SMP机器上合占了同一条CACHE如果CPU 0对结构体T1后半部做了修改则将导致CPU 1的CACHE LINE 1失效同样如果CPU 1对结构体T2前半部做了修改则也将导致CPU 0的CACHE LINE 1失效。如果CPU 0和CPU 1反复做相应的修改则导致的不良结果显而易见。本来逻辑上没有共享的结构体T1和T2实际上却共享了CACHE LINE 1这就是所谓的错误共享。 Linux源码里提供了利用GCC的__attribute__扩展属性定义的宏来做这种对齐处理在文件/linux-2.6.xx/include/linux/cache.h内可以找到多个相类似的宏比如 1 #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) 该宏可以用来修饰结构体字段作用是强制该字段地址与CACHE LINE映射起始地址对齐。 看/linux-2.6.xx/drivers/net/e100.c内结构体nic的实现三个____cacheline_aligned修饰字段表示强制这些字段与CACHE LINE映射起始地址对齐。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct nic { /* Begin: frequently used values: keep adjacent for cache effect */ u32 msg_enable ____cacheline_aligned; /* 4字节空洞 */ struct net_device *netdev; struct pci_dev *pdev; /* 40字节空洞 */ struct rx *rxs ____cacheline_aligned; struct rx *rx_to_use; struct rx *rx_to_clean; struct rfd blank_rfd; enum ru_state ru_running; /* 20字节空洞 */ spinlock_t cb_lock ____cacheline_aligned; spinlock_t cmd_lock; struct csr __iomem *csr; enum scb_cmd_lo cuc_cmd; unsigned int cbs_avail; struct napi_struct napi; … } 回到前面的问题如果我们对结构体T2的第一个字段加上____cacheline_aligned修饰则该错误共享即可解决。
2.3.只读字段和读写字段隔离对齐 只读字段和读写字段隔离对齐的目的就是为了尽量保证那些只读字段和读写字段分别集中在CACHE的不同CACHE LINE中。由于只读字段几乎不需要进行更新因而能在CACHE中得以稳定的缓存减少由于混合有读写字段导致的对应CACHE LINE的频繁失效问题以便提高效率而读写字段相对集中在一起这样也能保证当程序读写结构体时污染的CACHE LINE条数也就相对的较少。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct { /* ro data */ size_t block_count; // number of total blocks size_t meta_block_size; // sizeof per skb meta block size_t data_block_size; // sizeof per skb data block u8 *meta_base_addr; // base address of skb meta buffer u8 *data_base_addr; // base address of skb data buffer /* rw data */ size_t current_index ____cacheline_aligned; // index } bc_buff, * bc_buff_t; 3.申请地址连续的内存空间 随着地址空间由32位转到64位页内存管理的目录分级也越来越多4级的目录地址转换也是一笔不小是开销。硬件产商为我们提供了TLB缓冲加速虚拟地址到物理地址的换算。但是毕竟TLB是有限对地址连续的内存空间进行访问时TLB能得到更多的命中同时CACHE高速缓冲命中的几率也更大。 两段代码实现同一功能但第一种方法在实际使用中内存读写效率就会相对较好特别是在申请的内存很大时未考虑malloc异常 方法一 1 2 3 4 5 6 7 8 9 #define MAX 100 int i; char *p; struct box_d *box[MAX]; p (char *)malloc(sizeof(struct box_d) * MAX); for (i 0; i MAX; i ) { box[i] (struct box_d *)(p sizeof(struct box_d) * i); } 方法二 1 2 3 4 5 6 7 #define MAX 100 int i; struct box_d *box[MAX]; for (i 0; i MAX; i ) { box[i] (struct box_d *)malloc(sizeof(struct box_d)); } 另外如果我们使用更大页面比如2M或1G的分页机制同样能够提升性能因为相比于原本每页4K大小的分页机制应用程序申请同样大小的内存大页面分页机制需要的页面数目更少从而占用的TLB项目也更少减少虚拟地址到物理地址的转换次数的同时提高TLB的命中率缩短每次转换所需要的时间。因为大多数操作系统在分配内存时候都需要按页对齐所以大页面分页机制的缺点就是内存浪费相对比较严重。只有在物理内存足够充足的情况下大页面分页机制才能够体现出优势。
4.其它优化 4.1.预读指令读内存 提前预取内存中数据到CACHE内提高CACHE的命中率加速内存读取速度这是设计预读指令的主要目的。如果当前运算复杂度比较高那么预取和运算就可同步进行从而消除下一步内存访问的时延。相应的预读汇编指令有prefetch0、prefetch1、prefetch2、 prefetchnta。 预取指令只是给CPU一个提示所以它可被CPU忽略而且就算预取一段错误的地址也不会导致CPU异常。一般使用prefetchnta预取指令因为它不会污染CACHE它把每次取得的数据都存放到L2 CACHE的第一条CACHE LINE而另外几条指令会替换CACHE中最近最少使用的CACHE LINE。
4.2.非暂时移动指令写内存 我们知道为了保证CACHE与内存之间的数据一致性CPU对CACHE的写操作主要有两种方式同步到内存写透式Write Through和写回式Write-back。不管哪种同步方式都是要消耗性能的而在某些情况下写CACHE是不必要的 有哪些情况不需要写CACHE呢比如做数据拷贝高效memcpy函数实现时或者我们已经知道写的数据在最近一段时间内或者永远都不会再使用了那么此时就可以不用写CACHE让对应的CACHE LINE自动失效以便缓存其它数据。这在某些特殊场景非常有用相应的汇编指令有movntq、movntsd、movntss、movntps、movntpd、movntdq、movntdqa。 完整的利用预读指令和非暂时移动指令实现的高速内存拷贝函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void X_aligned_memcpy_sse2(void* dest, const void* src, const unsigned long size_t) { __asm { mov esi, src; //src pointer mov edi, dest; //dest pointer mov ebx, size_t; //ebx is our counter shr ebx, 7; //divide by 128 (8 * 128bit registers) loop_copy: prefetchnta 128[ESI]; //SSE2 prefetch prefetchnta 160[ESI]; prefetchnta 192[ESI]; prefetchnta 224[ESI]; movdqa xmm0, 0[ESI]; //move data from src to registers movdqa xmm1, 16[ESI]; movdqa xmm2, 32[ESI]; movdqa xmm3, 48[ESI]; movdqa xmm4, 64[ESI]; movdqa xmm5, 80[ESI]; movdqa xmm6, 96[ESI]; movdqa xmm7, 112[ESI]; movntdq 0[EDI], xmm0; //move data from registers to dest movntdq 16[EDI], xmm1; movntdq 32[EDI], xmm2; movntdq 48[EDI], xmm3; movntdq 64[EDI], xmm4; movntdq 80[EDI], xmm5; movntdq 96[EDI], xmm6; movntdq 112[EDI], xmm7; add esi, 128; add edi, 128; dec ebx; jnz loop_copy; //loop please loop_copy_end: } } 总结 要高效的访问内存必须充分利用系统CACHE的缓存功能因为就目前来说CACHE的访问速度比内存快太多了。具体优化方法有 1.用设计上压缩结构体大小。 2.结构体尽量做到机器字倍数对齐。 3.结构体中频繁访问的字段尽量放在机器字对齐的位置。 4.频繁读写的多个结构体变量尽量同时申请使得它们尽可能的分布在较小的线性空间范围内这样可利用TLB缓冲。 5.当结构体比较大时对结构体字段进行初始化或设置值时最好从第一个字段依次往后进行这样可保证对内存的访问是顺序进行。 6.额外的优化可以采用非暂时移动指令如movntdq与预读指令如prefetchnta。 7.特殊情况可考虑利用多媒体指令SSE2、SSE4等。 当然上面某些步骤之间存在冲突比如压缩结构体和结构体对齐这就需要实际综合考虑。
转载请保留地址http://lenky.info/2011/11/23/%e5%a6%82%e4%bd%95%e9%ab%98%e6%95%88%e7%9a%84%e8%ae%bf%e9%97%ae%e5%86%85%e5%ad%98/ 或 http://lenky.info/?p310