Seite 1 von 1

[Visual C++] /d2noftol3 & dtol3()

Verfasst: 20.12.2017, 00:24
von Krishty
Ich bin gestern drauf gestoßen und verstehe selber nicht recht, was vorgeht, darum sammle ich alles hier:
  • Visual C++ 2013 hat etwas an der Art geändert, wie float zu int konvertiert wird.
  • Diese Konvertierungsfunktionen werden vom Compiler erzeugt. Sie liegen nicht der CRT o.ä. bei und der Quelltext ist nicht einsehbar. Solche Compiler-internen Funktionen (allen voran __chkstk()) werden oft auch direkt in Assembler geschrieben.
  • Als eine der Konvertierungsfunktionen für Visual 2013 überarbeitet wurde, hat sich ein Fehler eingeschlichen, der den Floating-Point-Stack ruinieren kann. Siehe Ticket 824658 – cast double to unsigned int cause FPU stack overflow on c++ code compiled with VS2013 x86 toolset - by Cliff Ding.
    Charles Fu hat geschrieben:We have gotten several reports on this bug, and we have fixed it.
  • Betroffen ist ausschließlich x86-32 mit SSE2. Auf anderen Plattformen und ohne SSE2 wird die neue Konvertierung nicht benutzt.
  • Als Workaround wird ein versteckter Compiler-Parameter empfohlen:
    Charles Fu hat geschrieben:As a workaround, if you know which file causes this FP stack overflow, you can compile it with a hidden switch /d2noftol3.
  • Dieser Parameter funktioniert auch mit dem aktuellen Visual C++ 2017.
  • Das ist insofern interessant, als dass man nun direkt die Auswirkungen auf den Code messen kann. Und das habe ich getan.
  • Visual C++ 2017 mit /d2noftol3 (alte Konvertierung) baut in meine Anwendung folgende Symbole ein:

    Code: Alles auswählen

    	code
    _ftol2_.obj _ftol2           117 B
    _ftol2_.obj _ftol2_sse_excpt  26 B
    _ftol2_.obj _ftol2_pentium4   19 B
    _ftol2_.obj _ftol2_sse         9 B
    in Standardeinstellung (neue Konvertierung)

    Code: Alles auswählen

    	code
    _ftol3_.obj _dtol3_work     229 B
    _ftol3_.obj _ftol3_common   194 B
    _ftol3_.obj _ftol3_work     134 B
    _ftol3_.obj _ftol3_except    68 B
    _ftol3_.obj _ultod3          55 B
    _ftol3_.obj _ltod3           39 B
    _ftol3_.obj _dtol3_NaN       29 B
    _ftol3_.obj _ftol3_NaN       24 B
    _ftol3_.obj _ftol3_arg_error 18 B
    _ftol3_.obj _dtoul3          15 B
    _ftol3_.obj _dtoui3          15 B
    _ftol3_.obj _ftoul3          15 B
    _ftol3_.obj _ftoui3          15 B
    _ftol3_.obj _ftol3           13 B
    _ftol3_.obj _dtol3           13 B
    	data
    _ftol3_.obj _Int32ToUInt32    8 B
    _ftol3_.obj _DP2to32          8 B
    _ftol3_.obj _MantissaMask     8 B
    _ftol3_.obj _IntegerBit       8 B
    _ftol3_.obj _SignBit64        8 B
    _ftol3_.obj _MinFP64          8 B
    _ftol3_.obj _MinInt64         8 B
    _ftol3_.obj _MaxInt64         8 B
    _ftol3_.obj _MaxUInt64        8 B
    _ftol3_.obj _MaxFP32          8 B
    _ftol3_.obj _MinFP32          8 B
    _ftol3_.obj _MinSubInexact    8 B
    _ftol3_.obj _FpExcptTable     8 B
    _ftol3_.obj _i1075            4 B
    _ftol3_.obj _i1087            4 B
    _ftol3_.obj _x0800            4 B
    _ftol3_.obj _x17FF            4 B
    _ftol3_.obj _CWMask           2 B
  • Erstmal scheint die alte version sowohl SSE2 als auch SSE und CPUs ohne SSE zu unterstützen. Die neue Version wird allerdings nur mit SSE2 aktiv.
  • Dann sticht ins Auge, dass die neue Version fast fünf Mal so groß ist wie die alte (177 B gegenüber fast 1 KiB). Das geht damit einher, dass meine Programme auf x86-32-SSE2 rund 1 KiB kleiner werden, wenn ich /d2noftol3 nutze.
