C# 8: Read-only instance members in variable structures

In the previous article we introduced in C # read-only structure (Readonly struct) 1 and its close relative inparameters 2 .
Today we will discuss a feature introduced from C# 8: read-only instance members in a mutable structure (when the structure is mutable, the instance members that will not change the state of the structure are declared as readonly).

Reasons for introducing read-only instance members

Simply put, it is to improve performance .
We already know the read-only structure ( readonly struct) and inparameters can create a copy by reducing, to improve the performance of code running. When we create a read-only structure type, the compiler forces all members to be read-only (that is, no instance members modify their state). However, in some scenarios, for example, you have an existing API with publicly accessible fields or both mutable and immutable members. In this case, the type cannot be marked as readonly(because this concerns all instance members).

Usually, this is not much impact, but in the use incase parameters on the exception. For non-read-only structure inparameters, the compiler will create a defensive copy of the parameters of calling each instance member, because it can not guarantee that this call does not modify its internal state. This may cause a large number of copies to be created, and the overall performance is worse than when passing the structure directly by value (because passing by value will only create a copy once when passing parameters).

You look at an example to understand, we define such a general structure, then used as inarguments:

public struct Rect
{
    
    
    public float w;
    public float h;

    public float Area
    {
    
    
        get
        {
    
    
            return w * h;
        }
    }
}
public class SampleClass
{
    
    
    public float M(in Rect value)
    {
    
    
        return value.Area + value.Area;
    }
}

After compilation, the class SampleClassmethod Mcode to run the logic is actually this :

public float M([In] [IsReadOnly] ref Rect value)
{
    
    
    Rect rect = value;  //防御性副本
    float area = rect.Area;
    rect = value;       //防御性副本
    return area + rect.Area;
}

Read-only instance members in variable structures

We put above a variable structure Rectchanged a bit, add a readonlymethod GetAreaReadOnly, as follows:

public struct Rect
{
    
    
    public float w;
    public float h;

    public float Area
    {
    
    
        get
        {
    
    
            return w * h;
        }
    }

    public readonly float GetAreaReadOnly()
    {
    
    
        return Area; //警告 CS8656 从 "readonly" 成员调用非 readonly 成员 "Rect.Area.get" 将产生 "this" 的隐式副本。
    }
}

At this point, the code can be compiled, but a warning like this will be prompted: calling the non-readonly member "Rect.Area.get" from the "readonly" member will produce an implicit copy of "this".
National Cheng Kung University vernacular translation that is to say, in a read-only method we GetAreaReadOnlycall the non-read-only Areaattribute will have "this" defensive copy. After compiling a code demonstrates what method GetAreaReadOnlythe method body is actually operating logic is this :

[IsReadOnly]
public float GetAreaReadOnly()
{
    
    
    Rect rect = this; //防御性副本
    return rect.Area;
}

So in order to avoid creating unnecessary defensive copy and affect performance, we should give a read-only property or method calls the method body are plus readonlymodifiers (in this case, that is, to attribute Areaadd readonlymodifier).

Call a read-only instance member in a variable structure

Let's modify the above example again:

public struct Rect
{
    
    
    public float w;
    public float h;

    public readonly float Area
    {
    
    
        get
        {
    
    
            return w * h;
        }
    }

    public readonly float GetAreaReadOnly()
    {
    
    
        return Area;
    }

    public float GetArea()
    {
    
    
        return Area;
    }
}

public class SampleClass
{
    
    
    public float CallGetArea(Rect vector)
    {
    
    
        return vector.GetArea();
    }

    public float CallGetAreaIn(in Rect vector)
    {
    
    
        return vector.GetArea();
    }

    public float CallGetAreaReadOnly(in Rect vector)
    {
    
    
        //调用可变结构体中的只读实例成员
        return vector.GetAreaReadOnly();
    }
}

Class SampleClassis defined in three methods:

  • The first method is our usual calling method before;
  • In the second inargument to a variable structure, the non-read-only (method may modify the state of the structure) call;
  • In the third inparameter into the variable structure, read-only method call.

Let's focus on the difference between the second and third methods, or translate their IL code logic into easy-to-understand execution logic, as shown below :

public float CallGetAreaIn([In] [IsReadOnly] ref Rect vector)
{
    
    
    Rect rect = vector; //防御性副本
    return rect.GetArea();
}

public float CallGetAreaReadOnly([In] [IsReadOnly] ref Rect vector)
{
    
    
    return vector.GetAreaReadOnly();
}

It can be seen that CallGetAreaReadOnlywhen calling the (read-only) member method of the CallGetAreaInstructure, a local defensive copy is created less than (the non-read-only member method of the structure is called), so there should be an advantage in execution performance .

Performance analysis of read-only instance members

Performance improvement in the structure when the larger obvious, so in order to highlight the difference in performance of the method three, when I tested Rectadded 30 decimal attribute type structural body, then the class SampleClassis added in three The test method, the code is as follows:

public struct Rect
{
    
    
    public float w;
    public float h;

    public readonly float Area
    {
    
    
        get
        {
    
    
            return w * h;
        }
    }

    public readonly float GetAreaReadOnly()
    {
    
    
        return Area;
    }

    public float GetArea()
    {
    
    
        return Area;
    }

    public decimal Number1 {
    
     get; set; }
    public decimal Number2 {
    
     get; set; }
    //...
    public decimal Number30 {
    
     get; set; }
}

public class SampleClass
{
    
    
    const int loops = 50000000;
    Rect rectInstance;

    public SampleClass()
    {
    
    
        rectInstance = new Rect();
    }

    [Benchmark(Baseline = true)]
    public float DoNormalLoop()
    {
    
    
        float result = 0F;
        for (int i = 0; i < loops; i++)
        {
    
    
            result = CallGetArea(rectInstance);
        }
        return result;
    }

    [Benchmark]
    public float DoNormalLoopByIn()
    {
    
    
        float result = 0F;
        for (int i = 0; i < loops; i++)
        {
    
    
            result = CallGetAreaIn(in rectInstance);
        }
        return result;
    }

    [Benchmark]
    public float DoReadOnlyLoopByIn()
    {
    
    
        float result = 0F;
        for (int i = 0; i < loops; i++)
        {
    
    
            result = CallGetAreaReadOnly(in rectInstance);
        }
        return result;
    }

    public float CallGetArea(Rect vector)
    {
    
    
        return vector.GetArea();
    }

    public float CallGetAreaIn(in Rect vector)
    {
    
    
        return vector.GetArea();
    }

    public float CallGetAreaReadOnly(in Rect vector)
    {
    
    
        return vector.GetAreaReadOnly();
    }
}

Without the use of ina method parameter, means that each incoming call is a new copy of the variable; and the use of inmodifier method, each new copy of the variable is not passed, but deliver the same copy of read-only reference .

  • DoNormalLoop Methods, parameters without modifiers, pass in general structures, and call non-read-only methods of variable structures. This is a common practice in the past.
  • DoNormalLoopByInMethods, parameters plus inmodifier, the general structure of the incoming, not read-only calling method for a variable structure.
  • DoReadOnlyLoopByInMethods, parameters plus inmodifier, the general structure of the incoming call a read-only method for a variable structure.

Use the BenchmarkDotNet tool to test the running time of the three methods, and the results are as follows:

Method Mean Error StdDev Ratio RatioSD
DoNormalLoop 2.034 s 0.0392 s 0.0348 s 1.00 0.00
DoNormalLoopByIn 3.490 s 0.0667 s 0.0557 s 1.71 0.03
DoReadOnlyLoopByIn 1.041 s 0.0189 s 0.0202 s 0.51 0.01

From the results, when the variable structure, using the inread-only parameter method invocation structure, the performance is higher than the other two; using innon-parametric method calls a read only variable structure, the longest running time, a serious impact In order to improve performance, such calls should be avoided.

to sum up

  • When the structure is a mutable type, the members that will not cause change (that is, the state of the structure will not change) should be declared as readonly.
  • Examples of read-only invoked only when the members of the structure, using inthe parameters, can effectively improve performance.
  • readonlyModifiers are required on read-only attributes. The compiler does not assume that the getter visitor does not modify the state. Therefore, it must be explicitly declared on the attribute.
  • Automatic property can be omitted readonlymodifiers, because no matter readonlymodifier is present, the compiler will automatically implement all the getter as read-only.
  • Do not use innon-read-only instance members call the structure parameters, because a negative impact on performance.

Author: Technical Zemin
Publisher: Technical Translation Station

Public Number: Technical Translation Station


  1. https://ittranslator.cn/dotnet/csharp/2020/10/26/c-7-2-talk-about-readonly-struct.html Read-only structure in C# ↩︎

  2. https://ittranslator.cn/dotnet/csharp/2020/11/02/understanding-in-modifier-csharp.html in parameters and performance analysis in C# ↩︎

Guess you like

Origin blog.csdn.net/weixin_47498376/article/details/109676815