[英]What is the fastest way to store and access single bits in an N dimensional array of bits in c++?
[英]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, 8
和16, 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)來執行分支預測:源操作數根據標志寄存器的值有條件地移動到目標操作數。
正如@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.