Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CBOR] Use System.Half for encoding and decoding half-precision data items #38466

Merged
merged 4 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/libraries/System.Formats.Cbor/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@
<data name="Cbor_Reader_NotAFloatEncoding" xml:space="preserve">
<value>Data item does not encode a floating point number.</value>
</data>
<data name="Cbor_Reader_ReadingDoubleAsSingle" xml:space="preserve">
<value>Attempting to read double-precision floating point encoding as a single-precision value.</value>
<data name="Cbor_Reader_ReadingAsLowerPrecision" xml:space="preserve">
<value>Attempting to read floating point encoding as a lower-precision value.</value>
</data>
<data name="Cbor_Reader_NotABooleanEncoding" xml:space="preserve">
<value>CBOR simple value does not encode a boolean value.</value>
Expand Down Expand Up @@ -246,4 +246,4 @@
<data name="CborContentException_DefaultMessage" xml:space="preserve">
<value>The CBOR encoding is invalid.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<Compile Include="$(CommonPath)System\Marvin.cs" Link="Common\System\Marvin.cs" />
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs" Link="Common\System\Memory\PointerMemoryManager.cs" />
<Compile Include="System\Formats\Cbor\HalfHelpers.cs" />
<Compile Include="System\Formats\Cbor\Reader\CborReaderState.cs" />
<Compile Include="System\Formats\Cbor\Reader\CborReader.PeekState.cs" />
<Compile Include="System\Formats\Cbor\Reader\CborReader.SkipValue.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ public static bool RequiresCanonicalIntegerRepresentation(CborConformanceMode co
};
}

public static bool RequiresPreservingFloatPrecision(CborConformanceMode conformanceMode)
{
switch (conformanceMode)
{
case CborConformanceMode.Lax:
case CborConformanceMode.Strict:
case CborConformanceMode.Canonical:
return false;

case CborConformanceMode.Ctap2Canonical:
return true;

default:
throw new ArgumentOutOfRangeException(nameof(conformanceMode));
};
}

