1. 程式人生 > >DPDK中的memcpy效能優化及思考

DPDK中的memcpy效能優化及思考

記憶體拷貝(memcpy)這個操作看似簡單,但長期以來存在很多關於其優化的討論,各種程式語言庫也都有對應實現,而對於memcpy效能評估測試的討論就更多了。 那麼如下的memcpy實現到底有什麼問題?

void * simple_memcpy(void *dst, const void *src, size_t n) { const uint8_t *_src = src; uint8_t *_dst = dst; size_t i;

    for (i = 0; i < n; ++i)
            _dst[i] = _src[i];

    return dst;

} 1 2 3 4 5 6 7 8 9 10 11

void * simple_memcpy(void *dst, const void *src, size_t n) { const uint8_t *_src = src; uint8_t *_dst = dst; size_t i;

    for (i = 0; i < n; ++i)
            _dst[i] = _src[i];

    return dst;

}

    很簡單,首先,這看起來太簡單,不夠高階,氣勢上就先輸了;同時,程式碼沒有使用Vector指令,沒有指令級並行,沒有做地址對齊處理,最終效能完全依賴於編譯器的優化——然而這些並沒有什麼問題,在某些應用場景中這個函式的效能甚至會比glibc的memcpy效能更高——當然,這的確完全得益於編譯器的優化。
    本文的觀點是:不存在一個“最優”的適用於任何場景(硬體+軟體+資料)的memcpy實現。這也是DPDK中rte_memcpy存在的原因:不是glibc中的memcpy不夠優秀,而是它和DPDK中的核心應用場景之間不合適,有沒有覺得這種說法很耳熟?本文將著重探討如何針對具體應用進行memcpy(或其他任何程式)的效能優化。

常見的優化方法 關於memcpy的優化存在大量各種官方和民間資料,因此不再贅述,在這裡只簡單地進行總結。 通常memcpy的效能開銷包含:

  1. 資料的Load/Store2. 附加計算任務(例如地址對齊處理)

  2. 分支預測 通用的memcpy優化方向:1. 最大限度使用memory/cache頻寬(Vector指令、指令級並行)

  3. Load/Store地址對齊

  4. 集中順序訪問

  5. 適當使用non-temporal訪存執令

  6. 適當使用String指令來加速較大的拷貝 最後,所有的指令都經過CPU的流水線執行,因此對流水線效率的分析至關重要,需要優化指令順序以避免造成流水線阻塞。

如何進行優化 從2015年初開始DPDK對rte_memcpy做了數次優化,優化方向是加速DPDK中memcpy的應用場景,例如Vhost收發包,所有的分析和程式碼修改都可以通過git log進行檢視。關於如何著手進行優化這個問題,並沒有唯一的答案。最簡單直接的方法就是暴力破解法:僅憑經驗通過想象和猜測進行各種改進嘗試,並一一在目標場景中進行驗證,然後通過一組評價標準來選擇其中較優的一個。這是比較接近人工智慧的一種方法,也就是說其實這並不怎麼智慧,但是往往能夠得到一些效果。

另一種常見的方法是通過對現有程式碼進行執行時取樣分析,得出理論最佳效能,並分析現有程式碼的缺陷,思考是否存在改進方法。當然,這需要大量的經驗和分析,即是用人工代替人工智慧。一個DPDK 16.11中的真實案例:花了四周時間進行取樣和分析,最後將三行程式碼的位置進行了移動,得到了1.7倍的效能。這是暴力破解法很難完成的,因為搜尋空間實在太大。不過這種看似高階的方法也存在較大的風險,極有可能分析了四周然後不了了之。

至於如何進行取樣分析,如perf、vTune等都是非常有效的效能分析工具,其重點不在於工具多樣,而在於知道需要什麼資料。

最終用資料說話

