I needed this too, so I built the OP and populated all the reads / records (except for char and lines, since they are a bit special).
I also did a quick unit test to try. For streams containing only logical (or other custom types of sub-byte values), it is obviously 87.5% cheaper, and for a random mixed stream containing 75% of Boolean values, it was about 33% cheaper. Therefore, it may be useful for some scenarios.
Here are both classes, if someone else needs them, use at your own risk:
/// <summary> /// A binary writer that packs data into bits, to preserve space when using many bit/boolean values. Up to about 87.5% cheaper for streams that only contains boolean values. /// By: jsmars@gmail.com , based on posters classes in this post: https://stackoverflow.com/questions/7051939/bit-based-binarywriter-in-c-sharp /// </summary> public class BinaryBitWriter : BinaryWriter { public byte BitPosition { get; private set; } = 0; private bool[] curByte = new bool[8]; private System.Collections.BitArray ba; public BinaryBitWriter(Stream s) : base(s) { } public override void Flush() { flushBitBuffer(); base.Flush(); } public override void Write(byte[] buffer, int index, int count) { for (int i = index; i < index + count; i++) Write((byte)buffer[i]); } public override void Write(byte value) { ba = new BitArray(new byte[] { value }); for (byte i = 0; i < 8; i++) Write(ba[i]); } public override void Write(bool value) { curByte[BitPosition] = value; BitPosition++; if (BitPosition == 8) flushBitBuffer(); } public override void Write(char[] chars, int index, int count) { for (int i = index; i < index + count; i++) Write(chars[i]); } public override void Write(string value) { // write strings as normal for now, so flush the bits first flushBitBuffer(); base.Write(value); } public override void Write(decimal value) { var ints = decimal.GetBits(value); for (int i = 0; i < ints.Length; i++) Write(ints[i]); } public override void Write(float value) => Write(BitConverter.GetBytes(value)); public override void Write(ulong value) => Write(BitConverter.GetBytes(value)); public override void Write(long value) => Write(BitConverter.GetBytes(value)); public override void Write(uint value) => Write(BitConverter.GetBytes(value)); public override void Write(int value) => Write(BitConverter.GetBytes(value)); public override void Write(ushort value) => Write(BitConverter.GetBytes(value)); public override void Write(short value) => Write(BitConverter.GetBytes(value)); public override void Write(double value) => Write(BitConverter.GetBytes(value)); public override void Write(char[] value) => Write(value, 0, value.Length); public override void Write(char value) { // write strings as normal for now, so flush the bits first flushBitBuffer(); base.Write(value); //var b = BitConverter.GetBytes(value); //Write(b); } public override void Write(byte[] buffer) => Write(buffer, 0, buffer.Length); public override void Write(sbyte value) => Write((byte)value); void flushBitBuffer() { if (BitPosition == 0) // Nothing to flush return; base.Write(ConvertToByte(curByte)); BitPosition = 0; curByte = new bool[8]; } private static byte ConvertToByte(bool[] bools) { byte b = 0; byte bitIndex = 0; for (int i = 0; i < 8; i++) { if (bools[i]) b |= (byte)(((byte)1) << bitIndex); bitIndex++; } return b; } } public class BinaryBitReader : BinaryReader { public byte BitPosition { get; private set; } = 8; private bool[] curByte = new bool[8]; public BinaryBitReader(Stream s) : base(s) { } public override bool ReadBoolean() { if (BitPosition == 8) { var ba = new BitArray(new byte[] { base.ReadByte() }); ba.CopyTo(curByte, 0); BitPosition = 0; } bool b = curByte[BitPosition]; BitPosition++; return b; } public override byte ReadByte() { bool[] bar = new bool[8]; byte i; for (i = 0; i < 8; i++) { bar[i] = this.ReadBoolean(); } byte b = 0; byte bitIndex = 0; for (i = 0; i < 8; i++) { if (bar[i]) { b |= (byte)(((byte)1) << bitIndex); } bitIndex++; } return b; } public override byte[] ReadBytes(int count) { byte[] bytes = new byte[count]; for (int i = 0; i < count; i++) { bytes[i] = this.ReadByte(); } return bytes; } //public override int Read() => BitConverter.ToUInt64(ReadBytes(8), 0); public override int Read(byte[] buffer, int index, int count) { for (int i = index; i < index + count; i++) buffer[i] = ReadByte(); return count; // we can return this here, it will die at the above row if anything is off } public override int Read(char[] buffer, int index, int count) { for (int i = index; i < index + count; i++) buffer[i] = ReadChar(); return count; // we can return this here, it will die at the above row if anything is off } public override char ReadChar() { BitPosition = 8; return base.ReadChar(); //BitConverter.ToChar(ReadBytes(2), 0); } public override char[] ReadChars(int count) { var chars = new char[count]; Read(chars, 0, count); return chars; } public override decimal ReadDecimal() { int[] ints = new int[4]; for (int i = 0; i < ints.Length; i++) ints[i] = ReadInt32(); return new decimal(ints); } public override double ReadDouble() => BitConverter.ToDouble(ReadBytes(8), 0); public override short ReadInt16() => BitConverter.ToInt16(ReadBytes(2), 0); public override int ReadInt32() => BitConverter.ToInt32(ReadBytes(4), 0); public override long ReadInt64() => BitConverter.ToInt64(ReadBytes(8), 0); public override sbyte ReadSByte() => (sbyte)ReadByte(); public override float ReadSingle() => BitConverter.ToSingle(ReadBytes(4), 0); public override string ReadString() { BitPosition = 8; // Make sure we read a new byte when we start reading the string return base.ReadString(); } public override ushort ReadUInt16() => BitConverter.ToUInt16(ReadBytes(2), 0); public override uint ReadUInt32() => BitConverter.ToUInt32(ReadBytes(4), 0); public override ulong ReadUInt64() => BitConverter.ToUInt64(ReadBytes(8), 0); }
And unit tests:
public static bool UnitTest() { const int testPairs = 512; var bitstream = new MemoryStream(); var bitwriter = new BinaryBitWriter(bitstream); var bitreader = new BinaryBitReader(bitstream); byte[] bytes = new byte[] { 1, 2, 3, 4, 255 }; byte Byte = 128; bool Bool = true; char[] chars = new char[] { 'a', 'b', 'c' }; string str = "hello"; var Float = 2.5f; ulong Ulong = 12345678901234567890; long Long = 1122334455667788; uint Uint = 1234567890; int Int = 999998888; ushort UShort = 12345; short Short = 4321; double Double = 9.9; char Char = 'A'; sbyte Sbyte = -128; decimal Decimal = 10000.00001m; List<BBTest> pairs = new List<BBTest>(); // Make pairs of write and read tests pairs.Add(new BBTest(Bool, (w) => w.Write(Bool), (r) => { if (r.ReadBoolean() != Bool) throw new Exception(); })); pairs.Add(new BBTest(bytes, (w) => w.Write(bytes, 0, 5), (r) => { if (arrayCompare(r.ReadBytes(5), bytes)) throw new Exception(); })); pairs.Add(new BBTest(Byte, (w) => w.Write(Byte), (r) => { if (r.ReadByte() != Byte) throw new Exception(); })); pairs.Add(new BBTest(chars, (w) => w.Write(chars, 0, 3), (r) => { if (arrayCompare(r.ReadChars(3), chars)) throw new Exception(); })); ///////////// pairs.Add(new BBTest(str, (w) => w.Write(str), (r) => { string s; if ((s = r.ReadString()) != str) throw new Exception(); })); pairs.Add(new BBTest(Decimal, (w) => w.Write(Decimal), (r) => { if (r.ReadDecimal() != Decimal) throw new Exception(); })); pairs.Add(new BBTest(Float, (w) => w.Write(Float), (r) => { if (r.ReadSingle() != Float) throw new Exception(); })); pairs.Add(new BBTest(Ulong, (w) => w.Write(Ulong), (r) => { if (r.ReadUInt64() != Ulong) throw new Exception(); })); pairs.Add(new BBTest(Long, (w) => w.Write(Long), (r) => { if (r.ReadInt64() != Long) throw new Exception(); })); pairs.Add(new BBTest(Uint, (w) => w.Write(Uint), (r) => { if (r.ReadUInt32() != Uint) throw new Exception(); })); pairs.Add(new BBTest(Int, (w) => w.Write(Int), (r) => { if (r.ReadInt32() != Int) throw new Exception(); })); pairs.Add(new BBTest(UShort, (w) => w.Write(UShort), (r) => { if (r.ReadUInt16() != UShort) throw new Exception(); })); pairs.Add(new BBTest(Short, (w) => w.Write(Short), (r) => { if (r.ReadInt16() != Short) throw new Exception(); })); pairs.Add(new BBTest(Double, (w) => w.Write(Double), (r) => { if (r.ReadDouble() != Double) throw new Exception(); })); pairs.Add(new BBTest(Char, (w) => w.Write(Char), (r) => { if (r.ReadChar() != Char) throw new Exception(); })); /////////////// pairs.Add(new BBTest(bytes, (w) => w.Write(bytes), (r) => { if (arrayCompare(r.ReadBytes(5), bytes)) throw new Exception(); })); pairs.Add(new BBTest(Sbyte, (w) => w.Write(Sbyte), (r) => { if (r.ReadSByte() != Sbyte) throw new Exception(); })); // Now add all tests, and then a bunch of randomized tests, to make sure we test lots of combinations incase there is some offsetting error List<BBTest> test = new List<BBTest>(); test.AddRange(pairs); var rnd = new Random(); for (int i = 0; i < testPairs - test.Count; i++) { if (rnd.NextDouble() < 0.75) test.Add(pairs[0]); else test.Add(pairs[rnd.Next(pairs.Count)]); } // now write all the tests for (int i = 0; i < test.Count; i++) test[i].Writer(bitwriter); bitwriter.Flush(); // now reset the stream and test to see that they are the same bitstream.Position = 0; for (int i = 0; i < test.Count; i++) test[i].ReadTest(bitreader); // As comparison, lets write the same stuff to a normal binarywriter and compare sized var binstream = new MemoryStream(); var binwriter = new BinaryWriter(binstream); for (int i = 0; i < test.Count; i++) test[i].Writer(binwriter); binwriter.Flush(); var saved = 1 - bitstream.Length / (float)binstream.Length; var result = $"BinaryBitWriter was {(saved * 100).ToString("0.00")}% cheaper than a normal BinaryWriter with random data"; bool arrayCompare(IEnumerable a, IEnumerable b) { var B = b.GetEnumerator(); B.MoveNext(); foreach (var item in a) { if (item != B.Current) return false; B.MoveNext(); } return true; } return true; } delegate void writer(BinaryWriter w); delegate void reader(BinaryReader r); class BBTest { public object Object; public writer Writer; public reader ReadTest; public BBTest(object obj, writer w, reader r) { Object = obj; Writer = w; ReadTest = r; } public override string ToString() => Object.ToString(); }