Intel大坑之一:丟失的SSE2 128bit/64bit 位移指令,馬航MH370??


緣由

  最近在寫一些字符串函數的優化,興趣使然,可是寫的過程中,想要實現 128bit 的按 bit 邏輯位移,遇到了一個大坑,且聽我娓娓道來。

  如果要追究標題,更確切的是丟失的SSE2 128 bit / 64 bit  位移指令,已修改。

  我並不想用什么馬航370來博眼球,我也沒意識到這個能博眼球,當我寫下這個標題的時候,的確沒有馬航370這個字眼,可是當我寫到一半的時候,突然就冒出了馬航370這幾個字,如果你認真閱讀了我的文章,也許你也應該思考一下,這 128 bit / 64 bit 的位移指令到底是去哪了?石沉大海了?那不就跟馬航370一樣嗎,是一個謎,一個非常非常大的謎……

 

  如果你對 MMX, SSE 位移指令不太懂,可以先看看:

http://tommesani.com/index.php/simd/44-mmx-shift.html ,

這個比較容易理解,我當初學習 MMX, SSE 指令都是從這里開始的,
但是這個只寫到 MMX 指令集,更新的版本看后面的。

 

邏輯位移

對於 MMX, SSE 的位移指令,我們很自然的想到:

邏輯左移:PSLLW/PSLLD/PSLLQ,Shift Packed Data Left Logical (壓縮邏輯左移)

邏輯右移:PSRLW/PSRLD/PSRLQ,Shift Packed Data  Right Logical (壓縮邏輯右移)

顧名思義,W 指的是Word(字),D 指的 DWORD (雙字),Q 指的是 QWORD (四字),PSLLW 實現的是按 Word 的分組邏輯左移,

PSLLD 是按 DWORD 的分組邏輯左移,PSLLQ 是按 QWORD 實現的分組邏輯左移,這一切看起來都很 OK 。

這里以邏輯左移為例:

關於具體的邏輯左移指令的說明,可參考:

http://moeto.comoj.com/project/intel/instruct32_hh/vc256.htm

或者 http://x86.renejeschke.de/html/file_module_x86_id_259.html

右移也是類似的,在此不再螯述。

 

問題來了

  我們要實現的是 128bit 的邏輯位移,SSE2 里面有 PSLLDQ / PSRLDQ 指令,這里 DQ 即是 Double QWORD 的意思,

這不正好是我們需要的 128bit 按 bit 位移嗎?No!!別高興得太早,我們來看看 Intel 的文檔:

PSLLDQ--Packed Shift Left Logical Double Quadword

http://moeto.comoj.com/project/intel/instruct32_hh/vc255.htm

截圖如下:

我們看到,很遺憾,SSE2 並沒有實現 128bit 的按 bit 位移,PSLLDQ 只能實現 128bit 的按 byte 位移,即最小位移量必須是一個 byte (即8個bit),這非常不科學,更不科學的是位移量只能是立即數!考慮到 Intel 並未真正實現 128bit 數據處理(SSE 大多數指令都只實現了最多 64bit 粒度的數據處理,例如一個雙精度浮點數是 64bit 的),好吧,我們認了,但是!!但是!!Intel 你沒搞錯吧,PSLLDQ 的操作數只支持 imm8,imm8 意味着什么?imm8 是 8 位立即數的意思,那就是說我們只能在匯編里寫死(常數),不能使用任何寄存器來做位移量。What the fu*K??

好吧,這我們也認了。。。CPU 是你設計的,我們拿你沒辦法。說句題外話,如果 PSLLDQ 支持 reg32, reg64 寄存器位移的話, 會方便很多,因為我們可以先用 PSLLDQ 位移足夠位數的按 Byte 位移,然后再用 PSLLQ 位移剩下的剩余量(這是后話,為什么要這么用,到后面你就知道),可是,現在這種方法都不行!!這個 imm8 徹底讓我蛋碎了。。。PSLLQ 對於128 bit 寄存器一次只能移 16 位(先破埂了),那么意味這我們如果要用這種方法,要 if / jump 好幾次。。。

 

大坑開始

