The following two C#
functions differ only in swapping the left/right order of arguments to the equals operator, ==
. (The type of IsInitialized
is bool
). Using C# 7.1 and .NET 4.7.
static void A(ISupportInitialize x)
{
if ((x as ISupportInitializeNotification)?.IsInitialized == true)
throw null;
}
static void B(ISupportInitialize x)
{
if (true == (x as ISupportInitializeNotification)?.IsInitialized)
throw null;
}
But the IL code for the second one seems much more complex. For example, B is:
- 36 bytes longer (IL code);
- calls additional functions including
newobj
andinitobj
; - declares four locals versus just one.
IL for function 'A'…
[0] bool flag
nop
ldarg.0
isinst [System]ISupportInitializeNotification
dup
brtrue.s L_000e
pop
ldc.i4.0
br.s L_0013
L_000e: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
L_0013: stloc.0
ldloc.0
brfalse.s L_0019
ldnull
throw
L_0019: ret
IL for function 'B'…
[0] bool flag,
[1] bool flag2,
[2] valuetype [mscorlib]Nullable`1<bool> nullable,
[3] valuetype [mscorlib]Nullable`1<bool> nullable2
nop
ldc.i4.1
stloc.1
ldarg.0
isinst [System]ISupportInitializeNotification
dup
brtrue.s L_0018
pop
ldloca.s nullable2
initobj [mscorlib]Nullable`1<bool>
ldloc.3
br.s L_0022
L_0018: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0022: stloc.2
ldloc.1
ldloca.s nullable
call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
beq.s L_0030
ldc.i4.0
br.s L_0037
L_0030: ldloca.s nullable
call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0037: stloc.0
ldloc.0
brfalse.s L_003d
ldnull
throw
L_003d: ret
Questions
- Is there any functional, semantic, or other substantial runtime difference between A and B? (We're only interested in correctness here, not performance)
- If they are not functionally equivalent, what are the runtime conditions that can expose an observable difference?
- If they are functional equivalents, what is B doing (that always ends up with the same result as A), and what triggered its spasm? Does B have branches that can never execute?
- If the difference is explained by the difference between what appears on the left side of
==
, (here, a property referencing expression versus a literal value), can you indicate a section of the C# spec that describes the details. - Is there a reliable rule-of-thumb that can be used to predict the bloated IL at coding-time, and thus avoid creating it?
BONUS. How does the respective final JITted x86
or AMD64
code for each stack up?
[edit]
Additional notes based on feedback in the comments. First, a third variant was proposed, but it gives identical IL as A (for both Debug
and Release
builds). Sylistically, however, the C# for the new one does seem sleeker than A:
static void C(ISupportInitialize x)
{
if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
throw null;
}
Here also is the Release
IL for each function. Note that the asymmetry A/C vs. B is still evident with the Release
IL, so the original question still stands.
Release IL for functions 'A', 'C'…
ldarg.0
isinst [System]ISupportInitializeNotification
dup
brtrue.s L_000d
pop
ldc.i4.0
br.s L_0012
L_000d: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
brfalse.s L_0016
ldnull
throw
L_0016: ret
Release IL for function 'B'…
[0] valuetype [mscorlib]Nullable`1<bool> nullable,
[1] valuetype [mscorlib]Nullable`1<bool> nullable2
ldc.i4.1
ldarg.0
isinst [System]ISupportInitializeNotification
dup
brtrue.s L_0016
pop
ldloca.s nullable2
initobj [mscorlib]Nullable`1<bool>
ldloc.1
br.s L_0020
L_0016: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0020: stloc.0
ldloca.s nullable
call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
beq.s L_002d
ldc.i4.0
br.s L_0034
L_002d: ldloca.s nullable
call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0034: brfalse.s L_0038
ldnull
throw
L_0038: ret
Lastly, a version using new C# 7 syntax was mentioned which seems to produce the cleanest IL of all:
static void D(ISupportInitialize x)
{
if (x is ISupportInitializeNotification y && y.IsInitialized)
throw null;
}
Release IL for function 'D'…
[0] class [System]ISupportInitializeNotification y
ldarg.0
isinst [System]ISupportInitializeNotification
dup
stloc.0
brfalse.s L_0014
ldloc.0
callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
brfalse.s L_0014
ldnull
throw
L_0014: ret
[edit 2023]
Another new syntax, "property pattern matching", is available starting in C# 8. The following gives the same "best" IL as example D:
static void E(ISupportInitialize x)
{
if (x is ISupportInitializeNotification { IsInitialized: true })
throw null;
}
?.
, and trying to call IsInitialized if the nullable has a value – Rhythmist?
operator in your examples and check again, will it gives the same results? – Stichter?
changes everything, since then you also no longer need== true
, and you'd end up withif ((x as ISupportInitializeNotification).IsInitialized)…
, and furthermore at that point you might as well just doif (((ISupportInitializeNotification)x).IsInitialized)…
– Farantldc.i4.1
since that commits it to eventually doing a non-nullable compare. But you stop short of explaining why the same short-circuit isn't detected on the right side… I do realizeC#
guarantees left-to-right evaluation order perhaps in force here due to the possibilty of side-effectingIsInitialized
, but surely failure of left sidetrue
could be precluded...? Optimization bug? – Faranttrue
literal on the left side of the evaluation? Other than a stylistic desire to make the code look C++-like, that is? – ButtaroC
/C++
: it eliminates the possibility of unintentional assignment (via=
) when equality (==
) is intended, a nasty bug which can be hard to track down sinceif (a = b)…
looks so similar toif (a == b)…
(It's a moot issue inC#
). So the reason instead is that I generally like to have a vague notion of the IL I'll be getting as I writeC#
code, so surprises I don't understand require some investigation of "why." – Farantif ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
. Even better the new is-expressions with patterns:if (x is ISupportInitializeNotification y && y.IsInitialized)
. They both result in equal or shorter IL than A. – CarbazoleRelease
IL for all examples, including the two additional suggested by @Johnbot. The assymetry noted in the original question persists. – FarantIsInitialized
return? Is itbool
,Nullable<bool>
or something else? What version of VC#? For now, VTC for no minimal reproducible example. I really don't see anything like this in the spec and in a test withbool
, there's no difference. – CeutaISupportInitialize
andISupportInitializeNotification
are both defined in the .NET namespaceSystem.ComponentModel
, provided by the CLR inSystem.dll
. I omitted this information since I assumed it would be well-known and thus would only create clutter here. I have now modified the post to indicate this at the top. I believe the minimal example requirement is satisfied by the contrast between A and B, which are indeed complete, minimal, and verifiable. Please provide additional remarks if you aren't able to retract your VTC. Thanks. – FarantVC#
and.NET
versions I'm using. – Farant