2016-10-19 11 views
9

I次のコードスニペットがあります。だから、最後奇妙なのuint32_tは、配列変換をfloatに

WTF??!! 
magic = 2570980352.0000000000 
magic = 2570980608.0000000000 

:私はMSのVisual Studio 2015の下でそれをコンパイルする場合

#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; 
} 

Iが出力されていることがわかりますがarr_dst要素は前のものとは異なりますが、これらの2つの値は同じ値を変換することで得られ、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では再現されません。何か案は?

+0

私の前のコメントは少し愚かでした。アセンブリを生成して送信できますか?どのような結果が得られますか? – Asu

+0

を試すにはBollingers Answer ARR_SIZEをベクトル化幅偶数に減らします。それが結果を変更するかどうかを確認してください。 – Andreas

+2

@Asu VS2015が出力するもの:https://gist.github.com/senyai/3e4b6a9118418d1536476218459cd12d – Senyai

答えて

2

IMHOは、インテルが提供するブードー・インテルよりもはるかに優れているため、PowerPC imeplementation(Freescale MCP7450)に関する調査を行った。

浮動小数点演算では、FPUとベクトルユニットの浮動小数点演算の丸めが異なる場合があります。 FPUは、4つの丸めモードのうちの1つを使用するように構成できます。最も近い値(デフォルト)に丸め、正の無限大に向かって、負の無限大に向かって切り捨てます。しかし、ベクトルユニットは、特定の丸めルールを持ついくつかの選択命令で最も近いものに丸めることしかできません。 FPUの内部精度は106ビットです。ベクトルユニットはIEEE-754を満たしていますが、ドキュメントにはそれ以上のことは書かれていません。

結果を見ると、変換2570980608は元の整数に近いため、FPUのベクタ単位または異なる丸めモードよりも優れた内部精度が得られます。

+1

それだけです。値は '_control87(_MCW_RC、RC_DOWN);または' _control87(_MCW_RC、_RC_UP); 'を呼び出した後で真に等しい。私はFPU状態を設定するためのベストプラクティスは誰もそれを使用していないように思えます。 – Senyai

+0

@センヤイ:それは何をするのですか?リンクされたasmのベクトル化された(2570980352)変換もスカラー(2570980608)変換も、何もx87を使用しません。それはすべてのSSE数学です。あるいは、 '_control87'もMXCSRの丸めモードを設定していますか? –

+0

@Senyai:丸めモードを設定するための「ベストプラクティス」は、これを最も近い値に丸めた値にすることです(タイブレークでも)。これは、Bruce Dawsonが[浮動小数点決定論とFP設定に関する記事](https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/)で提案したものです。 –

5

MSVC++などの32ビットIEEE-754バイナリ浮動小数点は、精度の小数点以下6桁のみを提供します。あなたの開始値は、そのタイプのの範囲の範囲内にありますが、タイプuint32_tのほとんどの値の場合のように、そのタイプでは正確に表現できないようです。

同時に、x86またはx86_64プロセッサの浮動小数点ユニットは、MSVC++の64ビットdoubleよりも広い表現を使用します。ループが終了した後、最後に計算された配列要素は、拡張精度形式でFPUレジスタに残ります。プログラムは、その値をメモリから読み出すのではなく、レジスタから直接使用することができます。これは、以前の要素で行う必要があります。

プログラムが==の比較を実行する場合、狭い表現をより広く広げることによって比較を実行すると、2つの値は実際には等しくないと見なされる可能性があります。拡張精度からfloatへの往復と精度は失われます。いずれにしても、両方の値はprintf()に渡されるとdoubleに変換されます。実際に彼らが不平等を比較した場合、それらの変換の結果も異なる可能性があります。

私はMSVC++コンパイルオプションではありませんが、この動作を破棄する可能性があります。そのようなオプションは、「厳密な数学」や「厳密なfp」などの名前で時折使用されます。しかし、このようなオプションをオンにする(またはその反対にする)ことは、FP重いプログラムでは非常にコストがかかることに注意してください。

+0

プログラムから出力される2つの値は、最も近い2つのIEEE 32ビット浮動小数点数が元の整数になっているため、適切なトラックにいるはずです。しかし、異なる幅の浮動小数点型であっても、丸めが一貫して起こらないシナリオを想像することはできません。 –

+0

