Save data less in database using theory of bit in C#

ShenLongShenLong
3 min read

Using bit fields in C/C++ might be familiar to you. In C/C++, bit fields allow you to create multiple variables within a single byte, within the limits of the bit representation. Today, I’m sharing a similar technique for C#. It's important to note that this method doesn't exactly mirror C/C++ bit fields. Instead of optimizing variable size at runtime, it focuses on optimizing data storage. This post will guide you through this technique and compare it with C/C++.

Theory

The technique is simple: we’ll create a struct to store data, with variables defined by you. In this example, I'll use the uint type. The goal is to convert the struct to a long for storage and back to a struct when needed.

Practice

With the concept in mind, let's dive into the implementation. In C++, the : symbol is used to mark bit usage. Since C# lacks this, we’ll use Attribute to denote the required bit length.

Here's how we define an Attribute to specify bit length:

public class BitFieldsAttribute : Attribute
{
  uint length;
  public BitFieldsAttribute(uint length) 
  {
    this.length = length;
  }

  public uint Length
  {
    get { return length; }
  }
}

Now, let’s create a struct for testing:

struct Status
{
  [BitFields(1)]
  public uint IsOn;

  [BitFields(3)]
  public uint TimeToRun;

  [BitFields(4)]
  public uint TimeToEat;
};

In this struct, IsOn uses 1 bit, TimeToRun uses 3 bits, and TimeToEat uses 4 bits of a uint.

We’ve marked the bit lengths; the next step is converting the Status struct to a long and vice versa.

For this, we’ll create a Convertion class to handle the conversion:

public static class Convertion{
    public static long ToLong<T>(T t) where T : struct
    {
      long r = 0; 
      int offset = 0; 

      foreach (System.Reflection.FieldInfo f in t.GetType().GetFields())
      {
        object[] attrs = f.GetCustomAttributes(typeof(BitFieldsAttribute), false);
        if (attrs.Length == 1)
        {
          uint fieldLength = ((BitFieldsAttribute)attrs[0]).Length; 
          long mask = 0;
          for (int i = 0; i < fieldLength; i++)
            mask |= (uint)(1 << i);

          r |= ((UInt32)f.GetValue(t)! & mask) << offset;

          offset += (int)fieldLength;
        }
      }
      return r;
    }

Now, let's reverse the process, converting from long back to struct:

public static class Convertion{
  public static T FromLong<T>(long l) where T : struct
    {
      T t = new T(); 
      Object boxed = t; 
      int offset = 0; 

      foreach (System.Reflection.FieldInfo f in t.GetType().GetFields())
      {
        object[] attrs = f.GetCustomAttributes(typeof(BitFieldsAttribute), false);
        if (attrs.Length == 1)
        {

          uint fieldLength = ((BitFieldsAttribute)attrs[0]).Length;


          long mask = 0;
          for (int i = 0; i < fieldLength; i++)
            mask |= (uint)(1 << i);


          var value = Convert.ChangeType((l >> offset) & mask, f.FieldType);

          var fieldAttribute = typeof(T).GetField(f.Name, BindingFlags.Instance | BindingFlags.Public);

          fieldAttribute!.SetValue(boxed, value);

          t = (T)boxed;


          offset += (int)fieldLength;
        }
      }

      return t;
    }
}

Here’s how you can test it in the main method:

    Status s = new();
    s.IsOn = 1;
    s.TimeToRun = 5;
    s.TimeToEat = 7;

    int size = System.Runtime.InteropServices.Marshal.SizeOf(typeof(Status));
    Console.WriteLine("Bytes:" + size);

    long l = BitFieldsAttribute.Convertion.ToLong(s);
    Console.WriteLine("Convert to long:" + l);

    Status s2 = BitFieldsAttribute.Convertion.FromLong<Status>(l);
    Console.WriteLine("Convert from long:" + string.Format("IsOn:{0}, TimeToRun:{1}, TimeToEat:{2}", s2.IsOn, s2.TimeToRun, s2.TimeToEat));

This will output:

Bytes:12
Convert to long:123
Convert from long:IsOn:1, TimeToRun:5, TimeToEat:7

The complete code is available here.

Discussion

Notice something odd? In C/C++, bit fields take only 1 byte when printed, but here it’s 12 bytes! That’s because C++ bit fields truly occupy only the defined bits in the struct. In C#, however, the Status struct has three uint variables, so it uses 12 bytes at runtime. However, when stored, it compresses to a long, saving memory compared to runtime.

If you have alternative methods to optimize this further, feel free to share. Thanks!

References

Stackoverflow

10
Subscribe to my newsletter

Read articles from ShenLong directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ShenLong
ShenLong

I am a developer from VietNam. I learn C# the first time in the desktop subject in Ho Chi Minh University of Science. And, I love it. In this blog, I will write all of thing my knowlege about C# language. If you want find another tips in other industry, you can access https://www.levandong.dev.