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内部操作而不仅仅是内部操作,因此您将获得两倍的"开销".无论如何,我猜测大多数用例的"开销"可以忽略不计.
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)
正如您所看到的,checked和unchecked块只是一个源代码概念 - 在源(在源中)checked和unchecked上下文之间来回切换时没有发出IL .对于直接算术运算(在这种情况下,add和add.ovf)发出的操作码的变化是文本上包含在这些块中的.规范涵盖了受影响的操作:
以下操作受已检查和未检查的运算符和语句建立的溢出检查上下文的影响:
- 当操作数是整数类型时,预定义的++和 - 一元运算符(第7.6.9节和第7.7.5节).
- 当操作数是整数类型时,预定义的 - 一元运算符(第7.7.2节).
- 预定义的+, - ,*和/二元运算符(第7.7节),当两个操作数都是整数类型时.
- 从一个整数类型到另一个整数类型的显式数值转换(第6.2.1节),或从float或double到整数类型.
正如您所看到的,从a checked或unchecked块调用的方法将保留其正文,并且不会接收有关从其调用的上下文的任何信息.这也在规范中详细说明:
checked和unchecked运算符仅影响文本包含在"("和")"标记中的那些操作的溢出检查上下文.运算符对由于计算包含的表达式而调用的函数成员没有影响.
在示例中
Run Code Online (Sandbox Code Playgroud)class Test { static int Multiply(int x, int y) { return x * y; } static int F() { return checked(Multiply(1000000, 1000000)); } }使用F中的check不影响Multiply中x*y的计算,因此x*y在默认溢出检查上下文中计算.
如上所述,上面的IL是在打开C#编译器优化的情况下生成的.可以从没有这些优化的情况下发出的IL得出相同的结论.
除了上面的答案,我想澄清检查是如何执行的.我知道的唯一方法是检查OF和CF标志.该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(惊喜!)用于无符号溢出.我没有装配程序很长一段时间,但是看起来jb并jc是相同的指令(他们俩仅检查进位标志).我不知道为什么JIT开发人员决定使用jb而不是jc因为如果我是一个CPU制造者,我会做一个分支预测器来假设jo\jc跳转不太可能发生.
总而言之,没有其他指令被调用来在已检查和未检查模式之间切换,但checked只要在每个算术指令之后执行检查,块中的算术运算必须明显变慢.但是,我很确定现代CPU可以很好地处理这个问题.
我希望它有所帮助.
| 归档时间: |
|
| 查看次数: |
1259 次 |
| 最近记录: |