簡體   English   中英

英特爾 SSE:為什么 `_mm_extract_ps` 返回 `int` 而不是 `float`?

[英]Intel SSE: Why does `_mm_extract_ps` return `int` instead of `float`?

為什么_mm_extract_ps返回一個int而不是float

從 C 中的 XMM 寄存器讀取單個float的正確方法是什么?

或者更確切地說,問它的另一種方式是: _mm_set_ps指令的反面是什么?

沒有一個答案似乎真正回答了這個問題,為什么它會返回int

原因是, extractps指令實際上將向量的一個分量復制到一個通用寄存器。 返回一個 int 看起來確實很愚蠢,但這就是實際發生的事情 - 原始浮點值最終在一個通用寄存器中(它保存整數)。

如果您的編譯器配置為為所有浮點運算生成 SSE,那么最接近“提取”一個值到寄存器的方法是將值混洗到向量的低分量中,然后將其轉換為標量浮點數。 這應該會導致向量的該分量保留在 SSE 寄存器中:

/* returns the second component of the vector */
float foo(__m128 b)
{
    return _mm_cvtss_f32(_mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 2)));
}

_mm_cvtss_f32內在函數是免費的,它不生成指令,它只會使編譯器將 xmm 寄存器重新解釋為float ,因此可以這樣返回。

_mm_shuffle_ps將所需的值放入最低組件中。 _MM_SHUFFLE宏為生成的shufps指令生成立即操作數。

示例中的2從 127:0 寄存器的第 95:64 位(從頭開始的第 3 個 32 位分量,按內存順序)獲取浮點數並將其放入寄存器的 31:0 分量(開頭,在記憶順序)。

生成的代碼很可能會在寄存器中自然地返回值,就像任何其他浮點值返回一樣,不會低效地寫出內存並將其讀回。

如果您正在生成將 x87 FPU 用於浮點的代碼(對於未經 SSE 優化的普通 C 代碼),這可能會導致生成的代碼效率低下 - 編譯器可能會存儲 SSE 向量的分量然后使用fld將其讀回 x87 寄存器堆棧。 通常,64 位平台不使用 x87(它們對所有浮點使用 SSE,主要是標量指令,除非編譯器正在矢量化)。

我應該補充一點,我總是使用 C++,所以我不確定在 C 中通過值還是通過指針傳遞 __m128 是否更有效。在 C++ 中,我將使用const __m128 &並且這種代碼將在標題中,所以編譯器可以內聯。

令人困惑的是, int _mm_extract_ps()不是用於從向量中獲取標量float元素。 內在函數不會暴露指令的內存目標形式(這對於該目的很有用)。 這不是內在函數無法直接表達指令有用的所有內容的唯一情況。 :(

gcc 和 clang 知道 asm 指令是如何工作的,並且會在編譯其他 shuffle 時為您使用它; _mm_extract_ps結果鍵入float通常會導致來自 gcc 的可怕 asm( extractps eax, xmm0, 2 / mov [mem], eax )。

如果您認為_mm_extract_psIEEE 754 binary32 浮點位模式從 CPU 的 FP 域提取到整數域(作為 C 標量int ),而不是使用整數向量操作來操作 FP 位模式,則該名稱是有意義的。 根據我對 gcc、clang 和 icc(見下文)的測試,這是_mm_extract_ps在所有編譯器中編譯成良好 asm 的唯一“可移植”用例 其他任何東西都只是一個特定於編譯器的黑客來獲得你想要的 asm。


對應的 asm 指令是EXTRACTPS r/m32, xmm, imm8 請注意,目標可以是內存或整數寄存器,但不能是另一個 XMM 寄存器。 它是PEXTRD r/m32, xmm, imm8 (也在 SSE4.1 中)的 FP 等價物,其中整數寄存器目標形式更明顯有用。 EXTRACTPS 不是INSERTPS xmm1, xmm2/m32, imm8的反面。

也許與 PEXTRD 的這種相似性使內部實現更簡單,而不會損害提取到內存的用例(對於 asm,而不是內在函數),或者英特爾的 SSE4.1 設計人員認為這種方式實際上比非- 破壞性的 FP 域復制和洗牌(沒有 AVX,x86 嚴重缺乏)。 有些 FP 向量指令具有 XMM 源和內存或 xmm 目標,例如MOVSS xmm2/m32, xmm ,因此這種指令不會是新的。 有趣的事實:PEXTRD 和 EXTRACTPS 的操作碼僅在最后一位不同。


在匯編中,標量float只是 XMM 寄存器的低元素(或內存中的 4 個字節)。 XMM 的上層元素甚至不必將ADDSS等指令歸零即可工作,而不會引發任何額外的 FP 異常。 在 XMM 寄存器中傳遞/返回 FP 參數的調用約定(例如所有常見的 x86-64 ABI)中, float foo(float a)必須假定 XMM0 的上層元素在進入時持有垃圾,但可以在高元素中留下垃圾返回時的 XMM0。 更多信息)。

