Using ref to avoid copying larger C# structs

Posted: (EET/GMT+2)

 

If you have performance-sensitive C# code that passes larger struct types between methods, C#'s ref and in parameter modifiers can help avoid unnecessary data copying.

This is mainly useful in the hot paths of your code. For normal application code, prefer readability first and measure before changing method signatures.

Let's take an example. Let's say you have a struct with a meaningful number of values that no longer fits CPU registers. Such as:

public struct Measurement
{
    public double Value1;
    public double Value2;
    public double Value3;
    public double Value4;
}

When this struct is passed to a method normally, the value is copied:

public static double Sum(Measurement measurement)
{
    return measurement.Value1 + measurement.Value2;
}

For a small struct, this is usually fine. For larger structs used inside tight loops, the copy can become measurable, and thus degrade performance.

If the method only needs to read the value, use in:

static double Sum(in Measurement measurement)
{
    return measurement.Value1 + measurement.Value2;
}

The in modifier passes the value by reference, but prevents the method from modifying it.

If the method needs to update the original value, use ref instead:

static void Normalize(ref Measurement measurement)
{
    double total =
        measurement.Value1 +
        measurement.Value2 +
        measurement.Value3 +
        measurement.Value4;

    measurement.Value1 /= total;
    measurement.Value2 /= total;
    measurement.Value3 /= total;
    measurement.Value4 /= total;
}

Callers must also use ref, which makes the mutation visible at the call site:

Measurement measurement = new Measurement
{
    Value1 = 10,
    Value2 = 20,
    Value3 = 30,
    Value4 = 40
};

Normalize(ref measurement);

Tip: use in for read-only access, and ref only when the method must modify the original value.

Avoid using these modifiers everywhere. They make APIs more explicit, but also slightly more complex. For small structs such as int, DateTime, or simple IDs, the normal value-passing behavior is usually the better choice.

Keep in mind: benchmark before and after the change. This kind of optimization is most useful in numeric code, parsing, graphics, serialization, and other tight loops.