diff --git a/HdrHistogram.Benchmarking/ByteBuffer/ByteBufferBenchmark.cs b/HdrHistogram.Benchmarking/ByteBuffer/ByteBufferBenchmark.cs new file mode 100644 index 0000000..d479778 --- /dev/null +++ b/HdrHistogram.Benchmarking/ByteBuffer/ByteBufferBenchmark.cs @@ -0,0 +1,46 @@ +using BenchmarkDotNet.Attributes; +using HdrHistogram.Utilities; + +namespace HdrHistogram.Benchmarking.ByteBuffer +{ + [MemoryDiagnoser] + public class ByteBufferBenchmark + { + private Utilities.ByteBuffer _writeBuffer = null!; + private Utilities.ByteBuffer _readBuffer = null!; + private const int Iterations = 1000; + + [GlobalSetup] + public void Setup() + { + _writeBuffer = Utilities.ByteBuffer.Allocate(Iterations * sizeof(long)); + _readBuffer = Utilities.ByteBuffer.Allocate(Iterations * sizeof(long)); + for (int i = 0; i < Iterations; i++) + { + _readBuffer.PutLong(i * 12345678L); + } + } + + [Benchmark] + public void PutLong_After() + { + _writeBuffer.Position = 0; + for (int i = 0; i < Iterations; i++) + { + _writeBuffer.PutLong(i * 12345678L); + } + } + + [Benchmark] + public long GetLong_After() + { + _readBuffer.Position = 0; + long last = 0; + for (int i = 0; i < Iterations; i++) + { + last = _readBuffer.GetLong(); + } + return last; + } + } +} diff --git a/HdrHistogram.Benchmarking/Program.cs b/HdrHistogram.Benchmarking/Program.cs index 881c88e..8cc157f 100644 --- a/HdrHistogram.Benchmarking/Program.cs +++ b/HdrHistogram.Benchmarking/Program.cs @@ -24,6 +24,7 @@ static void Main(string[] args) typeof(LeadingZeroCount.LeadingZeroCount64BitBenchmark), typeof(LeadingZeroCount.LeadingZeroCount32BitBenchmark), typeof(Recording.Recording32BitBenchmark), + typeof(ByteBuffer.ByteBufferBenchmark), }); switcher.Run(args, config); } diff --git a/HdrHistogram.UnitTests/Utilities/ByteBufferTests.cs b/HdrHistogram.UnitTests/Utilities/ByteBufferTests.cs index 4db1220..1b98e9c 100644 --- a/HdrHistogram.UnitTests/Utilities/ByteBufferTests.cs +++ b/HdrHistogram.UnitTests/Utilities/ByteBufferTests.cs @@ -69,4 +69,99 @@ public override void Flush() { } public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } } + + public class ByteBufferReadWriteTests + { + [Theory] + [InlineData(42)] + [InlineData(-1)] + [InlineData(int.MaxValue)] + public void PutInt_and_GetInt_roundtrip(int value) + { + var buffer = ByteBuffer.Allocate(sizeof(int)); + buffer.PutInt(value); + buffer.Position = 0; + var result = buffer.GetInt(); + Assert.Equal(value, result); + Assert.Equal(sizeof(int), buffer.Position); + } + + [Theory] + [InlineData(4, 12345)] + [InlineData(8, -99999)] + public void PutInt_at_index_and_GetInt_roundtrip(int index, int value) + { + var buffer = ByteBuffer.Allocate(index + sizeof(int)); + buffer.Position = index; + int positionBefore = buffer.Position; + buffer.PutInt(index, value); + Assert.Equal(positionBefore, buffer.Position); + buffer.Position = index; + var result = buffer.GetInt(); + Assert.Equal(value, result); + } + + [Theory] + [InlineData(100L)] + [InlineData(-1L)] + [InlineData(long.MaxValue)] + public void PutLong_and_GetLong_roundtrip(long value) + { + var buffer = ByteBuffer.Allocate(sizeof(long)); + buffer.PutLong(value); + buffer.Position = 0; + var result = buffer.GetLong(); + Assert.Equal(value, result); + Assert.Equal(sizeof(long), buffer.Position); + } + + [Theory] + [InlineData(0.0)] + [InlineData(double.PositiveInfinity)] + [InlineData(3.14159265358979)] + public void PutDouble_and_GetDouble_roundtrip(double value) + { + var buffer = ByteBuffer.Allocate(sizeof(double)); + buffer.PutDouble(value); + buffer.Position = 0; + var result = buffer.GetDouble(); + Assert.Equal(BitConverter.DoubleToInt64Bits(value), BitConverter.DoubleToInt64Bits(result)); + Assert.Equal(sizeof(double), buffer.Position); + } + + [Fact] + public void PutDouble_and_GetDouble_roundtrip_NaN() + { + var buffer = ByteBuffer.Allocate(sizeof(double)); + buffer.PutDouble(double.NaN); + buffer.Position = 0; + var result = buffer.GetDouble(); + Assert.Equal(BitConverter.DoubleToInt64Bits(double.NaN), BitConverter.DoubleToInt64Bits(result)); + } + + [Theory] + [InlineData(new byte[] { 0x01, 0x00 }, (short)256)] + [InlineData(new byte[] { 0x00, 0x7F }, (short)127)] + public void GetShort_returns_big_endian_value(byte[] bytes, short expected) + { + var buffer = ByteBuffer.Allocate(bytes.Length); + Buffer.BlockCopy(bytes, 0, ByteBufferTestHelper.GetInternalBuffer(buffer), 0, bytes.Length); + buffer.Position = 0; + var result = buffer.GetShort(); + Assert.Equal(expected, result); + } + } + + /// + /// Test helper to access internal buffer via reflection for test setup. + /// + internal static class ByteBufferTestHelper + { + public static byte[] GetInternalBuffer(ByteBuffer buffer) + { + var field = typeof(ByteBuffer).GetField("_internalBuffer", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (byte[])field!.GetValue(buffer)!; + } + } } diff --git a/HdrHistogram/HdrHistogram.csproj b/HdrHistogram/HdrHistogram.csproj index 26279d1..8d6d4b6 100644 --- a/HdrHistogram/HdrHistogram.csproj +++ b/HdrHistogram/HdrHistogram.csproj @@ -21,6 +21,10 @@ + + + + bin\Release\net8.0\HdrHistogram.xml diff --git a/HdrHistogram/Utilities/ByteBuffer.cs b/HdrHistogram/Utilities/ByteBuffer.cs index 4ce956f..32fefcc 100644 --- a/HdrHistogram/Utilities/ByteBuffer.cs +++ b/HdrHistogram/Utilities/ByteBuffer.cs @@ -9,7 +9,7 @@ */ using System; -using System.Net; +using System.Buffers.Binary; namespace HdrHistogram.Utilities { @@ -108,8 +108,8 @@ public byte Get() /// The value of the at the current position. public short GetShort() { - var shortValue = IPAddress.HostToNetworkOrder(BitConverter.ToInt16(_internalBuffer, Position)); - Position += (sizeof(short)); + var shortValue = BinaryPrimitives.ReadInt16BigEndian(_internalBuffer.AsSpan(Position)); + Position += sizeof(short); return shortValue; } @@ -119,7 +119,7 @@ public short GetShort() /// The value of the at the current position. public int GetInt() { - var intValue = IPAddress.HostToNetworkOrder(BitConverter.ToInt32(_internalBuffer, Position)); + var intValue = BinaryPrimitives.ReadInt32BigEndian(_internalBuffer.AsSpan(Position)); Position += sizeof(int); return intValue; } @@ -130,7 +130,7 @@ public int GetInt() /// The value of the at the current position. public long GetLong() { - var longValue = IPAddress.HostToNetworkOrder(BitConverter.ToInt64(_internalBuffer, Position)); + var longValue = BinaryPrimitives.ReadInt64BigEndian(_internalBuffer.AsSpan(Position)); Position += sizeof(long); return longValue; } @@ -141,88 +141,9 @@ public long GetLong() /// The value of the at the current position. public double GetDouble() { - var doubleValue = Int64BitsToDouble(ToInt64(_internalBuffer, Position)); + var longBits = BinaryPrimitives.ReadInt64BigEndian(_internalBuffer.AsSpan(Position)); Position += sizeof(double); - return doubleValue; - } - - /// - /// Converts the specified 64-bit signed integer to a double-precision - /// floating point number. Note: the endianness of this converter does not - /// affect the returned value. - /// - /// The number to convert. - /// A double-precision floating point number whose value is equivalent to value. - private static double Int64BitsToDouble(long value) - { - return BitConverter.Int64BitsToDouble(value); - } - - /// - /// Returns a 64-bit signed integer converted from eight bytes at a specified position in a byte array. - /// - /// An array of bytes. - /// The starting position within value. - /// A 64-bit signed integer formed by eight bytes beginning at startIndex. - private static long ToInt64(byte[] value, int startIndex) - { - return CheckedFromBytes(value, startIndex, 8); - } - - /// - /// Checks the arguments for validity before calling FromBytes - /// (which can therefore assume the arguments are valid). - /// - /// The bytes to convert after checking - /// The index of the first byte to convert - /// The number of bytes to convert - /// - private static long CheckedFromBytes(byte[] value, int startIndex, int bytesToConvert) - { - CheckByteArgument(value, startIndex, bytesToConvert); - return FromBytes(value, startIndex, bytesToConvert); - } - - /// - /// Checks the given argument for validity. - /// - /// The byte array passed in - /// The start index passed in - /// The number of bytes required - /// value is a null reference - /// - /// startIndex is less than zero or greater than the length of value minus bytesRequired. - /// - private static void CheckByteArgument(byte[] value, int startIndex, int bytesRequired) - { -#pragma warning disable CA1510 - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } -#pragma warning restore CA1510 - if (startIndex < 0 || startIndex > value.Length - bytesRequired) - { - throw new ArgumentOutOfRangeException(nameof(startIndex)); - } - } - - /// - /// Returns a value built from the specified number of bytes from the given buffer, - /// starting at index. - /// - /// The data in byte array format - /// The first index to use - /// The number of bytes to use - /// The value built from the given bytes - private static long FromBytes(byte[] buffer, int startIndex, int bytesToConvert) - { - long ret = 0; - for (int i = 0; i < bytesToConvert; i++) - { - ret = unchecked((ret << 8) | buffer[startIndex + i]); - } - return ret; + return BitConverter.Int64BitsToDouble(longBits); } /// @@ -240,9 +161,8 @@ public void Put(byte value) /// The value to set the current position to. public void PutInt(int value) { - var intAsBytes = BitConverter.GetBytes(IPAddress.NetworkToHostOrder(value)); - Array.Copy(intAsBytes, 0, _internalBuffer, Position, intAsBytes.Length); - Position += intAsBytes.Length; + BinaryPrimitives.WriteInt32BigEndian(_internalBuffer.AsSpan(Position), value); + Position += sizeof(int); } /// @@ -255,8 +175,7 @@ public void PutInt(int value) /// public void PutInt(int index, int value) { - var intAsBytes = BitConverter.GetBytes(IPAddress.NetworkToHostOrder(value)); - Array.Copy(intAsBytes, 0, _internalBuffer, index, intAsBytes.Length); + BinaryPrimitives.WriteInt32BigEndian(_internalBuffer.AsSpan(index), value); // We don't increment the Position as this is an explicit write. } @@ -266,9 +185,8 @@ public void PutInt(int index, int value) /// The value to set the current position to. public void PutLong(long value) { - var longAsBytes = BitConverter.GetBytes(IPAddress.NetworkToHostOrder(value)); - Array.Copy(longAsBytes, 0, _internalBuffer, Position, longAsBytes.Length); - Position += longAsBytes.Length; + BinaryPrimitives.WriteInt64BigEndian(_internalBuffer.AsSpan(Position), value); + Position += sizeof(long); } /// @@ -277,11 +195,8 @@ public void PutLong(long value) /// The value to set the current position to. public void PutDouble(double value) { - //PutDouble(ix(CheckIndex(i, (1 << 3))), x); - var doubleAsBytes = BitConverter.GetBytes(value); - Array.Reverse(doubleAsBytes); - Array.Copy(doubleAsBytes, 0, _internalBuffer, Position, doubleAsBytes.Length); - Position += doubleAsBytes.Length; + BinaryPrimitives.WriteInt64BigEndian(_internalBuffer.AsSpan(Position), BitConverter.DoubleToInt64Bits(value)); + Position += sizeof(double); } ///