当前位置:文档之家› 如何高效的访问内存

如何高效的访问内存

影响内存访问速度的因素主要有: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 structbox_a{char a;short b;int c;char d;};大小:8字节1 2 3 4 5 6 7 structbox_b{char a;char d;short b;int c;};1.2.利用位域实际中,有些结构体字段并不需要那么大的存储空间,比如表示真假标记的flag 字段只取两个值之一,0或1,此时用1个bit 位即可,如果使用int 类型的单一字段就大大的浪费了空间。

示例:tcp.h1 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 structtcphdr {__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.利用unionunion 结构体也是压缩结构体大小的方法之一,它允许我们在某些情况下能对结构体的多个字段进行合并或把小字节字段存放到大字节字段内。

示例:skbuff.h1 2 3 4 5 6 7 8 9 10 11 structsk_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 structbox_c{char a;char d;short b;int c;int e;};大小:16字节1 2 structbox_d{3 4 5 6 7 8 9char 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 structnic {/* Begin: frequently used values: keep adjacent for cache effect */4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 u32msg_enable ____cacheline_aligned; /* 4字节空洞 */structnet_device *netdev;structpci_dev *pdev;/* 40字节空洞 */structrx*rxs ____cacheline_aligned;structrx *rx_to_use;structrx *rx_to_clean;structrfdblank_rfd;enumru_stateru_running;/* 20字节空洞 */spinlock_tcb_lock ____cacheline_aligned; spinlock_tcmd_lock;structcsr __iomem *csr;enumscb_cmd_locuc_cmd;unsigned intcbs_avail;structnapi_structnapi;…}回到前面的问题,如果我们对结构体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 typedefstruct {/* ro data */size_tblock_count; // number of total blockssize_tmeta_block_size; // sizeof per skb meta block size_tdata_block_size; // sizeof per skb data blocku8 *meta_base_addr; // base address of skb meta bufferu8 *data_base_addr; // base address of skb data buffer/* rw data */14 size_tcurrent_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 100int i;char *p;structbox_d *box[MAX];p = (char *)malloc(sizeof(structbox_d) * MAX);for (i = 0; i < MAX; i ++){box[i] = (structbox_d *)(p + sizeof(structbox_d) * i); }方法二:1 2 3 4 5 6 7 #define MAX 100int i;structbox_d *box[MAX];for (i = 0; i < MAX; i ++){box[i] = (structbox_d *)malloc(sizeof(structbox_d)); }另外,如果我们使用更大页面(比如2M 或1G )的分页机制,同样能够提升性能;因为相比于原本每页4K 大小的分页机制,应用程序申请同样大小的内存,大页面分页机制需要的页面数目更少,从而占用的TLB 项目也更少,减少虚拟地址到物理地址的转换次数的同时,提高TLB 的命中率,缩短每次转换所需要的时间。

相关主题