c++visual-c++ieee-754

Does the MSVC implementation of `signaling_NaN` comply with the the latest IEEE floating-point standard?


As far as I can tell, the MSVC implementation of signaling_NaN does not comply with IEEE 754-2019, the latest version of the IEEE floating-point standard.

Unfortunately, I do not have a copy of the IEEE standard, so I must rely on Wikipedia. In particular, Wikipedia states that the IEEE standard requires, in a quiet NaN, that the MSB of the significand be set to 1. The same bit should be set to 0, for signaling NaNs.

In MSVC, function std::numeric_limits<float>::signaling_NaN() sets it to 1. (See the demonstration program below.) Under IEEE 754-2019, that indicates a quiet NaN.

In addition, the MSVC implementation of signaling_NaN is inconsistent: types float and double are not treated analogously. Type double handles signaling_NaN as specified by IEEE 754-2019. Type float does not.

I am posting this question to get a second opinion: Have I missed something? Is Wikipedia wrong?

Background

According to Wikipedia, the 2019 revision of IEEE 754 requires that:

For binary interchange formats, the most significant bit of the significand field is exclusively used to distinguish between quiet and signaling NaNs. Moreover, it should be an is_quiet flag. That is, this bit is non-zero if the NaN is quiet, and zero if the NaN is signaling.

Earlier versions of the IEEE standard do not contain this requirement. Nevertheless, it was the method used in many implemetations. Under earlier versions of the standard (per Wikipedia), the bits used to indicate quiet and signaling NaN are implementation dependent.

The fact that Microsoft does not follow IEEE 754-2019 can be explained by asserting that Microsoft is following an earlier version of IEEE 754.

The fact that Microsoft treats float and double differently, however, suggests that something went wrong somewhere. It may be that an early version of MSVC introduced the inconsistency, and now Microsoft feels that it cannot correct it, without breaking existing code bases.

Or, perhaps, Microsoft does not know that an inconsistency exists.

Demonstration program

I ran the following program on MSVC, g++, and Clang. You can see the demo at Compiler Explorer.

Clang and g++ both adhere to IEEE 754-2019. Of the three, only MSVC does not.

Here is the output from MSVC. It shows that std::numeric_limits<float>::signaling_NaN() sets the is_quiet bit to 1, thus indicating a quiet NaN.

It also shows that std::numeric_limits<double>::signaling_NaN() sets the is_quiet bit to 0, as required under IEEE 754-2019.

Look for the lines that say, "is_quiet."

Predefined Macros
  __cplusplus          : 199711
  _MSVC_LANG           : 202302
  _MSC_VER             : 1943
  _MSC_FULL_VER        : 194334810
  _MSC_BUILD           : 0
========
 float
========

IEEE 754 Support
  std::numeric_limits< float >::is_iec559         : true
  std::numeric_limits< float >::has_quiet_NaN     : true
  std::numeric_limits< float >::has_signaling_NaN : true

std::numeric_limits< float >::quiet_NaN()
  bits        : 01111111110000000000000000000000
  exponent    : _11111111_______________________
  significand : _________10000000000000000000000
  is_quiet    : _________1______________________

std::numeric_limits< float >::signaling_NaN()
  bits        : 01111111110000000000000000000001
  exponent    : _11111111_______________________
  significand : _________10000000000000000000001
  is_quiet    : _________1______________________     <===== Whoops!
========
 double
========

IEEE 754 Support
  std::numeric_limits< double >::is_iec559         : true
  std::numeric_limits< double >::has_quiet_NaN     : true
  std::numeric_limits< double >::has_signaling_NaN : true

std::numeric_limits< double >::quiet_NaN()
  bits        : 0111111111111000000000000000000000000000000000000000000000000000
  exponent    : _11111111111____________________________________________________
  significand : ____________1000000000000000000000000000000000000000000000000000
  is_quiet    : ____________1___________________________________________________

std::numeric_limits< double >::signaling_NaN()
  bits        : 0111111111110000000000000000000000000000000000000000000000000001
  exponent    : _11111111111____________________________________________________
  significand : ____________0000000000000000000000000000000000000000000000000001
  is_quiet    : ____________0___________________________________________________

Source code

