DPDK优化点和NUMA架构

这篇博客先介绍在dpdk中使用到的一些优化点[后期如果遇到其他的会完善],然后是NUMA架构,看了官方说明,对于10Gbit/s光口,能每秒发送/接收1480w+的64Byte[以太帧头+ip头+tcp头+数据]的数据包,为什么性能这么好?一方面与我们平时编码习惯相关,另一方面dpdk的源码是值得研究的。以下列出的优化点与具体的网卡性能方面的优化不一样,后者是个有挑战性的事情,下面的一些点可以使用到与dpdk无关的开发中。

  1. likely和unlikely的使用
    在dpdk的example中,很多语句使用了如下的函数:
153    /* Send burst of TX packets, to second port of pair. */
154    const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0,
155          bufs, nb_rx);
156 
157    /* Free any unsent packets. */
158    if (unlikely(nb_tx < nb_rx)) {
159          uint16_t buf;
160          for (buf = nb_tx; buf < nb_rx; buf++)
161                rte_pktmbuf_free(bufs[buf]);
162    }
253    /* Read packet from the ring */
254    nb_pkt = rte_ring_sc_dequeue_burst(conf->rx_ring, (void **)mbufs,
255           burst_conf.ring_burst);
256    if (likely(nb_pkt)) {
257          int nb_sent = rte_sched_port_enqueue(conf->sched_port, mbufs,
258              nb_pkt);
259 
260          APP_STATS_ADD(conf->stat.nb_drop, nb_pkt - nb_sent);
261          APP_STATS_ADD(conf->stat.nb_rx, nb_pkt);
262    }
126 /* Branch prediction helpers. */
127 #ifndef likely
128 #define likely(c) __builtin_expect(!!(c), 1)
129 #endif              
130 #ifndef unlikely
131 #define unlikely(c) __builtin_expect(!!(c), 0)
132 #endif

其中__builtin_expect原型如下:
long __builtin_expect(long exp, long c);!!(c)的效果是得到一个布尔值,该函数的作用是更好的分支预测;使用likely() ,执行if后面的语句的机会更大,使用unlikely(),执行else后面的语句的机会更大,原理“It optimizes things by ordering the generated assembly code correctly, to optimize the usage of the processor pipeline. To do so, they arrange the code so that the likeliest branch is executed without performing any jmp instruction (which has the bad effect of flushing the processor pipeline).”
主要是分支预测失误,指令的跳转带来的性能会下降很多。为什么呢?从《深入理解计算机系统》书上摘取,p141:“另一方面,错误预测一个跳转要求处理器丢掉它为该跳转指令后所有指令已经做了的工作,然后再开始用从正确位置处起始的指令去填充流水线,大约会浪费20〜40个时钟周期”;从汇编代码层面理解参考文末第一个引用。

  1. 指令预取
    这里仅介绍的cache数据预取[空间局部性和时间局部性]。分为硬件预取和软件预取,前者根据不同的架构如NetBurst,由底层硬件预取单元根据一定条件自动激活预取,“一定的条件”比较复杂,比如读取的数据是回写的内存模型;没有连续的存储指令等,但有时并不一定能提高程序的执行效率。如在访问的数据结构没有规律的情况下,那么硬件预取会占用更多的带宽,浪费一级cache的空间,淘汰了程序本身存放在一级cache中的数据。
    用的较多的是软件预取,由指令PREFETCH0~PREFETCH2,PREFETCHNTA组成,在开发过程中,把即将用到的数据从内存中加载到cache,后期直接命中cache,减小了从内存直接读取的延迟,减小了处理器的等待时间。
    对于一级cache,延迟为3〜5个指令周期,二级cache为十几个指令周期,三级cache为几十个。
    在dpdk例子中,常看到这样的语句:
341      /* for traffic we receive, queue it up for transmit */
342      uint16_t i;
343      rte_prefetch_non_temporal((void *)bufs[0]);
344      rte_prefetch_non_temporal((void *)bufs[1]);
345      rte_prefetch_non_temporal((void *)bufs[2]);
346      for (i = 0; i < nb_rx; i++) {
347           struct output_buffer *outbuf;
348           uint8_t outp;
349           rte_prefetch_non_temporal((void *)bufs[i + 3]);
350           /*
351           * workers should update in_port to hold the
352           * output port value
353           */
354           outp = bufs[i]->port;
355           /* skip ports that are not enabled */
356           if ((enabled_port_mask & (1 << outp)) == 0)
357               continue;
358 
359           outbuf = &tx_buffers[outp];
360           outbuf->mbufs[outbuf->count++] = bufs[i];
361           if (outbuf->count == BURST_SIZE)
362               flush_one_port(outbuf, outp);
363      }      

