中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

給孩子做的飯網(wǎng)站網(wǎng)絡公關公司

給孩子做的飯網(wǎng)站,網(wǎng)絡公關公司,燈箱網(wǎng)站開發(fā),有沒有外國人做發(fā)明的網(wǎng)站引入 定位程序性能問題,相信大家都有很多很好的辦法,比如用top/uptime觀察負載和CPU使用率,用dstat/iostat觀察io情況,ptrace/meminfo/vmstat觀察內(nèi)存、上下文切換和軟硬中斷等等,但是如果具體到CPU問題,我…

引入

定位程序性能問題,相信大家都有很多很好的辦法,比如用top/uptime觀察負載和CPU使用率,用dstat/iostat觀察io情況,ptrace/meminfo/vmstat觀察內(nèi)存、上下文切換和軟硬中斷等等,但是如果具體到CPU問題,我們可能只能夠分析到CPU占用率很高或者系統(tǒng)負載很高,更細化的指標確無從著手;就算能夠知道IPC或者Retiring很高,優(yōu)化可能也一籌莫展。畢竟現(xiàn)代CPU微架構十分復雜,而且無論是Intel? 64 and IA-32 Architectures Software Developer Manuals還是Intel? 64 and IA-32 Architectures Optimization Reference Manual都太長了,作為軟件開發(fā)人員很難在里面找到自己想要的知識,這篇文章就結合Top-down Micro-architecture Analysis Methodology(TMAM)、ClickHouse源碼的優(yōu)化經(jīng)驗,聊一聊CPU高性能優(yōu)化具體該怎么分析怎么做。

微架構分析的TMAM方法

定位CPU問題,我們可以采用Intel的TMAM方法:https://cdrdv2.intel.com/v1/dl/getContent/671488?explicitVersion=true&fileName=248966-046A-software-optimization-manual.pdf
首先介紹下現(xiàn)代微架構的結構,如圖所示:
image.png
前端負責內(nèi)存取指(fetch instructions from memory)和轉譯微指令(translate them into micro-operations / uops)。轉譯后得到的微指令將被饋送到后端部分。
后端負責對每個微指令進行調(diào)度(schedule)、執(zhí)行(execute)和提交退役(commit / retire)。這種生產(chǎn)-消費的流水線模型就是使用隊列(“ready-uops-queue”)來緩存微指令,以待后端消費。
TMAM是什么?如何定位CPU問題?
而TMAM根據(jù)cpu時鐘周期(cycle)和CPU pipeline slot將CPU瓶頸分為Retiring、Fountend Bound、Backend Bound、Bad Speculation四種:
image.png
接下來我們看下引起這四種瓶頸的原因。
引起這四種瓶頸的原因是什么?

Bad Speculation

錯誤分支預測,是由于提交了不會retired的uops引起的。
分為兩類,一類是分支預測錯誤(Branch Misspredict),一類是機器清除(Machine Clears)。
若該分類的值大于一定程度時,需先調(diào)查解決該分類。因為哪怕此時其他分支也占據(jù)很大的比重,但可能暴露出這些問題的都是那些處于錯誤分支上的指令,并不能真實地反映出CPU運行該程序正確指令流的特性,所以需要先解決該分類的問題,再去調(diào)查研究其他的分支。

Branch Misspredict

Branch Misspredict
錯誤的分支預測導致的bound。

Machine Clears

Machine Clears
當CPU檢測到某些條件時,便會觸發(fā)Machine Clears操作,清除流水線上的指令,以保證CPU的合理正確運行。比如發(fā)生錯誤的Memory訪問順序(memory ordering violations);自修改代碼(self-modifying code);訪問非法地址空間(load illegal address ranges),這些操作都會觸發(fā)Machine Clear。

Frontend Bound

Front-End 職責:

  1. 取指令
  2. 將指令進行解碼成微指令
  3. 將指令分發(fā)給Back-End,每個周期最多分發(fā)4條微指令

所以這里指的是指令獲取、解碼過程中的瓶頸,可能是因為取指延遲、取指帶寬、指令Cache Miss、指令解碼效率低(如CPUID等)。
具體來說,它可以分成Frontend Latency和Frontend Bandwidth兩類

接下來我們結合SkyLake微架構來分別看一下這些指標的含義,具體的計算方法可以參考https://github.com/andikleen/pmu-tools,知乎上也有一篇講解https://zhuanlan.zhihu.com/p/61015720
SkyLake微架構

Frontend Latency

ICache_Misses
ICache就是我們通常指的L1指令緩存,ICache_Misses指的是L1指令緩存未命中。
ITLB_Misses
ITLB_Misses就是我們熟悉的TLB(Translation Lookaside Buffer,轉換后援緩沖器)未命中。
Branch_Resteers
Branch Resteers 是指當 CPU 預測分支指令(如條件跳轉指令)錯誤時,需要回滾到正確的程序路徑重新執(zhí)行分支指令的過程。這種情況通常發(fā)生在 CPU 預測錯誤的分支目標地址與實際分支目標地址不一致時,需要進行分支重定向(Branch Redirect)或者分支修正(Branch Correction)。Branch Resteers,還能繼續(xù)向下展開為三類,分別為Branch Misprediction,Machine Clears和new branch address clears三類。
DSB_Switches
這里指的是微指令譯碼器(Decode Pipeline)和微指令緩存(Decoded ICache)之間切換產(chǎn)生的延遲。
LCP
對于正在decode的指令,若發(fā)生dynamically changing prefix length,即有LCP,長度改變前綴,便會出現(xiàn)幾個周期的Stall。LCP就是指的這部分的延遲。
MS_Switches
MS ROM會存放一些CISC指令的uops流,比如CPUID指令,MS_Switches指的是微指令譯碼器(Decode Pipeline)和微指令緩存(Decoded ICache)切換到MSROM的開銷。

Frontend Bandwidth

MITE
微指令譯碼器(Decode Pipeline)效率問題引起的bound。
DSB
微指令緩存(Decoded ICache)的效率問題,沒有利用好DSB cache structure,或者在讀取的時候發(fā)生了Bank conflict引起的bound。
LSD
LSD是Loop Stream Detector的縮寫,它位于uOp Queue內(nèi)部,當檢測到循環(huán)指令全部在uOp Queue中時,只需要不斷從LSD中取出相應的uops序列即可,不需要前端譯碼。如果uops循環(huán)的大小與當前硬件的LSD結構不匹配,就會出現(xiàn)LSD帶寬不足的情況。

Backend Bound

Back-End 的職責:

  1. 接收Front-End 提交的微指令
  2. 必要時對Front-End 提交的微指令進行重排
  3. 從內(nèi)存中獲取對應的指令操作數(shù)
  4. 執(zhí)行微指令、提交結果到內(nèi)存

這里指的是指令執(zhí)行過程中獲取內(nèi)存、更新內(nèi)存、指令過載導致的瓶頸。

我們結合下面這個圖來看一下Backend Bound
image.png

Memory Bound

Memory Bound分為Store Buffer Bound、L1/L2/L3 Bound和Mem Bound。
Cache Line很小,在Scheduler中,沒被列進來。
判斷方法如下圖所示:
image.png

Core Bound

Core Bound分為Divider和Ports Utilization。
Divider指的是長延遲的除法操作可能會導致執(zhí)行串行化,造成短期執(zhí)行的饑餓期。
Ports Utilization指的是處理器中的指令流被迫按順序執(zhí)行,這反映了程序中指令級并行性(ILP)的缺乏。

Retiring

有效的uops(微指令)
**Retiring分為兩類:MicroCode Sequence和Base。**MicroCode Sequence就是我們上面提到的MSROM中的指令,它是通過Microcode Sequencer (MS) unit來生成的復雜CISC指令,
Retiring不代表沒有優(yōu)化空間,Micro Sequencer例如Floating Point之類的指令要避免。
高的Base往往意味著向量化能夠得到很大的提升。

怎么優(yōu)化這四個瓶頸?

熟悉語言和平臺特性、編譯優(yōu)化選項、通用屬性選項、Pragmas和編譯器內(nèi)置函數(shù)

從前面我們已經(jīng)知道了造成瓶頸的原因,那么從代碼層面我們可以怎么優(yōu)化呢?
要對于比較底層的問題進行優(yōu)化,那么對于編譯器做了什么、能提供什么必須有所了解。
下面我就從語言和平臺特性、編譯優(yōu)化選項、通用屬性選項和編譯器內(nèi)置函數(shù)這幾個方面簡單總結一下。
優(yōu)化選項、通用屬性選項、Pragmas詳見:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
https://www.rowleydownload.co.uk/arm/documentation/gnu/gcc/index.html#SEC_Contents
https://gcc.gnu.org/onlinedocs/gcc/Pragmas.html

語言和平臺特性

相信大家很清楚,熟悉語言和平臺特性是一切優(yōu)化的基礎,比如現(xiàn)代C++的STL、零拷貝、原地構造、移動語義、智能指針、constexpr等基礎知識;線程進程同步方法、內(nèi)存序以及不同同步方法/內(nèi)存序(屏障)的執(zhí)行開銷;Linux系統(tǒng)的特性比如SMP/NUMA、THP、IO_Uring、ebpf等等。這些方面不是本文的重點,因此不在此贅述。

優(yōu)化選項

除了我們熟知的靜態(tài)單賦值(SSA)、NRVO(具名返回值優(yōu)化)、寄存器圖著色、死代碼消除、常量傳播、循環(huán)展開、尾遞歸優(yōu)化、指令重排、自動內(nèi)聯(lián)、緩存預取、分支預測……外,現(xiàn)代編譯器還能進行很多強大的優(yōu)化,下面列出了O2和O3執(zhí)行的優(yōu)化,熟悉這些選項是很有用的。

O2

