-
-
Save vexx32/d2b7fff82f93635ea974bdbc0c26fa89 to your computer and use it in GitHub Desktop.
This file contains 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
using System; | |
using System.Numerics; | |
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Running; | |
namespace Benchmark | |
{ | |
[CoreJob] | |
[MemoryDiagnoser] | |
public class BinaryParse | |
{ | |
[Params( | |
"1100", | |
"01101011", | |
"1100011101011100", | |
"01111100110011001000010111001101", | |
"0010010000101101110010000010000001101100010100010010011100110101"/*, | |
"10010100001010100110100000010100010000101001110010000000101010011100011100111010011011001000100111011110111100001110010110101001", | |
"1110011111100101100110001111110111000101100110110001001100011110111111010000010011111101111100101001001110001001110000001001110110010100101010001111100111110010010010100100001101100110100001011111111010100001100011100000101000001101000111110001000110100101", | |
"01010010001111101000001001110001111110001000010010001100011111011110110100110010100010011000010111001011101100111010000111110111001010101111100101101000101010110100101111011001011000111111010000101101110100011100001101010000101110101000111000100000011000110100001111101011000101101011111101010001111101111110011000011101010110001011000010000000000100110000111100110001000001001011110001100000011111100011010001011011011100101000101111011101101100100000011011000111101111000101010100000101010010100101100110111000", | |
"0101001000111110100000100111000111111000100001001000110001111101111011010011001010001001100001011100101110110011101000011111011100101010111110010110100010101011010010111101100101100011111101000010110111010001110000110101000010111010100011100010000001100011010000111110101100010110101111110101000111110111111001100001110101011000101100001000000000010011000011110011000100000100101111000110000001111110001101000101101101110010100010111101110110110010000001101100011110111100010101010000010101001010010110011011100001010010001111101000001001110001111110001000010010001100011111011110110100110010100010011000010111001011101100111010000111110111001010101111100101101000101010110100101111011001011000111111010000101101110100011100001101010000101110101000111000100000011000110100001111101011000101101011111101010001111101111110011000011101010110001011000010000000000100110000111100110001000001001011110001100000011111100011010001011011011100101000101111011101101100100000011011000111101111000101010100000101010010100101100110111000" | |
*/)] | |
public string data; | |
[GlobalSetup] | |
public void GlobalSetup() | |
{ | |
} | |
public BinaryParse() | |
{ | |
} | |
[Benchmark] | |
public BigInteger CurrentParser() => ParseBinaryCurrent(data); | |
[Benchmark] | |
public BigInteger TweakTest() => ParseBinaryTweaked(data); | |
internal static BigInteger ParseBinaryCurrent(ReadOnlySpan<char> digits, bool unsigned = false) | |
{ | |
if (!unsigned) | |
{ | |
if (digits[0] == '0') | |
{ | |
unsigned = true; | |
} | |
else | |
{ | |
switch (digits.Length) | |
{ | |
// Only accept sign bits at these lengths: | |
case 8: // byte | |
case 16: // short | |
case 32: // int | |
case 64: // long | |
case 96: // decimal | |
case int n when n >= 128: // BigInteger | |
break; | |
default: | |
// If we do not flag these as unsigned, bigint assumes a sign bit for any (8 * n) string length | |
unsigned = true; | |
break; | |
} | |
} | |
} | |
// Only use heap allocation for very large numbers | |
const int MaxStackAllocation = 512; | |
// Calculate number of 8-bit bytes needed to hold the input, rounded up to next whole number. | |
int outputByteCount = (digits.Length + 7) / 8; | |
Span<byte> outputBytes = outputByteCount <= MaxStackAllocation ? stackalloc byte[outputByteCount] : new byte[outputByteCount]; | |
int outputByteIndex = outputBytes.Length - 1; | |
// We need to be prepared for any partial leading bytes, (e.g., 010|00000011|00101100), or cases | |
// where we only have less than 8 bits to work with from the beginning. | |
// | |
// Walk bytes right to left, stepping one whole byte at a time (if there are any whole bytes). | |
int byteWalker; | |
for (byteWalker = digits.Length - 1; byteWalker >= 7; byteWalker -= 8) | |
{ | |
// Use bit shifts and binary-or to sum the values in each byte. These calculations will | |
// create values higher than a single byte, but the higher bits will be stripped out when cast | |
// to byte. | |
// | |
// The low bits are added in separately to allow us to strip the higher 'noise' bits before we | |
// sum the values using binary-or. | |
// | |
// Simplified representation of logic: (byte)( (7)|(6)|(5)|(4) ) | ( ( (3)|(2)|(1)|(0) ) & 0b1111 ) | |
// | |
// N.B.: This code has been tested against a straight for loop iterating through the byte, and in no | |
// circumstance was it faster or more effective than this unrolled version. | |
outputBytes[outputByteIndex--] = | |
(byte)( | |
((digits[byteWalker - 7] << 7) | |
| (digits[byteWalker - 6] << 6) | |
| (digits[byteWalker - 5] << 5) | |
| (digits[byteWalker - 4] << 4) | |
) | |
| ( | |
((digits[byteWalker - 3] << 3) | |
| (digits[byteWalker - 2] << 2) | |
| (digits[byteWalker - 1] << 1) | |
| (digits[byteWalker]) | |
) & 0b1111 | |
) | |
); | |
} | |
// With complete bytes parsed, byteWalker is either at the partial byte start index, or at -1 | |
if (byteWalker >= 0) | |
{ | |
int currentByteValue = 0; | |
for (int i = 0; i <= byteWalker; i++) | |
{ | |
currentByteValue = (currentByteValue << 1) | (digits[i] - '0'); | |
} | |
outputBytes[outputByteIndex] = (byte)currentByteValue; | |
} | |
return new BigInteger(outputBytes, isUnsigned: unsigned, isBigEndian: true); | |
} | |
internal static BigInteger ParseBinaryTweaked(ReadOnlySpan<char> digits, bool unsigned = false) | |
{ | |
if (!unsigned) | |
{ | |
if (digits[0] == '0') | |
{ | |
unsigned = true; | |
} | |
else | |
{ | |
switch (digits.Length) | |
{ | |
// Only accept sign bits at these lengths: | |
case 8: // byte | |
case 16: // short | |
case 32: // int | |
case 64: // long | |
case 96: // decimal | |
case int n when n >= 128: // BigInteger | |
break; | |
default: | |
// If we do not flag these as unsigned, bigint assumes a sign bit for any (8 * n) string length | |
unsigned = true; | |
break; | |
} | |
} | |
} | |
// Only use heap allocation for very large numbers; preallocate for compiler optimizations | |
const int MaxStackAllocation = 512; | |
Span<byte> outputBytes = stackalloc byte[MaxStackAllocation]; | |
// If we haven't allocated enough, we need to use the heap. | |
int outputByteCount = (digits.Length + 7) / 8; | |
if (outputByteCount > MaxStackAllocation) | |
{ | |
outputBytes = new byte[outputByteCount]; | |
} | |
int outputByteIndex = outputBytes.Length - 1; | |
// We need to be prepared for any partial leading bytes, (e.g., 010|00000011|00101100), or cases | |
// where we only have less than 8 bits to work with from the beginning. | |
// | |
// Walk bytes right to left, stepping one whole byte at a time (if there are any whole bytes). | |
int byteWalker; | |
for (byteWalker = digits.Length - 1; byteWalker >= 7; byteWalker -= 8) | |
{ | |
// Use bit shifts and binary-or to sum the values in each byte. These calculations will | |
// create values higher than a single byte, but the higher bits will be stripped out when cast | |
// to byte. | |
// | |
// The low bits are added in separately to allow us to strip the higher 'noise' bits before we | |
// sum the values using binary-or. | |
// | |
// Simplified representation of logic: (byte)( (7)|(6)|(5)|(4) ) | ( ( (3)|(2)|(1)|(0) ) & 0b1111 ) | |
// | |
// N.B.: This code has been tested against a straight for loop iterating through the byte, and in no | |
// circumstance was it faster or more effective than this unrolled version. | |
outputBytes[outputByteIndex--] = | |
(byte)( | |
((digits[byteWalker - 7] << 7) | |
| (digits[byteWalker - 6] << 6) | |
| (digits[byteWalker - 5] << 5) | |
| (digits[byteWalker - 4] << 4) | |
) | |
| ( | |
((digits[byteWalker - 3] << 3) | |
| (digits[byteWalker - 2] << 2) | |
| (digits[byteWalker - 1] << 1) | |
| (digits[byteWalker]) | |
) & 0b1111 | |
) | |
); | |
} | |
// With complete bytes parsed, byteWalker is either at the partial byte start index, or at -1 | |
if (byteWalker >= 0) | |
{ | |
int currentByteValue = 0; | |
for (int i = 0; i <= byteWalker; i++) | |
{ | |
currentByteValue = (currentByteValue << 1) | (digits[i] - '0'); | |
} | |
outputBytes[outputByteIndex] = (byte)currentByteValue; | |
} | |
return new BigInteger(outputBytes, isUnsigned: unsigned, isBigEndian: true); | |
} | |
} | |
public class Program | |
{ | |
public static void Main(string[] args) | |
{ | |
var summary = BenchmarkRunner.Run<BinaryParse>(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment