Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a cost to entering and exiting a C# checked block?

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?

like image 924
Edward Brey Avatar asked Sep 17 '15 17:09

Edward Brey


People also ask

How much does an MRT train cost?

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.

Does MRT charge by time?

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.

How does MRT fare work?

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.


2 Answers

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 nops) 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.

like image 98
Justin Niessner Avatar answered Sep 17 '22 14:09

Justin Niessner


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.

like image 28
Theodoros Chatzigiannakis Avatar answered Sep 20 '22 14:09

Theodoros Chatzigiannakis