32

The only difference between MutableSlab and ImmutableSlab implementations is the readonly modifier applied on the handle field:

using System;
using System.Runtime.InteropServices;

public class Program
{
    class MutableSlab : IDisposable
    {
        private GCHandle handle;

        public MutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    class ImmutableSlab : IDisposable
    {
        private readonly GCHandle handle;

        public ImmutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    public static void Main()
    {
        var mutableSlab = new MutableSlab();
        var immutableSlab = new ImmutableSlab();

        mutableSlab.Dispose();
        immutableSlab.Dispose();

        Console.WriteLine($"{nameof(mutableSlab)}.handle.IsAllocated = {mutableSlab.IsAllocated}");
        Console.WriteLine($"{nameof(immutableSlab)}.handle.IsAllocated = {immutableSlab.IsAllocated}");
    }
}

But they produce different results:

mutableSlab.handle.IsAllocated = False
immutableSlab.handle.IsAllocated = True

GCHandle is a mutable struct and when you copy it then it behaves exactly like in scenario with immutableSlab.

Does the readonly modifier create a hidden copy of a field? Does it mean that it's not only a compile-time check? I couldn't find anything about this behaviour here. Is this behaviour documented?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    I won't post this as an answer since I'm not 100% sure about the behaviour of GC. But no, the readonly keyword doesn't introduce new fields. It does what it says on the tin. The behavior you observe is probably due to the GC not doing what you want it to. Try running GC.Collect(). The GC takes hints, not orders usually. – markonius Jul 01 '19 at 07:32
  • 2
    I'm writing an answer now... But for those who are impatient, here's a blog post I've written earlier: https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-fields/ – Jon Skeet Jul 01 '19 at 07:38
  • 1
    Member invocations via the read-only field creates a copy. It's not that there's an extra field - it's that the field is copied before invocation. – Jon Skeet Jul 01 '19 at 07:40
  • 1
    Note that Resharper actually warns about this; for `this.handle.Free();` in `ImmutableSlab` it gives the warning: *"Impure method is called for readonly field of value type."* – Matthew Watson Jul 01 '19 at 07:42

1 Answers1

32

Does the readonly modifier create a hidden copy of a field?

Calling a method or property on a read-only field of a regular struct type (outside the constructor or static constructor) first copies the field, yes. That's because the compiler doesn't know whether the property or method access would modify the value you call it on.

From the C# 5 ECMA specification:

Section 12.7.5.1 (Member access, general)

This classifies member accesses, including:

  • If I identifies a static field:
    • If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
    • Otherwise, the result is a variable, namely the static field I in E.

And:

  • If T is a struct-type and I identifies an instance field of that struct-type:
    • If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
    • Otherwise, the result is a variable, namely the field I in the struct instance given by E.

I'm not sure why the instance field part specifically refers to struct types, but the static field part doesn't. The important part is whether the expression is classified as a variable or a value. That's then important in function member invocation...

Section 12.6.6.1 (Function member invocation, general)

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

[...]

  • Otherwise, if the type of E is a value-type V, and M is declared or overridden in V:
    • [...]
    • If E is not classified as a variable, then a temporary local variable of E's type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E is a true variable is it possible for the caller to observe the changes that M makes to this.

Here's a self-contained example:

using System;
using System.Globalization;

struct Counter
{
    private int count;

    public int IncrementedCount => ++count;
}

class Test
{
    static readonly Counter readOnlyCounter;
    static Counter readWriteCounter;

    static void Main()
    {
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1

        Console.WriteLine(readWriteCounter.IncrementedCount); // 1
        Console.WriteLine(readWriteCounter.IncrementedCount); // 2
        Console.WriteLine(readWriteCounter.IncrementedCount); // 3
    }
}

Here's the IL for a call to readOnlyCounter.IncrementedCount:

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()

That copies the field value onto the stack, then calls the property... so the value of the field doesn't end up changing; it's incrementing count within the copy.

Compare that with the IL for the read-write field:

ldsflda    valuetype Counter Test::readWriteCounter
call       instance int32 Counter::get_IncrementedCount()

That makes the call directly on the field, so the field value ends up changing within the property.

Making a copy can be inefficient when the struct is large and the member doesn't mutate it. That's why in C# 7.2 and above, the readonly modifier can be applied to a struct. Here's another example:

using System;
using System.Globalization;

readonly struct ReadOnlyStruct
{
    public void NoOp() {}
}

class Test
{
    static readonly ReadOnlyStruct field1;
    static ReadOnlyStruct field2;

    static void Main()
    {
        field1.NoOp();
        field2.NoOp();
    }
}

With the readonly modifier on the struct itself, the field1.NoOp() call doesn't create a copy. If you remove the readonly modifier and recompile, you'll see that it creates a copy just like it did in readOnlyCounter.IncrementedCount.

I have a blog post from 2014 that I wrote having found that readonly fields were causing performance issues in Noda Time. Fortunately that's now fixed using the readonly modifier on the structs instead.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • `Calling a method or property on a read-only field of a regular struct type first copies the field`. I couldn't find this statement in documentation, but I think this is the implicit version of it: `Because value types directly contain their data, a field that is a readonly value type is immutable`. So if the field is not a readonly struct then when I call impure method that changes the state of this field then it must create a copy. Am I right? –  Jul 01 '19 at 08:12
  • @BART: Yes. Which documentation were you looking at? It *is* in the C# specification somewhere, but it may not be terribly easy to find. – Jon Skeet Jul 01 '19 at 08:27
  • @Jon Skeet I was looking at the microsoft site [readonly keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly), but there is no such explicit statement like yours. I will take a deeper look at C# specification. Thanks for the explanation –  Jul 01 '19 at 08:39
  • @BART: I've now quoted the relevant bit of the spec, and I'll look at the confusing part of the text. – Jon Skeet Jul 01 '19 at 08:39
  • Thanks for the comprehensive explanation, Jon! Before that I thought that readonly was only referring to the reference variable, not to the object itself. I learned something new! Thumbs up for that answer :-) – Matt Jul 01 '19 at 09:21
  • @Matt: It does just refer to the variable - but for value types, there's no separate object... the value of the variable *is* the data. – Jon Skeet Jul 01 '19 at 09:45
  • Right, I overlooked that `GCHandle` (where the readonly is applied to) is a **struct**, not an **object**. In that case, of course it is a value type. Thanks for the hint! I've updated the tags of the question to make it more clear. – Matt Jul 01 '19 at 11:30
  • Isn't it [`CIL`](http://en.wikipedia.org/wiki/Common_Intermediate_Language), not `IL`? – Peter Mortensen Jul 01 '19 at 23:03
  • @PeterMortensen: I very *very* rarely hear it called CIL. (Note ildasm, ilasm etc.) – Jon Skeet Jul 02 '19 at 05:55
  • @canton7: Interesting - that's the first I've heard of that, and I like to keep mostly up-to-date... will have to investigate further. – Jon Skeet Jul 02 '19 at 21:07
  • My goodness, I taught the great @JonSkeet something! [Readonly Instance Members](https://github.com/dotnet/csharplang/blob/1a46441156b13db6c845f4bbb886284387d73023/proposals/csharp-8.0/readonly-instance-members.md), [LDM](https://github.com/dotnet/csharplang/blob/1a46441156b13db6c845f4bbb886284387d73023/meetings/2018/LDM-2018-10-15.md#readonly-struct-members). The [Champion](https://github.com/dotnet/csharplang/issues/1710) seems out of date, see [`MethodSymbol.IsEffectivelyReadOnly`](http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Symbols/MethodSymbol.cs,318) – canton7 Jul 02 '19 at 21:12