rte_prefetch_non_temporal的功能与PREFETCH0对应,即将数据存放在每一级cache中,在使用完一次后是可以被淘汰出动的。“Prefetch a cache line into all cache levels (non-temporal/transient version)
The non-temporal prefetch is intended as a prefetch hint that processor will use the prefetched data only once or short period, unlike the rte_prefetch0() function which imply that prefetched data to use repeatedly.”
其实现是内嵌汇编代码:

 42 static inline void rte_prefetch0(const volatile void *p)
 43 {               
 44     asm volatile ("pld [%0]" : : "r" (p));
 45 }

 57 static inline void rte_prefetch_non_temporal(const volatile void *p)
 58 {
 59     /* non-temporal version not available, fallback to rte_prefetch0 */
 60     rte_prefetch0(p);
 61 }
  1. cache相关
    主要点是cache line,example中好些结构声明末尾有如下形式:
106 struct lcore_queue_conf {
107     unsigned n_rx_port;
108     unsigned rx_port_list[MAX_RX_QUEUE_PER_LCORE];
109 } __rte_cache_aligned;
#define RTE_CACHE_LINE_SIZE 64
#define __rte_cache_aligned __attribute__((__aligned__(RTE_CACHE_LINE_SIZE)))

就是定义一个变量时要按照cache line对齐,这样做的好处是一方面防止多线程中的false sharing而导致“抖动”问题,影响性能,另一方面如果变量所占空间跨line,则可能需要几次从内存加载和存储,再然后呢要求变量地址cache line对齐可能会浪费点内存空间。
cache line有四种状态,分别是modified,exclusive, shared, invalid,即MESI协议,为了解决一致性问题而出现的。很多理论性的可以查下wiki,我也记不清楚。“在dpdk中,避免多个核访问同一个内存地址或者数据结构。这样,每个核都避免与其他核共享数据,减少因为错误的数据共享而导致cache一致性的开销。”
具体cache相关的基础知识点可参考《深入理解计算机系统》,理解cache的工作原理,能写出较高效的代码。

  1. 本地内存
    这个点与NUMA架构有关,在此之前,有必要解释下SMP架构。
    SMP(Symmetric Multi Processing),即对称多处理系统,特点如下:
    a)所有的CPU共享全部资源,如总线,内存和I/O系统等;
    b)所有处理器都是平等的,没有主从关系;
    c)内存是统一结构,统一寻址的(UMA:Uniform Memory Access);
    d)cpu和内存,cpu之间是通过一条总线连接起来的;
    但有以下缺点:
    a)扩展能力非常有限;
    b)随着cpu个数增加,系统总线成为了瓶颈,cpu与内存之间的通信延迟加大;
    故出现了NUMA(Non Uniform Memory Access Architecture),即非一致存储器访问,特点如下:
    a)cpu和本地内存拥有更小的延迟与更大的带宽;
    b)整个内存可作为一个整体,任何cpu都能访问,跨本地内存访问较访问本地内存慢一些;
    c)每个cpu可以有本地总线,和内存一样,访问本地总线延迟低,添吐率高;
    两者如下图所示:
numa.png

而在dpdk中的example使用中,可以看见如下代码使用方式:

150 /* helper to create a mbuf pool */
151 struct rte_mempool *
152 rte_pktmbuf_pool_create(const char *name, unsigned n,
153     unsigned cache_size, uint16_t priv_size, uint16_t data_room_size,
154     int socket_id)

根据该线程所在的socket_id去创建内存;

1183 int
1184 rte_eth_rx_queue_setup(uint8_t port_id, uint16_t rx_queue_id,
1185                uint16_t nb_rx_desc, unsigned int socket_id,
1186                const struct rte_eth_rxconf *rx_conf,
1187                struct rte_mempool *mp)

1265 int
1266 rte_eth_tx_queue_setup(uint8_t port_id, uint16_t tx_queue_id,
1267                uint16_t nb_tx_desc, unsigned int socket_id,
1268                const struct rte_eth_txconf *tx_conf)

根据该线程所在的socket_id去创建端口的收发队列;

161 struct rte_ring *
162 rte_ring_create(const char *name, unsigned count, int socket_id, unsigned flags)

根据该线程所在的socket_id去创建无锁环形buffer。

还有无锁的使用,cpu亲和性,ddio,内存交叉访问等...

参考:
https://kernelnewbies.org/FAQ/LikelyUnlikely
http://docs.oracle.com/cd/E19253-01/819-7057/6n91f8su6/index.html
http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Memory/direct.html
https://en.wikipedia.org/wiki/MESI_protocol
https://en.wikipedia.org/wiki/False_sharing
https://en.wikipedia.org/wiki/Symmetric_multiprocessing
https://en.wikipedia.org/wiki/Non-uniform_memory_access
http://www.tuicool.com/articles/j6vY7nq

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容