Vaughan

Performance with ref readonly returns

.NET Core

When looking at performance of an application, there is a trade off between when you should be using value types and when you should be using reference types. People often talk about minimizing allocations to the heap for reference types but allocations themselves are actually very quick. The reason why its an issue is that those allocations need to be garbage collected eventually. This is time that the application is spending cleaning up memory instead of running your code. When looking at allocations, value types are great because they generally are just on the stack. Once the method is finished they are gone. Of course with everything there are trade offs, the stack is more limited than the heap and so you have size constraints on them and the fact that the whole object needs to be copied when passing it around.(side note: value types aren't always on the stack. They can be on the heap in some circumstances! eg: when boxed, fields of a class or elements of an array.)

When a class returns a value type like a struct, it normally returns a copy. What if I told you that you can keep your value type but still return a reference to it only? Sounds a little like the best of both worlds. Well as always there are trade offs.

Let me give an example. I created a Location class with a value type property of type Address.

public struct Address
{
    public Address(string city, string country)
    {
        City = city;
        Country = country;
    }
        public string City { get; set; }
        public string Country { get; set; }
}

public class Location
{
    Address _address;
    public Location()
    {
        _address = new Address("Johannesburg", "South Africa");
    }

    public Address Get()
    {
        return _address;
    }
}

Below is a test to show what we know. So getting the address from location and changing it doesn't change the default address in Location. Its a copy because its a value type.

[Test]
public void Get()
{
    Location location = new Location();

    var add = location.Get();

    Assert.AreEqual("Johannesburg", add.City);

    add.City = "London";

    var add2 = location.Get();

    Assert.AreEqual("Johannesburg", add2.City);
}

I'm now adding a new get by reference to the Location class. You will notice that your method and the return need to specify ref.

public class Location
{
    Address _address;
    public Location()
    {
        _address = new Address("Johannesburg", "South Africa");
    }

    public Address Get()
    {
        return _address;
    }

    public ref Address GetByRef()
    {
        return ref _address;
    }
}

Testing that method is interesting because now changing the address does change the underlying address of Location. This is great for performance but it does break encapsulation quite badly. My private class Location value can be mutated any anyone who can get it. Not ideal but at least the syntax is explicit.

[Test]
public void GetByRef()
{
    Location location = new Location();

    ref var add = ref location.GetByRef();

    Assert.AreEqual("Johannesburg", add.City);

    add.City = "London";

    var add2 = location.Get();

    Assert.AreEqual("London", add2.City);
}

A maybe better option is to use a ref readonly get. This works the same but the compiler won't let you modify the value returned.


public class Location
{
    Address _address;
    public Location()
    {
        _address = new Address("Johannesburg", "South Africa");
    }

    public Address Get()
    {
        return _address;
    }

    public ref Address GetByRef()
    {
        return ref _address;
    }

    public ref readonly Address GetByRefReadonly()
    {
        return ref _address;
    }
}

Testing this actually has a compile time error if you try modify the address which is much better than a run time surprise later!

[Test]
public void GetByRefReadonly()
{
    Location location = new Location();

    ref readonly var add = ref location.GetByRefReadonly();

    Assert.AreEqual("Johannesburg", add.City);

    //Will not compile
    //add.City = "London";
}

The syntax is a little cumbersome but its important to make it clear what you are doing. The only reason I can see to do this is for performance and performance means nothing without measuring.

To measure, I compared the memory allocation of the three methods with BenchmarkDotNet.


[MemoryDiagnoser]
public class LocationBenchmarks
{
    private Location location = new Location();

    [Benchmark]
    public void Get()
    {
        var address = location.Get();
    }

    [Benchmark]
    public void GetByRef()
    {
        ref readonly var address =  ref location.GetByRef();
    }

    [Benchmark]
    public void GetByRefReadonly()
    {
        ref readonly var address = ref location.GetByRefReadonly();
    }
}

Its a pretty simple method so these are very small numbers but you can see that none of them allocate anything as expected. The difference is the run time which is quite a lot quicker when using ref.

Related Posts

BMC logoBuy me a coffee