谈谈DPDK的Cache预取策略

4 篇文章 2 订阅
2 篇文章 0 订阅

系列文章目录

事实上,Cache对于绝大多数程序员来说都是透明不可见的。程序员在编写程序时不需要关 心是否有Cache的存在,有几级Cache,每级Cache的大小是多少;不需要关心Cache采取何种策略将指令和数据从内存中加载到Cache中;也不需要关心Cache何时将处理完毕的数据写回到内存中。这一切,都是硬件自动完成的。但是,硬件也不是完全智能的,能够完美无缺地处理各 种各样的情况,保证程序能够以最优的效率执行。因此,一些体系架构 引入了能够对Cache进行预取的指令,从而使一些对程序执行效率有很 高要求的程序员能够一定程度上控制Cache,加快程序的执行。


工作需要去仔细研究了一下《深入浅出DPDK》谈谈理解。


前言

Cache之所以能够提高系统性能,主要是程序执行存在局部性现象,即时间局部性和空间局部性。

  1. 时间局部性:是指程序即将用到的指令/数据可能就是目前正在使用的指令/数据。因此,当前用到的指令/数据在使用完毕之后可以暂时存放在Cache中,可以在将来的时候再被处理器用到。一个简单的例子就是一个循环语句的指令,当循环终止的条件满足之前,处理器需要反复执行循环语句中的指令。
  2. 空间局部性:是指程序即将用到的指令/数据可能与目前正在使 用的指令/数据在空间上相邻或者相近。因此,在处理器处理当前指令/ 数据时,可以从内存中把相邻区域的指令/数据读取到Cache中,这样, 当处理器需要处理相邻内存区域的指令/数据时,可以直接从Cache中读取,节省访问内存的时间。一个简单的例子就是一个需要顺序处理的数组。

所谓的Cache预取,也就是预测数据并取入到Cache中,是根据空间局部性和时间局部性,以及当前执行状态、历史执行过程、软件提示等信息,然后以一定的合理方法,在数据/指令被使用前取入Cache。这样,当数据/指令需要被使用时,就能快速从Cache中加载到处理器内部 进行运算和执行。

一、简单谈谈Cache预取的实际作用

虽然绝大多数Cache预取对程序员来说都是透明的,但是了解预取的基本原理还是很有必要的,这样可以帮助我们编写高效的程序。以下 就是两个相似的程序片段,但是执行效率却相差极大。

程序1:
for(int i = 0; i < 1024; i++) {
    for(int j = 0; j < 1024; j++) {
        arr[i][j] = num++;
	} 
}

程序2:
for(int i = 0; i < 1024; i++) {
    for(int j = 0; j < 1024; j++) {
        arr[j][i] = num++;
    }
}

可以清晰地看到程序1和程序2的执行顺序。程序1是按照数组在内存中的保存方式顺序访问,而程序2则是跳跃式访问。对于程序1,硬件预取单元能够自动预取接下来需要访问的数据到Cache,节省 访问内存的时间,从而提高程序1的执行效率;对于程序2,硬件不能够 识别数据访问的规律,因而不会预取,从而使程序2总是需要在内存中 读取数据,降低了执行的效率。

二、DPDK软件预取

预取指令

预取指令使软件开发者在性能相关区域,把即将用到的数据从内存中加载到Cache,这样当前数据处理完毕后,即将用到的数据已经在 Cache中,大大减小了从内存直接读取的开销,也减少了处理器等待的时间,从而提高了性能。增加预取指令并不是让软件开发者需要时时考虑到Cache的存在,让软件自己来管理Cache,而是在某些热点区域,或者性能相关区域能够通过显示地加载数据到Cache,提高程序执行的效率。不过,不正确地使用预取指令,造成Cache中负载过重或者无用数据的比例增加,反而还会造成程序性能下降,也有可能造成其他程序执 行效率降低(比如某程序大量加载数据到三级Cache,影响到其他程 序)。因此,软件开发者需要仔细衡量利弊,充分进行测试,才能够正 确地优化程序。需要指出的是,预取指令只对数据有效,对指令预取是 无效的。