public static bool RequiresUtf8Validation(CborConformanceMode conformanceMode)
{
switch (conformanceMode)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.using System;

using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Formats.Cbor
{
// Temporarily implements missing APIs for System.Half
// Remove class once https://github.com/dotnet/runtime/issues/38288 has been addressed
internal static class HalfHelpers
{
public const int SizeOfHalf = sizeof(short);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use sizeof(Half) inline where needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler complains that it doesn't have a predefined size, so would have to mark as unsafe. Presumably because it's a new type?


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Half ReadHalfBigEndian(ReadOnlySpan<byte> source)
{
return BitConverter.IsLittleEndian ?
Int16BitsToHalf(BinaryPrimitives.ReverseEndianness(MemoryMarshal.Read<short>(source))) :
MemoryMarshal.Read<Half>(source);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteHalfBigEndian(Span<byte> destination, Half value)
{
if (BitConverter.IsLittleEndian)
{
short tmp = BinaryPrimitives.ReverseEndianness(HalfToInt16Bits(value));
MemoryMarshal.Write(destination, ref tmp);
}
else
{
MemoryMarshal.Write(destination, ref value);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe short HalfToInt16Bits(Half value)
{
return *((short*)&value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe Half Int16BitsToHalf(short value)
{
return *(Half*)&value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,43 @@ namespace System.Formats.Cbor
{
public partial class CborReader
{
private const int SizeOfHalf = 2; // the size in bytes of an IEEE 754 Half-Precision float
/// <summary>
/// Reads the next data item as a half-precision floating point number (major type 7).
/// </summary>
/// <returns>The decoded value.</returns>
/// <exception cref="InvalidOperationException">
/// the next data item does not have the correct major type. -or-
/// the next simple value is not a floating-point number encoding. -or-
/// the encoded value is a double-precision float
/// </exception>
/// <exception cref="CborContentException">
/// the next value has an invalid CBOR encoding. -or-
/// there was an unexpected end of CBOR encoding data. -or-
/// the next value uses a CBOR encoding that is not valid under the current conformance mode.
/// </exception>
internal Half ReadHalf()
{
CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Simple);
ReadOnlySpan<byte> buffer = GetRemainingBytes();
Half result;

switch (header.AdditionalInfo)
{
case CborAdditionalInfo.Additional16BitData:
EnsureReadCapacity(buffer, 1 + HalfHelpers.SizeOfHalf);
result = HalfHelpers.ReadHalfBigEndian(buffer.Slice(1));
AdvanceBuffer(1 + HalfHelpers.SizeOfHalf);
AdvanceDataItemCounters();
return result;

case CborAdditionalInfo.Additional32BitData:
case CborAdditionalInfo.Additional64BitData:
throw new InvalidOperationException(SR.Cbor_Reader_ReadingAsLowerPrecision);
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved

default:
throw new InvalidOperationException(SR.Cbor_Reader_NotAFloatEncoding);
}
}

/// <summary>
/// Reads the next data item as a single-precision floating point number (major type 7).
Expand All @@ -33,9 +69,9 @@ public float ReadSingle()
switch (header.AdditionalInfo)
{
case CborAdditionalInfo.Additional16BitData:
EnsureReadCapacity(buffer, 1 + SizeOfHalf);
result = (float)ReadHalfBigEndian(buffer.Slice(1));
AdvanceBuffer(1 + SizeOfHalf);
EnsureReadCapacity(buffer, 1 + HalfHelpers.SizeOfHalf);
result = (float)HalfHelpers.ReadHalfBigEndian(buffer.Slice(1));
AdvanceBuffer(1 + HalfHelpers.SizeOfHalf);
AdvanceDataItemCounters();
return result;

Expand All @@ -47,7 +83,7 @@ public float ReadSingle()
return result;

case CborAdditionalInfo.Additional64BitData:
throw new InvalidOperationException(SR.Cbor_Reader_ReadingDoubleAsSingle);
throw new InvalidOperationException(SR.Cbor_Reader_ReadingAsLowerPrecision);

default:
throw new InvalidOperationException(SR.Cbor_Reader_NotAFloatEncoding);
Expand Down Expand Up @@ -77,9 +113,9 @@ public double ReadDouble()
switch (header.AdditionalInfo)
{
case CborAdditionalInfo.Additional16BitData:
EnsureReadCapacity(buffer, 1 + SizeOfHalf);
result = ReadHalfBigEndian(buffer.Slice(1));
AdvanceBuffer(1 + SizeOfHalf);
EnsureReadCapacity(buffer, 1 + HalfHelpers.SizeOfHalf);
result = (double)HalfHelpers.ReadHalfBigEndian(buffer.Slice(1));
AdvanceBuffer(1 + HalfHelpers.SizeOfHalf);
AdvanceDataItemCounters();
return result;

Expand Down Expand Up @@ -199,30 +235,5 @@ public CborSimpleValue ReadSimpleValue()
throw new InvalidOperationException(SR.Cbor_Reader_NotASimpleValueEncoding);
}
}

// half-precision float decoder adapted from https://tools.ietf.org/html/rfc7049#appendix-D
private static double ReadHalfBigEndian(ReadOnlySpan<byte> buffer)
{
int half = (buffer[0] << 8) + buffer[1];
bool isNegative = (half >> 15) != 0;
int exp = (half >> 10) & 0x1f;
int mant = half & 0x3ff;
double value;

if (exp == 0)
{
value = Math.ScaleB(mant, -24);
}
else if (exp != 31)
{
value = Math.ScaleB(mant + 1024, exp - 25);
}
else
{
value = (mant == 0) ? double.PositiveInfinity : double.NaN;
}

return isNegative ? -value : value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@
// See the LICENSE file in the project root for more information.

using System.Buffers.Binary;
using System.Runtime.CompilerServices;

namespace System.Formats.Cbor
{
public partial class CborWriter
{
// Implements major type 7 encoding per https://tools.ietf.org/html/rfc7049#section-2.1

/// <summary>
/// Writes a half-precision floating point number (major type 7).
/// </summary>
/// <param name="value">The value to write.</param>
/// <exception cref="InvalidOperationException">
/// Writing a new value exceeds the definite length of the parent data item. -or-
/// The major type of the encoded value is not permitted in the parent data item. -or-
/// The written data is not accepted under the current conformance mode
/// </exception>
internal void WriteHalf(Half value)
{
EnsureWriteCapacity(1 + HalfHelpers.SizeOfHalf);
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional16BitData));
HalfHelpers.WriteHalfBigEndian(_buffer.AsSpan(_offset), value);
_offset += HalfHelpers.SizeOfHalf;
AdvanceDataItemCounters();
}

/// <summary>
/// Writes a single-precision floating point number (major type 7).
/// </summary>
Expand All @@ -21,11 +40,15 @@ public partial class CborWriter
/// </exception>
public void WriteSingle(float value)
{
EnsureWriteCapacity(5);
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional32BitData));
BinaryPrimitives.WriteSingleBigEndian(_buffer.AsSpan(_offset), value);
_offset += 4;
AdvanceDataItemCounters();
if (!CborConformanceModeHelpers.RequiresPreservingFloatPrecision(ConformanceMode) &&
FloatSerializationHelpers.TryConvertSingleToHalf(value, out Half half))
{
WriteHalf(half);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why WriteHalf vs WriteSingleCore and WriteDoubleCore?

Copy link
Member Author

@eiriktsarpalis eiriktsarpalis Jun 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Write[Float]Core methods implement serialization for the particular precision without doing conformance mode checks and potential downsizing. WriteHalf doesn't need downsizing since it's the smallest type. Perhaps calling it something like WriteSingleNoDownsizing might reduce confusion?

}
else
{
WriteSingleCore(value);
}
}

/// <summary>
Expand All @@ -39,10 +62,39 @@ public void WriteSingle(float value)
/// </exception>
public void WriteDouble(double value)
{
EnsureWriteCapacity(9);
if (!CborConformanceModeHelpers.RequiresPreservingFloatPrecision(ConformanceMode) &&
FloatSerializationHelpers.TryConvertDoubleToSingle(value, out float single))
{
if (FloatSerializationHelpers.TryConvertSingleToHalf(single, out Half half))
{
WriteHalf(half);
}
else
{
WriteSingleCore(single);
}
}
else
{
WriteDoubleCore(value);
}
}

private void WriteSingleCore(float value)
{
EnsureWriteCapacity(1 + sizeof(float));
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional32BitData));
BinaryPrimitives.WriteSingleBigEndian(_buffer.AsSpan(_offset), value);
_offset += sizeof(float);
AdvanceDataItemCounters();
}

private void WriteDoubleCore(double value)
{
EnsureWriteCapacity(1 + sizeof(double));
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional64BitData));
BinaryPrimitives.WriteDoubleBigEndian(_buffer.AsSpan(_offset), value);
_offset += 8;
_offset += sizeof(double);
AdvanceDataItemCounters();
}

Expand Down Expand Up @@ -106,5 +158,22 @@ public void WriteSimpleValue(CborSimpleValue value)

AdvanceDataItemCounters();
}

private static class FloatSerializationHelpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryConvertDoubleToSingle(double value, out float result)
{
result = (float)value;
return BitConverter.DoubleToInt64Bits(result) == BitConverter.DoubleToInt64Bits(value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryConvertSingleToHalf(float value, out Half result)
{
result = (Half)value;
return BitConverter.SingleToInt32Bits((float)result) == BitConverter.SingleToInt32Bits(value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using System.Linq;
using Test.Cryptography;
Expand Down
1 change: 0 additions & 1 deletion src/libraries/System.Formats.Cbor/tests/CoseKeyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System.Diagnostics;
using System.Security.Cryptography;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using Test.Cryptography;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System.Linq;
using Test.Cryptography;
using Xunit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System.Linq;
using System.Numerics;
using Test.Cryptography;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using Test.Cryptography;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using System.Linq;
using Test.Cryptography;
using Xunit;
Expand Down
Loading