Bitwise operations are fundamental operations at the binary level, and they can be blazingly fast because they work directly on the bits of data. They can often be used to perform certain tasks more efficiently than other methods. In the context of .NET, bitwise operations can be particularly useful for tasks such as manipulating individual bits within integers or managing sets of flags.

Let’s use a classic example: checking whether a number is odd or even using bitwise operations.

Traditional Approach

To check if a number is even or odd, you might use the modulo operation:

bool isEven(int number)
{
    return number % 2 == 0;
}

Bitwise Approach

You can determine if a number is odd or even by inspecting its least significant bit. If the least significant bit is 1, the number is odd; otherwise, it’s even. This can be achieved using the bitwise AND operation:

bool isEven(int number)
{
    return (number & 1) == 0;
}

Explanation:

Every integer can be represented in binary. The least significant bit (rightmost bit) determines if a number is odd or even.

  • If the least significant bit is 1, the number is odd.
  • If the least significant bit is 0, the number is even.

Let’s take 5 as an example: Binary representation: 101

If you AND it with 1 (001 in binary), you get:

  101
& 001
-----
  001  (which is 1 in decimal)

Let’s see a more practical example that could bring great performance benefits to any application:

[MemoryDiagnoser]
public class BitVsClass
{
    private HashSet<Fruit> _fruits;
    private HashSet<Fruits> _fruitsBit;

    private static Fruit _fruit = new() { Name = "Mango" };
    private static Fruits _fruitBit = Fruits.Mango;

    [GlobalSetup]
    public void GlobalSetup()
    {
        _fruits = new HashSet<Fruit>
        {
            new Fruit { Name = "Tomato" },
            new Fruit { Name = "Banana" },
            new Fruit { Name = "Apple" },
            new Fruit { Name = "Orange" },
            new Fruit { Name = "PineApple" },
            new Fruit { Name = "WaterMelon" },
            new Fruit { Name = "Melon" },
            new Fruit { Name = "Mango" },
            new Fruit { Name = "Apricot" }
        };

        _fruitsBit = new HashSet<Fruits>
        { 
            Fruits.Tomato,
            Fruits.Banana,
            Fruits.Apple,
            Fruits.Orange,
            Fruits.PineApple,
            Fruits.WaterMelon,
            Fruits.Melon,
            Fruits.Mango,
            Fruits.Apricot
        };
    }

    [Benchmark]
    public bool CheckIfFruitExistsClass()
    {
        return _fruits.Contains(_fruit);
    }

    [Benchmark]
    public bool CheckIfFruitExistsBit()
    {
        return _fruitsBit.Contains(_fruitBit);
    }
}

record Fruit
{
    public string Name { get; set; }
}

[Flags]
public enum Fruits : long
{
    Tomato = 1 << 0,
    Banana = 1 << 1, //2
    Apple = 1 << 2, //4
    Orange = 1 << 3, //8
    PineApple = 1 << 4, //16
    WaterMelon = 1 << 5, //32
    Melon = 1 << 6, //64
    Mango = 1 << 7, //128
    Apricot = 1 << 8 //256
}

Using the above code we get these results:

MethodMeanErrorStdDevAllocated
CheckIfFruitExistsClass15.857 ns0.2194 ns0.2052 ns
CheckIfFruitExistsBit2.902 ns0.0039 ns0.0035 ns
Benchmark on .NET 8

Benefits of Bitwise Operations

  1. Performance: Bitwise operations are fundamental CPU instructions, making them extremely fast.
  2. Compactness: They often lead to more concise code.
  3. Flexibility: Useful for tasks like toggling individual bits or managing sets of flags without relying on external libraries.

Conclusion

Bitwise operations can offer significant performance benefits and are powerful tools in a developer’s toolkit. They allow for low-level manipulation of data which can be essential in certain applications. In .NET, the ability to use these operations directly provides developers with an efficient way to handle many tasks that might otherwise require more computationally intensive methods.

Leave a comment

Trending