簡體   English   中英

將位解壓縮為單精度浮點數的最快方法

[英]Fastest way to unpack bits into single precision floats

這是特定於平台的問題。 速度至關重要。 將字節解壓縮到8個單精度浮點數組中的最快方法是什么,以便將零映射為零,將映射映射為1?

我最終使用8位掩碼和7位移位解壓縮到8個int32,然后使用AVX指令將int32轉換為浮點數。

我的平台是Windows 64位在AVX(但沒有AVX2)的CPU上運行。 編譯器:Visual Studio 2013。

謝謝。

預處理不會更快嗎? 2 ^ 8種可能性非常多,但是再一次,將它分成兩部分,它只有2 ^ 4 = 16個變量。

使數組包含16個“值”,其中每個值都由4個具有正確值的浮點數填充。 然后你的成本只有2 *(從預處理數組復制數據到新數組)。

我不是太深入裝配,但是兩個副本應該比一些循環等更快。

unsigned char myByte; // input byte (pattern to create floats)
float preprocessingArrays[16][4] = {
    { 0.0f, 0.0f, 0.0f, 0.0f }, // 0000
    // ...
    { 1.0f, 1.0f, 1.0f, 1.0f }  // 1111
};

float result[8];
std::memcpy(&result[0], &preprocessingArrays[myByte >> 4][0], 16);
std::memcpy(&result[4], &preprocessingArrays[myByte & 15][0], 16);
// 16 = platform-specific -> floats should be 32bits -> 4bytes * 4 floats = 16

這是從手寫的,但正如你所看到的,我的循環將包含兩個memcpys,一個bitshift和一個二進制AND操作(或者只有一個,但更大,memcpy,如果你想對2 ^ 8值進行預處理)。

對於C(++)只有代碼,我認為這會打敗循環等但匯編程序代碼可能會更快,我不太確定。 也許您可以使用匯編程序執行memcpy操作,並在一次讀取整個4浮點數然后在另一個調用中寫入它。 AVX似乎最多支持16個256位寄存器,所以有可能只計算哪個寄存器(16個可能的值)復制值在哪里,這將非常快。

也不要自己寫這么多代碼,只需制作簡單的程序,為你打印預處理值,復制並粘貼到原始程序:)

循環,條件和通過內存中的實際數組當然不是矢量方式。 所以這是另一個想法,雖然它只在AVX中有點煩人。 因為沒有AVX2你幾乎什么都不用ymm寄存器(無論如何都沒用),只需使用兩個xmm寄存器然后在最后vinsertf128使用高部分來構成整個事物。 只要xmm寄存器上的操作使用VEX編碼指令(因此'v'就在所有內容前面,即使看起來似乎沒必要),這樣混合也是可以的。

無論如何,我們的想法是在每個dword中放置一個字節的副本,並且每個通道使用正確的位並比較形成掩碼。 最后,我們可以進行單個按位AND將掩碼轉換為0f或1f。

所以,首先到處獲取該字節,讓我們說它在eax ,並不重要:

vmovd xmm0, eax
vpshufd xmm0, xmm0, 0

提取正確的位:

vpand xmm0, xmm0, [low_mask]
vpand xmm1, xmm0, [high_mask]

所述掩模是1, 2, 4, 816, 32, 64, 128 (這是在存儲器指令,如果使用_mm_set_epi32它們必須周圍的其他方法)

比較形成面具:

vpxor xmm2, xmm2, xmm2
vpcmpgtd xmm0, xmm0, xmm2
vpcmpgtd xmm1, xmm1, xmm2

合並:

vinsertf128 ymm0, ymm0, xmm1, 1

變成0f或1f:

vandps ymm0, ymm0, [ones]

ones只有1f重復8次。

我不知道這是否更快,但值得一試。 此外,這些都沒有經過測試。

我試圖將它轉換為內在函數,但我不知道我在做什么(它沒有經過測試)。 另外,請注意它使用VEX前綴進行編譯,否則會導致昂貴的模式切換。

// broadcast
__m128i low = _mm_set1_epi32(mask);
__m128i high = _mm_set1_epi32(mask);
// extract bits
low = _mm_and_si128(low, _mm_set_epi32(8, 4, 2, 1));
high = _mm_and_si128(high, _mm_set_epi32(128, 64, 32, 16));
// form masks
low = _mm_cmpgt_epi32(low, _mm_setzero_si128());
high = _mm_cmpgt_epi32(high, _mm_setzero_si128());
// stupid no-op casts
__m256 low2 = _mm256_castps128_ps256(_mm_castsi128_ps(low));
__m128 high2 = _mm_castsi128_ps(high);
// merge
__m256 total = _mm256_insertf128_ps(low2, high2, 1);
// convert to 0f or 1f
total = _mm256_and_ps(total, _mm256_set1_ps(1.0f));

至少使用GCC,可生成OK代碼。 它使用vbroadcastss作為set1 (而不是我使用的vpshufd ),我不確定這個想法有多好(這意味着它必須通過內存反彈int)。

使用AVX2可以更簡單:

__m256i x = _mm256_set1_epi32(mask); 
x = _mm256_and_si256(x, _mm256_set_epi32(128, 64, 32, 16, 8, 4, 2, 1));
x = _mm256_cmpgt_epi32(x, _mm256_setzero_si256());
x = _mm256_and_si256(x, _mm256_set1_epi32(0x3F800000));
return _mm256_castsi256_ps(x);
void byteToFloat(const uint8_t               byteIn, 
                       float *const restrict floatOut)
{
     floatOut[0]=(byteIn&0x01)?1.0f:0.0f;
     floatOut[1]=(byteIn&0x02)?1.0f:0.0f;
     floatOut[2]=(byteIn&0x04)?1.0f:0.0f;
     floatOut[3]=(byteIn&0x08)?1.0f:0.0f;
     floatOut[4]=(byteIn&0x10)?1.0f:0.0f;
     floatOut[5]=(byteIn&0x20)?1.0f:0.0f;
     floatOut[6]=(byteIn&0x40)?1.0f:0.0f;
     floatOut[7]=(byteIn&0x80)?1.0f:0.0f;
}

在Intel和AMD的x86-64架構中,可以通過使用條件移動操作(cmove)來執行分支預測:源操作數根據標志寄存器的值有條件地移動到目標操作數。

http://en.wikipedia.org/wiki/Branch_predication

正如@RippeR所暗示的那樣,索引也是我的第一個猜測。

我的第二個猜測是這樣的:

switch(theChar){
 break; case   0: result[0] = 0; ... result[7] = 0;
 break; case   1: result[0] = 0; ... result[7] = 1;
 ...
 break; case 255: result[0] = 1; ... result[7] = 1;
}

這是羅嗦的代碼,但你可以得到預處理器來幫助你編寫代碼。

這可能更快的原因是開關應該變成跳轉表,並且移動應該很好地優化。

補充:如果您想知道預處理器如何提供幫助,請注意以下事項:

#define FOO(x,i) result[i] = !!((x) & (1<<(i)))
#define BAR(x) break; case x: FOO(x,0);FOO(x,1); ... FOO(x,7)
switch(theChar){
 BAR(0);
 BAR(1);
 ...
 BAR(255);
}

暫無
暫無

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

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