優化的最終目的是為了加速應用程式的效能,這就需要結合程式碼、資料特徵以及實際硬體平臺來進行評估,至於評估的方法則是五花八門。對於memcpy來說,使用micro benchmark可以很方便地得出一個一目瞭然的資料,不過這缺乏實際參考價值。因為memcpy演算法本身沒有太大的改進空間,相關優化都是針對具體平臺和應用場景所做的程式語言級和指令級的優化,而這個層面的優化都是針對特定的程式碼和硬體平臺而言的,不同的場景需要不同的優化方法。因此,一套memcpy的程式碼在某個micro benchmark中效能出色只能證明其指令流適應這個場景。

此外,通過計算某段memcpy的CPU週期數來評估效能也是不可取的。例如,DPDK中Vhost enqueuer/dequeue函式,不能簡單地通過rdtsc來標記memcpy佔用了多少個時鐘週期,從而判斷memcpy的效能。因為目前的CPU都有著非常複雜的流水線,支援預取和亂序執行,這就造成rdtsc測量小粒度時間間隔時誤差非常大,雖然加入序列化指令可以進行強制同步,但是這改變了指令流執行順序並降低了程式效能,違背了效能測試的初衷。同時,經過編譯器高度優化的程式碼,其指令相對於程式語言來說也是亂序的,如果強制順序編譯則會影響效能,其結果也不具有參考價值。此外,一段指令流的執行時間除流水線的理論執行時間外,還包括資料訪問帶來的延時,通過改變程式的行為很容易讓一段程式碼看起來執行時間更短,而實際上只是將某些資料訪問延時轉提前或延後而已。這些錯綜複雜的因素讓看似簡單的memcpy效能評估變得似乎毫無頭緒。

因此,任何優化工作都應該用最終的效能指標來作為評估依據。例如針對OvS在Cloud中的應用,其中的Vhost收發函式大量使用了memcpy,那麼針對這個特定的場景,應當以最終的包轉發速率作為效能評估標準。

下圖展示了一個接近實際應用場景的例子:首先測試基於DPDK 17.02的OvS-DPDK的包轉發效能,再通過使用glibc所提供的memcpy替換DPDK Vhost中rte_memcpy得到對比資料。測試結果顯示,僅通過使用rte_memcpy加速整個程式中的Vhost收發部分就能提供最大約22%的總頻寬提升。

如果你在使用DPDK時發現有效能不夠優化的地方,歡迎到[email protected]或DPDK微信群進行討論。最後,對於優化來說,達到最優或許並不是最重要的,因為根本達不到,就算達到也不能說(參見《廣告法》)——這些都不重要,優化的意義是離最優又更近了一步,併為目標業務帶來了實際的收益。

附錄 測試環境: · PVP流:使用IXIA發包到物理網絡卡,OvS-DPDK將物理網絡卡收到的包轉發到虛擬機器,虛擬機器則將包處理後通過OvS-DPDK傳送回物理網絡卡,最後回到IXIA

· 虛擬機器中使用DPDK testpmd的MAC-forwarding

· OvS-DPDK版本: Commit f56f0b73b67226a18f97be2198c0952dad534f1c

· DPDK版本:17.02

· GCC/GLIBC版本:6.2.1/2.23

· Linux:4.7.5-200.fc24.x86_64

· CPU:Intel® Xeon® CPU E5-2699 v3 @ 2.30GHz

OvS-DPDK編譯和啟動命令如下:

make ‘CFLAGS=-g -Ofast -march=native’

./ovs-vsctl –no-wait set Open_vSwitch . other_config:dpdk-init=true

./ovs-vsctl –no-wait set Open_vSwitch . other_config:dpdk-socket-mem=”1024,1024″

./ovs-vsctl add-br ovsbr0 — set bridge ovsbr0 datapath_type=netdev

./ovs-vsctl add-port ovsbr0 vhost-user1 — set Interface vhost-user1 type=dpdkvhostuser

./ovs-vsctl add-port ovsbr0 dpdk0 — set Interface dpdk0 type=dpdk options:dpdk-devargs=0000:06:00.0

./ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x10000

./ovs-ofctl del-flows ovsbr0

./ovs-ofctl add-flow ovsbr0 in_port=1,action=output:2

./ovs-ofctl add-flow ovsbr0 in_port=2,action=output:1

虛擬機器中使用DPDK testpmd進行轉發,命令如下:

set fwd mac

start