// app23.signaling_nan.main.cpp
// Demonstration Program for Stack Overflow Question
// https://stackoverflow.com/q/79707504/22193627
#include <bit>          // bit_cast
#include <cstddef>      // size_t
#include <cstdint>      // uint32_t, uint64_t
#include <iostream>     // cout, ostream
#include <limits>       // numeric_limits
#include <string>       // string
#include <string_view>  // string_view
namespace
{
    //==================================================================
    // put_predefined_macros
    //==================================================================
    void put_predefined_macros(std::ostream& log) {
        log << "Predefined Macros\n"
            << "  __cplusplus          : " << __cplusplus << '\n';
#if defined(_MSC_VER)
        // Compile with: /std:c++23preview
        log << "  _MSVC_LANG           : " << _MSVC_LANG << '\n'
            << "  _MSC_VER             : " << _MSC_VER << '\n'
            << "  _MSC_FULL_VER        : " << _MSC_FULL_VER << '\n'
            << "  _MSC_BUILD           : " << _MSC_BUILD << '\n';
#endif
#if defined(__GNUC__)
        // Compile with: -std=c++23
        log << "  __GNUC__             : " << __GNUC__ << '\n'
            << "  __GNUC_MINOR__       : " << __GNUC_MINOR__ << '\n'
            << "  __GNUC_PATCHLEVEL__  : " << __GNUC_PATCHLEVEL__ << '\n';
#endif
#if defined(__clang_major__)
        // Compile with: -std=c++23
        log << "  __clang_major__      : " << __clang_major__ << '\n'
            << "  __clang_minor__      : " << __clang_minor__ << '\n'
            << "  __clang_patchlevel__ : " << __clang_patchlevel__ << '\n';
#endif
        log << "\n\n\n";
    }
    //==================================================================
    // isolated_bits
    //==================================================================
    template< typename bits_type >
    std::string isolated_bits(
        bits_type const b,
        std::size_t const begin,
        std::size_t const end)
    {
        std::size_t const n_bits{ sizeof(bits_type) * 8uz };
        std::string s;
        s.reserve(n_bits);
        for (auto n{ n_bits }; n--;) {
            char const c{
                n < begin || n > end ? '_'
                : (b >> n) & 1u ? '1' : '0'
            };
            s.push_back(c);
        }
        return s;
    }
    //==================================================================
    // exponent_bits
    //==================================================================
    template< typename bits_type >
    std::string exponent_bits(bits_type const b) {
        return
            sizeof(bits_type) == 4uz ? isolated_bits(b, 23uz, 30uz)
            : sizeof(bits_type) == 8uz ? isolated_bits(b, 52uz, 62uz)
            : "";
    }
    //==================================================================
    // significand_bits
    //==================================================================
    template< typename bits_type >
    std::string significand_bits(bits_type const b) {
        return
            sizeof(bits_type) == 4uz ? isolated_bits(b, 0uz, 22uz)
            : sizeof(bits_type) == 8uz ? isolated_bits(b, 0uz, 51uz)
            : "";
    }
    //==================================================================
    // is_quiet_bit
    //==================================================================
    template< typename bits_type >
    std::string is_quiet_bit(bits_type const b) {
        return
            sizeof(bits_type) == 4uz ? isolated_bits(b, 22uz, 22uz)
            : sizeof(bits_type) == 8uz ? isolated_bits(b, 51uz, 51uz)
            : "";
    }
    //==================================================================
    // test_NaN
    //==================================================================
    template< typename NumT, typename bits_type >
    void test_NaN(
        std::ostream& log,
        std::string_view heading,
        NumT const nan)
    {
        auto const bits{ std::bit_cast<bits_type>(nan) };
        log << std::format(
            "{}\n"
            "  bits        : {:0>{}b}\n"
            "  exponent    : {}\n"
            "  significand : {}\n"
            "  is_quiet    : {}\n"
            "\n"
            , heading
            , bits
            , sizeof(bits_type) * 8uz
            , exponent_bits(bits)
            , significand_bits(bits)
            , is_quiet_bit(bits)
        );
    }
    //------------------------------------------------------------------
    template< typename NumT, typename bits_type >
    void test_NaN(std::ostream& log, std::string_view type_name)
    {
        std::string nl{
            std::format("std::numeric_limits< {} >::", type_name)
        };
        log << std::format(
            "========\n"
            " {}\n"
            "========\n"
            "\n"
            "IEEE 754 Support\n"
            "  {}{:17} : {}\n"
            "  {}{:17} : {}\n"
            "  {}{:17} : {}\n"
            "\n"
            , type_name
            , nl, "is_iec559", std::numeric_limits<NumT>::is_iec559
            , nl, "has_quiet_NaN", std::numeric_limits<NumT>::has_quiet_NaN
            , nl, "has_signaling_NaN", std::numeric_limits<NumT>::has_signaling_NaN
        );
        test_NaN<NumT, bits_type>(
            log,
            nl + "quiet_NaN()",
            std::numeric_limits<NumT>::quiet_NaN());
        test_NaN<NumT, bits_type>(
            log,
            nl + "signaling_NaN()",
            std::numeric_limits<NumT>::signaling_NaN());
        log << "\n\n";
    }
    //------------------------------------------------------------------
}
int main() {
    auto& log{ std::cout };
    put_predefined_macros(log);
    test_NaN<float, std::uint32_t>(log, "float");
    test_NaN<double, std::uint64_t>(log, "double");
}
// end file: app23.signaling_nan.main.cpp

Solution

  • This is a long standing bug in MSVC (since at least 2017, but I think there are earlier reports, e.g How to use std::signaling_nan?):

    std::numeric_limits<float>::signaling_NaN() is implemented via __builtin_nansf("1") which seems to return the wrong value.