-fauto-inc-dec:對自增和自減操作進行優(yōu)化,將其轉換為更高效的指令序列。
-fbranch-count-reg:使用寄存器來統(tǒng)計分支指令的執(zhí)行次數(shù),用于分支預測優(yōu)化。
-fcombine-stack-adjustments:合并連續(xù)的堆棧調(diào)整操作,以減少不必要的指令。
-fcompare-elim:消除不必要的比較操作,減少程序的運行時間。
-fcprop-registers:通過寄存器傳播常量的值,以減少內(nèi)存訪問。
-fdce:刪除未使用的代碼。
-fdefer-pop:推遲對堆棧的調(diào)整操作,以減少指令的數(shù)量。
-fdelayed-branch:推遲分支指令的執(zhí)行,以減少流水線的停頓。
-fdse:進行死代碼消除優(yōu)化,刪除不可達的代碼。
-fforward-propagate:進行常量傳播優(yōu)化,將常量傳播到使用該常量的代碼中。
-fguess-branch-probability:根據(jù)先前的執(zhí)行信息猜測分支指令的概率,以優(yōu)化分支預測。
-fif-conversion:對if語句進行優(yōu)化,將條件表達式轉換為更簡單的形式。
-fif-conversion2:進行更復雜的if語句優(yōu)化,包括通過更改條件的計算順序來提高性能。
-finline-functions-called-once:對只被調(diào)用一次的函數(shù)進行內(nèi)聯(lián)展開。
-fipa-modref:進行模塊間引用分析優(yōu)化,減少不必要的內(nèi)存操作。
-fipa-profile:根據(jù)程序的執(zhí)行信息進行優(yōu)化。
-fipa-pure-const:將純函數(shù)和常量傳播進行優(yōu)化。
-fipa-reference:進行引用分析優(yōu)化,減少不必要的內(nèi)存操作。
-fipa-reference-addressable:進行可尋址引用分析優(yōu)化,減少不必要的內(nèi)存操作。
-fmerge-constants:合并重復的常量,以減少內(nèi)存的使用。
-fmove-loop-invariants:將循環(huán)不變式移動到循環(huán)外部,以減少循環(huán)迭代次數(shù)。
-fomit-frame-pointer:優(yōu)化代碼以減少堆棧幀的使用。
-freorder-blocks:重新排序基本塊以優(yōu)化執(zhí)行路徑。
-fshrink-wrap:將變量的生命周期范圍縮小到最小,以減少內(nèi)存的使用。
-fshrink-wrap-separate:在函數(shù)中單獨進行縮小作用域的操作。
-fsplit-wide-types:將寬類型的變量分割為多個較窄的變量,以減少內(nèi)存的使用。
-fssa-backprop:通過SSA(靜態(tài)單賦值)形式的數(shù)據(jù)流分析來優(yōu)化代碼。
-fssa-phiopt:通過SSA形式的Phi函數(shù)優(yōu)化來優(yōu)化代碼。
-ftree-bit-ccp:進行位級的常量傳播優(yōu)化。
-ftree-ccp:進行常量傳播優(yōu)化。
-ftree-ch:進行復雜表達式優(yōu)化。
-ftree-coalesce-vars:合并變量來減少內(nèi)存的使用。
-ftree-copy-prop:進行復制傳播優(yōu)化。
-ftree-dce:進行死代碼消除優(yōu)化。
-ftree-dominator-opts:進行支配關系優(yōu)化。
-ftree-dse:進行死存儲消除優(yōu)化。
-ftree-forwprop:進行常量傳播和復制傳播的優(yōu)化。
-ftree-fre:進行冗余表達式消除優(yōu)化。
-ftree-phiprop:對Phi函數(shù)進行優(yōu)化。
-ftree-pta:進行指針分析優(yōu)化。
-ftree-scev-cprop:進行簡單標量表達式和常量傳播優(yōu)化。
-ftree-sink:將表達式移動到循環(huán)外部,以減少循環(huán)迭代次數(shù)。
-ftree-slsr:進行簡單局部標量替換優(yōu)化。
-ftree-sra:進行標量寄存器分配優(yōu)化。
-ftree-ter:進行三元表達式優(yōu)化。
-falign-functions:強制函數(shù)在內(nèi)存中按指定的對齊方式對齊。
-falign-jumps:強制跳轉指令在內(nèi)存中按指定的對齊方式對齊。
-falign-labels:強制標簽在內(nèi)存中按指定的對齊方式對齊。
-falign-loops:強制循環(huán)開始地址在內(nèi)存中按指定的對齊方式對齊。
-fcaller-saves:在函數(shù)調(diào)用時,保存調(diào)用者寄存器的值,以便被調(diào)用函數(shù)可以修改這些寄存器的值。
-fcode-hoisting:將可能的計算移動到循環(huán)外部,以減少循環(huán)迭代次數(shù)。
-fcrossjumping:在不同的控制流路徑中查找重復的代碼塊,并將其合并為一個共享的代碼塊。
-fcse-follow-jumps:在跳轉指令后面的代碼中進行公共子表達式消除。
-fcse-skip-blocks:跳過指定數(shù)量的基本塊,以提高公共子表達式消除的效率。
-fdelete-null-pointer-checks:刪除空指針檢查,以提高代碼的執(zhí)行速度。
-fdevirtualize:對虛函數(shù)調(diào)用進行優(yōu)化,將虛函數(shù)調(diào)用轉化為直接調(diào)用。
-fdevirtualize-speculatively:假設虛函數(shù)調(diào)用的目標是唯一的,并將其轉化為直接調(diào)用。
-fexpensive-optimizations:進行一些代價較高的優(yōu)化,可能會增加編譯時間。
-ffinite-loops:假設循環(huán)最多執(zhí)行有限次數(shù),進行一些循環(huán)優(yōu)化。
-fgcse:進行全局公共子表達式消除,刪除重復計算的代碼。
-fgcse-lm:對循環(huán)進行公共子表達式消除,刪除循環(huán)內(nèi)重復計算的代碼。
-fhoist-adjacent-loads:將相鄰的加載指令移動到循環(huán)外部,以減少循環(huán)迭代次數(shù)。
-finline-functions:對函數(shù)進行內(nèi)聯(lián)展開,將函數(shù)調(diào)用處替換為函數(shù)體。
-finline-small-functions:對小函數(shù)進行內(nèi)聯(lián)展開。
-findirect-inlining:對間接函數(shù)調(diào)用進行內(nèi)聯(lián)展開。
-fipa-bit-cp:進行位級的常量傳播優(yōu)化。
-fipa-cp:進行常量傳播優(yōu)化。
-fipa-icf:進行間接代碼優(yōu)化,合并相似的間接調(diào)用。
-fipa-ra:進行間接寄存器分配優(yōu)化。
-fipa-sra:進行間接寄存器分配優(yōu)化,同時進行標量寄存器分配優(yōu)化。
-fipa-vrp:進行值范圍傳播優(yōu)化。
-fisolate-erroneous-paths-dereference:對錯誤路徑上的指針解引用進行隔離。
-flra-remat:在循環(huán)中重新材料化值范圍,以減少循環(huán)迭代次數(shù)。
-foptimize-sibling-calls:對兄弟函數(shù)調(diào)用進行優(yōu)化。
-foptimize-strlen:對strlen函數(shù)進行優(yōu)化。
-fpartial-inlining:對函數(shù)進行部分內(nèi)聯(lián)展開。
-fpeephole2:進行指令級別的優(yōu)化。
-freorder-blocks-algorithm=stc:按指定的算法對基本塊進行重新排序。
-freorder-blocks-and-partition:對基本塊進行重新排序和分區(qū),以提高指令級優(yōu)化效果。
-freorder-functions:對函數(shù)進行重新排序,以提高指令級優(yōu)化效果。
-frerun-cse-after-loop:在循環(huán)后重新運行公共子表達式消除。
-fschedule-insns:對指令進行調(diào)度以提高執(zhí)行效率。
-fschedule-insns2 -fsched-interblock:對指令進行調(diào)度以提高執(zhí)行效率。
-fstore-merging:合并存儲操作,減少存儲操作的數(shù)量。
-fstrict-aliasing:啟用嚴格別名規(guī)則,優(yōu)化代碼對內(nèi)存的訪問。
-fthread-jumps:在多線程環(huán)境中,對線程間的跳轉進行優(yōu)化。
-ftree-builtin-call-dce:刪除未使用的內(nèi)建函數(shù)調(diào)用。
-ftree-pre:進行部分復寫消除優(yōu)化。
-ftree-switch-conversion:對switch語句進行轉換優(yōu)化。
-ftree-tail-merge:合并尾遞歸函數(shù)的調(diào)用。
-ftree-vrp:進行值范圍傳播優(yōu)化。

O3

-fgcse-after-reload:在寄存器分配之后進行全局公共子表達式消除(GCSE)優(yōu)化。
-fipa-cp-clone:通過復制函數(shù)來進行間接代碼傳播優(yōu)化。
-floop-interchange:進行循環(huán)交換優(yōu)化,改變循環(huán)的順序。
-floop-unroll-and-jam:進行循環(huán)展開和循環(huán)合并的優(yōu)化。
-fpeel-loops:將循環(huán)分解成多個部分,以減少循環(huán)迭代次數(shù)。
-fpredictive-commoning:通過提前計算和共享結果來進行預測性共享優(yōu)化。
-fsplit-loops:將循環(huán)分割為多個部分,以便更好地利用指令級并行性。
-fsplit-paths:將控制流路徑分割為多個部分,以便更好地利用指令級并行性。
-ftree-loop-distribution:將循環(huán)分布到多個線程或處理器上,以進行并行化處理。
-ftree-loop-vectorize:對循環(huán)進行向量化優(yōu)化,以利用SIMD指令。
-ftree-partial-pre:進行局部部分預測優(yōu)化,提前計算和共享部分結果。
-ftree-slp-vectorize:對循環(huán)進行超標量指令優(yōu)化,將多條指令合并為一條指令。
-funswitch-loops:對循環(huán)進行開關優(yōu)化,將循環(huán)展開成多個版本,通過開關語句來選擇執(zhí)行哪個版本。
-fvect-cost-model:使用向量化優(yōu)化的成本模型進行優(yōu)化。
-fvect-cost-model=dynamic:使用動態(tài)的向量化優(yōu)化成本模型進行優(yōu)化。
-fversion-loops-for-strides:對循環(huán)進行版本化優(yōu)化,根據(jù)迭代步長來選擇不同的版本進行執(zhí)行。

通用屬性選項

下面列出跟性能相關的Common Attributes,更多Attributes詳見https://www.rowleydownload.co.uk/arm/documentation/gnu/gcc/index.html#SEC_Contents
aligned (alignment)
aligned屬性指定函數(shù)第一條指令的最小對齊方式,以字節(jié)為單位。指定后,對齊方式必須是整數(shù)常數(shù) 2 的常量冪。不指定對齊參數(shù)意味著目標的理想對齊。
alloc_align (position)
該屬性可以應用于返回指針并采用至少一個整數(shù)或枚舉類型的參數(shù)的函數(shù)。它指示返回的指針在函數(shù)參數(shù) alloc_alignposition處給出的邊界上對齊。有意義的對齊是 2 大于 1 的冪。GCC 使用此信息來改進指針對齊分析。
例如

