Consider a loop like this:
for (int i = 0; i < end; ++i) // do something
If I know that i won't overflow, but I want a check against overflow, truncation, etc., in the "do something" part, am I better off with the checked
block inside or outside the loop?
for (int i = 0; i < end; ++i) checked { // do something }
or
checked { for (int i = 0; i < end; ++i) // do something }
More generally, is there a cost to switching between checked and unchecked mode?
It varies between 1.50 SGD ( US$ 1.10) and 2.50 SGD ( US$ 1.80) per journey. If you want to save on transport, the best option is the EZ-Link card or the Singapore Tourist Pass.
Bus and train (MRT and/or LRT) fares in Singapore are charged according to the total distance travelled in a journey and this is known as the Distance Fares system. It lets you make transfers without paying a charge each time you board a bus or train during a journey.
Fares are calculated based on the distance, which brings about a more integrated fare structure that allows commuters to make transfers without incurring additional costs. Distance Fares are computed on a journey basis, without a boarding charge being imposed for every transfer trip that makes up the journey.
If you really want to see the difference, check out some generated IL. Let's take a very simple example:
using System; public class Program { public static void Main() { for(int i = 0; i < 10; i++) { var b = int.MaxValue + i; } } }
And we get:
.maxstack 2 .locals init (int32 V_0, int32 V_1, bool V_2) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: br.s IL_0013 IL_0005: nop IL_0006: ldc.i4 0x7fffffff IL_000b: ldloc.0 IL_000c: add IL_000d: stloc.1 IL_000e: nop IL_000f: ldloc.0 IL_0010: ldc.i4.1 IL_0011: add IL_0012: stloc.0 IL_0013: ldloc.0 IL_0014: ldc.i4.s 10 IL_0016: clt IL_0018: stloc.2 IL_0019: ldloc.2 IL_001a: brtrue.s IL_0005 IL_001c: ret
Now, let's make sure we're checked:
public class Program { public static void Main() { for(int i = 0; i < 10; i++) { checked { var b = int.MaxValue + i; } } } }
And now we get the following IL:
.maxstack 2 .locals init (int32 V_0, int32 V_1, bool V_2) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: br.s IL_0015 IL_0005: nop IL_0006: nop IL_0007: ldc.i4 0x7fffffff IL_000c: ldloc.0 IL_000d: add.ovf IL_000e: stloc.1 IL_000f: nop IL_0010: nop IL_0011: ldloc.0 IL_0012: ldc.i4.1 IL_0013: add IL_0014: stloc.0 IL_0015: ldloc.0 IL_0016: ldc.i4.s 10 IL_0018: clt IL_001a: stloc.2 IL_001b: ldloc.2 IL_001c: brtrue.s IL_0005 IL_001e: ret
As you can see, the only difference (with the exception of some extra nop
s) is that our add operation emits add.ovf
rather than a simple add
. The only overhead you'll accrue is the difference is those operations.
Now, what happens if we move the checked
block to include the entire for
loop:
public class Program { public static void Main() { checked { for(int i = 0; i < 10; i++) { var b = int.MaxValue + i; } } } }
We get the new IL:
.maxstack 2 .locals init (int32 V_0, int32 V_1, bool V_2) IL_0000: nop IL_0001: nop IL_0002: ldc.i4.0 IL_0003: stloc.0 IL_0004: br.s IL_0014 IL_0006: nop IL_0007: ldc.i4 0x7fffffff IL_000c: ldloc.0 IL_000d: add.ovf IL_000e: stloc.1 IL_000f: nop IL_0010: ldloc.0 IL_0011: ldc.i4.1 IL_0012: add.ovf IL_0013: stloc.0 IL_0014: ldloc.0 IL_0015: ldc.i4.s 10 IL_0017: clt IL_0019: stloc.2 IL_001a: ldloc.2 IL_001b: brtrue.s IL_0006 IL_001d: nop IL_001e: ret
You can see that both of the add
operations have been converted to add.ovf
rather than just the inner operation so you're getting twice the "overhead". In any case, I'm guessing the "overhead" would be negligible for most use-cases.
checked
and unchecked
blocks don't appear at the IL level. They are only used in the C# source code to tell the compiler whether or not to pick the checking or non-checking IL instructions when overriding the default preference of the build configuration (which is set through a compiler flag).
Of course, typically there will be a performance difference due to the fact that different opcodes have been emitted for the arithmetic operations (but not due to entering or exiting the block). Checked arithmetic is generally expected to have some overhead over corresponding unchecked arithmetic.
As a matter of fact, consider this C# program:
class Program { static void Main(string[] args) { var a = 1; var b = 2; int u1, c1, u2, c2; Console.Write("unchecked add "); unchecked { u1 = a + b; } Console.WriteLine(u1); Console.Write("checked add "); checked { c1 = a + b; } Console.WriteLine(c1); Console.Write("unchecked call "); unchecked { u2 = Add(a, b); } Console.WriteLine(u2); Console.Write("checked call "); checked { c2 = Add(a, b); } Console.WriteLine(c2); } static int Add(int a, int b) { return a + b; } }
This is the generated IL, with optimizations turned on and with unchecked arithmetic by default:
.class private auto ansi beforefieldinit Checked.Program extends [mscorlib]System.Object { .method private hidebysig static int32 Add ( int32 a, int32 b ) cil managed { IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: ret } .method private hidebysig static void Main ( string[] args ) cil managed { .entrypoint .locals init ( [0] int32 b ) IL_0000: ldc.i4.1 IL_0001: ldc.i4.2 IL_0002: stloc.0 IL_0003: ldstr "unchecked add " IL_0008: call void [mscorlib]System.Console::Write(string) IL_000d: dup IL_000e: ldloc.0 IL_000f: add IL_0010: call void [mscorlib]System.Console::WriteLine(int32) IL_0015: ldstr "checked add " IL_001a: call void [mscorlib]System.Console::Write(string) IL_001f: dup IL_0020: ldloc.0 IL_0021: add.ovf IL_0022: call void [mscorlib]System.Console::WriteLine(int32) IL_0027: ldstr "unchecked call " IL_002c: call void [mscorlib]System.Console::Write(string) IL_0031: dup IL_0032: ldloc.0 IL_0033: call int32 Checked.Program::Add(int32, int32) IL_0038: call void [mscorlib]System.Console::WriteLine(int32) IL_003d: ldstr "checked call " IL_0042: call void [mscorlib]System.Console::Write(string) IL_0047: ldloc.0 IL_0048: call int32 Checked.Program::Add(int32, int32) IL_004d: call void [mscorlib]System.Console::WriteLine(int32) IL_0052: ret } }
As you can see, the checked
and unchecked
blocks are merely a source code concept - there is no IL emitted when switching back and forth between what was (in the source) a checked
and an unchecked
context. What changes is the opcodes emitted for direct arithmetic operations (in this case, add
and add.ovf
) that were textually enclosed in those blocks. The specification covers which operations are affected:
The following operations are affected by the overflow checking context established by the checked and unchecked operators and statements:
- The predefined ++ and -- unary operators (§7.6.9 and §7.7.5), when the operand is of an integral type.
- The predefined - unary operator (§7.7.2), when the operand is of an integral type.
- The predefined +, -, *, and / binary operators (§7.8), when both operands are of integral types.
- Explicit numeric conversions (§6.2.1) from one integral type to another integral type, or from float or double to an integral type.
And as you can see, a method called from a checked
or unchecked
block will retain its body and it will not receive any information about what context it was called from. This is also spelled out in the specification:
The checked and unchecked operators only affect the overflow checking context for those operations that are textually contained within the “(” and “)” tokens. The operators have no effect on function members that are invoked as a result of evaluating the contained expression.
In the example
class Test { static int Multiply(int x, int y) { return x * y; } static int F() { return checked(Multiply(1000000, 1000000)); } }
the use of checked in F does not affect the evaluation of x * y in Multiply, so x * y is evaluated in the default overflow checking context.
As noted, the above IL was generated with C# compiler optimizations turned on. The same conclusions can be drawn from the IL that's emitted without these optimizations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With