17

C# 7.2 introduced the in modifier for passing arguments by reference with the guarantee that the recipient will not modify the parameter.

This article says:

You should never use a non-readonly struct as the in parameters because it may negatively affect performance and could lead to an obscure behavior if the struct is mutable

What does this mean for built-in primitives such as int, double?

I would like to use in to express intent in code, but not at the cost of performance losses to defensive copies.

Questions

  • Is it safe to pass primitive types via in arguments and not have defensive copies made?
  • Are other commonly used framework structs such as DateTime, TimeSpan, Guid, ... considered readonly by the JIT?
    • If this varies by platform, how can we find out which types are safe in a given situation?
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • If the JIT made "defensive copies" of primitive value types to prevent "mutation", that would arguably be a bug, `in` or no `in`. Such copies would be provably unnecessary in all circumstances. The issue for "commonly used" structs is more interesting, but I suspect the JIT will simply not do anything special to prevent copying unless it's a `readonly struct`. You'll notice that framework structs are indeed [being augmented to have this](https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/DateTime.cs#L56). – Jeroen Mostert Jun 12 '18 at 12:36
  • Note that "structs" like `Int32` are all but irrelevant to the JIT; they nominally represent the type in the BCL, but this is mostly for reflection purposes. The JIT knows what to do with an `int` on the most basic level. So whether or not `Int32` is marked `readonly` should matter not one whit to the JIT (and indeed, [currently that's *not* marked `readonly`](https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Int32.cs#L15)). – Jeroen Mostert Jun 12 '18 at 12:39
  • 1
    If you want just express intention why don't you use attributes such as `[In]`? Using `in` modifier from caller side is optional anyway. And it is not displayed in IntelliSense either (though I still use ReSharper 2017 and not sure whether the newer version displays it). Attributes can then asserted by custom FxCop rules if you really want to apply restrictions... – György Kőszeg Jun 13 '18 at 09:57

4 Answers4

9

A quick test shows that, currently, yes, a defensive copy is created for built-in primitive types and structs.

Compiling the following code with VS 2017 (.NET 4.5.2, C# 7.2, release build):

using System;

class MyClass
{
    public readonly struct Immutable { public readonly int I; public void SomeMethod() { } }
    public struct Mutable { public int I; public void SomeMethod() { } }

    public void Test(Immutable immutable, Mutable mutable, int i, DateTime dateTime)
    {
        InImmutable(immutable);
        InMutable(mutable);
        InInt32(i);
        InDateTime(dateTime);
    }

    void InImmutable(in Immutable x) { x.SomeMethod(); }
    void InMutable(in Mutable x) { x.SomeMethod(); }
    void InInt32(in int x) { x.ToString(); }
    void InDateTime(in DateTime x) { x.ToString(); }

    public static void Main(string[] args) { }
}

yields the following result when decompiled with ILSpy:

...
private void InImmutable([System.Runtime.CompilerServices.IsReadOnly] [In] ref MyClass.Immutable x)
{
    x.SomeMethod();
}

private void InMutable([System.Runtime.CompilerServices.IsReadOnly] [In] ref MyClass.Mutable x)
{
    MyClass.Mutable mutable = x;
    mutable.SomeMethod();
}

private void InInt32([System.Runtime.CompilerServices.IsReadOnly] [In] ref int x)
{
    int num = x;
    num.ToString();
}

private void InDateTime([System.Runtime.CompilerServices.IsReadOnly] [In] ref DateTime x)
{
    DateTime dateTime = x;
    dateTime.ToString();
}
...

(or, if you prefer IL:)

IL_0000: ldarg.1
IL_0001: ldobj [mscorlib]System.DateTime
IL_0006: stloc.0
IL_0007: ldloca.s 0
IL_0009: call instance string [mscorlib]System.DateTime::ToString()
IL_000e: pop
IL_000f: ret
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • It's worth noting that, for the 64 bit CLR running on a 64-bit system, all primitives (not counting `decimal`) are word sized or smaller. Thus, it should be no cheaper to load the argument's address than its value. I would not bother marking primitive arguments as `in` even if there weren't a defensive copy being made. – Mike Strobel Jun 12 '18 at 13:24
  • 3
    @MikeStrobel: For performance purposes, you are right. However, the OP wants to use `in` for documentation purposes (to document that the value will stay constant within the method), which is a different use case. – Heinzi Jun 12 '18 at 13:33
4

With the current compiler, defensive copies do indeed appear to be made for both 'primitive' value types and other non-readonly structs. Specifically, they are generated similarly to how they are for readonly fields: when accessing a property or method that could potentially mutate the contents. The copies appear at each call site to a potentially mutating member, so if you invoke n such members, you'll end up making n defensive copies. As with readonly fields, you can avoid multiple copies by manually copying the original to a local.

Take a look at this suite of examples. You can view both the IL and the JIT assembly.

Is it safe to pass primitive types via in arguments and not have defensive copies made?

It depends on whether you access a method or property on the in parameter. If you do, you may see defensive copies. If not, you probably won't:

// Original:
int In(in int _) {
    _.ToString();
    _.GetHashCode();
    return _ >= 0 ? _ + 42 : _ - 42;
}

// Decompiled:
int In([In] [IsReadOnly] ref int _) {
    int num = _;
    num.ToString();    // invoke on copy
    num = _;
    num.GetHashCode(); // invoke on second copy
    if (_ < 0)
        return _ - 42; // use original in arithmetic
    return _ + 42;
}

Are other commonly used framework structs such as DateTime, TimeSpan, Guid, ... considered readonly by [the compiler]?

No, defensive copies will still be made at call sites for potentially mutating members on in parameters of these types. What's interesting, though, is that not all methods and properties are considered 'potentially mutating'. I noticed that if I called a default method implementation (e.g., ToString or GetHashCode), no defensive copies were emitted. However, as soon as I overrode those methods, the compiler created copies:

struct WithDefault {}
struct WithOverride { public override string ToString() => "RO"; }

// Original:
void In(in WithDefault d, in WithOverride o) {
    d.ToString();
    o.ToString();
}

// Decompiled:
private void In([In] [IsReadOnly] ref WithDefault d,
                [In] [IsReadOnly] ref WithOverride o) {
    d.ToString();            // invoke on original
    WithOverride withOverride = o;
    withOverride.ToString(); // invoke on copy
}

If this varies by platform, how can we find out which types are safe in a given situation?

Well, all types are 'safe'--the copies ensure that. I assume you're asking which types will avoid a defensive copy. As we've seen above, it's more complicated than "what's the type of the parameter"? There's no single copy: the copies are emitted at certain references to in parameters, e.g., where the reference is an invocation target. If no such references are present, no copies need to be made. Moreover, the decision whether to copy can depend on whether you invoke a member that is known to be safe or 'pure' vs. a member which could potentially mutate the a value type's contents.

For now, certain default methods seem to be treated as pure, and the compiler avoids making copies in those cases. If I had to guess, this is a result of preexisting behavior, and the compiler is utilizing some notion of 'read only' references that was originally developed for readonly fields. As you can see below (or in SharpLab), the behavior is similar. Note how the IL uses ldflda (load field by address) to push the invocation target onto the stack when calling WithDefault.ToString, but uses a ldfld, stloc, ldloca sequence to push a copy onto the stack when invoking WithOverride.ToString:

struct WithDefault {}
struct WithOverride { public override string ToString() => "RO"; }

static readonly WithDefault D;
static readonly WithOverride O;

// Original:
static void Test() {
    D.ToString();
    O.ToString();
}

// IL Disassembly:
.method private hidebysig static void Test () cil managed {
    .maxstack 1
    .locals init ([0] valuetype Overrides/WithOverride)

    // [WithDefault] Invoke on original by address:
    IL_0000: ldsflda valuetype Overrides/WithDefault Overrides::D
    IL_0005: constrained. Overrides/WithDefault
    IL_000b: callvirt instance string [mscorlib]System.Object::ToString()
    IL_0010: pop

    // [WithOverride] Copy original to local, invoke on copy by address:
    IL_0011: ldsfld valuetype Overrides/WithOverride Overrides::O
    IL_0016: stloc.0
    IL_0017: ldloca.s 0
    IL_0019: constrained. Overrides/WithOverride
    IL_001f: callvirt instance string [mscorlib]System.Object::ToString()
    IL_0024: pop
    IL_0025: ret
}

That said, now that read only references will presumably become more common, the 'white list' of methods that can be invoked without defensive copies may grow in the future. For now, it seems somewhat arbitrary.

Mike Strobel
  • 25,075
  • 57
  • 69
4

From the jit's standpoint in alters the calling convention for a parameter so that it is always passed by-reference. So for primitive types (which are cheap to copy) and normally passed by-value, there a small extra cost on both the caller's side and the callee's side if you use in. No defensive copies are made, however.

Eg in

using System;
using System.Runtime.CompilerServices;

class X
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    static int F0(in int x) { return x + 1; }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static int F1(int x) { return x + 1; }

    public static void Main()
    {
        int x = 33;
        F0(x);
        F0(x);
        F1(x);
        F1(x);
    }
}

The code for Main is

   C744242021000000     mov      dword ptr [rsp+20H], 33
   488D4C2420           lea      rcx, bword ptr [rsp+20H]
   E8DBFBFFFF           call     X:F0(byref):int
   488D4C2420           lea      rcx, bword ptr [rsp+20H]
   E8D1FBFFFF           call     X:F0(byref):int
   8B4C2420             mov      ecx, dword ptr [rsp+20H]
   E8D0FBFFFF           call     X:F1(int):int
   8B4C2420             mov      ecx, dword ptr [rsp+20H]
   E8C7FBFFFF           call     X:F1(int):int

Note because of the in x can't be enregistered.

And the code for F0 & F1 shows the former must now read the value from the byref:

;; F0
   8B01                 mov      eax, dword ptr [rcx]
   FFC0                 inc      eax
   C3                   ret

;; F1
   8D4101               lea      eax, [rcx+1]
   C3                   ret

This extra cost can usually be undone if the jit inlines, though not always.

Andy Ayers
  • 892
  • 6
  • 13
  • 3
    Defensive copies may indeed be made, but only when a potentially mutating member is invoked. If you use an `in int` in an arithmetic expression, no copy needs to be made, but if you call `Int32.ToString()`, you will see that a copy is indeed made. Moreover, the copies are made at the location of the reference, e.g., if you call `ToString()` twice, two separate copies will be made: one at each call site. – Mike Strobel Jun 13 '18 at 14:26
  • 1
    Fair enough. I was really looking for copies made by the caller of the method with `in` and not copies made by the method itself when calling something else. – Andy Ayers Jun 14 '18 at 17:30
0

What does this mean for built-in primitives such as int, double?

Nothing, int and double and all other built-in "primitives" are immutable. You can't mutate a double, an int or a DateTime. A typical framework type that would not be a good candidate is System.Drawing.Point for instance.

To be honest, the documentation could be a little bit clearer; readonly is a confusing term in this context, it should simply say the type should be immutable.

There is no rule to know if any given type is immutable or not; only a close inspection of the API can give you an idea or, if you are lucky, the documentation might state if it is or not.

InBetween
  • 32,319
  • 3
  • 50
  • 90
  • 2
    I get that primitive values are immutable in practice. The real question is whether the JIT sees them that way. – Drew Noakes Jun 09 '18 at 23:33
  • 1
    @DrewNoakes You are overthinking this. You’ve got a language restriction or recommendation; it says don’t use mutable types. Why are you worrying about the jitter at all? – InBetween Jun 10 '18 at 05:46
  • 1
    My interest is preventing the defensive copy. Whether that's done by the JIT or the compiler, the impact on perf would be the same. Did you read the article I linked to? – Drew Noakes Jun 10 '18 at 07:22
  • @DrewNoakes ah, sorry. Well my guess is that until the framework is updated and primitive types are marked as readonly, behavior will be as if the types are not guaranteed to be immutable. What i’m not altogether certain is if making a type readonly is a breaking change. If it is, then obviously all built in types will have to remain as is and maybe some special compiler magic will be implemented for specific framework types. – InBetween Jun 10 '18 at 12:30
  • 1
    The compiler already special cases primitives in many scenarios. Thanks for the guess, but I'm looking for a definitive answer. – Drew Noakes Jun 11 '18 at 10:55