void* my_memalign (size_t, size_t) attribute ((alloc_align (1)));

always_inline
強制內(nèi)聯(lián)函數(shù),無法內(nèi)聯(lián)會產(chǎn)生error
cold
函數(shù)上的cold屬性用于通知編譯器該函數(shù)不太可能被執(zhí)行。
const
告訴 GCC,對具有相同參數(shù)值的函數(shù)的后續(xù)調(diào)用可以替換為第一次調(diào)用的結果,而不管兩者之間的語句如何
例如:

int square (int) attribute ((const));

flatten
對于標記有此屬性的函數(shù),如果可能的話,此函數(shù)內(nèi)的每個調(diào)用都是內(nèi)聯(lián)的。用特性noinline和類似的函數(shù)聲明的函數(shù)不內(nèi)聯(lián)。是否考慮函數(shù)本身進行內(nèi)聯(lián)取決于其大小和當前的內(nèi)聯(lián)參數(shù)。
hot
函數(shù)上的屬性用于通知編譯器該函數(shù)是已編譯程序的熱點。該函數(shù)進行了更積極的優(yōu)化,在許多目標上,它被放置在文本部分的特殊子部分中,因此所有熱門函數(shù)看起來都很接近,從而改善了局部性。
pure
對pure函數(shù)的調(diào)用,如果函數(shù)除了返回值之外對程序的狀態(tài)沒有可觀察的影響,則可能需要進行優(yōu)化,例如常見的子表達式消除。使用 該屬性聲明此類函數(shù)允許 GCC 避免在重復調(diào)用具有相同參數(shù)值的函數(shù)時發(fā)出某些調(diào)用。
returns_nonnull
該屬性指定函數(shù)返回值應為非 null 指針。例如,聲明:

extern void *
mymalloc (size_t len) __attribute__((returns_nonnull));

允許編譯器根據(jù)返回值永遠不會為 null 的知識來優(yōu)化調(diào)用方。
target (string, )
多個目標后端實現(xiàn) target 屬性,以指定要使用與命令行上指定的目標選項不同的目標選項來編譯函數(shù)。原始目標命令行選項將被忽略。可以提供一個或多個字符串作為參數(shù)。每個字符串都由一個或多個逗號分隔的 -m 前綴后綴組成,共同構成與計算機相關的選項的名稱。

編譯器內(nèi)置函數(shù)

提醒:除了具有庫等價物的內(nèi)置函數(shù)(如標準C庫函數(shù)),或者擴展到庫調(diào)用的內(nèi)置函數(shù)外,GCC內(nèi)置函數(shù)總是內(nèi)聯(lián)擴展的,因此沒有相應的入口點,并且無法獲得它們的地址。試圖在函數(shù)調(diào)用以外的表達式中使用它們會導致編譯時錯誤。
void __builtin___clear_cache (void *begin, void *end)
此函數(shù)用于為包含開始和排除結束之間的內(nèi)存區(qū)域刷新處理器指令緩存。一些目標要求在修改包含代碼的內(nèi)存后刷新指令緩存,以獲得確定性行為。
void __builtin_prefetch (const void *addr, …)
此函數(shù)用于在訪問數(shù)據(jù)之前將數(shù)據(jù)移動到緩存中,從而最大限度地減少緩存未命中延遲。您可以將對__builtin_prefetch的調(diào)用插入到您知道內(nèi)存中可能很快被訪問的數(shù)據(jù)地址的代碼中。如果目標支持它們,則會生成數(shù)據(jù)預取指令。如果預取在訪問之前足夠早地完成,那么在訪問數(shù)據(jù)時數(shù)據(jù)將在緩存中。
long __builtin_expect (long exp, long c)
可以使用__builtin_expect為編譯器提供分支預測信息。一般來說,pgo是更好的(-fprofile arcs),因為程序員在預測程序?qū)嶋H執(zhí)行方面是出了名的糟糕。然而,有些應用程序很難收集這些數(shù)據(jù)。
下面是一個在clickhouse中使用分支預測的例子:
image.png

Pragmas

循環(huán)展開

循環(huán)展開現(xiàn)在編譯器都會自動做了,有時候可能需要限制循環(huán)展開。
比如clickhouse里面的一段:
image.png
對小的循環(huán)體進行 unroll 可能是劃算的,但最好不要 unroll 大的循環(huán)體,否則會造成uop decode pipeline的壓力反而變慢。

對齊

從整個體系結構的角度來看,對齊有幾個緯度:

  1. 總線尋址對齊
  2. 內(nèi)存頁對齊
  3. Cache Line對齊
  4. 緩存對齊

