這篇博客先介紹在dpdk中使用到的一些優(yōu)化點(diǎn)[后期如果遇到其他的會(huì)完善],然后是NUMA架構(gòu)找都,看了官方說(shuō)明唇辨,對(duì)于10Gbit/s光口,能每秒發(fā)送/接收1480w+的64Byte[以太幀頭+ip頭+tcp頭+數(shù)據(jù)]的數(shù)據(jù)包能耻,為什么性能這么好?一方面與我們平時(shí)編碼習(xí)慣相關(guān),另一方面dpdk的源碼是值得研究的。以下列出的優(yōu)化點(diǎn)與具體的網(wǎng)卡性能方面的優(yōu)化不一樣骂际,后者是個(gè)有挑戰(zhàn)性的事情尖殃,下面的一些點(diǎn)可以使用到與dpdk無(wú)關(guān)的開(kāi)發(fā)中。
- likely和unlikely的使用
在dpdk的example中劲绪,很多語(yǔ)句使用了如下的函數(shù):
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)的效果是得到一個(gè)布爾值,該函數(shù)的作用是更好的分支預(yù)測(cè)洪燥;使用likely() ,執(zhí)行if后面的語(yǔ)句的機(jī)會(huì)更大乳乌,使用unlikely()捧韵,執(zhí)行else后面的語(yǔ)句的機(jī)會(huì)更大,原理“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).”
主要是分支預(yù)測(cè)失誤汉操,指令的跳轉(zhuǎn)帶來(lái)的性能會(huì)下降很多再来。為什么呢?從《深入理解計(jì)算機(jī)系統(tǒng)》書(shū)上摘取,p141:“另一方面芒篷,錯(cuò)誤預(yù)測(cè)一個(gè)跳轉(zhuǎn)要求處理器丟掉它為該跳轉(zhuǎn)指令后所有指令已經(jīng)做了的工作搜变,然后再開(kāi)始用從正確位置處起始的指令去填充流水線,大約會(huì)浪費(fèi)20?40個(gè)時(shí)鐘周期”针炉;從匯編代碼層面理解參考文末第一個(gè)引用挠他。
- 指令預(yù)取
這里僅介紹的cache數(shù)據(jù)預(yù)取[空間局部性和時(shí)間局部性]。分為硬件預(yù)取和軟件預(yù)取篡帕,前者根據(jù)不同的架構(gòu)如NetBurst殖侵,由底層硬件預(yù)取單元根據(jù)一定條件自動(dòng)激活預(yù)取,“一定的條件”比較復(fù)雜赂苗,比如讀取的數(shù)據(jù)是回寫(xiě)的內(nèi)存模型愉耙;沒(méi)有連續(xù)的存儲(chǔ)指令等,但有時(shí)并不一定能提高程序的執(zhí)行效率拌滋。如在訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)沒(méi)有規(guī)律的情況下朴沿,那么硬件預(yù)取會(huì)占用更多的帶寬,浪費(fèi)一級(jí)cache的空間败砂,淘汰了程序本身存放在一級(jí)cache中的數(shù)據(jù)赌渣。
用的較多的是軟件預(yù)取,由指令PREFETCH0~PREFETCH2,PREFETCHNTA組成昌犹,在開(kāi)發(fā)過(guò)程中坚芜,把即將用到的數(shù)據(jù)從內(nèi)存中加載到cache,后期直接命中cache斜姥,減小了從內(nèi)存直接讀取的延遲鸿竖,減小了處理器的等待時(shí)間。
對(duì)于一級(jí)cache铸敏,延遲為3?5個(gè)指令周期缚忧,二級(jí)cache為十幾個(gè)指令周期,三級(jí)cache為幾十個(gè)杈笔。
在dpdk例子中闪水,常看到這樣的語(yǔ)句:
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對(duì)應(yīng)蒙具,即將數(shù)據(jù)存放在每一級(jí)cache中球榆,在使用完一次后是可以被淘汰出動(dòng)的〗ぃ“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.”
其實(shí)現(xiàn)是內(nèi)嵌匯編代碼:
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 }
- cache相關(guān)
主要點(diǎn)是cache line持钉,example中好些結(jié)構(gòu)聲明末尾有如下形式:
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)))
就是定義一個(gè)變量時(shí)要按照cache line對(duì)齊,這樣做的好處是一方面防止多線程中的false sharing而導(dǎo)致“抖動(dòng)”問(wèn)題融师,影響性能右钾,另一方面如果變量所占空間跨line,則可能需要幾次從內(nèi)存加載和存儲(chǔ),再然后呢要求變量地址cache line對(duì)齊可能會(huì)浪費(fèi)點(diǎn)內(nèi)存空間舀射。
cache line有四種狀態(tài)窘茁,分別是modified,exclusive, shared, invalid脆烟,即MESI協(xié)議山林,為了解決一致性問(wèn)題而出現(xiàn)的。很多理論性的可以查下wiki邢羔,我也記不清楚驼抹。“在dpdk中拜鹤,避免多個(gè)核訪問(wèn)同一個(gè)內(nèi)存地址或者數(shù)據(jù)結(jié)構(gòu)框冀。這樣,每個(gè)核都避免與其他核共享數(shù)據(jù)敏簿,減少因?yàn)殄e(cuò)誤的數(shù)據(jù)共享而導(dǎo)致cache一致性的開(kāi)銷明也。”
具體cache相關(guān)的基礎(chǔ)知識(shí)點(diǎn)可參考《深入理解計(jì)算機(jī)系統(tǒng)》惯裕,理解cache的工作原理温数,能寫(xiě)出較高效的代碼。
- 本地內(nèi)存
這個(gè)點(diǎn)與NUMA架構(gòu)有關(guān)蜻势,在此之前撑刺,有必要解釋下SMP架構(gòu)。
SMP(Symmetric Multi Processing)握玛,即對(duì)稱多處理系統(tǒng)够傍,特點(diǎn)如下:
a)所有的CPU共享全部資源,如總線挠铲,內(nèi)存和I/O系統(tǒng)等王带;
b)所有處理器都是平等的,沒(méi)有主從關(guān)系市殷;
c)內(nèi)存是統(tǒng)一結(jié)構(gòu),統(tǒng)一尋址的(UMA:Uniform Memory Access);
d)cpu和內(nèi)存刹衫,cpu之間是通過(guò)一條總線連接起來(lái)的醋寝;
但有以下缺點(diǎn):
a)擴(kuò)展能力非常有限;
b)隨著cpu個(gè)數(shù)增加带迟,系統(tǒng)總線成為了瓶頸音羞,cpu與內(nèi)存之間的通信延遲加大;
故出現(xiàn)了NUMA(Non Uniform Memory Access Architecture)仓犬,即非一致存儲(chǔ)器訪問(wèn)嗅绰,特點(diǎn)如下:
a)cpu和本地內(nèi)存擁有更小的延遲與更大的帶寬;
b)整個(gè)內(nèi)存可作為一個(gè)整體,任何cpu都能訪問(wèn)窘面,跨本地內(nèi)存訪問(wèn)較訪問(wèn)本地內(nèi)存慢一些翠语;
c)每個(gè)cpu可以有本地總線,和內(nèi)存一樣财边,訪問(wèn)本地總線延遲低肌括,添吐率高;
兩者如下圖所示:
而在dpdk中的example使用中酣难,可以看見(jiàn)如下代碼使用方式:
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)
根據(jù)該線程所在的socket_id去創(chuàng)建內(nèi)存谍夭;
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)
根據(jù)該線程所在的socket_id去創(chuàng)建端口的收發(fā)隊(duì)列;
161 struct rte_ring *
162 rte_ring_create(const char *name, unsigned count, int socket_id, unsigned flags)
根據(jù)該線程所在的socket_id去創(chuàng)建無(wú)鎖環(huán)形buffer憨募。
還有無(wú)鎖的使用紧索,cpu親和性,ddio菜谣,內(nèi)存交叉訪問(wèn)等...
參考:
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