系列文章目录
事实上,Cache对于绝大多数程序员来说都是透明不可见的。程序员在编写程序时不需要关 心是否有Cache的存在,有几级Cache,每级Cache的大小是多少;不需要关心Cache采取何种策略将指令和数据从内存中加载到Cache中;也不需要关心Cache何时将处理完毕的数据写回到内存中。这一切,都是硬件自动完成的。但是,硬件也不是完全智能的,能够完美无缺地处理各 种各样的情况,保证程序能够以最优的效率执行。因此,一些体系架构 引入了能够对Cache进行预取的指令,从而使一些对程序执行效率有很 高要求的程序员能够一定程度上控制Cache,加快程序的执行。
工作需要去仔细研究了一下《深入浅出DPDK》谈谈理解。
前言
Cache之所以能够提高系统性能,主要是程序执行存在局部性现象,即时间局部性和空间局部性。
- 时间局部性:是指程序即将用到的指令/数据可能就是目前正在使用的指令/数据。因此,当前用到的指令/数据在使用完毕之后可以暂时存放在Cache中,可以在将来的时候再被处理器用到。一个简单的例子就是一个循环语句的指令,当循环终止的条件满足之前,处理器需要反复执行循环语句中的指令。
- 空间局部性:是指程序即将用到的指令/数据可能与目前正在使 用的指令/数据在空间上相邻或者相近。因此,在处理器处理当前指令/ 数据时,可以从内存中把相邻区域的指令/数据读取到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个处理器时钟周期 就需要处理一个报文。那么,处理报文需要做一些什么事情呢?以下是 一个基本过程。
- 写接收描述符到内存,填充数据缓冲区指针,网卡收到报文后 就会根据这个地址把报文内容填充进去。
- 从内存中读取接收描述符(当收到报文时,网卡会更新该结 构)(内存读),从而确认是否收到报文。
- 从接收描述符确认收到报文时,从内存中读取控制结构体的指 针(内存读),再从内存中读取控制结构体(内存读),把从接收描述符读取的信息填充到该控制结构体。
- 更新接收队列寄存器,表示软件接收到了新的报文。
- 内存中读取报文头部(内存读),决定转发端口。
- 从控制结构体把报文信息填入到发送队列发送描述符,更新发 送队列寄存器。
- 从内存中读取发送描述符(内存读),检查是否有包被硬件传 送出去。
- 如果有的话,从内存中读取相应控制结构体(内存读),释放 数据缓冲区。
可以看出,处理一个报文的过程,需要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;
}