Win32のunsignedintへのダブルキャストは2,147,483,648に切り捨てられます


85

次のコードをコンパイルします。

出力(MSVC x86):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649

出力(MSVC x64):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649

ではMicrosoftのドキュメントからの変換に最大値符号付き整数には言及がないdoubleのはunsigned int

上記のすべての値は、関数が返さINT_MAXれる2147483648ときに切り捨てられます。

Visual Studio2019を使用してプログラムをビルドしています。これはgccでは発生しません。

私は何か間違ったことをしていますか?変換する安全な方法があるdoubleのではunsigned int


24
いいえ、何も悪いことはしていません(おそらく、Microsoftの「C」コンパイラを使おうとしている以外に)
AnttiHaapala20年

5
VS2017v15.9.18およびVS2019v16.4.1でテストされたmymachine™で動作します。[ヘルプ]> [フィードバックの送信]> [バグの報告]を使用して、バージョンについて知らせます。
ハンスパッサント

5
再現できますが、OPと同じ結果になります。VS201916.7.3。
anastaciu

2
@EricPostpischil確かに、それはINT_MIN
AnttiHaapala20年

回答:


70

コンパイラのバグ...

@anastaciuによって提供されたアセンブリから、直接キャストコードが呼び出します__ftol2_sse。これは、数値を符号付きlongに変換するようです。ルーチン名はftol2_sse、これがsse対応のマシンであるためですが、floatはx87浮動小数点レジスタにあります。

; Line 17
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

一方、間接キャストは

; Line 18
    call    _getDouble
    fstp    QWORD PTR _d$[ebp]
; Line 19
    movsd   xmm0, QWORD PTR _d$[ebp]
    call    __dtoui3
    push    eax
    push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

これは、double値をポップしてローカル変数に格納し、それをSSEレジスタにロードして__dtoui3、doubleからunsignedintへの変換ルーチンを呼び出します。

ダイレクトキャストの動作はC89に準拠していません。また、それ以降のリビジョンにも準拠していません。C89でさえ、次のように明示的に述べています。

整数型の値が符号なし型に変換されるときに実行される残りの操作は、浮動型の値が符号なし型に変換されるときに実行する必要はありません。したがって、移植可能な値の範囲は[0、Utype_MAX + 1)です。


問題は2005年からの継続である可能性があると思います-__ftol2おそらくこのコードで機能する変換関数が呼び出されていました。つまり、値を符号付き数値-2147483647に変換し、正しい値を生成していました。符号なし数値を解釈した場合の結果。

残念ながら__ftol2_sse、のドロップイン置換ではありません。__ftol2これは、最下位の値ビットをそのまま取得するのではなく、LONG_MIN/を返すことによって範囲外のエラーを通知します0x80000000。これは、ここではunsignedlongとして解釈されます。期待されていたすべて。doubleの値>への変換は未定義の動作になる__ftol2_sseためsigned long、の動作はに対して有効です。LONG_MAXsigned long


23

@AnttiHaapalaの回答に続いて、最適化/Oxを使用してコードをテストしたところ、__ftol2_sse使用されなくなったバグが削除されることがわかりました。

最適化getdouble()により、定数式の評価がインライン化および追加されたため、実行時に変換する必要がなくなり、バグがなくなりました。

好奇心から、さらにいくつかのテストを行いました。つまり、実行時にfloatからintへの変換を強制するようにコードを変更しました。この場合、結果は依然として正しいので、コンパイラーは最適化を使用して__dtoui3、両方の変換で使用します。

ただし、インライン化を防ぐ__declspec(noinline) double getDouble(){...}と、バグが元に戻ります。

__ftol2_sseは両方の変換で呼び出され2147483648、両方の状況で出力を行います。@ zwolの疑いは正しかったです。


コンパイルの詳細:

  • コマンドラインの使用:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • Visual Studioの場合:

    • 基本ランタイムチェックを無効にRTCしてデフォルトにProject -> Properties -> Code Generation設定します

    • で最適化を有効にしProject -> Properties -> Optimization最適化/ Oxに設定します。

    • デバッガーがx86モードの場合。


5
「最適化を有効にすると、未定義の動作は実際には未定義になります」のように面白いです=>コードは実際に正しく機能します:F
AnttiHaapala20年

3
@AnttiHaapala、はい、はい、Microsoftは最高です。
anastaciu

1
適用された最適化は、インライン化とその後の定数式の評価でした。実行時にfloatからintへの変換を実行しなくなりました。強制的getDoubleに行を外したり、コンパイラが定数であることを証明できない値を返すように変更したりすると、バグが再発するのではないかと思います。
zwol

1
@zwol、あなたは正しかった、オフラインを強制し、絶え間ない評価を防ぐことはバグを取り戻すだろうが、今回は両方の変換で。
anastaciu

7

誰もMSのasmを見たことがありません__ftol2_sse

結果から、安全にではなく、おそらくx87からsigned int/ long(Windowsでは両方とも32ビットタイプ)に変換されたと推測できuint32_tます。

x86 FP->整数の結果をオーバーフローする整数命令は、単に折り返し/切り捨てるだけではありません。正確な値が宛先で表現できない場合、Intelが「整数不定」と呼ぶものを生成します。上位ビットが設定され、他のビットはクリアされます。すなわち0x80000000

(または、FPの無効な例外がマスクされていない場合、起動して値は保存されません。ただし、デフォルトのFP環境では、すべてのFP例外がマスクされます。そのため、FP計算では、障害ではなくNaNを取得できます。)

これには、fistp(現在の丸めモードを使用する)のようなx87命令と(cvttsd2si eax, xmm00への切り捨てを使用する)のようなSSE2命令の両方が含まれますt

したがって、コンパイルdouble->unsignedへの呼び出しへの変換はバグ__ftol2_sseです。


サイドノート/タンジェント:

x86-64では、FP-> uint32_tをコンパイルしcvttsd2si rax, xmm0て、64ビットの符号付き宛先に変換し、整数の宛先の下半分(EAX)に必要なuint32_tを生成できます。

結果が0..2 ^ 32-1の範囲外の場合は、CおよびC ++ UBであるため、正または負の値が大きいと、整数の不定ビットパターンからRAX(EAX)の下半分がゼロのままになることは問題ありません。(整数->整数変換とは異なり、値のモジュロ削減は保証されません。 負のdoubleをunsigned intにキャストする動作は、C標準で定義されていますか?ARMとx86で異なる動作です。明確にするために、問題はありません。は未定義または実装定義の動作です。FP-> int64_tがある場合は、それを使用してFP-> uint32_tを効率的に実装できることを指摘しておきます。これにはx87が含まれます。fistp 64ビットモードで64ビット整数のみを直接処理できるSSE2命令とは異なり、32ビットおよび16ビットモードでも64ビット整数の宛先を書き込むことができます。


1
私はそのコードを調べたくなりますが、幸いなことに私はMSVCを持っていません...:D
AnttiHaapala20年

@AnttiHaapala:うん、どちらも行うI
ピーター・コルド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.