正如@doug 指出的那樣,其他隨機播放指令可用於將向量的浮點元素放入 xmm 寄存器的底部。 這在 SSE1/SSE2 中已經是一個主要解決的問題,而且似乎 EXTRACTPS 和 INSERTPS 並沒有試圖解決寄存器操作數的問題。


SSE4.1 INSERTPS xmm1, xmm2/m32, imm8是編譯器實現_mm_set_ss(function_arg)的最佳方法之一,當標量浮點數已經在寄存器中並且它們不能/不優化將上部元素歸零時。 對於 clang 以外的編譯器來說,大部分時間都是這樣)。 該鏈接問題還進一步討論了內在函數未能公開加載或存儲指令的版本,例如 EXTRACTPS、INSERTPS 和 PMOVZX,其內存操作數小於 128b(因此即使沒有 AVX 也不需要對齊)。 編寫安全的代碼是不可能像在 asm 中那樣高效地編譯的。

如果沒有 AVX 3 操作數 SHUFPS,x86 無法像整數PSHUFD那樣提供一種完全有效且通用的方法來復制和混洗 FP 向量。 SHUFPS是一種不同的野獸,除非與 src=dst 就地使用。 保留原始文件需要 MOVAPS,這會在 IvyBridge 之前在 CPU 上花費 uop 和延遲,並且總是會花費代碼大小。 在 FP 指令之間使用 PSHUFD 會產生延遲(旁路延遲)。 (有關一些技巧,請參閱此橫向和答案,例如使用 SSE3 MOVSHDUP)。

SSE4.1 INSERTPS 可以將一個元素提取到單獨的寄存器中,但 AFAIK 即使替換了所有原始值,它仍然依賴於目標的先前值。 像這樣的錯誤依賴不利於亂序執行。 將寄存器作為 INSERTPS 的目標進行異或歸零仍然是 2 微秒,並且在 SSE4.1 CPU 上具有比 MOVAPS+SHUFPS 更低的延遲,而沒有針對零延遲 MOVAPS 的 mov-elimination(僅限 Penryn、Nehalem、Sandybridge。如果您還有 Silvermont包括低功耗 CPU)。 不過,代碼大小稍差一些。


使用_mm_extract_ps然后將結果鍵入回浮動(如當前接受的答案及其評論中所建議的那樣)是一個壞主意。 在 gcc 或 icc 上,您的代碼很容易編譯為可怕的東西(例如 EXTRACTPS 到內存,然后加載回 XMM 寄存器)。 Clang 似乎對腦死亡行為免疫,並且使用自己選擇的隨機指令(包括適當使用 EXTRACTPS)進行通常的隨機編譯。

在 Godbolt 編譯器資源管理器上使用 gcc5.4 -O3 -msse4.1 -mtune=haswell 、clang3.8.1 和 icc17 嘗試了這些示例。 我使用的是 C 模式,而不是 C++,但在 GNU C++ 中允許基於聯合的類型雙關語作為 ISO C++ 的擴展。 類型雙關的指針轉換違反了 C99 和 C++ 中的嚴格別名,即使使用 GNU 擴展也是如此。

#include <immintrin.h>

// gcc:bad  clang:good  icc:good
void extr_unsafe_ptrcast(__m128 v, float *p) {
  // violates strict aliasing
  *(int*)p = _mm_extract_ps(v, 2);
}

  gcc:   # others extractps with a memory dest
    extractps       eax, xmm0, 2
    mov     DWORD PTR [rdi], eax
    ret


// gcc:good  clang:good  icc:bad
void extr_pun(__m128 v, float *p) {
  // union type punning is safe in C99 (and GNU C and GNU C++)
  union floatpun { int i; float f; } fp;
  fp.i = _mm_extract_ps(v, 2);
  *p = fp.f;     // compiles to an extractps straight to memory
}

   icc:
    vextractps eax, xmm0, 2
    mov       DWORD PTR [rdi], eax
    ret       


// gcc:good  clang:good  icc:horrible
void extr_gnu(__m128 v, float *p) {
  // gcc uses extractps with a memory dest, icc does extr_store
  *p = v[2];
}

 gcc/clang:
    extractps       DWORD PTR [rdi], xmm0, 2
 icc:
    vmovups   XMMWORD PTR [-24+rsp], xmm0
    mov       eax, DWORD PTR [-16+rsp]      # reload from red-zone tmp buffer
    mov       DWORD PTR [rdi], eax