@ MarkRansom、私は矛盾した丸めを示唆していません。私は実際に使用された2つの値は、(コンパイラによって最適化が適用されているために)さまざまな変換シーケンスの結果であり、最初は同じ丸めを期待する理由がないため、矛盾の根拠はありません。報告された結果がそのようなプログラムの動作の結果ではない*シナリオを想像することはできません。 –

+1

印刷された値のいずれかが最初の整数に等しい場合は、その引数を購入しますが、その両方の*は丸められます。したがって、一貫性のない丸めについての私のコメント。一般に、IEEE浮動小数点の丸めは非常に厳密に指定されています。 –

4

unsignedfloatの間の変換は、x86では単純ではありません。 (AVX512まで)それについての単一の指示はありません。一般的な手法は、署名して変換して結果をフィックスアップすることです。これを行うには複数の方法があります。 (this Q&A for some manually-vectorized methods with C intrinsicsを参照)。

MSVCは、最初の128戦略を1つの戦略でベクトル化し、最後のスカラー要素に対して異なる戦略(ベクトル化しない)を使用します。 double、次にdoubleからfloatまでです。


gccとclangは、ベクター化されたスカラー法の結果から2570980608.0を生成します。 2570980608 - 2570980487 = 1212570980487 - 2570980352 = 135(入力/出力の丸めなし)、gccとclangはこの場合丸められた結果を返します(0.5ulp未満の誤差)。それが可能なすべてのuint32_tに当てはまるのであればIDK(但し、そのうち2^32しかありません、we could exhaustively check)。ベクトル化されたループのMSVCの最終結果は0.5ulpよりもわずかに誤差がありますが、スカラー法はこの入力に対して正しく丸められています。

IEEE数学は+-*/sqrtが正しく生成(エラー未満0.5ulp)の結果を丸め、他の機能は(等log)は、このような厳密な要件がないことを要求します。 IDKはint-> float変換のための丸めにどのような要件があるかをIDKがMSVCの場合は厳密に(もしあなたが/fp:fastなどを使わなかったなら)正当なものです。

Bruce DawsonのFloating-Point Determinism blog post(彼の優れたFP数学シリーズの一部)も参照してください。ただし、整数< - > FP変換については言及していません。私たちは、MSVCはをやったOPによってリンクASMで見ることができます


(唯一面白いの指示までストリッピングし、手でコメント)

; 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 = [email protected] (a constant repeated in each of 4 float elements) 


    ####### The vectorized unsigned->float conversion strategy: 
    [email protected]:          ; 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 [email protected] ; }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  [email protected]  ; if(i>=129): always false 

    0007d  sub  edx, ecx  ; edx = 129-128 = 1 

...既知のいくつかは、より多くのばかげ-at-コンパイル時以降のジャンプ...双方向のOで

######## The scalar unsigned->float conversion strategy for the last element 
[email protected]: 
    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 [email protected][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 [email protected] 
    # Yes, this is a loop, which always runs exactly once for the last element 

f比較、clang、gccもコンパイル時にすべてを最適化するわけではありませんが、それらはクリーンアップループを必要とせず、それぞれのループの後に単一のスカラーストアまたは変換を行うだけです。 (あなたが指示しない限り、実際にはすべてが完全に展開されます)

Godbolt compiler explorerのコードを参照してください。

gccは、上下の16b半分を別々に浮動小数点に変換し、それらを65536で乗算して加算します。

Clangのunsigned - >float変換戦略は面白いです:それは決してcvt命令を使用しません。私は、符号なし整数の2つの16ビット半分を2つの浮動小数点の仮数に直接埋め込むと思う(指数を設定するいくつかのトリックで(ビット単位のブール値とADDPS)、gccのように下半分と上半分を一緒に追加する。

もちろん、64ビットコードにコンパイルする場合、スカラー変換ではuint32_tを64ビットにゼロ拡張し、署名付きint64_tとしてfloatに変換できます。 signed int64_tはuint32_tのすべての値を表すことができ、x86は64ビット符号付きintを効率的にfloatに変換できます。しかし、それはベクトル化しません。

+1

確かにgccは、すべての可能なuint32_tに対して正確に丸められた結果を生成します(すべてチェックする必要はありません)。丸めは高および低16ビットの半分の最後の追加でのみ行われるため、こちらもご覧ください[http:// stackoverflow .com/a/40766669/2439725)。この加算は、IEEE-754標準に従って0.5 ULP以内である。 – wim

関連する問題