C# can actually perform better than C++ in some scenarios by using unsafe code and pointers.

Let’s see how. Imagine you have a function that needs to do a lot of array manipulation, maybe a complex image processing filter or a scientific simulation. Normally, C# adds bounds checking to every array access. This is great for safety, but it can add up to a significant overhead in tight loops.

using System;

public class ImageProcessor
{
    public unsafe void ApplyFilter(byte[] pixelData, int width, int height)
    {
        fixed (byte* pPixelData = pixelData)
        {
            byte* ptr = pPixelData;
            int stride = width; // Assuming no padding for simplicity

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    // Accessing pixel data directly without bounds checks
                    byte currentPixel = *ptr;

                    // Perform some complex calculation (example: invert pixel)
                    byte newPixel = (byte)(255 - currentPixel);

                    // Write back modified pixel
                    *ptr = newPixel;

                    // Move pointer to the next pixel
                    ptr++;
                }
            }
        }
    }

    // Example usage
    public static void Main(string[] args)
    {
        int width = 1000;
        int height = 1000;
        byte[] pixels = new byte[width * height]; // Example: grayscale image

        // Initialize with some data
        for (int i = 0; i < pixels.Length; i++)
        {
            pixels[i] = (byte)(i % 255);
        }

        ImageProcessor processor = new ImageProcessor();

        // Measure performance
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        processor.ApplyFilter(pixels, width, height);
        stopwatch.Stop();

        Console.WriteLine($"Time taken: {stopwatch.ElapsedMilliseconds} ms");

        // You can optionally verify some results here
        // For example, check if the first pixel is inverted
        // Console.WriteLine($"First pixel original: {(byte)(0 % 255)}, after filter: {pixels[0]}");
    }
}

To compile and run this, you need to enable unsafe code in your project. For dotnet build or dotnet run, you’d add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> to your .csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

The core of performance gains here comes from bypassing the managed runtime’s safety checks. When you use fixed (byte* pPixelData = pixelData), you’re telling the garbage collector, "Don’t move pixelData for the duration of this block." This allows you to get a stable, unmanaged pointer (byte* ptr) directly to the memory location of the array’s first element.

Inside the loop, *ptr dereferences the pointer to read the byte value at that memory address. ptr++ then increments the pointer, moving it to the next byte in memory. This is exactly how you’d write it in C or C++, and it avoids the overhead of array index bounds checking that would normally happen with pixelData[x].

The fixed statement is crucial. Without it, the garbage collector could relocate the pixelData array at any time, invalidating your pointer. fixed pins the object in memory.

The stride variable is important when dealing with multi-dimensional arrays or when rows might have padding (which is common in image formats). In this simplified example, we assume no padding, so the stride is just the width. If there were padding, you’d calculate the pointer for the next row by adding stride to the current pointer, rather than just width * sizeof(byte).

Consider a scenario where you’re processing a large buffer of raw network data or performing highly repetitive mathematical operations on large datasets. In these cases, the cumulative effect of skipping bounds checks and direct memory manipulation can lead to performance improvements that are noticeable, sometimes even rivaling native code.

The stackalloc keyword is another tool in the unsafe arsenal. It allows you to allocate memory on the stack, which is much faster than heap allocation. This is ideal for small, temporary buffers that you don’t need to last beyond the scope of a method.

public unsafe void ProcessDataStack(int size)
{
    // Allocate 100 bytes on the stack
    byte* buffer = stackalloc byte[100];

    // Use the stack-allocated buffer
    for (int i = 0; i < size && i < 100; i++)
    {
        buffer[i] = (byte)i;
    }
    // buffer is automatically deallocated when the method exits
}

This is fundamentally different from Array.Resize or new byte[], which allocate on the managed heap. stackalloc is a direct mapping to C-style stack allocation.

The most surprising thing about using unsafe in C# is how it interacts with the JIT compiler. While you’re writing code that looks like C/C++, the JIT compiler can still apply sophisticated optimizations, sometimes even better than what a traditional C++ compiler might achieve for equivalent code, because it has deeper knowledge of the .NET runtime and type system.

The next thing you’ll likely encounter is the need to manage unmanaged resources explicitly when using pointers, if the memory you’re pointing to wasn’t allocated by C# (e.g., via P/Invoke).

Want structured learning?

Take the full Csharp course →