簡體   English   中英

如何在不使用任何SSE指令的情況下設置__m128i?

[英]How can I set __m128i without using of any SSE instruction?

我有很多函數使用相同的常量__m128i值。 例如:

const __m128i K8 = _mm_setr_epi8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
const __m128i K16 = _mm_setr_epi16(1, 2, 3, 4, 5, 6, 7, 8);
const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);

所以我想將所有這些常量存儲在一個地方。 但是有一個問題:我在運行時檢查現有的CPU擴展。 如果CPU不支持例如SSE(或AVX),那么在常量初始化期間程序將崩潰。

那么可以在不使用SSE的情況下初始化這些常量嗎?

可以在不使用SSE指令的情況下初始化__m128i向量,但這取決於編譯器如何定義__m128i。

對於Microsoft Visual Studio,您可以定義下一個宏(它將__m128i定義為char [16]):

template <class T> inline char GetChar(T value, size_t index)
{
    return ((char*)&value)[index];
}

#define AS_CHAR(a) char(a)

#define AS_2CHARS(a) \
    GetChar(int16_t(a), 0), GetChar(int16_t(a), 1)

#define AS_4CHARS(a) \
    GetChar(int32_t(a), 0), GetChar(int32_t(a), 1), \
    GetChar(int32_t(a), 2), GetChar(int32_t(a), 3)

#define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
    {AS_CHAR(a0), AS_CHAR(a1), AS_CHAR(a2), AS_CHAR(a3), \
     AS_CHAR(a4), AS_CHAR(a5), AS_CHAR(a6), AS_CHAR(a7), \
     AS_CHAR(a8), AS_CHAR(a9), AS_CHAR(aa), AS_CHAR(ab), \
     AS_CHAR(ac), AS_CHAR(ad), AS_CHAR(ae), AS_CHAR(af)}

#define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
    {AS_2CHARS(a0), AS_2CHARS(a1), AS_2CHARS(a2), AS_2CHARS(a3), \
     AS_2CHARS(a4), AS_2CHARS(a5), AS_2CHARS(a6), AS_2CHARS(a7)}

#define _MM_SETR_EPI32(a0, a1, a2, a3) \
    {AS_4CHARS(a0), AS_4CHARS(a1), AS_4CHARS(a2), AS_4CHARS(a3)}       

對於GCC,它將(它將__m128i定義為long long [2]):

#define CHAR_AS_LONGLONG(a) (((long long)a) & 0xFF)

#define SHORT_AS_LONGLONG(a) (((long long)a) & 0xFFFF)

#define INT_AS_LONGLONG(a) (((long long)a) & 0xFFFFFFFF)

#define LL_SETR_EPI8(a, b, c, d, e, f, g, h) \
    CHAR_AS_LONGLONG(a) | (CHAR_AS_LONGLONG(b) << 8) | \
    (CHAR_AS_LONGLONG(c) << 16) | (CHAR_AS_LONGLONG(d) << 24) | \
    (CHAR_AS_LONGLONG(e) << 32) | (CHAR_AS_LONGLONG(f) << 40) | \
    (CHAR_AS_LONGLONG(g) << 48) | (CHAR_AS_LONGLONG(h) << 56)

#define LL_SETR_EPI16(a, b, c, d) \
    SHORT_AS_LONGLONG(a) | (SHORT_AS_LONGLONG(b) << 16) | \
    (SHORT_AS_LONGLONG(c) << 32) | (SHORT_AS_LONGLONG(d) << 48)

#define LL_SETR_EPI32(a, b) \
    INT_AS_LONGLONG(a) | (INT_AS_LONGLONG(b) << 32)        

#define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
    {LL_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7), LL_SETR_EPI8(a8, a9, aa, ab, ac, ad, ae, af)}

#define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
    {LL_SETR_EPI16(a0, a1, a2, a3), LL_SETR_EPI16(a4, a5, a6, a7)}

#define _MM_SETR_EPI32(a0, a1, a2, a3) \
    {LL_SETR_EPI32(a0, a1), LL_SETR_EPI32(a2, a3)}        

所以在你的代碼初始化__m128i常量將是這樣的:

const __m128i K8 = _MM_SETR_EPI8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
const __m128i K16 = _MM_SETR_EPI16(1, 2, 3, 4, 5, 6, 7, 8);
const __m128i K32 = _MM_SETR_EPI32(1, 2, 3, 4);

我建議將初始化數據全局定義為標量數據,然后將其本地加載到const __m128i

static const uint8_t gK8[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

static inline foo()
{
    const __m128i K8 = _mm_loadu_si128((__m128i *)gK8);

    // ...
}

你可以使用聯盟。

union M128 {
   char[16] i8;
   __m128i i128;
};

const M128 k8 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

如果M128聯合在本地定義,您使用循環,這應該沒有性能開銷(它將在循環開始時加載到內存中)。 因為它包含__m128i類型的變量,所以M128繼承了正確的對齊方式。

void foo()
{
   M128 k8 = ...;
   // use k8.i128 in your for loop
}

如果它在其他地方定義,則需要在啟動循環之前復制到本地寄存器,否則編譯器可能無法對其進行優化。

void foo()
{
    __m128i tmp = k8.i128;
    // for loop here
}

這將把k8加載到cpu寄存器並在循環期間保持在那里,只要有足夠的空閑寄存器來執行循環體。

根據您使用的編譯器,這些聯合可能已經定義(VS確實),但編譯器提供的定義可能不可移植。

你通常不需要這個。 編譯器非常擅長將相同的存儲用於使用相同常量的多個函數。 就像合並相同的字符串文字的多個實例為一個字符串常量,多個實例相同的_mm_set*在不同的功能由相同的矢量常數所有負載(或在飛行中產生用於_mm_setzero_si128()_mm_set1_epi8(-1)

使用Godbolt的二進制輸出(反匯編)模式可以查看是否從同一塊內存加載不同的函數。 查看它添加的注釋,它將RIP相對地址解析為絕對地址。

  • gcc: 所有相同的常量共享相同的存儲 ,無論它們來自自動矢量化還是_mm_set 即使16B常數是32B的子集,32B常數也不能與16B常數重疊。

  • clang: 相同的常量共享存儲 16B和32B常數不重疊,即使一個是另一個的子集。 一些使用重復常量的函數使用AVX2 vpbroadcastd廣播加載(它甚至不會在Intel SnB系列CPU上使用ALU uop)。 出於某種原因,它選擇基於操作的元素大小而不是常量的重復性來執行此操作。 請注意,clang的asm輸出會為每次使用重復常量,但最終的二進制不會。

  • MSVC: 相同的常量共享存儲 與gcc的功能幾乎相同。 (完整的asm輸出很難通過;使用搜索。我只能通過main找到.exe的路徑來獲取asm,然后計算出使用cl.exe -O2 /FAs生成的asm輸出的路徑cl.exe -O2 /FAs和運行system("type .../foo.asm") )。

編譯器很擅長這個,因為它不是一個新問題。 從編譯器的早期開始就存在字符串。

我沒有檢查它是否適用於源文件(例如,對於多個編譯單元中使用的內聯向量函數)。 如果仍然想靜態/全球向量常數,見下圖:


這似乎沒有一種簡單方便的靜態初始化靜態/全局__m128 C編譯器甚至不接受_mm_set*作為初始化器,因為它的功能類似於函數。 他們沒有利用他們實際上可以通過它看到編譯時常量16B的事實

const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);   // Illegal in C
// C++: generates a constructor that copies from .rodata to the BSS

盡管構造函數只需要SSE1或SSE2,但無論如何都不需要它。 這太糟糕了。 不要這樣做 您最終會兩次支付常量的內存成本。


Fabio的union答案看起來是靜態初始化向量常量的最佳可移植方式,但這意味着您必須訪問__m128i聯合成員。 它可能有助於將相關常量分組到彼此附近(希望在同一緩存行中),即使它們被分散的函數使用也是如此。 還有不可移植的方法,也就是說(例如,將相關的常量放在他們自己的ELF部分中,使用GNU C __attribute__ ((section ("constants_for_task_A"))) )。 希望可以將它們組合在.rodata部分(它成為.text部分的一部分)。

暫無
暫無

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

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