隨着互聯網的高速發展,內容量的提高以及對內容智能的需求、雲產業的快速突起,做爲互聯網的計算基石服務器的形態以及使用成爲了煊赫一時的話題,全球各家大型互聯網公司都持續的在服務器平臺上有很是大的動做,譬如facebook的OCP等,而整個服務器的生態鏈也獲得了促進和發展。隨着服務器硬件性能的提高和網絡硬件的開放,傳統PC機的處理性能甚者能夠和網絡設備相媲美。另外一方面SDN技術的發展,基礎架構網絡逐漸偏向基於通用計算平臺或模塊化計算平臺的架構融合,來支持多樣化的網絡功能,傳統的PC機器在分佈式計算平臺上的優點更爲明顯。在這些針對海量數據處理或海量用戶的服務場景,高性能編程顯得尤其重要。html
本文講述了從C10K到C10M過程當中編程模式的改變;接着介紹了Intel DPDK開發套件如何突破操做系統限制,給開發高性能網絡服務的程序員帶來的福音;以後總結高性能程序設計的一些其它的優化方法;最後分享咱們利用DPDK技術來實現的高性能網關設備和服務程序案例。前端
前10年中,網絡程序性能優化的目標主要是爲了解決C10K問題,其研究主要集中在如何管理數萬個客戶端併發鏈接,各類I/O框架下如何進行性能優化,以及操做系統參數的一些優化。java
當前,解決C10K問題的服務器已經很是多。Nginx和Lighttpd兩款很是優秀的基於事件驅動的web服務框架,Tornado和Django則是基於python開發的非阻塞的web框架;Yaws和Cowboy則是用Erlang開發輕量級web框架。這些軟件使得C10K已經再也不是問題了。node
今天,C10M成爲新的研究主題了。也許你會感到奇怪,千萬級併發不是網絡設備的性能嗎?那是設備廠商該作的事情吧,答案在之前是,但現在不是。在互聯網設備廠商相對封閉軟件體系架構中,咱們不多關注設備內部的配置。可是當你去拆開一款交換機以後就會發現,裏面極可能就是咱們PC機使用的x86芯片,即便不是x86,那也是標準的RISC處理器。也就是說如今的互聯網硬件設備實際上不多是硬件組成——大部分都是軟件來實現的。因此千萬併發應該是咱們軟件開發人員應該去研究的問題!騰訊自研的Bobcat就是一個基於x86平臺的自研設備,性能可達千萬級別。python
咱們知道,在解決C10K問題的時候,須要將軟件設計爲異步模式,使用epoll來高效的處理網絡讀寫事件。但在面對C10M問題中,這樣設計是反而比較糟糕,爲何這麼說呢?linux
一方面在基於多線程的服務器設計框架中,在沒有請求到來的時候,線程將會休眠,當數據到來時,將由操做系統喚醒對應的線程,也就是說內核須要負責線程間頻繁的上下文切換,咱們是在依靠操做系統調度系統來服務網絡包的調度。程序員
另外一方面在以Ngnix爲表明的服務器場景,看上去僅使用一個線程監聽Epoll事件來避免上下文切換,但咱們仍將繁重的事件通知工做交由操做系統來處理。web
最後要說的是,網卡驅動收包自己也是一個異步的過程,通常是當十幾個或者更多的數據包到達以後經過軟中斷例程一次性將數據包遞交到內核,而中斷性能自己就不高。相比老的suse內核,tlinux系統也只不過讓多隊列網卡把中斷分散在不一樣CPU核心上來提升收發包性能,要是能避免中斷就更好了。算法
在千萬級併發場景下,咱們的目標是要回到最原始的方式,使用輪詢方式來完成一切操做,這樣才能提高性能。數據庫
Unix誕生之初就是爲電話電報控制而設計的,它的控制平面和數據轉發平面沒有分離,不適合處理大規模網絡數據包。若是能讓應用程序直接接管網絡數據包處理、內存管理以及CPU調度,那麼性能能夠獲得一個質的提高。
爲了達到這個目標,第一個要解決的問題就是繞過Linux內核協議棧,由於Linux內核協議棧性能並非很優秀,若是讓每個數據包都通過Linux協議棧來處理,那將會很是的慢。像Wind River和6 Wind Gate等公司自研的內核協議棧宣稱比Linux UDP/TCP協議棧性能至少提升500%以上,所以能不用Linux協議棧就不用。
不用協議棧的話固然就須要本身寫驅動了,應用程序直接使用驅動的接口來收發報文。PF_RING,Netmap和intelDPDK等能夠幫助你完成這些工做,並不須要咱們本身去花費太多時間。
Intel官方測試文檔給出了一個性能測試數據,在1S Sandbridge-EP 8*2.0GHz cores服務器上進行性能測試,不用內核協議棧在用戶態下吞吐量可高達80Mpps(每一個包處理消耗大約200 cpu clocks),相比之下,使用Linux內核協議棧性能連1Mpps都沒法達到。
多核的可擴展性對性能提高也是很是重要的,由於服務器中CPU頻率提高愈來愈慢,納米級工藝改進已是很是困難的事情了,但能夠作的是讓服務器擁有更多的CPU和核心,像國家超級計算中心的天河二號使用了超過3w顆Xeon E5來提升性能。在程序設計過程當中,即便在多核環境下也很快會碰到瓶頸,單純的增長了處理器個數並不能線性提高程序性能,反而會使總體性能愈來愈低。
一是由於編寫代碼的質量問題,沒有充分利用多核的並行性,
二是服務器軟件和硬件自己的一些特性成爲新的瓶頸,像總線競爭、存儲體公用等諸多影響性能平行擴展的因素。
那麼,咱們怎樣才能讓程序能在多個CPU核心上平行擴展:儘可能讓每一個核維護獨立數據結構;使用原子操做來避免衝突;使用無鎖數據結構避免線程間相互等待;設置CPU親緣性,將操做系統和應用進程綁定到特定的內核上,避免CPU資源競爭;在NUMA架構下儘可能避免遠端內存訪問。固然本身來實現無鎖結構時要很是當心,避免出現ABA問題和不一樣CPU架構下的內存模型的差別。
內存的訪問速度永遠也趕不上cache和cpu的頻率,爲了能讓性能平行擴展,最好是少訪問。
從內存消耗來看,若是每一個用戶鏈接佔用2K的內存,10M個用戶將消耗20G內存,而操做系統的三級cache連20M都達不到,這麼多併發鏈接的狀況下必然致使cache失效,從而頻繁的訪問內存來獲取數據。而一次內存訪問大約須要300 cpuclocks,這期間CPU幾乎被空閒。所以減小訪存次數來避免cachemisses是咱們設計的目標。
指針不要隨意指向任意內存地址,由於這樣每一次指針的間接訪問可能會致使屢次cache misses,最好將須要訪問的數據放到一塊兒,方便一次性加載到cache中使用。
按照4K頁來計算,32G的數據須要佔用64M的頁表,使得頁表甚至沒法放到cache中,這樣每次數據訪問可能須要兩次訪問到內存,所以建議使用2M甚至1G的大頁表來解決這個問題。
C10M的思想就是將控制層留給Linux作,其它數據層所有由應用程序來處理。
減小系統調度、系統調用、系統中斷,上下文切換等
摒棄Linux內核協議棧,可使用PF_RING,Netmap,intelDPDK來本身實現驅動;
使用多核編程技術替代多線程,將OS綁在指定核上運行;
使用大頁面,減小訪問;
採用無鎖技術解競爭
Intel® DPDK全稱Intel Data Plane Development Kit,是intel提供的數據平面開發工具集,爲Intel architecture(IA)處理器架構下用戶空間高效的數據包處理提供庫函數和驅動的支持,它不一樣於Linux系統以通用性設計爲目的,而是專一於網絡應用中數據包的高性能處理。目前已經驗證能夠運行在大多數Linux操做系統上,包括FreeBSD 9.二、Fedora release1八、Ubuntu 12.04 LTS、RedHat Enterprise Linux 6.3和Suse EnterpriseLinux 11 SP2等。DPDK使用了BSDLicense,極大的方便了企業在其基礎上來實現本身的協議棧或者應用。
須要強調的是,DPDK應用程序是運行在用戶空間上利用自身提供的數據平面庫來收發數據包,繞過了Linux內核協議棧對數據包處理過程。Linux內核將DPDK應用程序看做是一個普通的用戶態進程,包括它的編譯、鏈接和加載方式和普通程序沒有什麼兩樣。DPDK程序啓動後只能有一個主線程,而後建立一些子線程並綁定到指定CPU核心上運行。
DPDK核心組件由一系列庫函數和驅動組成,爲高性能數據包處理提供基礎操做。內核態模塊主要實現輪詢模式的網卡驅動和接口,並提供PCI設備的初始化工做;用戶態模塊則提供大量給用戶直接調用的函數。
DPDK架構圖
EAL(Environment Abstraction Layer)即環境抽象層,爲應用提供了一個通用接口,隱藏了與底層庫與設備打交道的相關細節。EAL實現了DPDK運行的初始化工做,基於大頁表的內存分配,多核親緣性設置,原子和鎖操做,並將PCI設備地址映射到用戶空間,方便應用程序訪問。
Buffer Manager API經過預先從EAL上分配固定大小的多個內存對象,避免了在運行過程當中動態進行內存分配和回收來提升效率,經常用做數據包buffer來使用。
Queue Manager API以高效的方式實現了無鎖的FIFO環形隊列,適合與一個生產者多個消費者、一個消費者多個生產者模型來避免等待,而且支持批量無鎖的操做。
Flow Classification API經過Intel SSE基於多元組實現了高效的hash算法,以便快速的將數據包進行分類處理。該API通常用於路由查找過程當中的最長前綴匹配中,安全產品中根據Flow五元組來標記不一樣用戶的場景也可使用。
PMD則實現了Intel 1GbE、10GbE和40GbE網卡下基於輪詢收發包的工做模式,大大加速網卡收發包性能。
DPDK的核心組件
展現了DPDK核心組件的依賴關係,詳細介紹能夠參考《Intel Data Plane Development kit:Software Architecture Specification》。
當前Linux操做系統都是經過中斷方式通知CPU來收發數據包,咱們假定網卡每收到10個據包觸發一次軟中斷,一個CPU核心每秒最多處理2w次中斷,那麼當一個核每秒收到20w個包時就佔用了100%,此刻它沒作其它任何操做。
DPDK針對Intel網卡實現了基於輪詢方式的PMD(Poll Mode Drivers)驅動,該驅動由API、用戶空間運行的驅動程序構成,該驅動使用無中斷方式直接操做網卡的接收和發送隊列(除了鏈路狀態通知仍必須採用中斷方式之外)。目前PMD驅動支持Intel的大部分1G、10G和40G的網卡。
PMD驅動從網卡上接收到數據包後,會直接經過DMA方式傳輸到預分配的內存中,同時更新無鎖環形隊列中的數據包指針,不斷輪詢的應用程序很快就能感知收到數據包,並在預分配的內存地址上直接處理數據包,這個過程很是簡潔。若是要是讓Linux來處理收包過程,首先網卡經過中斷方式通知協議棧對數據包進行處理,協議棧先會對數據包進行合法性進行必要的校驗,而後判斷數據包目標是否本機的socket,知足條件則會將數據包拷貝一份向上遞交給用戶socket來處理,不只處理路徑冗長,還須要從內核到應用層的一次拷貝過程。
爲實現物理地址到虛擬地址的轉換,Linux通常經過查找TLB來進行快速映射,若是在查找TLB沒有命中,就會觸發一次缺頁中斷,將訪問內存來從新刷新TLB頁表。Linux下默認頁大小爲4K,當用戶程序佔用4M的內存時,就須要1K的頁表項,若是使用2M的頁面,那麼只須要2條頁表項,這樣有兩個好處:第一是使用hugepage的內存所需的頁表項比較少,對於須要大量內存的進程來講節省了不少開銷,像oracle之類的大型數據庫優化都使用了大頁面配置;第二是TLB衝突機率下降,TLB是cpu中單獨的一塊高速cache,通常只能容納100條頁表項,採用hugepage能夠大大下降TLB miss的開銷。
DPDK目前支持了2M和1G兩種方式的hugepage。經過修改默認/etc/grub.conf中hugepage配置爲「default_hugepagesz=1Ghugepagesz=1G hugepages=32 isolcpus=0-22」,而後經過mount –thugetlbfs nodev /mnt/huge就將hugepage文件系統hugetlbfs掛在/mnt/huge目錄下,而後用戶進程就可使用mmap映射hugepage目標文件來使用大頁面了。測試代表應用使用大頁表比使用4K的頁表性能提升10%~15%
多線程編程早已不是什麼新鮮的事物了,多線程的初衷是提升總體應用程序的性能,可是若是不加註意,就會將多線程的建立和銷燬開銷,鎖競爭,訪存衝突,cache失效,上下文切換等諸多消耗性能的因素引入進來。這也是Ngnix使用單線程模型能得到比Apache多線程下性能更高的祕籍。
爲了進一步提升性能,就必須仔細斟酌考慮線程在CPU不一樣核上的分佈狀況,這也就是常說的多核編程。多核編程和多線程有很大的不一樣:多線程是指每一個CPU上能夠運行多個線程,涉及到線程調度、鎖機制以及上下文的切換;而多核則是每一個CPU核一個線程,核心之間訪問數據無需上鎖。爲了最大限度減小線程調度的資源消耗,須要將Linux綁定在特定的核上,釋放其他核心來專供應用程序使用。
同時還須要考慮CPU特性和系統是否支持NUMA架構,若是支持的話,不一樣插槽上CPU的進程要避免訪問遠端內存,儘可能訪問本端內存。
總的來講,爲了獲得千萬級併發,DPDK使用以下技術來達到目的:使用PMD替代中斷模式;將每個進程單獨綁定到一個核心上,並讓CPU從這些核上隔離開來;批量操做來減小內存和PCI設備的訪問;使用預取和對齊方式來提供CPU執行效率;減小多核之間的數據共享並使用無鎖隊列;使用大頁面。
除了UDP服務器程序,DPDK還有不少的場景能應用得上。一些須要處理海量數據包的應用場景均可以用上,包括但不侷限於如下場景:NAT設備,負載均衡設備,IPS/IDS檢測系統,TOR(Top of Rack)交換機,防火牆等,甚至web cache和web server也能夠基於DPDK來極大地提升性能。
除了DPDK提供的一些是思想外,咱們的程序性能還能怎樣進一步提升性能呢?
運算指令的執行速度是很是快,大多數在一個CPU cycle內就能完成,甚至經過流水線一個cycle能完成多條指令。但在實際執行過程當中,處理器須要花費大量的時間去存儲器來取指令和數據,在獲取到數據以前,處理器基本處於空閒狀態。那麼爲了提升性能,縮短服務器響應時間,咱們能夠怎樣來減小訪存操做呢?
少用數組和指針,多用局部變量。由於簡單的局部變量會放到寄存器中,而數組和指針都必須經過內存訪問才能獲取數據;
少用全局變量。全局變量被多個模塊或函數使用,不會放到寄存器中。
一次多訪問一些數據。就比如咱們出去買東西同樣,一次多帶一些東西更省時間。咱們可使用多操做數的指令,來提升計算效率,DPDK最新版本配合向量指令集(AVX)可使CPU處理數據包性能提高10%以上。
本身管理內存分配。頻繁調用malloc和free函數是致使性能下降的重要緣由,不只僅是函數調用自己很是耗時,並且會致使大量內存碎片。因爲空間比較分散,也進一步增大了cache misses的機率。
進程間傳遞指針而非整個數據塊。在高速處理數據包過程當中特別須要注意,前端線程和後端線程儘可能在同一個內存地址來操做數據包,而不該該進行多餘拷貝,這也是Linux系統沒法處理百萬級併發響應的根本緣由,有興趣的能夠搜索「零拷貝」的相關文章。
現在CPU早已不是在每次取數據和指令都先去訪問內存,而是優先訪問cache,若是cache命中則無需訪存,而訪問同一個cache中數據的開銷很是小。下圖展現了L1~L3級cache和內存的訪問延時。
三級Cache性能模型
Cache有效性得益於空間局部性(附近的數據也會被用到)和時間局部性(從此一段時間內會被屢次訪問)原理,經過合理的使用cache,可以使得應用程序性能獲得大幅提高。下面舉一個實際的例子來讓你們理解cache大小對程序性能的影響。
模擬cache大小對程序性能的影響
這裏對上面的測試結果解釋一下:在K<1024時,訪問數組arr的步長不超過1024*4byte=4KB,而咱們的測試機器L1 cache使用的是8路組關聯的4K大小的cache,一次最多cache住4K的數據,所以在K<1024時候,cache從內存讀取數據次數是同樣的,因此執行時間差異不大;而當K>1024時候,訪問數組步長爲cache大小的倍數,固然訪存次數也成倍較少(能夠經過perf工具來跟蹤分析),所以執行時間成倍減小。Cache大小能夠經過命令lscpu、cat/proc/cpuinfo,或者在目錄/sys/devices/system/cpu/cpu0/cache/中進行查看。
熟知cache的大小,瞭解程序運行的時間和空間上局部性原來,對於咱們合理利用cache,提高性能很是重要。同時要少用靜態變量,由於靜態變量分配在全局數據段,在一個反覆調用的函數內訪問該變量會致使cache的頻繁換入換出,而若是是使用堆棧上的局部變量,函數每次調用時CPU能夠直接在緩存中命中它。最後,循環體要簡單,指令cache也僅僅有幾K,過長的循環體會致使屢次從內存中讀取指令,cache優點蕩然無存。
多線程中爲了不上鎖,可使用一個數組,每一個線程獨立使用數組中的一個項,互不衝突。從邏輯上看這樣的設計很是完美,但實際中運行速度並無太大改善,緣由就在下面慢慢來解釋了。
cache line是cache從主存copy數據的最小單元,cpu從不直接訪問主存,而是經過cache間接訪問主存,在訪問主存以前會遍歷一遍cache line來查找主存地址是否在某個cache line中,若是沒找到則將內存copy到cache line中,而後從cache line獲取數據。
多核CPU中每一個核都擁有本身的L1/L2 cache,當運行多線程程序時,儘管算法上不須要共享變量,但實際執行中兩個線程訪問同一cache line的數據時就會引發衝突,每一個線程在讀取本身的數據時也會把別人的cacheline讀進來,這時一個核修改改變量,CPU的cache一致性算法會迫使另外一個核的cache中包含該變量所在的cache line無效,這就產生了false sharing(僞共享)問題。
false sharing示意圖
Falsing sharing會致使大量的cache衝突,應該儘可能避免。下面是一個典型的例子,假如在CPU的4個核上分別運行4個線程,傳遞參數爲0~3,將會觸發false sharing。
false sharing測試代碼
下面總結一下文章《Avoiding and IdentifyingFalse Sharing Among Threads》裏面的建議:訪問全局變量和動態分配內存是falsesharing問題產生的根源,固然訪問在內存中相鄰的但徹底不一樣的全局變量也可能會致使false sharing,多使用線程本地變量是解決false sharing的根源辦法。
固然,在平時編程中避免不了要使用全局變量時,這時能夠將多個線程訪問的變量放置在不一樣的cache line中,這樣經過犧牲一些內存空間來換取高性能。
首先談到的是內存對齊:根據不一樣存儲硬件的配置來優化程序,性能也可以獲得極大的提高。在硬件層次,確保對象位於不一樣channel和rank的起始地址,這樣能保證對象並並行加載。
Channel是指內存上北橋上面的獨立內存接口,一個內存通道通常由64位數據總線和8位控制總線構成,不一樣通道能夠並行工做,當有兩個channel同時工做就是咱們平時所說「雙通道」,其效果等價於128位數據總線的帶寬。Rank是指DIMM上經過一部分或者全部內存顆粒產生的一個64位的area,同一條內存上的不一樣rank由於共享數據總線而不能被同時訪問,但內存能夠利用交錯的片選信號來訪問不一樣rank中數據。
雙通道、4R的DIMM
在以上的情形中,假定數據包是64bytes大小,那麼最優的對齊方式是在每一個數據包間填充12bytes。
其次談到的是字節對齊:衆所周知,內存最小的存儲單元爲字節,在32位CPU中,寄存器也是32位的,爲了保證訪問更加高效,在32位系統中變量存儲的起始地址默認是4的倍數(64位系統則是8的倍數),定義一個32位變量時,只須要一次內存訪問便可將變量加載到寄存器中,這些工做都是編譯器完成的,不需人工干預,固然咱們可使用__attribute__((aligned(n)))來改變對齊的默認值。
最後談到的是cache對齊,這也是程序開發中須要關注的。Cache line是CPU從內存加載數據的最小單位,通常L1 cache的cache line大小爲64字節。若是CPU訪問的變量不在cache中,就須要先從內存調入到cache,調度的最小單位就是cache line。所以,內存訪問若是沒有按照cache line邊界對齊,就會多讀寫一次內存和cache了。
爲了解決單核帶來的CPU性能不足,出現了SMP,但傳統的SMP系統中,全部處理器共享系統總線,當處理器數目愈來愈多時,系統總線競爭加大,系統總線稱爲新的瓶頸。NUMA(非統一內存訪問)技術解決了SMP系統可擴展性問題,已成爲當今高性能服務器的主流體系結構之一。
NUMA系統節點通常是由一組CPU和本地內存組成。NUMA調度器負責將進程在同一節點的CPU間調度,除非負載過高,才遷移到其它節點,但這會致使數據訪問延時增大。下圖是2顆CPU支持NUMA架構的示意圖,每顆CPU物理上有4個核心。
NMUA架構示意圖
在Nehalem微架構下,鏈接CPU的雙向QPI總線鏈接理論最大值能夠達到25.6GB/s的數據傳送,單向則是12.8GB/s,遠方/本地延遲比是約1.5倍。所以在萬兆服務器應用開發時,咱們須要仔細斟酌和合理的使用內存,避免CPU訪問遠端內存產生沒必要要的延時,也要留心在高速處理數據包時受到QPI總線帶寬的瓶頸。因爲業務邏輯目前還沒法作到如此簡潔,QPI並未成爲系統瓶頸,但騰訊Bobcat項目在處理在轉發高達10G流量時就碰到了這個問題!
注意,並非公司的全部服務器都支持NUMA的,這個功能須要操做系統、CPU和主板同時支持,能夠經過numactl --show來查看numa是否有效,目前L2機型是支持了NUMA架構的,普通的C一、B6等服務器則沒有支持NUMA特性。
進程上下文切換(context switch,簡稱CS)對程序性能的影響每每會被你們忽視,但它其實一直在默默扼殺着程序的性能!上下文切換是指CPU控制權由運行任務轉移到另外一個就緒任務所發生的事件:此時須要保存進程狀態和寄存器值等,不只浪費了CPU的時鐘週期,還會致使cache中進程相關數據失效等。
那麼如何來減小進程上下文切換呢?咱們首先須要瞭解哪些場景會觸發CS操做。首先就介紹的就是不可控的場景:進程時間片到期;更高優先級進程搶佔CPU。其次是可控場景:休眠當前進程(pthread_cond_wait);喚醒其它進程(pthread_cond_signal);加鎖函數、互斥量、信號量、select、sleep等很是多函數都是可控的。
對於可控場景是在應用編程須要考慮的問題,只要程序邏輯設計合理就能較少CS的次數。對於不可控場景,首先想到的是適當減小活躍進程或線程數量,所以保證活躍進程數目不超過CPU個數是一個明智的選擇;而後有些場景下,咱們並不知道有多少個活躍線程的時候怎麼來保證上下文切換次數最少呢?這是咱們就須要使用線程池模型:讓每一個線程工做前都持有帶計數器的信號量,在信號量達到最大值以前,每一個線程被喚醒時僅進行一次上下文切換,當信號量達到最大值時,其它線程都不會再競爭資源了。
現代處理器都是經過多級流水來提升指令執行速度,爲了保持流水線充滿待執行指令,CPU必須提早獲取指令。當程序中遇到分支或條件跳轉語句時,問題就來了,處理器不肯定下一條指令,這是就會使用分支預測邏輯來判斷進入流水的下一條指令。
從P5處理器開始引入了分組預測機制,若是預測的一個分支指令加入流水線,以後卻發現它是錯誤的分支,處理器要回退該錯誤預測執行的工做,再用正確的指令填充流水線。這樣一個錯誤的預測會嚴重浪費時鐘週期,致使程序性能降低。《計算機體系結構:量化研究方法》指出分支指令產生的性能影響爲10%~30%,流水線越長,性能影響越大。Core i7和Xen等較新的處理器當分支預測失效時無需刷新所有流水,當錯誤指令加載和計算仍會致使一部分開銷。
若是咱們已經明確知道一個條件發生的機率是偏大仍是偏小,那麼能夠在程序中顯示使用likely、unlikely預處理指令,來指示編譯器在生成彙編代碼時候對指令進行優化,加快執行速度。
這兩個宏在內核中定義以下:
#definelikely(x) __builtin_expect((x),1) #defineunlikely(x) __builtin_expect((x),0)
下面咱們從指令級併發的角度來考察從cache對程序性能的影響
int[] a = new int[2]; for (int i=0; i<steps; i++) { a[0]++; a[0]++; } // 循環1 for (int i=0; i<steps; i++) { a[0]++; a[1]++; } // 循環2
在咱們的測試機上運行這兩個循環,第一個循環須要1.6s,第二個循環只要0.8s,這是爲何呢?由於第一個循環體內,操做相互依賴,必須等第一次a[0]++執行完後才能執行後續操做;而第二個循環因爲是不一樣內存的訪問,可用作到併發執行。
這個緣由其實就是和CPU中的流水線有關,像Pentium處理器就有U/V兩條流水,而且能夠獨自獨立讀寫緩存,循環2能夠將兩條指令安排在不一樣流水線上執行,性能獲得極大提高。另外兩條流水線是非對稱的,簡單指令(mpv,add,push,inc,cmp,lea等)能夠在兩條流水上並行執行、位操做和跳轉操做併發的前提是在特定流水線上工做、而某些複雜指令卻只能獨佔CPU。
須要補充說明,由於上面舉得例子很是簡單,而在實際代碼編寫過程當中,每每使用的變量很是多,邏輯也比較複雜,數據在內存中的分佈也會影響cache的命中次數,而且分支預測致使的CPU中指令亂序執行,很是難去分析執行流程。所以在程序設計中,要作到指令級並行,須要有意識的注意指令間的配對,儘可能使用簡單指令,還要在順序上減小上下文的依賴。
爲了利用空間局部性,同時也爲了覆蓋數據從內存傳輸到CPU的延遲,能夠在數據被用到以前就將其調入緩存,這一技術稱爲預取Prefetch,加載整個cache便是一種預取。CPU在進行計算過程當中能夠並行的對數據進行預取操做,所以預取使得數據/指令加載與CPU執行指令能夠並行進行。
預取能夠經過硬件或軟件控制。典型的硬件指令預取會在緩存因失效從內存載入一個塊的同時,把該塊以後緊鄰的一個塊也傳輸過來。第二個塊不會直接進入緩存,而是被排入指令流緩衝器(Instruction Stream Buffer)中。以後,當第二個內存訪問指令到來時,會並行嘗試從緩存和流緩衝器中讀取。若是該數據剛好在流緩衝器中,則取消緩存訪問指令,並將返回流緩衝器中的數據。同時,發出起一次新的預取。若是數據並不在流緩衝器中,則須要將緩衝器清空。
軟件控制則多由編譯器進行。指令集會提供預取指令供編譯器優化時使用。編譯器則負責分析代碼,並把預取指令適當地插入其中。這類指令直接把目標預取數據載入緩存。若是咱們在編程中能顯示的調用預取指令,就能大大提升效率。若是讀取的內容僅僅被訪問一次,prefetch也沒有意義。
在使用預取指令時,必須考慮調用時機和實施強度。若是過早地進行預取,則有可能在預取數據被用到以前就已經由於衝突置換被清除。若是預取得太多或太頻繁,則預取數據有可能將那些更加確實地會被用到的數據取代出緩存,反而會增長開銷。
一開始在處理進程開發過程當中增長了大量預取操做,可是性能反而降低了,由於在處理進程中對於每一個數據包分析邏輯比較複雜,數據預取填充的cache很快就被業務邏輯指令和數據替換了無數遍嗎,所以預取必定要得當
先從一個簡單的例子入手:
函數F1實現 | 函數F2實現 |
int i=0,j=0; int i=0,j=0; static a[1000][500000] = {0}; for (j=0;j<500000;j++){ for (i=0;i<1000;i++){ a[i][j] = 2*a[i][j]; } } |
int i=0,j=0; static a[1000][500000] = {0}; for (i=0;i<1000;i++){ for (j=0;j<500000;j++){ a[i][j] = 2*a[i][j]; } } |
運行左邊的程序須要10s,運行右邊的程序只需不到4s。產生這個現象的緣由就是右邊的例子中由於a[i][0]訪問cache失效後,會從內存中讀取至少一條cache line數據(64bytes),所以cache中充滿了a[i][0]~a[i][15]的結果,這樣後面15次循環均可以命中cache了。
所以,數據cache大小很是有限,循環中數組訪問的順序很是重要,對性能的影響不容小覷。
另外,若是兩個循環體能夠合併到一個循環而不影響程序結果,則應該合併。由於經過合併,原來第二個循環中的指令會在指令cache中被命中。
其次,指令cache和數據cache同樣都很是小,循環中編寫的指令必定要精簡,非循環內部的操做能夠放到外面去,不然一旦循環體中指令長度超過cache大小就會致使沒必要要的置換。
再次,須要考慮cache的置換策略,例如cache使用的是LRU算法的話,在編寫多層嵌套循環時須要考慮被置換出去的數據越少越好。
最後,若是能少用循環的話就更好了!
4.11其它優化建議
儘可能減小函數調用,每一次調用都要進行壓棧、保存寄存器和執行指令跳轉等都會耗費很多時間,能夠將一些小的函數寫成內聯,或直接用宏或語句代替。
對於提升性能空間換時間也是一個很是重要的設計理念,Bloom Filter就是一個典型的例子,還有一些位圖、hash的思想也是不錯的選擇。
在高性能程序設計時,要減小過保護,除了會影響程序執行的一些關鍵路徑和參數要進行校驗外,其它參數不必定非得要檢查,畢竟錯誤狀況是少數。
儘可能用整型代替浮點數,少用乘除、求餘運算,這些操做會佔用更多的CPU週期,能提早計算的表達式要提早計算出結果。
充分利用編譯器選項,-O3幫助你進行文件內部最深層次的優化,使用其它編譯器如icc能編譯出在x86平臺下運行更快的程序。
延時計算,最近用不上的變量就不要去初始化,操做系統爲了提升性能也使用COW(copy-on-write)策略,在fork子進程的時候不會當即複製進程的全部頁表。
固然,當你花費大量的時間也就是爲了利用硬件的某個特性,左思右想以後或許獲取了一點點性能的提高,其實更重要的是在程序設計時候換一種思路,也許就會柳暗花明,從邏輯上或算法上優化得到的效果可能遠遠大於針對硬件特性優化的效果,在程序設計之初必定要深刻理解業務邏輯,最大程度上簡化流程,針對硬件特性的優化必定要是在程序邏輯優化以後進行才能更有效。
在高性能服務器程序開發中,經過網卡中斷收發報文是第一道瓶頸,爲了不中斷方式的網卡驅動,能夠經過卸載Linux自帶的網卡驅動,代而使用DPDK提供的PMD模式的驅動,避免了中斷方式收發數據包,經過輪詢方式直接從網卡隊列收發數據包並經過DMA方式遞交到應用層內存中,大大提高了收發包性能。
相比較Linux默認的4K頁表,在應用程序中使用1G的大頁表,能夠大大減小缺頁中斷,提高內存的訪問速度。
在CPU親緣性設置方面,將Linux運行在單獨的內核上,實際的數據收發包或者處理邏輯綁定在單獨的內核上,避免進程間競爭和上下文切換。
爲了方便在不一樣數據中心間進行數據傳輸,通常經過建設專線來知足要求。可是專線建設成本高,時間長,當容量增加過快時擴容緩慢,若是在基礎架構側可以利用公網來進行一部分數據的傳輸,就能緩解專線的壓力了。另外一方面,基於網絡的容災需求,大部分專線利用率都低於50%,同時專線是按帶寬收費的,浪費了一半以上的成本,如何提升專線利用率的同時還可以低成本的保證網絡容災需求,成爲一個必須解決的問題。大流量的公網傳輸平臺在這種背景下應用而生,但要達到此目的,必定要避免成爲數據轉發的瓶頸。所以必須保證數據報文轉發的高性能。
Bobcat是自研的利用公網來傳輸業務數據的一種網關設備,它具有報文收發,尋址,報文封裝和校驗等一些列功能。在實現上,bobcat僅使用一個CPU核心來運行Linux操做系統,依賴於Linux完整的協議棧來實現管理功能。其它CPU核心都用來作IPP(Ingress Packet Processor)、EPP(Egress Packet Processor)以及報文轉發功能。
具體工做流程以下:svr將包傳遞到城域網核心,根據路由來決定是否跨城;包到達了Wan,則根據DSCP值來區分專線和大流量平臺;在包到達Bobcat平臺後,將進行目標Bobcat的查表,頭部封裝,同時打散成多個傳輸通道給於傳遞;bobcat收到傳遞過來的報文則進行頭部校驗,須要保證不給篡改和重複的序列號;並向接收端的Wan核心轉發;wan core這根據目標地址傳遞man core。
因爲引入多核,Bobcat須要處理報文發送的時序問題,多核的並行處理會致使EPP後的報文亂序,所以要對報文進行重排序,確保從物理端口發出去的順序和收到的順序一致,所以這裏EPP僅有單核來處理,避免重排序的複雜性,固然系統的總體性能也取決於EPP的轉發能力。在實際測試中,Bobcat在轉發64bytes小包時幾乎能夠達到10Gbps的線速。
基於UDP的無狀態特性,能夠很方便的使用DPDK來提供底層的收發數據包框架,本身實現協議的解析和處理過程,而不用藉助於繁重的Linux內核協議棧。
親緣性設置方面,在一臺雙CPU的12顆物理核的機器上,由於開啓了超線程,邏輯覈實際是24個,但因爲兩顆CPU之間的通訊須要使用QPI,會增大報文處理時延。所以,只利用其一顆CPU的12個超核來進行數據處理。通過不斷優化,雙端口的轉發性能也能夠達到線速。這種系統架構中,處理進程共運行11個並分佈在同一顆CPU的邏輯核上,其中兩個disaptch進程用來收發網卡數據包,並均勻的將數據包放到和處理進程公用的9個無鎖隊列中。
一種UDP服務程序的親緣性設置方案
另外本身來處理數據包,從以太網數據幀開始向上層協議分析,並進行必要的校驗,只過濾出須要處理的UDP報文,不去使用複雜且低效的Linux協議棧來處理報文。
數據包的讀取都是使用批量操做,當網卡隊列在收到32個數據包後,一次性將數據包傳輸到內存中;同時使用HPET時鐘(RDTSC指令也行)定時處理不夠32數據包的情形,避免響應延時。
使用無鎖數據結構,進程間使用無鎖的環形隊列,dispatch進程將數據放不一樣的無鎖的ring buf裏,供處理進程來獲取,避免了加鎖的等待延時。使用了預取操做,主要用在了從網卡隊列讀取數據包進行檢驗後傳遞到內存的過程當中,這樣在對數據幀校驗過程當中的同時,也並行地將下一個數據包放到cache當中,節省了數據傳輸延時。
合理的使用分支預測,在大機率條件語句前加上likely,反之加unlikely。
預分配內存,依據隊列大小,爲每一個進程預先獨立分配UDP報文處理所須要的空間,在構造UDP應答包結構的時候直接經過空閒指針鏈表獲取便可。避免拷貝數據包。Dispatch進程將數據報文經過DMA方式傳遞到內存以後,只是將報文地址放入到ring buf,UDP處理進程能夠在原地址處直接來解析數據包並就地修改,而後通知Dispatch是否要由網卡發出去,或者丟棄。
數據包頭部和尾部預留空間,這樣在修改數據包的內容或填充新的頭部、尾部的時候不須要從新申請空間了,而直接在原處修改便可。減小內存訪問是優化的重點,也是難點,上面的不少方法其實均可以減小內存訪問,但最重要的是要在程序中避免複雜變量的拷貝,多使用指針,這也須要很是當心的編寫代碼。也不能爲了減小內存拷貝而把全部字符串複製都修改成指針,這將致使極難維護和調試。
經過硬件指令加速hash計算,甚者直接使用intel的crc指令來計算hash,比傳統純軟件hash算法性能大幅提高。避免過渡校驗:一個未被修改的參數從頭至尾只須要校驗一次,事先能確保字符串以\0結尾後面也能夠減小一些判斷機制。
還有不少其它的用來提升程序性能的編程方法和技巧可能沒列舉出來。總之,在設計和開發系統以前須要對硬件和操做系統有較深刻的瞭解,並保持着以追求性能爲目標的心態來編寫每一個函數和模塊,那麼寫出來的程序性能也不會差了。在高性能服務程序設計過程當中,也不可能把上述各項優化點作到最優,而是結合時間成本上的考慮,獲得一個合理的優化性能目標,最重要的仍是要保證穩定性。
[1]. www.kegel.com/c10k.html ,Internet
[2]. c10m.robertgraham.com,Internet
[3]. I ntel Data Plane Development kit:Software Architecture Specification , Intel
[4]. Avoiding and Identifying False Sharing Among Threads, Intel
[5]. 《大話處理器》,清華大學出版社