add support for reading and writing QUIC's ufloat16

This commit is contained in:
Lucas Clemente
2016-04-26 16:47:26 +02:00
parent 83df3be65e
commit df08f109e7
2 changed files with 230 additions and 0 deletions

86
utils/float16.go Normal file
View File

@@ -0,0 +1,86 @@
package utils
import (
"bytes"
"io"
"math"
)
// We define an unsigned 16-bit floating point value, inspired by IEEE floats
// (http://en.wikipedia.org/wiki/Half_precision_floating-point_format),
// with 5-bit exponent (bias 1), 11-bit mantissa (effective 12 with hidden
// bit) and denormals, but without signs, transfinites or fractions. Wire format
// 16 bits (little-endian byte order) are split into exponent (high 5) and
// mantissa (low 11) and decoded as:
// uint64_t value;
// if (exponent == 0) value = mantissa;
// else value = (mantissa | 1 << 11) << (exponent - 1)
const uFloat16ExponentBits = 5
const uFloat16MaxExponent = (1 << uFloat16ExponentBits) - 2 // 30
const uFloat16MantissaBits = 16 - uFloat16ExponentBits // 11
const uFloat16MantissaEffectiveBits = uFloat16MantissaBits + 1 // 12
const uFloat16MaxValue = ((uint64(1) << uFloat16MantissaEffectiveBits) - 1) << uFloat16MaxExponent // 0x3FFC0000000
// ReadUfloat16 reads a float in the QUIC-float16 format and returns its uint64 representation
func ReadUfloat16(b io.ByteReader) (uint64, error) {
val, err := ReadUint16(b)
if err != nil {
return 0, err
}
res := uint64(val)
if res < (1 << uFloat16MantissaEffectiveBits) {
// Fast path: either the value is denormalized (no hidden bit), or
// normalized (hidden bit set, exponent offset by one) with exponent zero.
// Zero exponent offset by one sets the bit exactly where the hidden bit is.
// So in both cases the value encodes itself.
return res, nil
}
exponent := val >> uFloat16MantissaBits // No sign extend on uint!
// After the fast pass, the exponent is at least one (offset by one).
// Un-offset the exponent.
exponent--
// Here we need to clear the exponent and set the hidden bit. We have already
// decremented the exponent, so when we subtract it, it leaves behind the
// hidden bit.
res -= uint64(exponent) << uFloat16MantissaBits
res <<= exponent
return res, nil
}
// WriteUfloat16 writes a float in the QUIC-float16 format from its uint64 representation
func WriteUfloat16(b *bytes.Buffer, value uint64) {
var result uint16
if value < (uint64(1) << uFloat16MantissaEffectiveBits) {
// Fast path: either the value is denormalized, or has exponent zero.
// Both cases are represented by the value itself.
result = uint16(value)
} else if value >= uFloat16MaxValue {
// Value is out of range; clamp it to the maximum representable.
result = math.MaxUint16
} else {
// The highest bit is between position 13 and 42 (zero-based), which
// corresponds to exponent 1-30. In the output, mantissa is from 0 to 10,
// hidden bit is 11 and exponent is 11 to 15. Shift the highest bit to 11
// and count the shifts.
exponent := uint16(0)
for offset := uint16(16); offset > 0; offset /= 2 {
// Right-shift the value until the highest bit is in position 11.
// For offset of 16, 8, 4, 2 and 1 (binary search over 1-30),
// shift if the bit is at or above 11 + offset.
if value >= (uint64(1) << (uFloat16MantissaBits + offset)) {
exponent += offset
value >>= offset
}
}
// Hidden bit (position 11) is set. We should remove it and increment the
// exponent. Equivalently, we just add it to the exponent.
// This hides the bit.
result = (uint16(value) + (exponent << uFloat16MantissaBits))
}
WriteUint16(b, result)
}

144
utils/float16_test.go Normal file
View File

@@ -0,0 +1,144 @@
package utils
import (
"bytes"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("float16", func() {
It("reads", func() {
testcases := []struct {
expected uint64
binary uint16
}{
// There are fewer decoding test cases because encoding truncates, and
// decoding returns the smallest expansion.
// Small numbers represent themselves.
{0, 0},
{1, 1},
{2, 2},
{3, 3},
{4, 4},
{5, 5},
{6, 6},
{7, 7},
{15, 15},
{31, 31},
{42, 42},
{123, 123},
{1234, 1234},
// Check transition through 2^11.
{2046, 2046},
{2047, 2047},
{2048, 2048},
{2049, 2049},
// Running out of mantissa at 2^12.
{4094, 4094},
{4095, 4095},
{4096, 4096},
{4098, 4097},
{4100, 4098},
// Check transition through 2^13.
{8190, 6143},
{8192, 6144},
{8196, 6145},
// Half-way through the exponents.
{0x7FF8000, 0x87FF},
{0x8000000, 0x8800},
{0xFFF0000, 0x8FFF},
{0x10000000, 0x9000},
// Transition into the largest exponent.
{0x1FFE0000000, 0xF7FF},
{0x20000000000, 0xF800},
{0x20040000000, 0xF801},
// Transition into the max value.
{0x3FF80000000, 0xFFFE},
{0x3FFC0000000, 0xFFFF},
}
for _, testcase := range testcases {
b := &bytes.Buffer{}
WriteUint16(b, testcase.binary)
val, err := ReadUfloat16(b)
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal(testcase.expected))
}
})
It("writes", func() {
testcases := []struct {
decoded uint64
encoded uint16
}{
// Small numbers represent themselves.
{0, 0},
{1, 1},
{2, 2},
{3, 3},
{4, 4},
{5, 5},
{6, 6},
{7, 7},
{15, 15},
{31, 31},
{42, 42},
{123, 123},
{1234, 1234},
// Check transition through 2^11.
{2046, 2046},
{2047, 2047},
{2048, 2048},
{2049, 2049},
// Running out of mantissa at 2^12.
{4094, 4094},
{4095, 4095},
{4096, 4096},
{4097, 4096},
{4098, 4097},
{4099, 4097},
{4100, 4098},
{4101, 4098},
// Check transition through 2^13.
{8190, 6143},
{8191, 6143},
{8192, 6144},
{8193, 6144},
{8194, 6144},
{8195, 6144},
{8196, 6145},
{8197, 6145},
// Half-way through the exponents.
{0x7FF8000, 0x87FF},
{0x7FFFFFF, 0x87FF},
{0x8000000, 0x8800},
{0xFFF0000, 0x8FFF},
{0xFFFFFFF, 0x8FFF},
{0x10000000, 0x9000},
// Transition into the largest exponent.
{0x1FFFFFFFFFE, 0xF7FF},
{0x1FFFFFFFFFF, 0xF7FF},
{0x20000000000, 0xF800},
{0x20000000001, 0xF800},
{0x2003FFFFFFE, 0xF800},
{0x2003FFFFFFF, 0xF800},
{0x20040000000, 0xF801},
{0x20040000001, 0xF801},
// Transition into the max value and clamping.
{0x3FF80000000, 0xFFFE},
{0x3FFBFFFFFFF, 0xFFFE},
{0x3FFC0000000, 0xFFFF},
{0x3FFC0000001, 0xFFFF},
{0x3FFFFFFFFFF, 0xFFFF},
{0x40000000000, 0xFFFF},
{0xFFFFFFFFFFFFFFFF, 0xFFFF},
}
for _, testcase := range testcases {
b := &bytes.Buffer{}
WriteUfloat16(b, testcase.decoded)
val, err := ReadUint16(b)
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal(testcase.encoded))
}
})
})