[英]Strange uint32_t to float array conversion
我有以下代碼片段:
#include <cstdio>
#include <cstdint>
static const size_t ARR_SIZE = 129;
int main()
{
uint32_t value = 2570980487;
uint32_t arr[ARR_SIZE];
for (int x = 0; x < ARR_SIZE; ++x)
arr[x] = value;
float arr_dst[ARR_SIZE];
for (int x = 0; x < ARR_SIZE; ++x)
{
arr_dst[x] = static_cast<float>(arr[x]);
}
printf("%s\n", arr_dst[ARR_SIZE - 1] == arr_dst[ARR_SIZE - 2] ? "OK" : "WTF??!!");
printf("magic = %0.10f\n", arr_dst[ARR_SIZE - 2]);
printf("magic = %0.10f\n", arr_dst[ARR_SIZE - 1]);
return 0;
}
如果我在MS Visual Studio 2015下編譯它,我可以看到輸出是:
WTF??!!
magic = 2570980352.0000000000
magic = 2570980608.0000000000
所以最后一個arr_dst
元素與前一個元素不同,但這兩個值是通過轉換相同的值來獲得的,該值填充了arr數組! 這是一個錯誤嗎?
我注意到如果我以下面的方式修改轉換循環,我得到“OK”結果:
for (int x = 0; x < ARR_SIZE; ++x)
{
if (x == 0)
x = 0;
arr_dst[x] = static_cast<float>(arr[x]);
}
所以這可能是矢量化優化的一些問題。
此行為不會在gcc 4.8上重現。 有任何想法嗎?
32位IEEE-754二進制浮點數(如MSVC ++使用)僅提供6-7個十進制數字的精度。 您的起始值完全在該類型的范圍內 ,但似乎不能完全表示該類型,因為大多數類型為uint32_t
的情況確實如此。
與此同時,x86或x86_64處理器的浮點單元使用比MSVC ++的64位double
精度更寬的表示。 似乎在循環退出后,最后計算的數組元素以其擴展精度形式保留在FPU寄存器中。 然后,程序可以直接從寄存器中使用該值,而不是從存儲器中讀取它,這有必要對先前的元素進行讀取。
如果程序通過將較窄的表示推廣到更寬的而不是相反的方式來執行==
比較,那么這兩個值可能確實比較不相等,因為從擴展精度到float
和返回的往返失去了精度。 無論如何,當傳遞給printf()
時,兩個值都轉換為double
類型; 如果他們確實比較了不平等,那么這些轉換的結果也可能不同。
我沒有使用MSVC ++編譯選項,但很可能有一個可以解決這種行為。 這些選項有時會使用諸如“嚴格數學”或“嚴格fp”之類的名稱。 但請注意,在FP重型程序中打開這樣的選項(或關閉其相反的選項)可能會非常昂貴。
在x86上, unsigned
和float
之間的轉換並不簡單; 它沒有單一指令(直到AVX512)。 一種常見的技術是轉換為簽名然后修復結果。 有多種方法可以做到這一點。 (有關使用C內在函數的一些手動矢量化方法,請參閱此問答 ,並非所有方法都具有完美的舍入結果。)
MSVC使用一個策略對前128個進行矢量化,然后對最后一個標量元素使用不同的策略(不會向量化),包括轉換為double
,然后從double
轉換為float
。
gcc和clang從它們的矢量化和標量方法產生2570980608.0
結果。 2570980608 - 2570980487 = 121
,和2570980487 - 2570980352 = 135
(沒有輸入/輸出的舍入),因此gcc和clang在這種情況下產生正確的舍入結果(小於0.5ulp的錯誤)。 IDK如果對於每個可能的uint32_t都是如此(但是它們只有2 ^ 32, 我們可以詳盡地檢查 )。 MSVC的向量化循環的最終結果略有超過0.5ulp的誤差,但標量方法正確舍入為此輸入。
IEEE數學要求+
-
*
/
和sqrt
產生正確的舍入結果(小於0.5ulp的錯誤),但其他函數(如log
)沒有這么嚴格的要求。 IDK對於int-> float轉換的舍入要求是什么,所以IDK如果MSVC做什么是嚴格合法的(如果你沒有使用/fp:fast
或任何東西)。
另請參閱Bruce Dawson的浮點確定性博客文章 (他關於FP數學的優秀系列的一部分),盡管他沒有提到整數< - > FP轉換。
我們可以在OP中看到,由MSVC做了什么(只刪除了有趣的指令並手工評論) :
; Function compile flags: /Ogtp
# assembler macro constants
_arr_dst$ = -1040 ; size = 516
_arr$ = -520 ; size = 516
_main PROC ; COMDAT
00013 mov edx, 129
00018 mov eax, -1723986809 ; this is your unsigned 2570980487
0001d mov ecx, edx
00023 lea edi, DWORD PTR _arr$[esp+1088] ; edi=arr
0002a rep stosd ; memset in chunks of 4B
# arr[0..128] = 2570980487 at this point
0002c xor ecx, ecx ; i = 0
# xmm2 = 0.0 in each element (i.e. all-zero)
# xmm3 = __xmm@4f8000004f8000004f8000004f800000 (a constant repeated in each of 4 float elements)
####### The vectorized unsigned->float conversion strategy:
$LL7@main: ; do{
00030 movups xmm0, XMMWORD PTR _arr$[esp+ecx*4+1088] ; load 4 uint32_t
00038 cvtdq2ps xmm1, xmm0 ; SIGNED int to Single-precision float
0003b movaps xmm0, xmm1
0003e cmpltps xmm0, xmm2 ; xmm0 = (xmm0 < 0.0)
00042 andps xmm0, xmm3 ; mask the magic constant
00045 addps xmm0, xmm1 ; x += (x<0.0) ? magic_constant : 0.0f;
# There's no instruction for converting from unsigned to float, so compilers use inconvenient techniques like this to correct the result of converting as signed.
00048 movups XMMWORD PTR _arr_dst$[esp+ecx*4+1088], xmm0 ; store 4 floats to arr_dst
; and repeat the same thing again, with addresses that are 16B higher (+1104)
; i.e. this loop is unrolled by two
0006a add ecx, 8 ; i+=8 (two vectors of 4 elements)
0006d cmp ecx, 128
00073 jb SHORT $LL7@main ; }while(i<128)
#### End of vectorized loop
# and then IDK what MSVC smoking; both these values are known at compile time. Is /Ogtp not full optimization?
# I don't see a branch target that would let execution reach this code
# other than by falling out of the loop that ends with ecx=128
00075 cmp ecx, edx
00077 jae $LN21@main ; if(i>=129): always false
0007d sub edx, ecx ; edx = 129-128 = 1
......一些更為荒謬的已知編譯時間后來跳...
######## The scalar unsigned->float conversion strategy for the last element
$LC15@main:
00140 mov eax, DWORD PTR _arr$[esp+ecx*4+1088]
00147 movd xmm0, eax
# eax = xmm0[0] = arr[128]
0014b cvtdq2pd xmm0, xmm0 ; convert the last element TO DOUBLE
0014f shr eax, 31 ; shift the sign bit to bit 1, so eax = 0 or 1
; then eax indexes a 16B constant, selecting either 0 or 0x41f0... (as whatever double that represents)
00152 addsd xmm0, QWORD PTR __xmm@41f00000000000000000000000000000[eax*8]
0015b cvtpd2ps xmm0, xmm0 ; double -> float
0015f movss DWORD PTR _arr_dst$[esp+ecx*4+1088], xmm0 ; and store it
00165 inc ecx ; ++i;
00166 cmp ecx, 129 ; } while(i<129)
0016c jb SHORT $LC15@main
# Yes, this is a loop, which always runs exactly once for the last element
作為比較,clang和gcc也沒有在編譯時優化整個事情,但是他們確實意識到他們不需要清理循環 ,只需要執行單個標量存儲或在相應的循環之后進行轉換。 (clang實際上完全展開了一切,除非你告訴它不要。)
請參閱Godbolt編譯器資源管理器中的代碼。
gcc只是將上半部分和下半部分分別轉換為浮動,並將它們與乘以65536並加上相加。
Clang的unsigned
- > float
轉換策略很有意思:它從不使用cvt
指令。 我認為它將無符號整數的兩個16位半部分直接填充到兩個浮點數的尾數中(使用一些技巧來設置指數(按位布爾值和ADDPS),然后像gcc那樣將低和高一半加在一起。
當然,如果編譯為64位代碼,標量轉換只需將uint32_t
零擴展為64位,並將其轉換為帶符號的int64_t為float。 有符號的int64_t可以表示uint32_t的每個值,x86可以將64位有符號的int轉換為有效的float。 但這並沒有矢量化。
我對PowerPC imeplementation(飛思卡爾MCP7450)進行了調查,因為他們恕我直言的故事要比英特爾提出的任何伏都教都要好得多。
事實證明浮點單元,FPU和向量單元可能具有不同的浮點運算舍入。 FPU可以配置為使用四種舍入模式之一; 舍入到最接近(默認),截斷,朝向正無窮大並朝向負無窮大。 然而,矢量單元僅能夠舍入到最近,具有一些具有特定舍入規則的選擇指令。 FPU的內部精度為106位。 矢量單元符合IEEE-754標准,但文檔沒有更多說明。
查看結果,轉換2570980608更接近原始整數,表明FPU具有比向量單元或不同舍入模式更好的內部精度。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.