Added HexFloat reading to HexFloat.

This allows reading of hex-encoded floats.
This commit is contained in:
Andrew Woloszyn 2015-10-23 13:26:02 -04:00 committed by David Neto
parent c2887f98bc
commit 0b61c593a2
2 changed files with 315 additions and 30 deletions

View File

@ -28,6 +28,7 @@
#define _LIBSPIRV_UTIL_HEX_FLOAT_H_
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <iomanip>
@ -151,6 +152,19 @@ class HexFloat {
"The number of bits do not fit");
};
// Returns 4 bits represented by the hex character.
inline uint8_t get_nibble_from_character(char character) {
const char* dec = "0123456789";
const char* lower = "abcdef";
const char* upper = "ABCDEF";
if (auto p = strchr(dec, character)) return p - dec;
if (auto p = strchr(lower, character)) return p - lower + 0xa;
if (auto p = strchr(upper, character)) return p - upper + 0xa;
assert(false && "This was called with a non-hex character");
return 0;
}
// Outputs the given HexFloat to the stream.
template <typename T, typename Traits>
std::ostream& operator<<(std::ostream& os, const HexFloat<T, Traits>& value) {
@ -217,6 +231,227 @@ std::ostream& operator<<(std::ostream& os, const HexFloat<T, Traits>& value) {
os << "p" << std::dec << (int_exponent >= 0 ? "+" : "") << int_exponent;
return os;
}
template <typename T, typename Traits>
inline std::istream& ParseNormalFloat(
std::istream& is, bool negate_value, HexFloat<T, Traits>& value) {
T val;
is >> val;
if (negate_value) {
val = -val;
}
value.set_value(val);
return is;
}
// Reads a HexFloat from the given stream.
// If the float is not encoded as a hex-float then it will be parsed
// as a regular float.
// This may fail if your stream does not support at least one unget.
// Nan values can be encoded with "0x1.<not zero>p+exponent_bias".
// This would normally overflow a float and round to
// infinity but this special pattern is the exact representation for a NaN,
// and therefore is actually encoded as the correct NaN. To encode inf,
// either 0x0p+exponent_bias can be spcified or any exponent greater than
// exponent_bias.
// Examples using IEEE 32-bit float encoding.
// 0x1.0p+128 (+inf)
// -0x1.0p-128 (-inf)
//
// 0x1.1p+128 (+Nan)
// -0x1.1p+128 (-Nan)
//
// 0x1p+129 (+inf)
// -0x1p+129 (-inf)
template <typename T, typename Traits>
std::istream& operator>>(std::istream& is, HexFloat<T, Traits>& value) {
using HF = HexFloat<T, Traits>;
using uint_type = typename HF::uint_type;
using int_type = typename HF::int_type;
value.set_value(T(0));
if (is.flags() & std::ios::skipws) {
// If the user wants to skip whitespace , then we should obey that.
while (std::isspace(is.peek())) {
is.get();
}
}
char next_char = is.peek();
bool negate_value = false;
if (next_char != '-' && next_char != '0') {
return ParseNormalFloat(is, negate_value, value);
}
if (next_char == '-') {
negate_value = true;
is.get();
next_char = is.peek();
}
if (next_char == '0') {
is.get(); // We may have to unget this.
char maybe_hex_start = is.peek();
if (maybe_hex_start != 'x' && maybe_hex_start != 'X') {
is.unget();
return ParseNormalFloat(is, negate_value, value);
} else {
is.get(); // Throw away the 'x';
}
} else {
return ParseNormalFloat(is, negate_value, value);
}
// This "looks" like a hex-float so treat it as one.
bool seen_p = false;
bool seen_dot = false;
uint_type fraction_index = 0;
uint_type fraction = 0;
int_type exponent = HF::exponent_bias;
// Strip off leading zeros so we don't have to special-case them later.
while ((next_char = is.peek()) == '0') {
is.get();
}
bool is_denorm =
true; // Assume denorm "representation" until we hear otherwise.
// NB: This does not mean the value is actually denorm,
// it just means that it was written 0.
bool bits_written = false; // Stays false until we write a bit.
while (!seen_p && !seen_dot) {
// Handle characters that are left of the fractional part.
if (next_char == '.') {
seen_dot = true;
} else if (next_char == 'p') {
seen_p = true;
} else if (::isxdigit(next_char)) {
// We know this is not denormalized since we have stripped all leading
// zeroes and we are not a ".".
is_denorm = false;
uint8_t number = get_nibble_from_character(next_char);
for (int i = 0; i < 4; ++i, number <<= 1) {
uint_type write_bit = (number & 0x8) ? 0x1 : 0x0;
if (bits_written) {
// If we are here the bits represented belong in the fractional
// part of the float, and we have to adjust the exponent accordingly.
fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
exponent += 1;
}
bits_written |= write_bit;
}
} else {
// We have not found our exponent yet, so we have to fail.
is.setstate(std::ios::failbit);
return is;
}
is.get();
next_char = is.peek();
}
bits_written = false;
while (seen_dot && !seen_p) {
// Handle only fractional parts now.
if (next_char == 'p') {
seen_p = true;
} else if (::isxdigit(next_char)) {
int number = get_nibble_from_character(next_char);
for (int i = 0; i < 4; ++i, number <<= 1) {
uint_type write_bit = (number & 0x8) ? 0x01 : 0x00;
bits_written |= write_bit;
if (is_denorm && !bits_written) {
// Handle modifying the exponent here this way we can handle
// an arbitrary number of hex values without overflowing our
// integer.
exponent -= 1;
} else {
fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
}
}
} else {
// We still have not found our 'p' exponent yet, so this is not a valid
// hex-float.
is.setstate(std::ios::failbit);
return is;
}
is.get();
next_char = is.peek();
}
bool seen_sign = false;
int8_t exponent_sign = 1;
int_type written_exponent = 0;
while (true) {
if ((next_char == '-' || next_char == '+')) {
if (seen_sign) {
is.setstate(std::ios::failbit);
return is;
}
seen_sign = true;
exponent_sign = (next_char == '-') ? -1 : 1;
} else if (::isdigit(next_char)) {
// Hex-floats express their exponent as decimal.
written_exponent *= 10;
written_exponent += next_char - '0';
} else {
break;
}
is.get();
next_char = is.peek();
}
written_exponent *= exponent_sign;
exponent += written_exponent;
bool is_zero = is_denorm && (fraction == 0);
if (is_denorm && !is_zero) {
fraction <<= 1;
exponent -= 1;
} else if (is_zero) {
exponent = 0;
}
if (exponent <= 0 && !is_zero) {
fraction >>= 1;
fraction |= static_cast<uint_type>(1) << HF::top_bit_left_shift;
}
fraction = (fraction >> HF::fraction_right_shift) & HF::fraction_encode_mask;
const uint_type max_exponent =
SetBits<uint_type, 0, HF::num_exponent_bits>::get;
// Handle actual denorm numbers
while (exponent < 0 && !is_zero) {
fraction >>= 1;
exponent += 1;
fraction &= HF::fraction_encode_mask;
if (fraction == 0) {
// We have underflowed our fraction. We should clamp to zero.
is_zero = true;
exponent = 0;
}
}
// We have overflowed so we should be inf/-inf.
if (exponent > max_exponent) {
exponent = max_exponent;
fraction = 0;
}
uint_type output_bits = static_cast<uint_type>(negate_value ? 1 : 0)
<< HF::top_bit_left_shift;
output_bits |= fraction;
output_bits |= (exponent << HF::exponent_left_shift) & HF::exponent_mask;
T output_float = spvutils::BitwiseCast<T>(output_bits);
value.set_value(output_float);
return is;
}
}
#endif // _LIBSPIRV_UTIL_HEX_FLOAT_H_