好吧,我們退而求其次,既然你不能實現 128 bit 的按 bit 位移,那我們分成兩個 64 bit 的位移來實現好了,無非是多一次判斷,多一次合並,雖然效率沒有直接128 bit 位移的高,但是苦於你沒實現嘛,只能這么干了。。。
好吧,我們開始吧。。。。GO!!!好了,我們換成 PSLLQ 了,執行PSLLQ xmm0, 32 或 PSLLQ xmm0, ecx (這里ecx的值為32),咦?xmm0怎么全為0了??啊,怎么回事??

我們回過頭來重新看看 intel 的文檔:

重點看兩個我用紅線框起來的,當 PSLLQ 作用於 64 bit 的寄存器時,我們看到是最大支持 COUNT = 64 位的位移(嚴格意義上講是 max = 63,這個不糾結了,習慣問題,下同);

但是當 PSLLQ 作用於 128 bit 寄存器時,奇怪的事情發生了,最大只支持 COUNT = 16 位的位移(嚴格意義上是15位),如上圖所示。

如果不是重新看 Intel 的文檔,如果不是調試中發現問題,誰能想到最多只能移15位???Intel 的腦袋是被門夾了嗎??Why??MMX 寄存器上都可以實現最多 63 位的位移, SSE 寄存器為什么就不可以?雖然我們知道 MMX 寄存器和 SSE 寄存器是不一樣的,分開的,MMX 寄存器是借用 x87 浮點寄存器來實現 MMX 指令的,可是你在 MMX 寄存器上實現了 64 bit 的位移,為什么在 128 bit 的 SSE 寄存器上卻只能移最多 15 位??你說難以實現,我認了,我不太懂為什么那么難,我們只能認了,可是你卻實現了 128 bit 的按 byte 位移的 PSLLDQ 指令,這又作何解釋??本來顧名思義,PSLLDQ 就來就應該是實現 128 bit 的按 bit 位移,限於歷史原因,這個沒實現我可以理解,可是你沒有理由在 PSLLQ 作用於 128 bit 的 SSE 寄存器時卻最多只能位移 15 位吧??這真的有那么難嗎??真的難嗎????真的那么難,你又是怎么實現 PSLLDQ 的 128 bit 按 Byte 位移的??

 

尋求答案

帶着這些疑問,我們問了一下 Google 老先生,搜索“128 bit shift”,發現 N 多小伙伴都遇到過這個問題,例如:

Looking for sse 128 bit shift operation for non-immediate shift value

What is SSE !@#$% good for? #2: Bit vector operations

 

最后,Google老先生告訴了我們一個最好的解答,來自 Intel 的論壇,在這里:

Missing instruction in SSE: PSLLDQ with _bit_ shift amount?

 

是這樣的,截圖如下:

首先,Intel 是承認這個 missing instruction(丟失的指令)的,我們也意識到 missing instruction 無處不在,只是這個有點過分。

上面的回復,大意是:(E文不是太好,用 Google 輔助翻譯的,見諒)

 

Hi Geoff,

  我們的一個工程師提供了以下回應,並做一些澄清。

 

  你這個問題是正確的,對於 SIMD(單指令多數據流) 來說,在當前的指令集里,bit 位移是比按 byte 位移難於實現的(指的是 SSE 寄存器

的 128 bit 按 bit 位移)。不幸的是,這不是一個小改變,實現一個這樣的按 bit 位移指令。這里有更多的改變比簡單的在立即字節里適應位移

距離--硬件實際完成按 bit 位移是一個被限制的問題。

  如果你有一個使用案例關於為什么這個操作是有用的,隨着應用程序將受益於這個操作,這是我們有興趣聽到的。一般情況下,我們試圖

設計新的指令來滿足特定的需求,而不是只是提供 "missing instuctions“ (丟失指令)的支持。從實際情況來看,有很多這樣的 "missing instuctions“

——更有趣的問題是,如果在實際的應用中應對這些 "missing instuctions“ 所帶來的問題。

 

博主觀點:

  對於 128 bit 的按 bit 位移比較難以實現,這我能理解,可是 PSLLQ 對於 SSE 的 128bit 寄存器只能最多位移15位我就不能理解了……

SSE2 的 128bit/64bit 位移你在哪里,為什么是15而不是31,63?親愛的馬航MH370,你到底在哪里?為什么要選擇飛中國的航班?為什么??

 

解決之道

