I have situation similar to this:
interface IStorage
{
bool TryGetValue<T>(out T result) where T : struct;
}
class Storage<T> : IStorage where T : struct
{
readonly T value;
public Storage(T val)
{
value = val;
}
public bool TryGetValue<T2>(out T2 result) where T2 : struct
{
if(value is T2 val)
{
result = val;
return true;
}
result = default;
return false;
}
}
In the program, instances implementing IStorage are passed around, and they can be queried for a value of a particular type. I could use something like IStorage<T> and type test it to check if it supports the type, but it would make the code messier since there are other types implementing IStorage that decide whether they support the type or not at runtime.
Now I wonder about value is T2 val. Its purpose is to check whether T and T2 are the same types (and hence compatible), since both are value types. Specialized for T, TryGetValue should return true, and it should return false for all other types.
I am not sure whether this is the best implementation of the check. There are basically two general steps to solve it:
- Determine if
TandT2are the same. - Reinterpret
valueasT2and return it.
There are two other solutions to this problem that could be considered: casting the value to object and checking and unboxing it, or __refvalue(__makeref(value), T2) but that is probably not guaranteed to work on all platforms.
Now value is T2 val looks quite nicely, conveys the meaning well, but I am also wondering about the performance implications and possible optimizations. When I disassemble the method, it turns to this:
.locals init ([0] !!T2 val,
[1] !T V_1)
IL_0000: ldarg.0
IL_0001: ldfld !0 value
IL_0006: dup
IL_0007: stloc.1
IL_0008: box !T
IL_000d: isinst !!T2
IL_0012: brfalse.s IL_0029
IL_0014: ldloc.1
IL_0015: box !T
IL_001a: unbox.any !!T2
IL_001f: stloc.0
IL_0020: ldarg.1
IL_0021: ldloc.0
IL_0022: stobj !!T2
IL_0027: ldc.i4.1
IL_0028: ret
IL_0029: ldarg.1
IL_002a: initobj !!T2
IL_0030: ldc.i4.0
IL_0031: ret
So it turns out the expression boxes the value not only once, but twice, the first time for isinst and the second time for unbox.any, so not only does it hide boxing (which is generally considered quite expensive), but it does it twice.
I have two questions: Is there a better way to achieve this kind of specialization? Is it possible this CIL code, while looking quite inefficient, is optimized later at runtime by JIT?
In this particular case, I'd expect the runtime to infer that the only instantiation where T is T2 should return true, and it should ignore all other code, including the check. Could this be the case?