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
, ... consideredreadonly
by the JIT?- If this varies by platform, how can we find out which types are safe in a given situation?
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):
yields the following result when decompiled with ILSpy:
(or, if you prefer IL:)
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 withreadonly
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.
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: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
orGetHashCode
), no defensive copies were emitted. However, as soon as I overrode those methods, the compiler created copies: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 usesldflda
(load field by address) to push the invocation target onto the stack when callingWithDefault.ToString
, but uses aldfld
,stloc
,ldloca
sequence to push a copy onto the stack when invokingWithOverride.ToString
: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.
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 usein
. No defensive copies are made, however.Eg in
The code for
Main
isNote 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:This extra cost can usually be undone if the jit inlines, though not always.
Nothing,
int
anddouble
and all other built-in "primitives" are immutable. You can't mutate adouble
, anint
or aDateTime
. A typical framework type that would not be a good candidate isSystem.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.