Mir stellt sich daher die Frage, was zur Hölle sie da geändert haben, dass die Konvertierung derart in Größe und Komplexität explodiert ist.
  • Rein vom Bauchgefühl und den spärlichen Symbolinformationen her würde ich auch schätzen, dass ftol3() C++ ist, während ftol2() noch Assembler war.
  • Die neue Version scheint double zu unterstützen (_SignBit64, _MantissaMask), obwohl mein Programm keine doubles nutzt. Das schlägt natürlich gut ein.
Wenn ich mehr herausgefunden habe, schreibe ich’s hier.

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 20.12.2017, 00:28
von xq
Ich kann zwar nix beisteuern, möchte aber durchaus interesse an einer Aufklärung anmelden. Bin gespannt!

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 02.10.2018, 23:43
von Krishty
Aaaalso …

Die Funktion ist – wie erwartet – dafür da, auf 32-Bit-x86 float zu unsigned int zu konvertieren. Allerdings nicht zu signed int, denn dafür nutzt VC den SSE2-Befehl CVTTSS2SI (für Normalsterbliche erreichbar als _mm_cvttss_si32()).

Visual C++ erzeugt jedes Mal einen Aufruf, wenn ihr einen Cast wie bspw. (uint32_t)float_var verbaut.

Die alte Version der Funktion – _ftol2() – hat drei verschiedene Implementierungen: x486 (nutzt die FPU), SSE2, und noch irgendwas. Ich vermute, dass irgendwo der Wurm drin war und sie deshalb ersetzt wurde. Aus der Clang-Mailing List kann ich z. B. herleiten, dass sie keine Calling Convention hat, sondern einfach direkt kreuz und quer auf Register des Aufrufers zurückgreift. Ich vermute, dass es Probleme mit dem Control Word gab.

Für Visual Studio 2013 wurde alles refactored, und nun haben wir _ftol3(). Das ist riesig groß, teilt seinen Code mit _dtol() (für double), und ich habe keine Ahnung, was es noch alles tut. Ist mir auch egal.

Um das loszuwerden:
  • Castet float niemals zu unsigned int, sondern besser zu signed int.
  • Falls kein Weg daran vorbeiführt, kommt ihr möglicherweise mit reduziertem Wertebereich aus. Es gibt z. B. hier einen tollen Trick, wie ihr auf 32-Bit-CPUs ein float oder double zu 52-Bit-int casten könnt, unsigned oder signed.
Auf 64-Bit x86 wird die Funktion niemals genutzt, denn dort kann die Hardware CVTTSS2SI mit 64-Bit-Integern (_mm_cvttss_si64() oder _mm_cvttss_si64x()).

Meine eigene Version hat mir ziemlich genau 512 B Befehle gegenüber _ftol2() gespart, also über 1000 B gegenüber _ftol3(). Und deutlich schneller ist der Code jetzt auch noch.

Ich hätte ja gern die Implementierung von Clang oder GCC gesehen, aber Godbolt kann leider nur 64-Bit.

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 00:33
von Spiele Programmierer
Godbolt kann 32 bit, einfach -m32 machen. ;)
https://gcc.godbolt.org/z/KDNROV

Clang macht cvttss2si zweimal: Einmal für den Fall dass es ganz normal geht für 0 - 0x7fffffff und ein zweites mal, einfach Verschoben, falls die Zahl in den Bereich 0x80000000 - 0xffffffff fällt und wählt dann das Ergebnis aus. Zählen tue ich 40 Bytes für den eigentlichen Cast, egal ob -O3 oder -Oz.
GCC macht irgendwas mit legacy x87 Befehlen, aber es scheint wenigstens nicht recht viel länger zu sein.
Da kann ich mich nur wundern was der MSVC da wieder treibt?

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 00:59
von Krishty
Spiele Programmierer hat geschrieben:Godbolt kann 32 bit, einfach -m32 machen. ;)
Uuuuugh stimmt; Brainfart! Dann habe ich ja morgen was Schönes zu analysieren; danke :)

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 15:12
von Krishty
Okay; ich hab’s durch.