// gcc:good  clang:good  icc:poor
void extr_shuf(__m128 v, float *p) {
  __m128 e2 = _mm_shuffle_ps(v,v, 2);
  *p = _mm_cvtss_f32(e2);  // gcc uses extractps
}

 icc:   (others: extractps right to memory)
    vshufps   xmm1, xmm0, xmm0, 2
    vmovss    DWORD PTR [rdi], xmm1

當您希望在 xmm 寄存器中獲得最終結果時,由編譯器來優化您的提取並做一些完全不同的事情。 Gcc 和 clang 都成功了,但 ICC 沒有。

// gcc:good  clang:good  icc:bad
float ret_pun(__m128 v) {
  union floatpun { int i; float f; } fp;
  fp.i = _mm_extract_ps(v, 2);
  return fp.f;
}

  gcc:
    unpckhps        xmm0, xmm0
  clang:
    shufpd  xmm0, xmm0, 1
  icc17:
    vextractps DWORD PTR [-8+rsp], xmm0, 2
    vmovss    xmm0, DWORD PTR [-8+rsp]

請注意, icc 對extr_pun也表現不佳,因此它不喜歡基於聯合的類型雙關。

這里明顯的贏家是使用_mm_shuffle_ps(v,v, 2) “手動”進行洗牌,並使用_mm_cvtss_f32 我們從每個編譯器中獲得了寄存器和內存目標的最佳代碼,除了 ICC 未能將 EXTRACTPS 用於內存目標的情況。 使用 AVX,SHUFPS + 獨立存儲在 Intel CPU 上仍然只有 2 微秒,只是更大的代碼大小並且需要一個 tmp 寄存器。 但是,如果沒有 AVX,不破壞原始向量將花費 MOVAPS:/


根據Agner Fog 的指令表,除 Nehalem 之外的所有 Intel CPU 都使用多個微指令實現 PEXTRD 和 EXTRACTPS 的寄存器目標版本:通常只需一個 shuffle uop + 一個 MOVD uop 即可將數據從向量域移動到 gp 整數。 Nehalem 寄存器目標 EXTRACTPS 為端口 5 的 1 uop,具有 1+2 周期延遲(1 + 旁路延遲)。

我不知道為什么他們設法將 EXTRACTPS 實現為單個 uop 而不是 PEXTRD(這是 2 uop,並以 2+1 周期延遲運行)。 Nehalem MOVD 為 1 uop(並在任何 ALU 端口上運行),具有 1+1 周期延遲。 (我認為 +1 是用於 vec-int 和通用整數 regs 之間的旁路延遲)。

Nehalem 非常關心向量 FP 與整數域。 SnB 系列 CPU 在域之間的旁路延遲延遲更小(有時為零)。

PEXTRD 和 EXTRACTPS 的內存目標版本都是 Nehalem 上的 2 微指令。

在 Broadwell 及更高版本上,內存目標 EXTRACTPS 和 PEXTRD 為 2 微指令,但在 Sandybridge 通過 Haswell 上,內存目標 EXTRACTPS 為 3 微指令。 Memory-destination PEXTRD 在除了 Sandybridge 之外的所有東西上都是 2 微指令,它是 3。這看起來很奇怪,而且 Agner Fog 的表有時確實有錯誤,但這是可能的。 微融合不適用於某些微架構上的某些指令。

如果任何一條指令被證明對任何重要的事情都非常有用(例如,內部循環內部),CPU 設計人員將構建可以將整個事情作為一個 uop 來完成的執行單元(或者對於 memory-dest 來說可能是 2 個)。 但這可能需要更多內部 uop 格式的位(Sandybridge 對其進行了簡化)。

有趣的事實: _mm_extract_epi32(vec, 0)編譯(在大多數編譯器上)為movd eax, xmm0 eax, xmm0 ,它比pextrd eax, xmm0, 0更短、更快。

有趣的是,它們在 Nehalem 上的表現不同(它非常關心向量 FP 與整數域,並且在 Penryn (45nm Core2) 中引入 SSE4.1 后不久就出現了)。 具有寄存器目標的 EXTRACTPS 為 1 uop,具有 1+2 周期延遲(來自 FP 和整數域之間的旁路延遲的 +2)。 PEXTRD 為 2 uop,並以 2+1 周期延遲運行。

MSDN docs ,我相信您可以將結果轉換為浮點數。

請注意,從他們的示例中,0xc0a40000 值相當於 -5.125 (a.m128_f32[1])。

更新:我強烈推薦@doug65536 和@PeterCordes(下)的答案來代替我的答案,這顯然會在許多編譯器上生成性能不佳的代碼。

嘗試_mm_storeu_psSSE 存儲操作的任何變體。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM