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
Quesions
- 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