进入和退出c#检查块是否有成本?

本文关键字:是否 检查 退出 | 更新日期: 2023-09-27 18:13:15

考虑这样一个循环:

for (int i = 0; i < end; ++i)
    // do something

如果我知道 I 不会溢出,但我想要检查溢出,截断等,在"做某事"部分,我是在循环内部还是外部使用checked块更好?

for (int i = 0; i < end; ++i)
    checked {
         // do something
    }

checked {
    for (int i = 0; i < end; ++i)
         // do something
}

更一般地说,在检查模式和未检查模式之间切换是否有成本?

进入和退出c#检查块是否有成本?

如果您真的想要查看差异,请查看一些生成的IL。

using System;
public class Program 
{
    public static void Main()
    {
        for(int i = 0; i < 10; i++)
        {
            var b = int.MaxValue + i;
        }
    }
}

得到:

.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

现在,让我们确保我们被选中了:

public class Program 
{
    public static void Main()
    {
        for(int i = 0; i < 10; i++)
        {
            checked
            {
                var b = int.MaxValue + i;
            }
        }
    }
}

现在我们得到如下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

正如您所看到的,唯一的区别(除了一些额外的nop)是我们的add操作发出add.ovf而不是简单的add。唯一会增加的开销就是这些操作的差异。

现在,如果我们移动checked块来包含整个for循环会发生什么:

public class Program 
{
    public static void Main()
    {
        checked
        {
            for(int i = 0; i < 10; i++)
            {
                var b = int.MaxValue + i;
            }
        }
    }
}

得到新的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

你可以看到两个add操作都被转换为add.ovf,而不仅仅是内部操作,所以你得到了两倍的"开销"。在任何情况下,我猜"开销"对于大多数用例来说是可以忽略不计的。

checkedunchecked块不出现在IL级别。它们只在c#源代码中使用,当覆盖构建配置的默认首选项(通过编译器标志设置)时,告诉编译器是否选择检查或不检查IL指令。

当然,通常会有性能差异,这是由于为算术运算发出了不同的操作码(但不是由于进入或退出块)。检查算术通常会比相应的未检查算术有一些开销。

实际上,考虑一下这个c#程序:

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;
    }
}

这是生成的IL,默认情况下启用了优化和未检查的算法:

.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
    }
}

您可以看到,checkedunchecked块仅仅是一个源代码概念——当(在源代码中)在checkedunchecked上下文中来回切换时,没有发出IL。改变的是直接算术运算(在本例中是addadd.ovf)所发出的操作码,这些操作码在文本上包含在这些块中。该规范涵盖了受影响的操作:

以下操作受已检查和未检查的操作符和语句建立的溢出检查上下文的影响:

  • 当操作数为整型时,预定义的++和——一元操作符(§7.6.9和§7.7.5)。
  • 当操作数为整型时,预定义的一元操作符(§7.7.2)。
  • 预定义的+、-、*和/二进制操作符(§7.8),当两个操作数都是整型时。
  • 从一种整型到另一种整型,或从浮点型或双精度型到整型的显式数值转换(§6.2.1)。

正如你所看到的,从checkedunchecked块中调用的方法将保留其主体,并且它将不会接收到关于它是从哪个上下文调用的任何信息。这在规范中也有说明:

已检查和未检查的操作符仅影响文本中包含在"("answers")"令牌中的那些操作的溢出检查上下文。操作符对作为对所包含表达式求值的结果而调用的函数成员没有影响。

在示例

class Test
{
  static int Multiply(int x, int y) {
      return x * y;
  }
  static int F() {
      return checked(Multiply(1000000, 1000000));
  }
}

在F中使用checked不影响在Multiply中对x * y求值,因此x * y在默认的溢出检查上下文中求值。

如前所述,上面的IL是在c#编译器优化打开的情况下生成的。从没有进行这些优化的IL中也可以得出相同的结论。

除了上面的答案,我还想澄清一下检查是如何执行的。我知道的唯一方法是检查OFCF标志。CF标志由无符号算术指令设置,而OF标志由有符号算术指令设置。

这些标志可以用seto'setc指令读取,或者(最常用的方式)我们可以使用jo'jc跳转指令,如果设置了OF'CF标志,该指令将跳转到所需的地址。

但是,有一个问题。jo'jc是一个"条件"跳转,这对CPU管道来说是一个彻底的痛苦。所以我想也许有另一种方法可以做到这一点,比如设置一个特殊的寄存器,当检测到溢出时中断执行,所以我决定找出微软的JIT是如何做到的。

我相信你们大多数人都听说了微软已经开放了。net子集的源代码,命名为。net Core。. net Core的源代码中包含了CoreCLR,所以我对它进行了深入研究。溢出检测代码在CodeGen::genCheckOverflow(GenTreePtr tree)方法中生成(第2484行)。可以清楚地看到,jo指令用于有符号溢出检查,jb(奇怪!)用于无符号溢出检查。我没有在汇编编程很长一段时间,但它看起来像jbjc是相同的指令(他们都检查进位标志)。我不知道为什么JIT开发人员决定使用jb而不是jc,因为如果我是cpu制造商,我会制作一个分支预测器,假设jo'jc跳转是不太可能发生的。

综上所述,没有调用额外的指令来切换检查和未检查模式,但是checked块中的算术运算一定会明显变慢,只要在每个算术指令之后执行检查。然而,我很确定现代的cpu可以很好地处理这个问题。

我希望它有帮助。

"更一般地说,在选中模式和未选中模式之间切换是否有成本? "

不,在你的例子中没有。唯一的开销是++i

在这两种情况下,c#编译器将生成add.ovf, sub.ovf, mul.ovfconv.ovf

但是当循环在checked block内时,++i

将会有一个额外的add.ovf