Clangs float→long ist auf x86-32 tatsächlich optimal. Weil ich neben Truncation auch Rounding brauche, hier mein Pseudocode für das gleiche unter Visual C++:

Code: Alles auswählen

SInt4B roundedToSInt4B(float const x) {
	return _mm_cvt_ss2si(_mm_set_ps1(x));
}

UInt4B roundedToUInt4B(float const x) {
	if(x < 2'147'483'648.0f) {
		return roundedToSInt4B(x);
	} else {
		return UInt4B(roundedToSInt4B(x - 2'147'483'648.0f)) ^ 0x8000'0000;
	}
}
Für 64-Bit-Integer kommt man auf x86-32 leider nicht um die FPU herum; weder auf Clang noch auf GCC noch sonstwo:

Code: Alles auswählen

// Rounds to the nearest integer.
//  • must be in [-9 223 372 036 854 775 808, +9 223 371 487 098 961 920] range
//    – deviation from 8-B integer limits is due to floating-point accuracy
PURE_FUNCTION SInt8B roundedToSInt8B(float value) {
	MUST(-9'223'372'036'854'775'808.0f <= value && value <= +9'223'371'487'098'961'920.0f);

#	if COMPILED_FOR_X86_64

		// Use x64’s CVTSS2SI instruction.
#		if COMPILED_BY_VISUAL_CPP
			return _mm_cvtss_si64x(_mm_set_ss(value));
#		elif COMPILED_BY_CLANG || COMPILED_BY_GCC
			return __builtin_ia32_cvtss2si64((floatx4){ value, 0, 0, 0 });
#		endif

#	elif COMPILED_FOR_X86_32

		// No SSE2 hardware support – just the FPU’s FISTP instruction:
		// • on all compilers, “llroundf()” goes straight to CRT/libc, which we don’t use
		// • hand-written functions cause weird code generation
		//   – “_ftol3()” on Visual C++ (almost 600 B of code; custom calling convention) – avoid at any cost
		//   – FPU control word weirdness on Clang/GCC
		// • inline assembly is the only solution
#		if COMPILED_BY_VISUAL_CPP

			SInt8B result;
			__asm {
				fld value
				fistp result
			}
			return result;

#		elif COMPILED_BY_CLANG || COMPILED_BY_GCC

			SInt8B result;
			__asm__("fistpq %0" : "=m"(result) : "t"(value) : "st"); // store output in memory; read input to FPU registers (“st”)
			return result;

#		endif

#	endif
}

// Rounds to the nearest integer.
//  • must be in [-9 223 372 036 854 775 808, +9 223 372 036 854 774 784] range
//    – deviation from 8-B integer limits is due to floating-point accuracy
PURE_FUNCTION SInt8B roundedToSInt8B(double value) {
	MUST(-9'223'372'036'854'775'808.0 <= value && value <= 9'223'372'036'854'774'784.0);

#	if COMPILED_FOR_X86_64

		// Use x64’s CVTSD2SI instruction.
#		if COMPILED_BY_VISUAL_CPP
			return _mm_cvtsd_si64x(_mm_set_sd(value));
#		elif COMPILED_BY_CLANG || COMPILED_BY_GCC
			return __builtin_ia32_cvtsd2si64((doublex2){ value, 0 });
#		endif

#	elif COMPILED_FOR_X86_32

		// No SSE2 hardware support – just the FPU’s FISTP instruction:
		// • on all compilers, “llround()” goes straight to CRT/libc, which we don’t use
		// • hand-written functions cause weird code generation
		//   – “_ftol3()” on Visual C++ (almost 600 B of code; custom calling convention) – avoid at any cost
		//   – FPU control word weirdness on Clang/GCC
		// • inline assembly is the only solution
#		if COMPILED_BY_VISUAL_CPP

			SInt8B result;
			__asm {
				fld value
				fistp result
			}
			return result;

#		elif COMPILED_BY_CLANG || COMPILED_BY_GCC

			SInt8B result;
			__asm__("fistpq %0" : "=m"(result) : "t"(value) : "st"); // store output in memory; read input to FPU registers (“st”)
			return result;

#		endif

#	endif
}
Beachtet, dass die Behandlung von nicht darstellbaren Werten (zu groß/klein; INF, NaN) nicht definiert ist. Das dürfte bei C++-Casts aber genauso sein.

Gegenüber der Version von gestern spart mir das nochmal 16 B Befehle und 16 B Konstanten im Viewer :)

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 18:59
von Spiele Programmierer
Ob es eine Möglichkeit gibt, _ftol3 mit einer eigenen Version zu ersetzen?
Diese Details finde ich ja immer besonders. interessant.

Ich habe jetzt nochmal ein wenig darüber nachgedacht, und einen neuen Vorschlag (ohne x87):

Code: Alles auswählen

#include <emmintrin.h>

unsigned int roundedToUInt4B(double f)
{
    return (unsigned int)_mm_cvtsi128_si32(_mm_castpd_si128(_mm_set_sd(f + (1ull << 52))));
    // Portable code but generates much worse assembly on Clang at least:
    //   union { double flt; unsigned int i; } conv;
    //   conv.flt = f + (1ull << 52);
    //   return conv.i;
}
(Godbolt Link)
Mit nur 16 Bytes dürfte das schwer zu toppen sein. Sollte eigentlich sogar bezüglich dem Rounding Mode stimmen, die Addition sollte ja nach dem aktuellen Mode runden.
Wenn man Werte außerhalb des gültigen Bereichs nimmt, kommen schwachsinnige Ergebnisse raus, aber das ist wohl eh Undefined Behaviour.

Ich habe jetzt nach dem gleichen Prinzip auch noch schnell versucht eine 64 Bit (uint64_t) Variante zu machen, aber zumindest im Punkto Codesize kann die mit über 100 Bytes den x87 Code wohl leider nicht schlagen:

Code: Alles auswählen

#include <emmintrin.h>

unsigned long long roundedToUInt8B_SSE2(double flt)
{
    const __m128d input = _mm_set_sd(flt);
    const __m128d inputIsLowMask = _mm_cmplt_sd(input, _mm_set1_pd(1ull << 52));
    const __m128d inputAdjustedIfSmall = _mm_add_sd(input,
        _mm_and_pd(inputIsLowMask, _mm_set1_pd(1ull << 52))); // Move integer into mantissa (and round) by adding.
    const __m128i mantissa = _mm_and_si128( // Extract mantissa
        _mm_castpd_si128(inputAdjustedIfSmall),
        _mm_set_epi32(0, 0, 0x000fffff, 0xffffffff));
    const __m128i actualMantissa = _mm_or_si128(mantissa,
        _mm_andnot_si128(_mm_castpd_si128(inputIsLowMask),
        _mm_set_epi32(0, 0, 0x00100000, 0))); // Add the hidden bit if we need it.
    const __m128i exponent = _mm_sub_epi16(
        _mm_srli_epi64(_mm_castpd_si128(inputAdjustedIfSmall), 52),
        _mm_set_epi16(0, 0, 0, 0, 0, 0, 0, 0x433));
    const __m128i result = _mm_sll_epi64(actualMantissa, exponent);
    return
        (((unsigned long long)(unsigned int)_mm_cvtsi128_si32(result))) |
        (((unsigned long long)(unsigned int)_mm_cvtsi128_si32(_mm_srli_si128(result, 4))) << 32);
}

unsigned long long roundedToUInt8B_Portable(double f)
{
    bool low = f < (1ull << 52);
    union { double flt; unsigned long long i; } conv;
    conv.flt = f + (low ? (1ull << 52) : 0); // Move integer into mantissa (and round) by adding.
    const unsigned long long mantissa = conv.i & ((1ull << 52) - 1); // Extract mantissa
    const int exponent = ((conv.i >> 52) - 0x433);
    const unsigned long long actualMantissa = mantissa | (low ? 0 : (1ull << 52)); // Add the hidden bit if we need it.
    const unsigned long long result = actualMantissa << (exponent & 31);
    return result;
}
(Godbolt Link)

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 19:54
von Krishty
Spiele Programmierer hat geschrieben:Ob es eine Möglichkeit gibt, _ftol3 mit einer eigenen Version zu ersetzen?
Diese Details finde ich ja immer besonders. interessant.
Ja, die Möglichkeit gibt es zweifellos. Du musst aber Assembler benutzen, weil meines Wissens nach die Calling Convention nicht standardisiert ist (dazu müsste es in der Clang-Mailinglist eine Diskussion geben). Ich habe auch eigene Assembler-Versionen von Stack Probes usw.; würde bei ftol3() aber davon abraten, weil sich der Compiler möglicherweise auf unbekannte Annahmen stützt (bzgl. Floating Point Exceptions, NaN-Handling, usw. – weißt du wirklich, was den Visual C++-Entwicklern dabei durch den Kopf ging?) …
Ich habe jetzt nochmal ein wenig darüber nachgedacht, und einen neuen Vorschlag (ohne x87):
Ja; wirklich gut – den hatte ich bis gestern abend für die Konvertierung zu 64-Bit-Integer. Ich vergleiche das nachher nochmal …
Mit nur 16 Bytes dürfte das schwer zu toppen sein.
*hust* Plus 16 Bytes Konstanten *hust*
Sollte eigentlich sogar bezüglich dem Rounding Mode stimmen, die Addition sollte ja nach dem aktuellen Mode runden.
Japp!
Wenn man Werte außerhalb des gültigen Bereichs nimmt, kommen schwachsinnige Ergebnisse raus, aber das ist wohl eh Undefined Behaviour.
Exakt.
Ich habe jetzt nach dem gleichen Prinzip auch noch schnell versucht eine 64 Bit (uint64_t) Variante zu machen, aber zumindest im Punkto Codesize kann die mit über 100 Bytes den x87 Code wohl leider nicht schlagen:
Ohne das im Detail analysiert zu haben, klingen die Konstanten stark nach denen, die _ftol3() in meine Programme gepackt hatte … ich schätze, dass Microsoft das ähnlich (nur schlechter optimiert) gelöst hatte …

Re: [Visual C++] /d2noftol3 & dtol3()

Verfasst: 03.10.2018, 21:54
von Krishty
Ähm … nur zur Info … GCC 8 übersetzt deine Intrinsics zurück zu FPU-Code und macht dabei einen Memory Roundtrip :D :D :D Das hätte ich eigentlich eher von Visual C++ 2010 erwartet.

Visual C++ wiederum macht ein MOVAPS zu viel, aber … pfeif’ drauf.

Dass die union portabel ist, halte ich auch für einen landläufigen Irrtum. Der portable Weg dürfte memcpy() sein.

Nun aber zum Guten: Deine Konvertierungen sind deutlich kompakter. Wieder 64 Bytes gepurztelt – danke!

Hier noch meine Unit Tests für alle, die sich ebenfalls dran versuchen wollen:

Code: Alles auswählen

static void test_roundedToSInt4B_float() {
	FAIL_IF(             0 != roundedToSInt4B(             0.0f));
	FAIL_IF(             0 != roundedToSInt4B(            -0.0f));
	FAIL_IF(             0 != roundedToSInt4B(             0.49f));
	FAIL_IF(             0 != roundedToSInt4B(            -0.49f));
	FAIL_IF(             1 != roundedToSInt4B(             1.49f));
	FAIL_IF(            -1 != roundedToSInt4B(            -1.49f));
	FAIL_IF(           123 != roundedToSInt4B(           123.49f));
	FAIL_IF(          -123 != roundedToSInt4B(          -123.49f));
	FAIL_IF(    16'777'215 != roundedToSInt4B(    16'777'215.0f));
	FAIL_IF(   -16'777'215 != roundedToSInt4B(   -16'777'215.0f));
	FAIL_IF(    16'777'216 != roundedToSInt4B(    16'777'216.0f)); // last int to be represented properly
	FAIL_IF(   -16'777'216 != roundedToSInt4B(   -16'777'216.0f)); // last int to be represented properly
	FAIL_IF(    16'777'218 != roundedToSInt4B(    16'777'218.0f));
	FAIL_IF(   -16'777'218 != roundedToSInt4B(   -16'777'218.0f));
	FAIL_IF( 2'147'483'520 != roundedToSInt4B( 2'147'483'520.0f));
	FAIL_IF(-2'147'483'520 != roundedToSInt4B(-2'147'483'520.0f));
}
static void test_roundedToSInt4B_double() {
	FAIL_IF(             0   != roundedToSInt4B(             0.0));
	FAIL_IF(             0   != roundedToSInt4B(            -0.0));
	FAIL_IF(             0   != roundedToSInt4B(             0.49));
	FAIL_IF(             0   != roundedToSInt4B(            -0.49));
	FAIL_IF(             1   != roundedToSInt4B(             1.49));
	FAIL_IF(            -1   != roundedToSInt4B(            -1.49));
	FAIL_IF(           123   != roundedToSInt4B(           123.49));
	FAIL_IF(          -123   != roundedToSInt4B(          -123.49));
	FAIL_IF(    16'777'215   != roundedToSInt4B(    16'777'215.0));
	FAIL_IF(   -16'777'215   != roundedToSInt4B(   -16'777'215.0));
	FAIL_IF(    16'777'216   != roundedToSInt4B(    16'777'216.0)); // last int to be represented properly
	FAIL_IF(   -16'777'216   != roundedToSInt4B(   -16'777'216.0)); // last int to be represented properly
	FAIL_IF(    16'777'218   != roundedToSInt4B(    16'777'218.0));
	FAIL_IF(   -16'777'218   != roundedToSInt4B(   -16'777'218.0));
	FAIL_IF( 2'147'483'520   != roundedToSInt4B( 2'147'483'520.0)); // nearest 32-bit float before 2³²
	FAIL_IF(-2'147'483'520   != roundedToSInt4B(-2'147'483'520.0)); // nearest 32-bit float before -2³²
	FAIL_IF( 2'147'483'647   != roundedToSInt4B( 2'147'483'647.0));
	FAIL_IF(-2'147'483'647-1 != roundedToSInt4B(-2'147'483'648.0));
}