预取指令是汇编指令,对于很多软件开发者来说,直接插入汇编指 令不是很方便,一些程序库也提供了相应的软件版本。比 如“mmintrin.h”提供了如下的函数原型:

void _mm_prefetch(char *p, int i);
//p是需要预取的内存地址,i对应相应的预取指令

接下来,我们将以DPDK中PMD(Polling Mode Driver)驱动中的 一个程序片段看看DPDK是如何利用预取指令的。

DPDK中的预取

在讨论之前,我们需要了解另外一个和性能相关的话题。DPDK一 个处理器核每秒钟大概能够处理33M个报文,大概每30纳秒需要处理一 个报文,假设处理器的主频是2.7GHz,那么大概每80个处理器时钟周期 就需要处理一个报文。那么,处理报文需要做一些什么事情呢?以下是 一个基本过程。

  1. 写接收描述符到内存,填充数据缓冲区指针,网卡收到报文后 就会根据这个地址把报文内容填充进去。
  2. 从内存中读取接收描述符(当收到报文时,网卡会更新该结 构)(内存读),从而确认是否收到报文。
  3. 从接收描述符确认收到报文时,从内存中读取控制结构体的指 针(内存读),再从内存中读取控制结构体(内存读),把从接收描述符读取的信息填充到该控制结构体。
  4. 更新接收队列寄存器,表示软件接收到了新的报文。
  5. 内存中读取报文头部(内存读),决定转发端口。
  6. 从控制结构体把报文信息填入到发送队列发送描述符,更新发 送队列寄存器。
  7. 从内存中读取发送描述符(内存读),检查是否有包被硬件传 送出去。
  8. 如果有的话,从内存中读取相应控制结构体(内存读),释放 数据缓冲区。

可以看出,处理一个报文的过程,需要6次读取内存(“内存读”)。处理器从一级Cache读取数据需要3~5个时 钟周期,二级是十几个时钟周期,三级是几十个时钟周期,而内存则需 要几百个时钟周期。从性能数据来说,每80个时钟周期就要处理一个报文。

因此,DPDK必须保证所有需要读取的数据都在Cache中,否则一 旦出现Cache不命中,性能将会严重下降。为了保证这点,DPDK采用 了多种技术来进行优化,预取只是其中的一种。而从上面的介绍可以看出,控制结构体和数据缓冲区的读取都没有 遵循硬件预取的原则,因此DPDK必须用一些预取指令来提前加载相应 数据。以下就是部分接收报文的代码。

while (nb_rx < nb_pkts) {
	//读取接收描述符
	rxdp = &rx_ring[rx_id]; 
	staterr = rxdp->wb.upper.status_error;
	//检查是否有报文收到
	if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
         break;
     rxd = *rxdp;
	//分配数据缓冲区
	nmb = rte_rxmbuf_alloc(rxq->mb_pool); nb_hold++;
  
	//读取控制结构体
	rxe = &sw_ring[rx_id];
	......
	rx_id++;
	if (rx_id == rxq->nb_rx_desc)
	rx_id = 0;
	//预取下一个控制结构体mbuf rte_ixgbe_prefetch(sw_ring[rx_id].mbuf); //预取接收描述符和控制结构体指针
	if ((rx_id & 0x3) == 0) {
	rte_ixgbe_prefetch(&rx_ring[rx_id]);
	rte_ixgbe_prefetch(&sw_ring[rx_id]); }
......
	//预取报文
	rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off); //把接收描述符读取的信息存储在控制结构体mbuf中
	rxm->nb_segs = 1;
	rxm->next = NULL;
	rxm->pkt_len = pkt_len;
	rxm->data_len = pkt_len;
	rxm->port = rxq->port_id;
......
	rx_pkts[nb_rx++] = rxm;
}

总结

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值