Created
May 5, 2023 20:47
-
-
Save westonpace/35b619c4c9e90824d93ecb8306a9e169 to your computer and use it in GitHub Desktop.
Benchmark comparing two different ways to create a buffer builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net7.0</TargetFramework> | |
<ImplicitUsings>enable</ImplicitUsings> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" /> | |
</ItemGroup> | |
</Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Licensed to the Apache Software Foundation (ASF) under one or more | |
// contributor license agreements. See the NOTICE file distributed with | |
// this work for additional information regarding copyright ownership. | |
// The ASF licenses this file to You under the Apache License, Version 2.0 | |
// (the "License"); you may not use this file except in compliance with | |
// the License. You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Configs; | |
using BenchmarkDotNet.Engines; | |
using BenchmarkDotNet.Running; | |
using System; | |
using System.Diagnostics; | |
using System.Runtime.CompilerServices; | |
using System.Runtime.InteropServices; | |
public static class BitUtility | |
{ | |
private static ReadOnlySpan<byte> PopcountTable => new byte[] { | |
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, | |
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, | |
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, | |
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | |
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, | |
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | |
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, | |
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, | |
}; | |
private static ReadOnlySpan<byte> BitMask => new byte[] { | |
1, 2, 4, 8, 16, 32, 64, 128 | |
}; | |
public static bool GetBit(byte data, int index) => | |
((data >> index) & 1) != 0; | |
public static bool GetBit(ReadOnlySpan<byte> data, int index) => | |
(data[index / 8] & BitMask[index % 8]) != 0; | |
public static void ClearBit(Span<byte> data, int index) | |
{ | |
data[index / 8] &= (byte)~BitMask[index % 8]; | |
} | |
public static void SetBit(Span<byte> data, int index) | |
{ | |
data[index / 8] |= BitMask[index % 8]; | |
} | |
public static void SetBit(ref byte data, int index, bool value) | |
{ | |
int mod = index % 8; | |
data = value | |
? (byte)(data | BitMask[mod]) | |
: (byte)(data & ~BitMask[mod]); | |
} | |
public static void SetBit(Span<byte> data, int index, bool value) | |
{ | |
int idx = index / 8; | |
int mod = index % 8; | |
data[idx] = value | |
? (byte)(data[idx] | BitMask[mod]) | |
: (byte)(data[idx] & ~BitMask[mod]); | |
} | |
public static void ToggleBit(Span<byte> data, int index) | |
{ | |
data[index / 8] ^= BitMask[index % 8]; | |
} | |
/// <summary> | |
/// Counts the number of set bits in a span of bytes starting | |
/// at a specific bit offset. | |
/// </summary> | |
/// <param name="data">Span to count bits</param> | |
/// <param name="offset">Bit offset to start counting from</param> | |
/// <returns>Count of set (one) bits</returns> | |
public static int CountBits(ReadOnlySpan<byte> data, int offset) => | |
CountBits(data, offset, data.Length * 8 - offset); | |
/// <summary> | |
/// Counts the number of set bits in a span of bytes starting | |
/// at a specific bit offset, and limiting to a certain number of bits | |
/// in the span. | |
/// </summary> | |
/// <param name="data">Span to count bits.</param> | |
/// <param name="offset">Bit offset to start counting from.</param> | |
/// <param name="length">Maximum of bits in the span to consider.</param> | |
/// <returns>Count of set (one) bits</returns> | |
public static int CountBits(ReadOnlySpan<byte> data, int offset, int length) | |
{ | |
int startByteIndex = offset / 8; | |
int startBitOffset = offset % 8; | |
int endByteIndex = (offset + length - 1) / 8; | |
int endBitOffset = (offset + length - 1) % 8; | |
if (startBitOffset < 0) | |
return 0; | |
int count = 0; | |
if (startByteIndex == endByteIndex) | |
{ | |
// Range starts and ends within the same byte. | |
var slice = data.Slice(startByteIndex, 1); | |
for (int i = startBitOffset; i <= endBitOffset; i++) | |
count += GetBit(slice, i) ? 1 : 0; | |
return count; | |
} | |
// If the starting index and ending index are not byte-aligned, | |
// we'll need to count bits the slow way. If they are | |
// byte-aligned, and for all other bytes in the 'middle', we | |
// can use a faster byte-aligned count. | |
int fullByteStartIndex = startBitOffset == 0 ? startByteIndex : startByteIndex + 1; | |
int fullByteEndIndex = endBitOffset == 7 ? endByteIndex : endByteIndex - 1; | |
if (startBitOffset != 0) | |
{ | |
var slice = data.Slice(startByteIndex, 1); | |
for (int i = startBitOffset; i <= 7; i++) | |
count += GetBit(slice, i) ? 1 : 0; | |
} | |
if (fullByteEndIndex >= fullByteStartIndex) | |
{ | |
var slice = data.Slice(fullByteStartIndex, fullByteEndIndex - fullByteStartIndex + 1); | |
count += CountBits(slice); | |
} | |
if (endBitOffset != 7) | |
{ | |
var slice = data.Slice(endByteIndex, 1); | |
for (int i = 0; i <= endBitOffset; i++) | |
count += GetBit(slice, i) ? 1 : 0; | |
} | |
return count; | |
} | |
/// <summary> | |
/// Counts the number of set bits in a span of bytes. | |
/// </summary> | |
/// <param name="data">Span to count bits</param> | |
/// <returns>Count of set (one) bits.</returns> | |
public static int CountBits(ReadOnlySpan<byte> data) | |
{ | |
int count = 0; | |
foreach (byte t in data) | |
count += PopcountTable[t]; | |
return count; | |
} | |
/// <summary> | |
/// Rounds an integer to the nearest multiple of 64. | |
/// </summary> | |
/// <param name="n">Integer to round.</param> | |
/// <returns>Integer rounded to the nearest multiple of 64.</returns> | |
public static long RoundUpToMultipleOf64(long n) => | |
RoundUpToMultiplePowerOfTwo(n, 64); | |
/// <summary> | |
/// Rounds an integer to the nearest multiple of 8. | |
/// </summary> | |
/// <param name="n">Integer to round.</param> | |
/// <returns>Integer rounded to the nearest multiple of 8.</returns> | |
public static long RoundUpToMultipleOf8(long n) => | |
RoundUpToMultiplePowerOfTwo(n, 8); | |
/// <summary> | |
/// Rounds an integer up to the nearest multiple of factor, where | |
/// factor must be a power of two. | |
/// | |
/// This function does not throw when the factor is not a power of two. | |
/// </summary> | |
/// <param name="n">Integer to round up.</param> | |
/// <param name="factor">Power of two factor to round up to.</param> | |
/// <returns>Integer rounded up to the nearest power of two.</returns> | |
public static long RoundUpToMultiplePowerOfTwo(long n, int factor) | |
{ | |
// Assert that factor is a power of two. | |
Debug.Assert(factor > 0 && (factor & (factor - 1)) == 0); | |
return (n + (factor - 1)) & ~(factor - 1); | |
} | |
internal static bool IsMultipleOf8(long n) => n % 8 == 0; | |
/// <summary> | |
/// Calculates the number of bytes required to store n bits. | |
/// </summary> | |
/// <param name="n">number of bits</param> | |
/// <returns>number of bytes</returns> | |
public static int ByteCount(int n) | |
{ | |
Debug.Assert(n >= 0); | |
return n / 8 + (n % 8 != 0 ? 1 : 0); // ceil(n / 8) | |
} | |
internal static int ReadInt32(ReadOnlyMemory<byte> value) | |
{ | |
Debug.Assert(value.Length >= sizeof(int)); | |
return Unsafe.ReadUnaligned<int>(ref MemoryMarshal.GetReference(value.Span)); | |
} | |
// Bytes | |
public static byte ToByte(ref byte data, ReadOnlySpan<bool> bits) | |
{ | |
for (int i = 0; i < Math.Min(8, bits.Length); i++) | |
{ | |
SetBit(ref data, i, bits[i]); | |
} | |
return data; | |
} | |
public static void ToBytes(Span<byte> bytes, ReadOnlySpan<bool> bits) | |
{ | |
for (int i = 0; i < bytes.Length; i++) | |
{ | |
ToByte(ref bytes[i], bits.Slice(i * 8)); | |
} | |
} | |
// Bits | |
public static void ToBits(Span<bool> bools, byte value) | |
{ | |
for (int i = 0; i < 8; i++) | |
{ | |
bools[i] = GetBit(value, i); | |
} | |
} | |
public static bool[] ToBits(byte value) | |
{ | |
bool[] boolArray = new bool[8]; // initialize bool array with correct length | |
for (int i = 0; i < 8; i++) | |
{ | |
boolArray[i] = GetBit(value, i); | |
} | |
return boolArray; | |
} | |
public static void ToBits(Span<bool> bits, ReadOnlySpan<byte> bytes) | |
{ | |
for (int i = 0; i < bytes.Length; i++) | |
{ | |
ToBits(bits.Slice(i * 8, 8), bytes[i]); | |
} | |
} | |
public static Span<bool> ToBits(ReadOnlySpan<byte> bytes) | |
{ | |
Span<bool> bits = new bool[bytes.Length * 8].AsSpan(); | |
ToBits(bits, bytes); | |
return bits; | |
} | |
internal static int NextPowerOfTwo(int n) | |
{ | |
// 16 for int32 | |
n--; | |
n |= n >> 1; | |
n |= n >> 2; | |
n |= n >> 4; | |
n |= n >> 8; | |
n |= n >> 16; | |
n++; | |
return n; | |
} | |
} | |
public class BufferBuilder | |
{ | |
public class BitBuffer | |
{ | |
private readonly bool[] _bits; | |
public int Length { get; private set; } | |
public int AvailableLength => Capacity - Length; | |
public int Capacity; | |
public bool IsFull => Length == Capacity; | |
public byte ToByte(ref byte data) => BitUtility.ToByte(ref data, _bits); | |
public BitBuffer(int capacity = 8) | |
{ | |
Capacity = capacity; | |
_bits = new bool[capacity]; | |
Length = 0; | |
} | |
public void Append(bool bit) => _bits[Length++] = bit; | |
public void Fill(ReadOnlySpan<bool> bits) | |
{ | |
bits.CopyTo(_bits.AsSpan().Slice(Length, bits.Length)); | |
Length += bits.Length; | |
} | |
public void Reset() | |
{ | |
for (int i = 0; i < _bits.Length; i++) | |
{ | |
_bits[i] = false; | |
} | |
Length = 0; | |
} | |
} | |
private const int DefaultCapacity = 64; | |
public int ByteLength { get; private set; } | |
public Memory<byte> Memory { get; private set; } | |
public BitBuffer BitOverhead { get; } | |
/// <summary> | |
/// Creates an instance of the <see cref="BufferBuilder"/> class. | |
/// </summary> | |
/// <param name="valueBitSize">Number of bits of one value item.</param> | |
/// <param name="capacity">Number of items of initial capacity to reserve.</param> | |
public BufferBuilder(int capacity = DefaultCapacity) | |
{ | |
Memory = new byte[capacity]; | |
BitOverhead = new BitBuffer(); | |
ByteLength = 0; | |
} | |
private void CommitBitBuffer(bool force = false) | |
{ | |
if (BitOverhead.IsFull || force) | |
{ | |
EnsureAdditionalBytes(1); | |
BitOverhead.ToByte(ref Memory.Span[ByteLength]); | |
BitOverhead.Reset(); | |
ByteLength++; | |
} | |
} | |
public BufferBuilder AppendBit(bool bit) | |
{ | |
BitOverhead.Append(bit); | |
CommitBitBuffer(); | |
return this; | |
} | |
public BufferBuilder AppendBits(ReadOnlySpan<bool> bits) | |
{ | |
if (BitOverhead.Length > 0) | |
{ | |
int available = BitOverhead.AvailableLength; | |
if (bits.Length > available) | |
{ | |
// Fill byte buffer | |
BitOverhead.Fill(bits.Slice(0, available)); | |
// Commit to memory and reset | |
CommitBitBuffer(); | |
bits = bits.Slice(available); | |
} | |
else | |
{ | |
// Fill byte buffer | |
BitOverhead.Fill(bits); | |
bits = ReadOnlySpan<bool>.Empty; | |
} | |
} | |
if (bits.Length > 0) | |
{ | |
int byteEnd = bits.Length / 8; | |
int bitEnd = byteEnd * 8; | |
if (byteEnd > 0) | |
{ | |
// Ensure byte length | |
EnsureAdditionalBytes(byteEnd); | |
// Raw Span copy to memory | |
BitUtility.ToBytes(Memory.Span.Slice(ByteLength, byteEnd), bits.Slice(0, bitEnd)); | |
ByteLength += byteEnd; | |
bits = bits.Slice(bitEnd); | |
} | |
if (bits.Length > 0) | |
{ | |
// Fill byte buffer with last unfilled | |
BitOverhead.Fill(bits); | |
} | |
} | |
return this; | |
} | |
public BufferBuilder AppendBits(bool value, int count) | |
{ | |
Span<bool> span = new bool[count]; | |
// default bool are already false | |
if (value) | |
for (int i = 0; i < count; i++) | |
span[i] = value; | |
return AppendBits(span); | |
} | |
public BufferBuilder AppendByte(byte byteValue) | |
{ | |
if (BitOverhead.Length > 0) | |
{ | |
// Fill current bit buffer | |
int available = BitOverhead.AvailableLength; | |
// Convert byte to bit array | |
Span<bool> bits = BitUtility.ToBits(byteValue).AsSpan(); | |
// Fill byte buffer | |
BitOverhead.Fill(bits.Slice(0, available)); | |
// Commit to memory and reset | |
CommitBitBuffer(); | |
// Fill new bit buffer | |
BitOverhead.Fill(bits.Slice(available)); | |
} | |
else | |
{ | |
EnsureAdditionalBytes(1); | |
// Raw add to memory | |
Memory.Span[ByteLength] = byteValue; | |
ByteLength++; | |
} | |
return this; | |
} | |
public BufferBuilder AppendBytes(ReadOnlySpan<byte> bytes) | |
{ | |
if (BitOverhead.Length == 0) | |
{ | |
EnsureAdditionalBytes(bytes.Length); | |
// Raw Span copy to memory | |
bytes.CopyTo(Memory.Span.Slice(ByteLength, bytes.Length)); | |
ByteLength += bytes.Length; | |
} | |
else | |
{ | |
// Convert Bytes to Bits streamed in batchsize = 64 | |
int offset = 0; | |
while (offset < bytes.Length) | |
{ | |
int remainingBytes = bytes.Length - offset; | |
int bufferLength = Math.Min(128, remainingBytes); | |
// Bits span | |
Span<bool> buffer = new bool[bufferLength * 8]; | |
// Fill bits | |
BitUtility.ToBits(buffer, bytes.Slice(offset, bufferLength)); | |
// Append batch bits | |
AppendBits(buffer); | |
offset += bufferLength; | |
} | |
} | |
return this; | |
} | |
internal BufferBuilder ReserveBytes(int numBytes) | |
{ | |
EnsureAdditionalBytes(numBytes); | |
return this; | |
} | |
internal BufferBuilder ResizeBytes(int numBytes) | |
{ | |
EnsureBytes(numBytes); | |
ByteLength = numBytes; | |
return this; | |
} | |
public BufferBuilder Clear() | |
{ | |
Memory.Span.Fill(default); | |
ByteLength = 0; | |
return this; | |
} | |
private void EnsureAdditionalBytes(int numBytes) => EnsureBytes(checked(ByteLength + numBytes)); | |
internal void EnsureBytes(int numBytes) | |
{ | |
if (numBytes > Memory.Length) | |
{ | |
int twice = checked(Memory.Length * 2); | |
Reallocate(twice < numBytes ? BitUtility.NextPowerOfTwo(numBytes) : twice); | |
} | |
} | |
private void Reallocate(int numBytes) | |
{ | |
var memory = new Memory<byte>(new byte[numBytes]); | |
Memory.CopyTo(memory); | |
Memory = memory; | |
} | |
} | |
public class NoOverheadBufferBuilder | |
{ | |
private const int DefaultCapacity = 64; | |
public int ByteLength { get; private set; } | |
public int BitOffset { get; private set; } | |
public Memory<byte> Memory { get; private set; } | |
/// <summary> | |
/// Creates an instance of the <see cref="BufferBuilder"/> class. | |
/// </summary> | |
/// <param name="valueBitSize">Number of bits of one value item.</param> | |
/// <param name="capacity">Number of items of initial capacity to reserve.</param> | |
public NoOverheadBufferBuilder(int capacity = DefaultCapacity) | |
{ | |
Memory = new byte[capacity]; | |
ByteLength = 0; | |
BitOffset = 0; | |
} | |
public void AppendBit(bool bit) | |
{ | |
BitUtility.SetBit(ref Memory.Span[ByteLength], BitOffset, bit); | |
BitOffset++; | |
if (BitOffset == 8) | |
{ | |
BitOffset = 0; | |
ByteLength++; | |
EnsureBytes(ByteLength); | |
} | |
} | |
public void AppendBits(bool value, int count) | |
{ | |
int remainderBits = Math.Min(count, 8 - BitOffset); | |
int wholeBytes = (count - remainderBits) / 8; | |
int trailingBits = count - remainderBits - (wholeBytes * 8); | |
int newBytes = (trailingBits > 0) ? wholeBytes + 1 : wholeBytes; | |
EnsureAdditionalBytes(newBytes); | |
var span = Memory.Span; | |
for (int i = 0; i < remainderBits; i++) | |
{ | |
BitUtility.SetBit(ref span[ByteLength], BitOffset, value); | |
BitOffset++; | |
} | |
if (BitOffset == 8) | |
{ | |
BitOffset = 0; | |
ByteLength++; | |
} | |
if (wholeBytes > 0) | |
{ | |
var fill = (byte)(value ? 0xFF : 0x00); | |
span.Slice(ByteLength, wholeBytes).Fill(fill); | |
} | |
ByteLength += wholeBytes; | |
for (int i = 0; i < trailingBits; i++) | |
{ | |
BitUtility.SetBit(ref span[ByteLength], BitOffset, value); | |
BitOffset++; | |
} | |
} | |
public void Clear() | |
{ | |
Memory.Span.Fill(default); | |
ByteLength = 0; | |
BitOffset = 0; | |
} | |
private void EnsureAdditionalBytes(int numBytes) => EnsureBytes(checked(ByteLength + numBytes)); | |
internal void EnsureBytes(int numBytes) | |
{ | |
if (numBytes > Memory.Length) | |
{ | |
int twice = checked(Memory.Length * 2); | |
Reallocate(twice < numBytes ? BitUtility.NextPowerOfTwo(numBytes) : twice); | |
} | |
} | |
private void Reallocate(int numBytes) | |
{ | |
var memory = new Memory<byte>(new byte[numBytes]); | |
Memory.CopyTo(memory); | |
Memory = memory; | |
} | |
} | |
public class Benchmarks | |
{ | |
private BufferBuilder builder = new BufferBuilder(); | |
private NoOverheadBufferBuilder noOverheadBuilder = new NoOverheadBufferBuilder(); | |
[Params(1, 100, 10000, 100000)] | |
public int NumBits { get; set; } | |
[Benchmark] | |
public void AppendFalse() | |
{ | |
builder.AppendBits(false, NumBits); | |
builder.Clear(); | |
} | |
[Benchmark] | |
public void AppendTrue() | |
{ | |
builder.AppendBits(true, NumBits); | |
builder.Clear(); | |
} | |
[Benchmark] | |
public void AppendFalseNoOverhead() | |
{ | |
noOverheadBuilder.AppendBits(false, NumBits); | |
noOverheadBuilder.Clear(); | |
} | |
[Benchmark] | |
public void AppendTrueNoOverhead() | |
{ | |
noOverheadBuilder.AppendBits(true, NumBits); | |
noOverheadBuilder.Clear(); | |
} | |
} | |
public class Program | |
{ | |
public static void Main() | |
{ | |
// BenchmarkRunner.Run<Benchmarks>(new DebugInProcessConfig()); | |
BenchmarkRunner.Run<Benchmarks>(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment