进入和退出C#检查块是否需要付费?

Edw*_*rey 29 c# integer-arithmetic

考虑这样的循环:

for (int i = 0; i < end; ++i)
    // do something
Run Code Online (Sandbox Code Playgroud)

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

for (int i = 0; i < end; ++i)
    checked {
         // do something
    }
Run Code Online (Sandbox Code Playgroud)

要么

checked {
    for (int i = 0; i < end; ++i)
         // do something
}
Run Code Online (Sandbox Code Playgroud)

更一般地说,在已检查和未检查模式之间切换是否需要付费?

Jus*_*ner 19

如果你真的想看到差异,请查看一些生成的IL.我们来看一个非常简单的例子:

using System;

public class Program 
{
    public static void Main()
    {
        for(int i = 0; i < 10; i++)
        {
            var b = int.MaxValue + i;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们得到:

.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
Run Code Online (Sandbox Code Playgroud)

现在,让我们确保检查:

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

现在我们得到以下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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,唯一的区别(除了一些额外的nops)是我们的添加操作发出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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们得到了新的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
Run Code Online (Sandbox Code Playgroud)

您可以看到两个add操作都已转换为add.ovf内部操作而不仅仅是内部操作,因此您将获得两倍的"开销".无论如何,我猜测大多数用例的"开销"可以忽略不计.

  • 结论是对的,但推理是错误的.重要的是JIT生成的x86.IL无关紧要. (6认同)
  • @usr:两件事生成不同IL的事实并不意味着他们会生成不同的x86代码,但如果两件事生成相同的IL,那么任何x86代码都将是相同的. (4认同)
  • 这个MSIL不是很相关,因为看起来它是由编译器在调试模式下生成的,没有优化. (2认同)
  • @JustinNiessner:可以优化循环增量(因为优化器可以证明没有溢出).所有`nop`和无用的`br.s`指令肯定会消失.额外的局部变量将消失.与本机代码优化的规则相反,读取优化的MSIL通常比调试模式更容易. (2认同)

The*_*kis 9

checked并且unchecked块不会出现在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是生成的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
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,checkedunchecked块只是一个源代码概念 - 在源(在源中)checkedunchecked上下文之间来回切换时没有发出IL .对于直接算术运算(在这种情况下,addadd.ovf)发出的操作码的变化是文本上包含在这些块中的.规范涵盖了受影响的操作:

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

  • 当操作数是整数类型时,预定义的++和 - 一元运算符(第7.6.9节和第7.7.5节).
  • 当操作数是整数类型时,预定义的 - 一元运算符(第7.7.2节).
  • 预定义的+, - ,*和/二元运算符(第7.7节),当两个操作数都是整数类型时.
  • 从一个整数类型到另一个整数类型的显式数值转换(第6.2.1节),或从float或double到整数类型.

正如您所看到的,从a checkedunchecked块调用的方法将保留其正文,并且不会接收有关从其调用的上下文的任何信息.这也在规范中详细说明:

checked和unchecked运算符仅影响文本包含在"("和")"标记中的那些操作的溢出检查上下文.运算符对由于计算包含的表达式而调用的函数成员没有影响.

在示例中

class Test
{
  static int Multiply(int x, int y) {
      return x * y;
  }
  static int F() {
      return checked(Multiply(1000000, 1000000));
  }
}
Run Code Online (Sandbox Code Playgroud)

使用F中的check不影响Multiply中x*y的计算,因此x*y在默认溢出检查上下文中计算.

如上所述,上面的IL是在打开C#编译器优化的情况下生成的.可以从没有这些优化的情况下发出的IL得出相同的结论.

  • 启用优化,从性能角度来看,调试MSIL并不有趣. (5认同)

Eld*_*iev 6

除了上面的答案,我想澄清检查是如何执行的.我知道的唯一方法是检查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可以很好地处理这个问题.

我希望它有所帮助.

  • 你是专门讨论x86/x64架构的,对吧?我认为你应该明确表示,.Net也可以在其他架构上运行. (2认同)
  • "jb"和"jc"助记符组装成相同的位模式.我认为Intel指的是当进位设置为"jb"[目标低于源]时跳转的指令,如果进位未设置为"jae"[目的地高于或等于源]则跳转的指令与" ja"[目的地高于源,即进位标志清零且Z标志未设置]和"jbe"[目的地低于或等于源,即进位设置或Z标志设置]. (2认同)