The IL shows this: The first call is boxed:
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0 // a
IL_0003: ldc.i4.1
IL_0004: stloc.1 // b
IL_0005: ldloca.s 01 // b
IL_0007: ldloc.0 // a
IL_0008: box System.Int32
IL_000D: call System.Int16.Equals
IL_0012: call System.Console.WriteLine
IL_0017: nop
IL_0018: ret
The second is not:
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0 // a
IL_0003: ldc.i4.1
IL_0004: stloc.1 // b
IL_0005: ldloca.s 00 // a
IL_0007: ldloc.1 // b
IL_0008: call System.Int32.Equals
IL_000D: call System.Console.WriteLine
IL_0012: nop
IL_0013: ret
The IL generated for the generic method is always boxing:
Test<T1,T2>:
IL_0000: nop
IL_0001: ldarga.s 02
IL_0003: ldarg.1
IL_0004: box T1
IL_0009: constrained. T2
IL_000F: callvirt System.Object.Equals
IL_0014: call System.Console.WriteLine
IL_0019: nop
IL_001A: ldarga.s 01
IL_001C: ldarg.2
IL_001D: box T2
IL_0022: constrained. T1
IL_0028: callvirt System.Object.Equals
IL_002D: call System.Console.WriteLine
IL_0032: nop
IL_0033: ret
and a call to this method is done with:
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0 // a
IL_0003: ldc.i4.1
IL_0004: stloc.1 // b
IL_0005: ldarg.0
IL_0006: ldloc.0 // a
IL_0007: ldloc.1 // b
IL_0008: call Test<Int32,Int16>
IL_000D: nop
IL_000E: ret
so, even when the type is known at compile time, the values will still be boxed.