static void test_roundedToUInt4B_float() {
	FAIL_IF(            0 != roundedToUInt4B(            0.0f));
	FAIL_IF(            0 != roundedToUInt4B(           -0.0f));
	FAIL_IF(            0 != roundedToUInt4B(            0.49f));
	FAIL_IF(            1 != roundedToUInt4B(            1.49f));
	FAIL_IF(          123 != roundedToUInt4B(          123.49f));
	FAIL_IF(   16'777'215 != roundedToUInt4B(   16'777'215.0f));
	FAIL_IF(   16'777'216 != roundedToUInt4B(   16'777'216.0f)); // last integer to be represented properly
	FAIL_IF(   16'777'218 != roundedToUInt4B(   16'777'218.0f));
	FAIL_IF(2'147'483'520 != roundedToUInt4B(2'147'483'520.0f));
	FAIL_IF(4'294'967'040 != roundedToUInt4B(4'294'967'040.0f));
}
static void test_roundedToUInt4B_double() {
	FAIL_IF(            0 != roundedToUInt4B(            0.0));
	FAIL_IF(            0 != roundedToUInt4B(           -0.0));
	FAIL_IF(            0 != roundedToUInt4B(            0.49));
	FAIL_IF(            1 != roundedToUInt4B(            1.49));
	FAIL_IF(          123 != roundedToUInt4B(          123.49));
	FAIL_IF(   16'777'215 != roundedToUInt4B(   16'777'215.0));
	FAIL_IF(   16'777'216 != roundedToUInt4B(   16'777'216.0)); // last 32-bit integer to be represented properly
	FAIL_IF(   16'777'217 != roundedToUInt4B(   16'777'217.0));
	FAIL_IF(2'147'483'520 != roundedToUInt4B(2'147'483'520.0)); // nearest 32-bit float before 2³¹
	FAIL_IF(2'147'483'648 != roundedToUInt4B(2'147'483'648.0));
	FAIL_IF(4'294'967'040 != roundedToUInt4B(4'294'967'040.0)); // nearest 32-bit float before 2³²
	FAIL_IF(4'294'967'295 != roundedToUInt4B(4'294'967'295.0));
}

static void test_roundedToSInt8B_float() {
	FAIL_IF(                         0i64     != roundedTo8B(                         0.0f));
	FAIL_IF(                         0i64     != roundedTo8B(                        -0.0f));
	FAIL_IF(                         0i64     != roundedTo8B(                         0.49f));
	FAIL_IF(                         0i64     != roundedTo8B(                        -0.49f));
	FAIL_IF(                         1i64     != roundedTo8B(                         1.49f));
	FAIL_IF(                        -1i64     != roundedTo8B(                        -1.49f));
	FAIL_IF(                       123i64     != roundedTo8B(                       123.49f));
	FAIL_IF(                      -123i64     != roundedTo8B(                      -123.49f));
	FAIL_IF(             2'147'483'520i64     != roundedTo8B(             2'147'483'520.0f)); // nearest 32-bit float before 2³¹
	FAIL_IF(            -2'147'483'520i64     != roundedTo8B(            -2'147'483'520.0f)); // nearest 32-bit float before -2³²
	FAIL_IF(             2'147'483'648i64     != roundedTo8B(             2'147'483'648.0f));
	FAIL_IF(            -2'147'483'648i64     != roundedTo8B(            -2'147'483'648.0f));
	FAIL_IF(     9'007'199'254'740'992i64     != roundedTo8B(     9'007'199'254'740'992.0f)); // last int to be represented properly
	FAIL_IF(    -9'007'199'254'740'992i64     != roundedTo8B(    -9'007'199'254'740'992.0f));
	FAIL_IF( 9'223'371'487'098'961'920i64     != roundedTo8B( 9'223'371'487'098'961'920.0f)); // nearest 32-bit float before 2⁶³
	FAIL_IF(-9'223'371'487'098'961'920i64     != roundedTo8B(-9'223'371'487'098'961'920.0f)); // nearest 32-bit float before -2⁶³
	FAIL_IF(-9'223'372'036'854'775'807i64 - 1 != roundedTo8B(-9'223'372'036'854'775'808.0f)); // -2⁶³
}
static void test_roundedToSInt8B_double() {
	FAIL_IF(                         0i64     != roundedTo8B(                         0.0));
	FAIL_IF(                         0i64     != roundedTo8B(                        -0.0));
	FAIL_IF(                         0i64     != roundedTo8B(                         0.49));
	FAIL_IF(                         0i64     != roundedTo8B(                        -0.49));
	FAIL_IF(                         1i64     != roundedTo8B(                         1.49));
	FAIL_IF(                        -1i64     != roundedTo8B(                        -1.49));
	FAIL_IF(                       123i64     != roundedTo8B(                       123.49));
	FAIL_IF(                      -123i64     != roundedTo8B(                      -123.49));
	FAIL_IF(             2'147'483'648i64     != roundedTo8B(             2'147'483'648.0)); // 2³¹
	FAIL_IF(            -2'147'483'648i64     != roundedTo8B(            -2'147'483'648.0));
	FAIL_IF(             4'294'967'296i64     != roundedTo8B(             4'294'967'296.0)); // 2³²
	FAIL_IF(            -4'294'967'296i64     != roundedTo8B(            -4'294'967'296.0));
	FAIL_IF(     9'007'199'254'740'991i64     != roundedTo8B(     9'007'199'254'740'991.0));
	FAIL_IF(    -9'007'199'254'740'991i64     != roundedTo8B(    -9'007'199'254'740'991.0));
	FAIL_IF(     9'007'199'254'740'992i64     != roundedTo8B(     9'007'199'254'740'992.0)); // last int to be represented properly
	FAIL_IF(    -9'007'199'254'740'992i64     != roundedTo8B(    -9'007'199'254'740'992.0));
	FAIL_IF(     9'007'199'254'740'992i64     != roundedTo8B(     9'007'199'254'740'993.0)); // not exactly representable
	FAIL_IF(    -9'007'199'254'740'992i64     != roundedTo8B(    -9'007'199'254'740'993.0));
	FAIL_IF( 9'223'372'036'854'774'784i64     != roundedTo8B( 9'223'372'036'854'774'784.0)); // nearest 64-bit float before 2⁶³
	FAIL_IF(-9'223'372'036'854'774'784i64     != roundedTo8B(-9'223'372'036'854'774'784.0));
	FAIL_IF(-9'223'372'036'854'775'807i64 - 1 != roundedTo8B(-9'223'372'036'854'775'808.0)); // -2⁶³
}