如何高效的访问内存

影响内存访问速度的因素主要有:
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/?p=310

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/435328.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初学者怎样看懂python代码_入门编程(初学者怎样看懂代码)

你既然喜欢编程&#xff0c;就应该认认真真的学习一门语言&#xff0c;学习微软的就先从vb开始&#xff0c;vb是比较好的入门语言&#xff0c;可视化的&#xff0c;比较简单&#xff0c;是非常好的入门语言。书籍最少应该准备两. 先认认真真的学习一门语言&#xff0c;学习微软…

MAVEN安装和配置

maven官网下载 https://maven.apache.org/download.cgi

MIPS架构的医院智能导诊系统设计

摘要&#xff1a;通过研究基于MIPS架构的SMP8654芯片的硬件架构&#xff0c;并且利用芯片内部的图形加速引擎GFX的方式实现了具有高清视频显示和图片文字处理功能的播放器。系统以嵌入式Linux和MiniGUI为平台设计了智能导诊系统&#xff0c;提高了医院的导诊就医的服务效率。智…

C、CPP const 详解

1.const修饰变量一般有两种写法&#xff1a; constTYPE value;TYPE constvalue;这两种写法在本质上是一样的。它的含义是&#xff1a;const修饰的类型为TYPE的变量value是不可变的。对于一个非指针的类型TYPE&#xff0c;无论怎么写&#xff0c;都是一个含义&#xff0c;即valu…

arcgis在线地图插件安装

软件下载链接 https://download.csdn.net/download/qq_39397927/15761863

hadoop namenode启动不了_集群版hadoop安装,写给大忙人看的

导语 如果之前的单机版hadoop环境安装满足不了你&#xff0c;集群版hadoop一定合你胃口&#xff0c;轻松入手。目录 集群规划前置条件配置免密登录 3.1 生成密匙 3.2 免密登录 3.3 验证免密登录集群搭建 4.1 下载并解压 4.2 配置环境变量 4.4 修改配置 4.4 分发程序 4.5 初始化…

patch文件制作

一、为单个文件打补丁1、首先我用的ubuntu12 os&#xff0c;cat >>test0<<eof但是这命令执行得是root身份more命令功能&#xff1a;让画面在显示满一页时暂停&#xff0c;此时可按空格健继续显示下一个画面&#xff0c;或按Q键停止显示。more test0:查看test0内容2…

[转]百万级访问网站前期的技术准备

作者&#xff1a;一路凯歌来源&#xff1a;http://zhiyi.us/ 开了自己域名的博客&#xff0c;第一篇就得来个重磅一点的才对得起这4美金的域名。作为一个技术从业者十年&#xff0c;逛了十年发现有些知识东一榔头西一棒槌的得满世界 看个遍才整理出个头绪&#xff0c;那咱就系统…

java string逆序_java经典入门算法题,java初学者必备

java经典入门算法题开头求关注警告喜欢这样文章的可以关注我&#xff0c;我会持续更新&#xff0c;你们的关注是我更新的动力&#xff01;需要更多java学习资料的也可以私信我&#xff01;祝关注我的人都&#xff1a;身体健康&#xff0c;财源广进&#xff0c;福如东海,寿比南山…

u-boot的patch文件制作

首先明白为什么要制作patch文件&#xff0c;因为u-boot的移植过程需要根据实际需要修改通用u-boot&#xff0c;如果每次手工修改的话&#xff0c;太麻烦&#xff0c;所以用了patch文件一步到位&#xff0c;这点类似于makefile的作用&#xff0c;哈哈1.了解 diff 和 patch。diff…

Powershell 最大值堆栈实现

Powershell 最大值堆栈实现 下面代码基于一个算法题目来实现一个用线性时间得到堆栈最大值的代码。 cls$maxStackConut 5$stackTopIndex -1$stack New-Object int[] ($maxStackConut) $link2NextMaxItem New-Object int[] ($maxStackConut)$maxStackItemIndex-1 function Pus…

python主线程执行_在Django vi中的主线程中执行Python函数

我创建了Django视图“graph”&#xff0c;目的是显示从matplotlib.pyplot模块。我编写了我的函数plot\u bubbles&#xff08;返回amatplotlib.figure.figure对象&#xff09;在脚本数据中_分析.py导入到视图.py脚本。在 Tkinter只能在主线程上运行&#xff0c;我的网页在我第一…

postgis创建空间数据库(pgadmin4)

打开软件 看我之前的安装步骤的用户密码为postgres 点击save 查看是否具有空间数据 1.查看是否存在空间数据表 2.查看是否具有空间函数

Outlook最小到系统托盘

Outlook最小到系统托盘 默认状态下outlook最小化以后在任务栏上还占一个位置&#xff0c;又不常用&#xff0c;关了又不能实时接收到邮件&#xff0c;可以通过以下方法隐藏到系统托盘&#xff0c;解决方法如下&#xff1a; 如果你用得是Office2007: 1 打开注册表&#xff1a;开…

嵌入式linux的学习笔记-共享内存(六)

共享内存共享内存是可以被多个进程共享访问的一部分物理内存,如果多个进程都把一个内存区映射到自身的虚拟地址空间,则这些进程就可以直接访问该共享的内存区域,从而通过共享内存的方式实现多进程间的通讯,共享内存是进程间数据通讯的最快方法.共享内存的实现分为两步:1,创建共…

SQL Server 2012 安装

SQL Server 2012 安装 安装包在这里&#xff1a;https://pan.baidu.com/s/1_sgxN8P-pzj7uZeAR0VlKQ 提取码&#xff1a;mnvj 文件比较大&#xff0c;慢慢下载吧 下载好后是这样的&#xff1a; 双击打开&#xff0c;点击 setup.exe 安装&#xff1a; 选择“安装”&#xff0…

矢量数据导入数据库

打开数据导入工具 设置数据库链接 用户名和密码都是postgres 链接成功 arcgis10.3之前编码是gbk之后为utf-8 目前作者用的是10.2版本导出的数据 shp文件名和路径名必须为英文 添加数据 成功导入 追加表的属性要一致 查看导入数据 查看是否导入成功 创建两个字段

@qualifier注解_常见的 Spring 注解概览

点击上方 Java后端&#xff0c;选择 设为星标优质文章&#xff0c;及时送达从Java5.0开始&#xff0c;Java开始支持注解。Spring做为Java生态中的领军框架&#xff0c;从2.5版本后也开始支持注解。相比起之前使用xml来配置Spring框架&#xff0c;使用注解提供了更多的控制Sprin…

IOCP

select()或是其它异步方法不同的是&#xff0c;一个套接字[socket]与一个完成端口关联了起来&#xff0c;然后就可继续进行正常的Winsock操作了。然而&#xff0c;当一个事件发生的时候&#xff0c;此完成端口就将被操作系统加入一个队列中。然后应用程序可以对核心层进行查询以…