View File

@ -35,18 +35,32 @@
namespace {
using ::testing::Eq;
using spvutils::BitwiseCast;
using HexFloatEncodeTest =
using HexFloatTest =
::testing::TestWithParam<std::pair<float, std::string>>;
using DecodeHexFloatTest =
::testing::TestWithParam<std::pair<std::string, float>>;
TEST_P(HexFloatEncodeTest, EncodeCorrectly) {
TEST_P(HexFloatTest, EncodeCorrectly) {
std::stringstream ss;
ss << spvutils::HexFloat<float>(std::get<0>(GetParam()));
EXPECT_THAT(ss.str(), Eq(std::get<1>(GetParam())));
}
TEST_P(HexFloatTest, DecodeCorrectly) {
std::stringstream ss(std::get<1>(GetParam()));
spvutils::HexFloat<float> myFloat(0.f);
ss >> myFloat;
float expected = std::get<0>(GetParam());
// Check that the two floats are bitwise equal. We do it this way because
// nan != nan, and so the test would fail even if they were exactly the same.
EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
Eq(BitwiseCast<uint32_t>(std::get<0>(GetParam()))));
}
INSTANTIATE_TEST_CASE_P(
Float32Tests, HexFloatEncodeTest,
Float32Tests, HexFloatTest,
::testing::ValuesIn(std::vector<std::pair<float, std::string>>({
{0.f, "0x0p+0"},
{1.f, "0x1p+0"},
@ -78,40 +92,76 @@ INSTANTIATE_TEST_CASE_P(
{1.0f / -1024.f - 1.0f / 8.f, "-0x1.02p-3"},
// lowest non-denorm
{1.0 / (powf(2.0f, 126.0f)), "0x1p-126"},
{-1.0 / (powf(2.0f, 126.0f)), "-0x1p-126"},
{ldexp(1.0f, -126), "0x1p-126"},
{ldexp(-1.0f, -126), "-0x1p-126"},
// Denormalized values
{1.0 / (powf(2.0f, 127.0f)), "0x1p-127"},
{(1.0 / (powf(2.0f, 127.0f))) / 2.0f, "0x1p-128"},
{(1.0 / (powf(2.0f, 127.0f))) / 4.0f, "0x1p-129"},
{(1.0 / (powf(2.0f, 127.0f))) / 8.0f, "0x1p-130"},
{-1.0 / (powf(2.0f, 127.0f)), "-0x1p-127"},
{(-1.0 / (powf(2.0f, 127.0f))) / 2.0f, "-0x1p-128"},
{(-1.0 / (powf(2.0f, 127.0f))) / 4.0f, "-0x1p-129"},
{(-1.0 / (powf(2.0f, 127.0f))) / 8.0f, "-0x1p-130"},
{(1.0 / (powf(2.0f, 127.0f))) +
((1.0 / (powf(2.0f, 127.0f))) / 2.0f), "0x1.8p-127"},
{(1.0 / (powf(2.0f, 127.0f)) / 2.0f) +
((1.0 / (powf(2.0f, 127.0f))) / 4.0f), "0x1.8p-128"},
{ldexp(1.0f, -127), "0x1p-127"},
{ldexp(1.0f, -127) / 2.0f, "0x1p-128"},
{ldexp(1.0f, -127) / 4.0f, "0x1p-129"},
{ldexp(1.0f, -127) / 8.0f, "0x1p-130"},
{ldexp(-1.0f, -127), "-0x1p-127"},
{ldexp(-1.0f, -127) / 2.0f, "-0x1p-128"},
{ldexp(-1.0f, -127) / 4.0f, "-0x1p-129"},
{ldexp(-1.0f, -127) / 8.0f, "-0x1p-130"},
{ldexp(1.0, -127) + (ldexp(1.0, -127) / 2.0f), "0x1.8p-127"},
{ldexp(1.0, -127) / 2.0 + (ldexp(1.0, -127) / 4.0f), "0x1.8p-128"},
// Various NAN and INF cases
{spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -inf
{spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"}, // inf
{spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -nan
{spvutils::BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"}, // -nan
{spvutils::BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"}, // -nan
{spvutils::BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"}, // -nan
{spvutils::BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"}, // -nan
{spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"}, // +nan
{spvutils::BitwiseCast<float>(0x7F800100), "0x1.0002p+128"}, // +nan
{spvutils::BitwiseCast<float>(0x7F800c00), "0x1.0018p+128"}, // +nan
{spvutils::BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"}, // +nan
{spvutils::BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"}, // +nan
{BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -inf
{BitwiseCast<float>(0x7F800000), "0x1p+128"}, // inf
{BitwiseCast<float>(0xFFC00000), "-0x1.8p+128"}, // -nan
{BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"}, // -nan
{BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"}, // -nan
{BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"}, // -nan
{BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"}, // -nan
{BitwiseCast<float>(0x7FC00000), "0x1.8p+128"}, // +nan
{BitwiseCast<float>(0x7F800100), "0x1.0002p+128"}, // +nan
{BitwiseCast<float>(0x7f800c00), "0x1.0018p+128"}, // +nan
{BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"}, // +nan
{BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"}, // +nan
})));
TEST_P(DecodeHexFloatTest, DecodeCorrectly) {
std::stringstream ss(std::get<0>(GetParam()));
spvutils::HexFloat<float> myFloat(0.f);
ss >> myFloat;
float expected = std::get<1>(GetParam());
// Check that the two floats are bitwise equal. We do it this way because
// nan != nan, and so the test would fail even if they were exactly the same.
EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
Eq(BitwiseCast<uint32_t>(std::get<1>(GetParam()))));
}
INSTANTIATE_TEST_CASE_P(
Float32DecodeTests, DecodeHexFloatTest,
::testing::ValuesIn(std::vector<std::pair<std::string, float>>({
{"0x0p+000", 0.f},
{"0x0p0", 0.f},
{"0x0p-0", 0.f},
// inf cases
{"-0x1p+128", BitwiseCast<float>(0xFF800000)}, // -inf
{"0x32p+127", BitwiseCast<float>(0x7F800000)}, // inf
{"0x32p+500", BitwiseCast<float>(0x7F800000)}, // inf
{"-0x32p+127", BitwiseCast<float>(0xFF800000)}, // -inf
// flush to zero cases
{"0x1p-500", 0.f}, // Exponent underflows.
{"-0x1p-500", -0.f},
{"0x0.00000000001p-126", 0.f}, // Fraction causes underfloat.
{"-0x0.0000000001p-127", -0.f},
{"-0x0.01p-142", -0.f}, // Fraction causes undeflow to underflow more.
{"0x0.01p-142", 0.f},
// Some floats that do not encode the same way as the decode.
{"0x2p+0", 2.f},
{"0xFFp+0", 255.f},
{"0x0.8p+0", 0.5f},
{"0x0.4p+0", 0.25f},
})));
// TODO(awoloszyn): Add some more decoding tests with "abnormal" formats.
// TODO(awoloszyn): Add double tests
// TODO(awoloszyn): Add fp16 tests and HexFloatTraits.