總線尋址對齊
總線對齊,64位機器就是8byte對齊,這一點在現(xiàn)代CPU中已經(jīng)不重要了。編程時不需要考慮這個問題(見下圖[見下圖,圖片來自https://lemire.me/blog/2012/05/31/data-alignment-for-speed-myth-or-reality/])。
image.png
內(nèi)存頁對齊
內(nèi)存頁對齊,也就是4K對齊,內(nèi)核線性區(qū)分配和b+樹中間節(jié)點都會使用4k頁對齊,這主要是一些磁盤和內(nèi)存結構需要考慮的。
Cache Line對齊
cpu緩存行對齊,也就是64byte對齊。這是最關鍵的一點:首先它可以防止(UMP架構下)MESI協(xié)議導致的緩存行失效(偽共享)。其次,它對性能有很大影響
(見下圖,圖片來自https://github.com/zhangz/KnowledgeBase/blob/master/Essential%20.NET/Gallery%20of%20Processor%20Cache%20Effects.pdf)。
image.png
L1/L2緩存對齊
L1 L2緩存,一般是64k和**256KB to 32MB,**同樣對性能有較大影響。如下圖:
image.png
比如使用avx-512,將數(shù)據(jù)與64個字節(jié)對齊時可以通過_mm512_load_pd將數(shù)據(jù)直接加載到zmmm寄存器中,并在其上應用SIMD指令,然后通過_mm512 _stream_pd將其存儲回。
注意謹慎使用這里的指令,如果不進行大量的向量化計算,只會造成內(nèi)存浪費。
相反,大多數(shù)情況下需要的是1字節(jié)填充來節(jié)省內(nèi)存。
比如:
image.png

restrict和#pargma ivdep

使用__restrict__ 顯式告訴編譯器參數(shù)是內(nèi)存中的不同位置,這可以節(jié)省一條指令。
image.png
使用#pargma ivdep 斷言循環(huán)不攜帶依賴項
image.png

自動向量化和自動并行

#pargma simd 提示編譯器向量化, 如果不能向量化則警告
#pargma vector 強制向量化,忽略啟發(fā)式規(guī)則
#pargma omp 使用openmp自動并行
比如:
image.png
image.png

優(yōu)化Frontend Bound

如果Frontend Bound是瓶頸的話,那么我們可以考慮從下面幾個方面進行優(yōu)化:
ICache_Misses
對于ICache_Misses,正確識別熱點代碼很重要,需要我們充分利用編譯器的PGO 特性:-fprofile-generate -fprofile-use,LTO則會加大內(nèi)聯(lián),對于這方面有反作用。
另外,可以通過手動標識__attribute__ ((hot)) attribute ((cold)) 和long __builtin_expect (long exp, long c)調(diào)整代碼命中率來優(yōu)化ICache_Misses。
ITLB_Misses
ITLB_Misses優(yōu)化可以考慮通過減少上下文切換和THP(內(nèi)存大頁)來實現(xiàn)。
Branch_Resteers
Branch_Resteers主要是要減少分支預測的錯誤率,這個會在優(yōu)化Bad Speculation中集中說。
LCP
LCP是一個平臺相關的問題,比如產(chǎn)生了0x66,0x67前綴的指令或者REX.W指令,這個時候要結合匯編代碼看下為什么產(chǎn)生這個瓶頸。
MS_Switches
減少CISC指令比如CPUID指令的使用
MITE
如果微指令譯碼器(Decode Pipeline)出現(xiàn)了效率問題,那主要考慮微碼長度問題,是不是需要nounroll,比如上面舉的clang的例子。
LSD
LSD主要考慮減小uop循環(huán)的大小。

優(yōu)化Bad Speculation

消除Bad Speculation主要是通過優(yōu)化分支預測進行,參考https://zhuanlan.zhihu.com/p/357699203

  1. 消除分支可以減少預測的可能性能:比如小的循環(huán)可以展開比如循環(huán)次數(shù)小于64次(可以使用GCC選項 -funroll-loops將循環(huán)全部展開),當然選用激進的優(yōu)化選項要做好bench
  2. 盡量用if 代替:? ,不建議使用a=b>0? x:y 后者不能做分支預測
  3. 盡可能減少組合條件,使用單一條件比如:if(a||b) {}else{}
  4. 對于多case的switch,盡可能將最可能執(zhí)行的case 放在最前面
  5. 在使用if的地方盡可能使用gcc的內(nèi)置分支預測特性
  6. 避免間接跳轉和調(diào)用 在c++中比如switch、函數(shù)指針或者虛函數(shù)在生成匯編語言的時候都可能存在多個跳轉目標,這個也是會影響分支預測的結果,雖然BTB可改善這些但是畢竟BTB的資源是很有限的。

優(yōu)化Retiring和BackendBound的利器–向量化

優(yōu)化BackendBound,我們需要盡量使用數(shù)據(jù)對齊并且利用緩存行,還要避免偽共享。
接下來主要是介紹今天的另一個重點:向量化。當我們遇到Retiring和BackendBound瓶頸的時候,大多可以使用向量化來優(yōu)化它們。

什么是SIMD向量化和SIMT向量化以及如何選擇

無論是CPU還是GPU,基本的并行手段主要有三種:
(1) instruction-level parallelism (ILP) 指令并行 如超標量、流水線(在GPU中叫做流水線并行)
(2) thread-level parallelism (TLP) 如openmp 和 pthread (在GPU中叫數(shù)據(jù)并行或者SIMT)
(3) vector-level parallelism 如SIMD(在GPU中叫張量并行)
但是GPU和CPU的側重點是不一樣的,CPU是大核模式,天生支持指令并行,包括超標量、流水線等技術,主要致力于提升IPC;GPU是眾核模式,天生支持數(shù)據(jù)并行或者說線程并行,主要致力于提升并行計算能力。
那么什么樣的應用適合于GPU加速呢?考慮下面幾個問題:

  1. 瓶頸是memory latency bound還是memory bandwidth bound,一般以帶寬為瓶頸的程序更適合使用GPU加速。https://www.brendangregg.com/blog/2017-05-09/cpu-utilization-is-wrong.html 這篇文章給出了分析方法
  2. PCIE傳輸性能 加速效果能超過CPU和GPU間的PCIE傳輸?shù)拈_銷嗎?
  3. 是否是并發(fā)場景?GPU在高并發(fā)場景下會有明顯性能下降。
  4. 是否需要落盤?例如,在物化算子中(必須使用全量數(shù)據(jù)進行運算的算子),內(nèi)存往往不夠用,必須落盤,GPU就不適用于這種場景?;蛘哂斜容^大的中間結果,往往也是不合適的。
  5. 瓶頸是計算嗎?如果瓶頸既有IO,又有CPU,那么使用GPU往往是不劃算的。

可以說,除了圖像處理、機器學習等領域,一般都不需要使用GPU加速。

使用Intrinsics替代匯編

隨著優(yōu)化越來越靠近底層,語言的抽象層次越深,那么我們能感受到的Gap也就越大,比如如果在微架構的層面進行優(yōu)化,那么C++已經(jīng)力不從心了,有時候可能需要嵌入式匯編的幫助。
但是,匯編并不是好的范式。因為它難以維護、容易出錯、可移植性差。
我們看幾個高性能開源庫的實踐:
image.png
從這些優(yōu)秀開源的實踐我們能總結出來:什么時候使用匯編,什么時候使用SIMD有幾個基本原則:

  1. 如果編譯器能知道怎么優(yōu)化是最好的(絕大多數(shù)情況下),那么不要復雜化代碼。
  2. 編譯器的優(yōu)勢是聰明,但你的優(yōu)勢是知道的多,因此提示編譯器而不是手寫匯編/SIMD。
  3. 99%的情況下不要使用SIMD,如果你發(fā)現(xiàn)無法成功提示編譯器,并且這里的性能 _真的 _很重要,那么可以使用SIMD,但是要注意跨平臺的問題,并測試你的代碼真的超過了-O3下的編譯器。
  4. 盡量不要使用匯編,除非你找到了SIMD庫(https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html**)的問題 或者 你要控制內(nèi)核態(tài)資源(比如進程切換和進程棧資源)[比如mimalloc brpc等等] **

經(jīng)常使用的SIMD操作

所謂的SIMD,就是用MMX指令集(64位SIMD寄存器)或者SSE/AVX/AVX512指令集(128位SIMD寄存器),做數(shù)據(jù)的并行化處理。它有如下幾個基本操作:

  • 遮罩 Masking
  • 排列
  • 選擇性加載 / 存儲
  • 壓縮 / 擴展
  • 選擇性聚集 / 散開

這部分可以參考Pavlo的15721課程: 15721.courses.cs.cmu.edu/spring2023/slides/08-vectorization.pdf

遮罩

image.png
image.png
比如:
__m128i _mm_mask_abs_epi16 (__m128i src, __mmask8 k, __m128i a)

  • src:一個__m128i類型的SIMD向量,用作結果的初始值。
  • k:一個__mmask8類型的掩碼向量,指示哪些數(shù)據(jù)元素需要執(zhí)行絕對值操作。
  • a:一個__m128i類型的SIMD向量,作為絕對值操作的輸入。

函數(shù)的功能是將a向量中根據(jù)k掩碼指示的元素取絕對值,然后將結果存儲到src向量對應的位置上。換句話說,只有k掩碼向量中對應位置為1的元素會被執(zhí)行絕對值操作。
例如,假設有以下輸入:

  • src向量:[4, -6, 3, -2, -7, 8, -1, 5]
  • k掩碼向量:[1, 0, 1, 0, 1, 0, 1, 0]
  • a向量:[2, -3, 6, -4, 1, -9, 5, -2]

則執(zhí)行_mm_mask_abs_epi16(src, k, a)操作后,結果向量將為:
[2, -6, 6, -2, 1, 8, 5, 5]
又比如:
int _mm_movemask_epi8 (__m128i a)

重排

對于每個通道,將索引向量中指定的偏移量處的輸入向量的值復制到目標向量中。在 AVX-512 之前,數(shù)據(jù)庫管理系統(tǒng)必須將數(shù)據(jù)從 SIMD 寄存器寫入內(nèi)存,然后再寫回 SIMD 寄存器。而 AVX-512 指令集引入了新的 PERMUTE 操作,可以直接在 SIMD 寄存器內(nèi)部完成元素重排,大大提高了性能。
image.png
blend:
在SIMD(Single Instruction, Multiple Data)編程中,Blend(混合)是一種操作,用于將兩個向量按照指定的規(guī)則進行混合?;旌喜僮魍ǔJ菍蓚€向量的對應元素進行混合,生成一個新的向量。

選擇性加載 / 存儲

image.png
選擇性加載從內(nèi)存中讀取滿足特定條件的數(shù)據(jù)元素,而選擇性存儲將數(shù)據(jù)元素寫回內(nèi)存

壓縮 / 擴展

image.png
用于減少數(shù)據(jù)存儲需求和提高內(nèi)存訪問效率。
壓縮操作將數(shù)據(jù)集中的冗余信息刪除,減小數(shù)據(jù)的存儲空間。擴展操作則是壓縮的逆過程,將壓縮后的數(shù)據(jù)還原為原始格式

選擇性聚集 / 散開

image.png
用于重組數(shù)據(jù)的技術。
選擇性聚集從一個數(shù)據(jù)集中提取滿足特定條件的元素,并將它們組合成一個新的、更緊湊的數(shù)據(jù)集。
選擇性散開是選擇性聚集的逆操作,它將數(shù)據(jù)集中的元素根據(jù)特定條件分散到一個更大的數(shù)據(jù)集中。
這兩種操作可以提高數(shù)據(jù)處理效率,特別是在需要對數(shù)據(jù)進行過濾、合并或分組等操作時。
Make the most out of your SIMD investments: counter control flow divergence in compiled query pipelines

AVX512 降頻問題

image.png
以下情況不需要擔心降頻:

  1. 沒有或者較少有浮點運算或整數(shù)乘法的指令
  2. 目標平臺不具有512位寄存器
  3. 對于AVX512,從輕量到重量使用,降頻在15%~40%

從Clickhouse看SIMD優(yōu)化

clickhouse里面針對三種SIMD指令集進行了優(yōu)化,分別是SSE、AVX、NEON。前面也提到了,這幾個指令集用的都是128位寄存器。

#ifdef __SSE2__
#include <emmintrin.h>
#endif#if USE_MULTITARGET_CODE
#include <immintrin.h>
#endif#if defined(__aarch64__) && defined(__ARM_NEON)
#    include <arm_neon.h>
#      pragma clang diagnostic ignored "-Wreserved-identifier"
#endif

一共在代碼里出現(xiàn)了7處。
所以,就算是OLAP這種CPU密集+訪存密集型的應用,手寫SIMD也只是小部分情況。

memcpy

https://github.com/ClickHouse/ClickHouse/blob/b0eb670776c58af040dc488f1428c313f9eea1ab/base/glibc-compatibility/memcpy/memcpy.h#L97
clickhouse重寫了glibc的memcpy,這里作者提到:

  1. 如果用 -ftree-loop-distribute-patterns可能會導致編譯器優(yōu)化為自帶的memcpy,而又會重新調(diào)用到重寫的memcpy,導致遞歸調(diào)用,所以必須禁用掉。
  2. 用AVX512有兩個問題:一個是降頻,第二個是SSE切換AVX512的性能開銷。
  3. 然后作者列了幾個影響性能的因素:
    1. 預取指令,因為預取指令的大小不確定,而且在ARM中性能比較差,所以這里沒有預取
    2. 對齊,這里使用不對齊的加載和對齊的存儲
    3. 循環(huán)展開次數(shù),這里固定為8次
  4. attribute((no_sanitize(“coverage”)))禁用行數(shù)統(tǒng)計
  5. 最后作者提到memcpy可能會在編譯時被優(yōu)化為比較小的其它指令,可以使用-fbuiltin-memcpy或者手動調(diào)用__builtin_memcpy來避免
#include <stddef.h>#include <emmintrin.h>/** Custom memcpy implementation for ClickHouse.* It has the following benefits over using glibc's implementation:* 1. Avoiding dependency on specific version of glibc's symbol, like memcpy@@GLIBC_2.14 for portability.* 2. Avoiding indirect call via PLT due to shared linking, that can be less efficient.* 3. It's possible to include this header and call inline_memcpy directly for better inlining or interprocedural analysis.* 4. Better results on our performance tests on current CPUs: up to 25% on some queries and up to 0.7%..1% in average across all queries.** Writing our own memcpy is extremely difficult for the following reasons:* 1. The optimal variant depends on the specific CPU model.* 2. The optimal variant depends on the distribution of size arguments.* 3. It depends on the number of threads copying data concurrently.* 4. It also depends on how the calling code is using the copied data and how the different memcpy calls are related to each other.* Due to vast range of scenarios it makes proper testing especially difficult.* When writing our own memcpy there is a risk to overoptimize it* on non-representative microbenchmarks while making real-world use cases actually worse.** Most of the benchmarks for memcpy on the internet are wrong.** Let's look at the details:** For small size, the order of branches in code is important.* There are variants with specific order of branches (like here or in glibc)* or with jump table (in asm code see example from Cosmopolitan libc:* https://github.com/jart/cosmopolitan/blob/de09bec215675e9b0beb722df89c6f794da74f3f/libc/nexgen32e/memcpy.S#L61)* or with Duff device in C (see https://github.com/skywind3000/FastMemcpy/)** It's also important how to copy uneven sizes.* Almost every implementation, including this, is using two overlapping movs.** It is important to disable -ftree-loop-distribute-patterns when compiling memcpy implementation,* otherwise the compiler can replace internal loops to a call to memcpy that will lead to infinite recursion.** For larger sizes it's important to choose the instructions used:* - SSE or AVX or AVX-512;* - rep movsb;* Performance will depend on the size threshold, on the CPU model, on the "erms" flag* ("Enhansed Rep MovS" - it indicates that performance of "rep movsb" is decent for large sizes)* https://stackoverflow.com/questions/43343231/enhanced-rep-movsb-for-memcpy** Using AVX-512 can be bad due to throttling.* Using AVX can be bad if most code is using SSE due to switching penalty* (it also depends on the usage of "vzeroupper" instruction).* But in some cases AVX gives a win.** It also depends on how many times the loop will be unrolled.* We are unrolling the loop 8 times (by the number of available registers), but it not always the best.** It also depends on the usage of aligned or unaligned loads/stores.* We are using unaligned loads and aligned stores.** It also depends on the usage of prefetch instructions. It makes sense on some Intel CPUs but can slow down performance on AMD.* Setting up correct offset for prefetching is non-obvious.** Non-temporary (cache bypassing) stores can be used for very large sizes (more than a half of L3 cache).* But the exact threshold is unclear - when doing memcpy from multiple threads the optimal threshold can be lower,* because L3 cache is shared (and L2 cache is partially shared).** Very large size of memcpy typically indicates suboptimal (not cache friendly) algorithms in code or unrealistic scenarios,* so we don't pay attention to using non-temporary stores.** On recent Intel CPUs, the presence of "erms" makes "rep movsb" the most beneficial,* even comparing to non-temporary aligned unrolled stores even with the most wide registers.** memcpy can be written in asm, C or C++. The latter can also use inline asm.* The asm implementation can be better to make sure that compiler won't make the code worse,* to ensure the order of branches, the code layout, the usage of all required registers.* But if it is located in separate translation unit, inlining will not be possible* (inline asm can be used to overcome this limitation).* Sometimes C or C++ code can be further optimized by compiler.* For example, clang is capable replacing SSE intrinsics to AVX code if -mavx is used.** Please note that compiler can replace plain code to memcpy and vice versa.* - memcpy with compile-time known small size is replaced to simple instructions without a call to memcpy;*   it is controlled by -fbuiltin-memcpy and can be manually ensured by calling __builtin_memcpy.*   This is often used to implement unaligned load/store without undefined behaviour in C++.* - a loop with copying bytes can be recognized and replaced by a call to memcpy;*   it is controlled by -ftree-loop-distribute-patterns.* - also note that a loop with copying bytes can be unrolled, peeled and vectorized that will give you*   inline code somewhat similar to a decent implementation of memcpy.** This description is up to date as of Mar 2021.** How to test the memcpy implementation for performance:* 1. Test on real production workload.* 2. For synthetic test, see utils/memcpy-bench, but make sure you will do the best to exhaust the wide range of scenarios.** TODO: Add self-tuning memcpy with bayesian bandits algorithm for large sizes.* See https://habr.com/en/company/yandex/blog/457612/*/__attribute__((no_sanitize("coverage")))
static inline void * inline_memcpy(void * __restrict dst_, const void * __restrict src_, size_t size)
{/// We will use pointer arithmetic, so char pointer will be used./// Note that __restrict makes sense (otherwise compiler will reload data from memory/// instead of using the value of registers due to possible aliasing).char * __restrict dst = reinterpret_cast<char * __restrict>(dst_);const char * __restrict src = reinterpret_cast<const char * __restrict>(src_);/// Standard memcpy returns the original value of dst. It is rarely used but we have to do it./// If you use memcpy with small but non-constant sizes, you can call inline_memcpy directly/// for inlining and removing this single instruction.void * ret = dst;tail:/// Small sizes and tails after the loop for large sizes./// The order of branches is important but in fact the optimal order depends on the distribution of sizes in your application./// This order of branches is from the disassembly of glibc's code./// We copy chunks of possibly uneven size with two overlapping movs./// Example: to copy 5 bytes [0, 1, 2, 3, 4] we will copy tail [1, 2, 3, 4] first and then head [0, 1, 2, 3].// 不對齊的加載 兩個重疊的movsif (size <= 16){if (size >= 8){/// Chunks of 8..16 bytes.__builtin_memcpy(dst + size - 8, src + size - 8, 8);__builtin_memcpy(dst, src, 8);}else if (size >= 4){/// Chunks of 4..7 bytes.__builtin_memcpy(dst + size - 4, src + size - 4, 4);__builtin_memcpy(dst, src, 4);}else if (size >= 2){/// Chunks of 2..3 bytes.__builtin_memcpy(dst + size - 2, src + size - 2, 2);__builtin_memcpy(dst, src, 2);}else if (size >= 1){/// A single byte.*dst = *src;}/// No bytes remaining.}else{// 這里src和dst不可能同時128對齊,因此/// Medium and large sizes.if (size <= 128){/// Medium size, not enough for full loop unrolling./// We will copy the last 16 bytes._mm_storeu_si128(reinterpret_cast<__m128i *>(dst + size - 16), _mm_loadu_si128(reinterpret_cast<const __m128i *>(src + size - 16)));/// Then we will copy every 16 bytes from the beginning in a loop./// The last loop iteration will possibly overwrite some part of already copied last 16 bytes./// This is Ok, similar to the code for small sizes above.while (size > 16){_mm_storeu_si128(reinterpret_cast<__m128i *>(dst), _mm_loadu_si128(reinterpret_cast<const __m128i *>(src)));dst += 16;src += 16;size -= 16;}}else{/// Large size with fully unrolled loop./// Align destination to 16 bytes boundary.size_t padding = (16 - (reinterpret_cast<size_t>(dst) & 15)) & 15;/// If not aligned - we will copy first 16 bytes with unaligned stores.if (padding > 0){__m128i head = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src));_mm_storeu_si128(reinterpret_cast<__m128i*>(dst), head);dst += padding;src += padding;size -= padding;}/// Aligned unrolled copy. We will use half of available SSE registers./// It's not possible to have both src and dst aligned./// So, we will use aligned stores and unaligned loads.__m128i c0, c1, c2, c3, c4, c5, c6, c7;while (size >= 128){c0 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 0);c1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 1);c2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 2);c3 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 3);c4 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 4);c5 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 5);c6 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 6);c7 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src) + 7);src += 128;_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 0), c0);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 1), c1);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 2), c2);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 3), c3);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 4), c4);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 5), c5);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 6), c6);_mm_store_si128((reinterpret_cast<__m128i*>(dst) + 7), c7);dst += 128;size -= 128;}/// The latest remaining 0..127 bytes will be processed as usual.goto tail;}}return ret;
}

這里使用了一半的SSE寄存器(8個)來做,可能是考慮到32位平臺上只有8個,而64位平臺則可以進行展開。
我們同樣可以參考一些別的memcpy實現(xiàn),比如韋易笑的FastMemcpy:
image.png
FastMemcpy使用了預取指令,clickhouse里面也有完整代碼作為benchmark https://github.com/skywind3000/FastMemcpy/blob/master/FastMemcpy.h
image.png
FastMemcpy的問題在于引入了大量的跳轉表導致緩存行失效,因此CK并沒有將它作為默認的Memcpy實現(xiàn)。

MergeTreeRangeReader

mergetree是clickhouse的列式存儲結構,跟ORC很像,不過索引是分開存的(而且沒有bloomfilter)。具體可以看:https://bohutang.me/2020/06/26/clickhouse-and-friends-merge-tree-disk-layout/
在讀取 ClickHouse 的 MergeTree 表時,首先會對表中的數(shù)據(jù)進行預過濾,以減少讀取的數(shù)據(jù)量,從而提高查詢性能,它將 current_filter 和已有的 final_filter (如果存在)進行組合,創(chuàng)建一個新的過濾條件 filter,這個過濾條件將被應用在每個數(shù)據(jù)塊的開頭。
使用向量化的代碼在https://github.com/ClickHouse/ClickHouse/blob/4279dd2bf11841d8f68bdea78f3d8668a2c4289b/src/Storages/MergeTree/MergeTreeRangeReader.cpp#L730
這段代碼的作用就是計算兩個地址之間0位的大小。
使用godbolt分析下:
image.png
image.png
因為是逐位次比較,編譯器不知道中間位數(shù)的多少,如果引入表跳轉會導致緩存行失效的問題,所以編譯器只使用普通寄存器進行。
但是在clickhouse場景下,這兩個地址之間往往差距很大,所以這里加了分支。

bytes64MaskToBits64Mask

https://github.com/ClickHouse/ClickHouse/blob/fc67d2c0e984098e492c1111c8b5e3c705a80e86/src/Columns/ColumnsCommon.h#L27C1-L27C1
這段代碼就很簡單,取64*64位的掩碼到64位中。


/// Transform 64-byte mask to 64-bit mask
inline UInt64 bytes64MaskToBits64Mask(const UInt8 * bytes64)
{
#if defined(__AVX512F__) && defined(__AVX512BW__)const __m512i vbytes = _mm512_loadu_si512(reinterpret_cast<const void *>(bytes64));UInt64 res = _mm512_testn_epi8_mask(vbytes, vbytes);
#elif defined(__AVX__) && defined(__AVX2__)const __m256i zero32 = _mm256_setzero_si256();UInt64 res =(static_cast<UInt64>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(_mm256_loadu_si256(reinterpret_cast<const __m256i *>(bytes64)), zero32))) & 0xffffffff)| (static_cast<UInt64>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(_mm256_loadu_si256(reinterpret_cast<const __m256i *>(bytes64+32)), zero32))) << 32);
#elif defined(__SSE2__)const __m128i zero16 = _mm_setzero_si128();UInt64 res =(static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64)), zero16))) & 0xffff)| ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 16)), zero16))) << 16) & 0xffff0000)| ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 32)), zero16))) << 32) & 0xffff00000000)| ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 48)), zero16))) << 48) & 0xffff000000000000);
#elif defined(__aarch64__) && defined(__ARM_NEON)const uint8x16_t bitmask = {0x01, 0x02, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80};const auto * src = reinterpret_cast<const unsigned char *>(bytes64);const uint8x16_t p0 = vceqzq_u8(vld1q_u8(src));const uint8x16_t p1 = vceqzq_u8(vld1q_u8(src + 16));const uint8x16_t p2 = vceqzq_u8(vld1q_u8(src + 32));const uint8x16_t p3 = vceqzq_u8(vld1q_u8(src + 48));uint8x16_t t0 = vandq_u8(p0, bitmask);uint8x16_t t1 = vandq_u8(p1, bitmask);uint8x16_t t2 = vandq_u8(p2, bitmask);uint8x16_t t3 = vandq_u8(p3, bitmask);uint8x16_t sum0 = vpaddq_u8(t0, t1);uint8x16_t sum1 = vpaddq_u8(t2, t3);sum0 = vpaddq_u8(sum0, sum1);sum0 = vpaddq_u8(sum0, sum0);UInt64 res = vgetq_lane_u64(vreinterpretq_u64_u8(sum0), 0);
#elseUInt64 res = 0;for (size_t i = 0; i < 64; ++i)res |= static_cast<UInt64>(0 == bytes64[i]) << i;
#endifreturn ~res;
}

這里無論我用STL容器還是指針,加什么編譯選項,GCC都無法優(yōu)化為SIMD指令,Clang的優(yōu)化效果也不好,所以我們可以看出,編譯器對SIMD的支持大部分情況不如手寫的好。
image.png

http://www.risenshineclean.com/news/61231.html

相關文章:

  • 旅游網(wǎng)站開發(fā)背景及意義公司網(wǎng)站建設步驟
  • 日本親子游哪個網(wǎng)站做的好處寧波seo外包快速推廣
  • 做網(wǎng)站不掙錢鎮(zhèn)江優(yōu)化推廣
  • 免費企業(yè)推廣網(wǎng)站網(wǎng)站開發(fā)工程師
  • 萬網(wǎng)網(wǎng)站建設流程做優(yōu)化關鍵詞
  • 網(wǎng)頁特效代碼網(wǎng)站石家莊新聞最新消息
  • 建設部投訴網(wǎng)站網(wǎng)站創(chuàng)建的流程是什么
  • 擔路網(wǎng)絡科技有限公司的證書靠譜seo整站優(yōu)化外包
  • 山西省網(wǎng)站建設哪家好百度官網(wǎng)認證免費
  • seo每日工作內(nèi)容寧波seo公司網(wǎng)站推廣
  • 云工廠網(wǎng)站建設濟南疫情最新消息
  • 本溪做網(wǎng)站制作網(wǎng)頁完整步驟
  • 千圖主站的功能介紹seo搜索優(yōu)化服務
  • 網(wǎng)站建設需要哪些方面google海外版入口
  • 調(diào)查問卷在哪個網(wǎng)站做免費ip地址代理
  • 網(wǎng)站seo優(yōu)化教程谷歌官方網(wǎng)站
  • 網(wǎng)站建設費應怎樣做會計分錄青島做網(wǎng)站的公司哪家好
  • 東莞長安做網(wǎng)站百度網(wǎng)盤客服中心電話
  • 哪一些網(wǎng)站使用vue做的2023國內(nèi)外重大新聞事件10條
  • 網(wǎng)站關鍵詞優(yōu)化排名最新戰(zhàn)爭新聞事件今天
  • 軟件工程開發(fā)師工資杭州seo價格
  • 互聯(lián)網(wǎng)網(wǎng)站有哪些外國黃岡網(wǎng)站推廣平臺
  • 企業(yè)網(wǎng)站最下面的那欄叫啥成都本地推廣平臺
  • ps網(wǎng)頁設計步驟淮安網(wǎng)站seo
  • 企業(yè)建設網(wǎng)站目的是什么推廣廣告
  • 網(wǎng)站建設成功案例宣傳色盲能治好嗎
  • 網(wǎng)站建設都需要什么技術人員網(wǎng)站優(yōu)化網(wǎng)絡推廣seo
  • 南昌微網(wǎng)站建設廈門人才網(wǎng)官網(wǎng)招聘
  • 學做網(wǎng)站培訓 上海網(wǎng)站分析
  • 手機前端開發(fā)軟件工具如何軟件網(wǎng)站優(yōu)化公司