解決的辦法有很多種,前面也講過一個,就是:如果你要左移 count 位,先用 PSLLDQ 位移 x * 8 位, 這個是純 128 bit 的位移,然后再用 PSLLQ 位移剩下的 y = (count - x * 8) 位,這里 y 要小於 16。但是由於 PSLLDQ 只能執行imm8立即數,所以你要先 if / jump 判斷一下 count 的值,分別執行 PSLLDQ xmm0, 32; 或 PSLLDQ xmm0, 16; 或 PSLLDQ xmm0, 8; PSLLDQ xmm0, 4; PSLLDQ xmm0, 2; 以后,再執行 PSLLQ 位移剩下的 Y 位。這里PSLLDQ xmm0, 32也許可以用別的 SSE Shuffle 指令代替,但是是一樣的,最大的問題是你要先 if 先判斷一下再執行相應的指令,這種方法並不見得高效。

我們再來找一些好一點的辦法:

既然,SSE 里我們沒辦法實現 64 bit 的位移,但是 MMX 寄存器里是可以的,但是我們又要在 SSE 寄存器里實現,那么我們可以先把數據從 SSE 寄存器里轉移到 MMX 寄存器,位移好了,再合並到 SSE 寄存器里。雖然這個過程有點繁瑣,但是相比上面第一種方法,還是高效了不少,而且有一關鍵的地方,很多時候,我們要做這個位移,都是接近最終輸出結果的時候,這個時候就不必把數據合並回 SSE 寄存器了,可以直接用 MMX 寄存器的值作為輸出即可,這樣又快了一點兒,還不賴。

還有沒有解決的辦法,應該還有,容我再想一想,或者讀者你也想想?有網友貼了 AVX 版的 VPSLLDQ 指令說明,可是同樣只支持 imm8 立即數,而且並不是所有人的 CPU 都支持 AVX 的,博主本人的 CPU 就不支持。

(由於博主是睡到一半起來關電腦寫下的這篇文章,所以我先去休息一會,有空再來補全這一塊)

 

后記

我們現在遇到的問題做一個比喻,就是:我們前面有三條路,一條是大路,一條是小路,一條是其他未知的路,我們以為大路(PSLLDQ)最快,於是選擇先走了大路,結果發現直接走是過不去的;轉而選擇小路(PSLLQ),走小路,結果發現有個陷阱,這個陷阱讓我們到不了目的地,只能達到1/4;然后再回過頭來看看大路,大路其實可以過去的,但是踩下去以后全是泥潭(只支持立即數的128位 byte 位移),要走過去,很艱難。那么我們只能選擇第三條未知的路了(各種其他指令的組合模擬實現)。

Intel 的 MMX, SSE 各種缺失的指令由來已久,指令的設計也是混亂不勘,還有一個比較著名的就是只實現了POR, PAND, PANDN(and not),沒有實現 PNOT (即對MMX, SSE寄存器取反),雖然 PNOT 的確可以用 PANDN 實現(你至少需要2個寄存器),或者用 PCMPEQB xmm0, xmm0 來實現 全置 1 的操作,但是有可能增加了寄存器的占用,可能會增加指令周期,反正是各種不好,雖然影響不算大,但是有時候寄存器捉襟見肘的時候,還是非常蛋疼的。

還有一點更可笑的是,我跟你說了,你一定會相信 Intel 是荒唐的,我們的確需要的是無符號的邏輯左/右移,但是如果你要實現的是有符號的右移(算術右移),

可以使用 PSRAW/PSRAD 指令 - 壓縮算術右移,另外說一下,不存在算術左移,因為算術左移邏輯左移是一回事,可參考:

http://moeto.comoj.com/project/intel/instruct32_hh/vc257.htm

非常可笑的是,在這里,Intel 卻實現了對於 128 bit 寄存器最多 31 位的位移,更可笑的,對於 64 bit 寄存器最多的位移數也是 31(見上面的鏈接),這你能懂???

到底是我們的智商有問題,還是 Intel 的智商有問題?!!

看下圖:

PS:糾正一下,上面這個舉例是錯誤的(是我看錯了),Intel 並沒有實現 PSRAQ,上面這個是 PSRAD 的,是針對 DWORD 的,而不是 QWORD,所以他這么實現是正確的,這里並沒有問題。

這個問題,在另外一個著名的帖子里也有提到:

千分求匯編優化:UInt96x96To192(...)

到目前為止,都未實現 PSRAQ 指令。

. <END> .


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM