為什麼X86的寄存器數量沒有隨著性能的提升而增加?
我記得從上個世紀的8086到現在最新的skylake,基礎的整數寄存器數量好像一直是那麼多(好像到了奔騰後來增加了r8-r15整數寄存器),然後因為有了SSE以及AVX等擴展指令集之後又多了ymm0-ymm15這類媒體寄存器。
想請問一下現在的技術情況下存儲密度可以做得很高,為什麼無論是整數寄存器還是擴展指令集的專用寄存器數量卻一直做的那麼少呢?
但是如果寄存器數量增多的話,豈不是可以少做一些棧操作或者把數據丟入內存的操作,畢竟棧是在內存中,會比操作寄存器慢很多吧。
我能想到的原因是後來的CPU都開始有多級緩存了,但是緩存和寄存器的速度差異有多大呢?這個速度差異用什麼方式可以測試出來?
這個問題大概是三方面的:
從編譯的角度看,寄存器壓力(局部的數量不夠用導致不得不訪存),在寄存器多到一定程度(大概16到32),就不那麼大了。
從亂序執行硬體看,因為你說的這些處理器都有寄存器重命名,很多還特別優化了reg2reg mov操作,所以對於寄存器不夠用導致的額外搬移,在這些處理器上並不會有非常大的性能損失(或者說軟體在這方面的優化空間有限)。
另外x86處理器的訪存單元會特別優化,像st fwd ld都做的比較激進,所以你用一些內存讀寫也不影響性能。
當然上述是對普通常見程序而言的,特別的程序一定是能造成寄存器不夠用溢出的,x86到amd64過度期間有不少這方面的研究有各種珠玉在前了(除了某簽名里寫「民科」的大神),不過還是來補充幾點,順便回答一下題干中的疑問,給提問者一個全面的答案。
補充之前先廢話兩句
- r8-r15是x86_64才能訪問的。類似的還有sil,dil之類的。但這部分是屬於前端(frontend)區別(x86指令集和x86_64指令集)
- 這個問題的最最最簡短的答案,其他人也基本都提到了,就是寄存器重命名(register renaming)
- 強烈推薦看這篇答案。kknd1394:為什麼X86的寄存器數量沒有隨著性能的提升而增加?當你能看懂這篇答案提到的每一個名詞的時候,你就不用看我接下來的廢話了。
現在x86體系基本可以理解為一個"CISC"解碼前端(frontend),加一個複雜的後端(具有一定的RISC特徵)。其中寄存器這一塊,前端解碼時負責識別邏輯寄存器,然後根據指令去選擇已有的物理寄存器映射或者是新建一個映射。所以其實上你看到的這些邏輯寄存器名,對於後端的物理寄存器來講是根本不知道的。
舉個例子,當你要用寄存器R0(或者EAX/RAX,隨便怎麼叫都好)的時候,如果這個指令是一個完全覆蓋式的,比如mov(此外還有一部分特別優化過的已知結果的指令,如xor R0,R0),那麼解碼時候完全不需要去管已有映射,直接重新找一個空閑的物理寄存器,並將這個映射寫到表(處理器內部的表)中即可。而反過來,如果你的指令時需要讀的,比如add R0, x (R0 := R0 +x),則需要去找已有映射。
其實寄存器重命名某種意義上可以視作out-of-order-execution的一個extension。out-of-order-execution還是看維基上的解釋更直接一點,我就不長篇大論了。
此外,超線程(HT/SMT)技術其實也和這個有一定關聯,這部分自己領悟,也不知道知乎有沒有相關問答,如果有的話也許看完這個你對這部分就更理解了。
最後,計算機架構這東西是所有課程裡面(除了務虛的計算機思維),大學教學和實際應用/實際科技發展離得最遠的部分了,建議不要以大學學到的那一點點東西來評價你看到的東西。
下面簡單答一下題主的問題。
想請問一下現在的技術情況下存儲密度可以做得很高,為什麼無論是整數寄存器還是擴展指令集的專用寄存器數量卻一直做的那麼少呢?
因為向後兼容,邏輯寄存器數量不可能增加。而物理寄存器的數量實際早已遠遠大於邏輯寄存器的數量。
但是如果寄存器數量增多的話,豈不是可以少做一些棧操作或者把數據丟入內存的操作,畢竟棧是在內存中,會比操作寄存器慢很多吧。
實際上無論Intel還是AMD,現在訪問棧的速度都和訪問寄存器是同一個水平線的(不考慮回寫的問題的話,僅考慮指令能繼續執行)。但是,由於解碼長度限制(如果對彙編有一定了解的應該知道,訪問棧的時候機器碼比寄存器長,使得一個周期可解碼的指令數量變少)、訪問要佔用額外寄存器,以及棧上面數據的同步的問題,實際情況會造成一段代碼里變數放在棧上比寄存器內要慢,這就是為什麼仍然編譯器在做優化的時候會儘可能把循環內的變數放在寄存器上的原因。
我能想到的原因是後來的CPU都開始有多級緩存了,但是緩存和寄存器的速度差異有多大呢?這個速度差異用什麼方式可以測試出來?
上面的解答你應該知道和你能想到的原因其實無關了。
實際上是增加了的。現代的x86寄存器內部是個接近於RISC的東西,寄存器的數量比暴露給你的多。為了保持API不變,有「寄存器重命名」一類的機制,x86指令也是先翻譯成微指令。
兼容問題使指令不能隨意變了,architecturally visible的寄存器也就不好變了即使是微指令層面可見的寄存器,由於編程模型/workload的限制,ILP已經很難再挖掘了。挖掘不出來新的ILP,不管是rf size/rw bandwidth,還是issue width,rob size,eu number,這些試圖增大平行窗口的設計選擇都面臨同樣的問題。gpu編程模型/workload到處是無腦並行,所以可以無腦堆core/eu,每個core里無腦堆register到幾千幾萬,運行時分給一堆線程用。
skylake-sp的avx512指令集已經做到32個zmm向量寄存器了,ARMv8也都是32個。而且這裡面說的寄存器都是指邏輯寄存器,實際的物理寄存器要多的多。x86內部有register rename單元,幫助把邏輯寄存器映射到物理寄存器,解決waw和war型寄存器依賴,這就可以很大程度緩解邏輯寄存器不夠用的問題。
增加寄存器可以從某種程度上提高性能,而現在的很多體系結構也確實這麼做,比如PowerPC的通用寄存器都有32個,但是,也相應的增加成本。相較於這些成本投入,不如增加cache和提高指令執行速度明顯。
提供自己的幾個觀點:
- 寄存器永遠都不嫌多,認為夠用了的只是編譯器的inline的深度不夠。。
- 當然寄存器多了之後,編譯器太難造了是真的。
- 可以用於x64 x86所有定址方式的」通用寄存器「 的數量受限於指令集的長度和格式,換而言之如果能夠多多讓已有的寄存器都支持定址(就像x64允許rip定址一樣),這就很爽了。。
- 要實現3應該硬體實現也有些困難。。
- 隨著流水線提前計算技術的越來越強大,那些藏在幕後的unamed 寄存器可以無限制的增加數量並發揮他們的威力,,當然前提是分支和跳轉的預測要足夠準確。。
- 5的強大抵消了很多x64通用寄存器不夠多的劣勢,但是也是有上限的,畢竟流程預測不可能無限度的一直準確下去(inline則可以無限inline耗盡你所有有名的寄存器),這就是有名分和沒名分的差距啊。。。
增加寄存器數量就要修改指令集,x86指令集發布了這麼多年,上面有無數軟體,要修改指令集是不可能。
當然可以增加新的指令集,比如x64,MMX,SSE,AVX等等,這些指令集確實增加了寄存器數量,但指令集一旦確定就不能再改了。
另外寄存器也不是越多越好,一來會增加指令集的長度,另外當線程切換的時候需要保存當前線程所有的寄存器狀態,寄存器太多會增加切換的代價。
最後,CPU內部使用的是微指令,其實是有很多寄存器的,利用寄存器換名技術,同一寄存器名字可以對應多個實際寄存器,使得不相關的指令可以被同時執行。想請問一下現在的技術情況下存儲密度可以做得很高,為什麼無論是整數寄存器還是擴展指令集的專用寄存器數量卻一直做的那麼少呢?
CPU執行的微指令使用的寄存器堆和彙編可見的寄存器是兩碼事。作為功能部件的通用寄存器布線和存儲數據的單元也是兩碼事……
在引入了微指令之後,收到的機器指令會被二進位翻譯成多條實際執行微指令。微指令的執行細節隨時都可以變化,現在x86機器碼只不過就是對外做了個兼容罷了。
事實上,在引入了動態流水線和寄存器重命名之後,物理寄存器和彙編使用寄存器編號的已經解耦,在沒有路徑依賴的情況下,彙編代碼使用的某個寄存器是可能被派發到任意一個可用寄存器中的。
但是,這的確只能「極大地緩解」——對於相同的指令,可見窗口的大小還是有點影響的。
但是如果寄存器數量增多的話,豈不是可以少做一些棧操作或者把數據丟入內存的操作,畢竟棧是在內存中,會比操作寄存器慢很多吧。
我能想到的原因是後來的CPU都開始有多級緩存了,但是緩存和寄存器的速度差異有多大呢?這個速度差異用什麼方式可以測試出來?
這個不一定,只是增加可見寄存器的話,性能未必有多大提成,複雜度和成本卻肯定會提高不少,當然,最主要的是你得改指令集…
CPU訪問延遲差異可以在這個網站查到。(寄存器的延遲是0個cycle,即立刻就能用。)
以skylake為例:
- L1 Data Cache Latency = 4 cycles for simple access via pointer
- L1 Data Cache Latency = 5 cycles for access with complex address calculation (size_t n, *p; n = p[n]).
- L2 Cache Latency = 12 cycles
- L3 Cache Latency = 42 cycles (core 0) (i7-6700 Skylake 4.0 GHz)
- L3 Cache Latency = 38 cycles (i7-7700K 4 GHz, Kaby Lake)
- RAM Latency = 42 cycles + 51 ns (i7-6700 Skylake)
本表頁面:http://www.7-cpu.com/cpu/Skylake.html
說實話,這個延遲一般不是測出來的,是設計的性能指標
其實,目前CPU的性能瓶頸往往是在訪存導致的延遲和分支預測器上(多核的話還有一個核心間的數據同步問題)。同時,CPU的功耗大頭往往是分支預測器。所以,真正決定通用處理器的指標在於亂序執行窗口大小、分支預測準確率、數據的load-to-use周期等。絕大多數影響性能的原因都是運算組件需要的數據沒有被準備好導致的停頓,而這三者都是為了餵飽運算組件存在的。比如,在完全沒有預讀的況下,訪問一次內存,兩百多個時鐘周期就過去了。
這個問題不知道有沒有標準答案,個人感覺可能是沒有必要。另外複雜指令集增加寄存器對設計難度增加很多的。
說說不必的原因,ARM需要更多的寄存器是因為它的每條指令都需要多個寄存器,因為它不支持「add 寄存器 內存數據"這種操作。所有數據先載入到寄存器再運算。x86則是支持的。本質是一些寄存器隱藏起來,用戶不必知道。你要提高寄存器數量無非是想少點spill,減少訪存壓力。但是x86-64有其他優化。
1. 64上通用寄存器十六個,不少也不多,編譯器spill壓力主要來自過量內聯,當然多的寄存器會更好的減少spill,但color graph 寄存器分配較適合處理寄存器壓力大場景。當然jit編譯器常用的linear scan就不太適合。2. 超標量機亂序執行加上寄存器重命名,很像編譯器里常用的ssa。這些技術帶來speculative執行3.複雜的指令流水stage,高達10+種4.這個是我個人猜測,看看x86的指令編碼,如果加到更多,編碼咋表示了,現在後加的那8個,r8-r15,編碼就很奇葩,在前面加個rex前綴,裡面偷一個bit,opcode後面跟著的ModRM位元組是原來上古時期設計的,可惜裡面只有三個bit表示寄存器,這三個bit加前面那個rex你的一個bit湊出來四個bit。x86-64編碼是變長,1-15位元組都可能,又要支持亂序執行,其解碼器一定神乎其技的複雜。在揉入幾個bit加寄存器是不是也會影響decoder了?RISC設計哲學,至少是原教旨的,就是運算操作全在寄存器之間進行,相對大的寄存器文件,16到32個通用寄存器,訪存全靠Load/Store類指令。
x86那是歷史包袱,CISC的傳統,寄存器文件(或者更傳統的說法:寄存器堆)不大,還各有各的規定用途,不是那麼通用(比如SI、DI),運算操作可以在寄存器和內存之間進行,各種複雜變態的定址方式等等。
增加寄存器文件數量那就是徹底更改體系結構了,單純的x86是不敢的(有點兒小打小鬧,加幾個控制寄存器,浮點部分弄弄),x86_64才敢這麼干。
你看到的EAX、EBX等等寄存器是彙編語言程序員能看到的部分,屬於體系結構內容,最早規定了幾個,不管是386還是Core i7都是一樣的,這樣才有了軟體兼容性一說。這裡很多人給你提到的「寄存器重命名」等等「後端」是程序員看不到的,屬於具體設計實現、計算機組成的內容,是亂序執行等等加速技術實現的基礎,不同代的產品具體設計可以翻天覆地大不相同。
從IBM S/360開始開創的系列化設計路線的特點就是相對固定的體系結構和花樣翻新的具體設計實現。這是體系結構課的最基礎內容了吧。
推薦閱讀:
※為什麼要有指針?
※Android 會像 Windows 一樣,打敗 iOS 嗎?
※如何提高寫代碼的水平?
※Windows 下進行 C/C++ 開發,Eclipse 和 Visual Studio 哪個好?從編譯速度、UI、方便程度上如何比較?
※這個開源的6千行UI框架,能打敗QT,MFC嗎?