当没有抛出异常时,try/catch块是否会损害性能?

在与微软员工进行代码检查时,我们在try{}块中遇到了一大段代码。她和一位IT代表表示,这可能会对代码的性能产生影响。事实上,他们建议大部分代码应该在try/catch块之外,并且只检查重要的部分。这位微软员工补充说,即将发布的白皮书对错误的尝试/捕获块提出了警告。

我环顾四周,发现它会影响优化,但它似乎只适用于变量之间的作用域共享。

我不是在问代码的可维护性,甚至不是在问如何处理正确的异常(毫无疑问,有问题的代码需要重构)。我也不是指使用异常进行流控制,这在大多数情况下显然是错误的。这些都是重要的问题(有些更重要),但不是这里的重点。

抛出异常时,try/catch块如何影响性能?

104555 次浏览

. net异常模型的非常全面的解释

Rico Mariani的表演花絮:异常代价:何时抛出,何时不抛出

第一种成本是静态成本 异常处理的开销 你的代码。管理例外 实际上在这里做得比较好, 我指的是静态成本 比c++低很多。为什么 这个吗?静态成本是 发生在两种地方的: 首先,实际的网站 尝试/最后/抓住/扔 这些构造的代码。其次,在 未经管理的代码,这就是秘密 与跟踪有关的费用 所有必须存在的对象 在事件中被破坏 异常。有一个 相当多的清理逻辑 那一定是现在的鬼鬼祟祟的 部分原因是即使是代码也没有 本身就是扔,接或者其他 现在还公开使用异常吗 承担着知道如何去做的责任

他在Zaslavskiy:

根据Chris Brumme的注释:有 还有一个与事实有关的成本 有些优化没有进行 由JIT在现场执行 抓住< / p >

不。如果try/finally块排除的琐碎优化实际上对您的程序有可衡量的影响,那么您可能一开始就不应该使用. net。

检查它。

static public void Main(string[] args)
{
Stopwatch w = new Stopwatch();
double d = 0;


w.Start();


for (int i = 0; i < 10000000; i++)
{
try
{
d = Math.Sin(1);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}


w.Stop();
Console.WriteLine(w.Elapsed);
w.Reset();
w.Start();


for (int i = 0; i < 10000000; i++)
{
d = Math.Sin(1);
}


w.Stop();
Console.WriteLine(w.Elapsed);
}

输出:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

以毫秒为单位:

449
416

新代码:

for (int j = 0; j < 10; j++)
{
Stopwatch w = new Stopwatch();
double d = 0;
w.Start();


for (int i = 0; i < 10000000; i++)
{
try
{
d = Math.Sin(d);
}


catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}


finally
{
d = Math.Sin(d);
}
}


w.Stop();
Console.Write("   try/catch/finally: ");
Console.WriteLine(w.ElapsedMilliseconds);
w.Reset();
d = 0;
w.Start();


for (int i = 0; i < 10000000; i++)
{
d = Math.Sin(d);
d = Math.Sin(d);
}


w.Stop();
Console.Write("No try/catch/finally: ");
Console.WriteLine(w.ElapsedMilliseconds);
Console.WriteLine();
}

新结果:

   try/catch/finally: 382
No try/catch/finally: 332


try/catch/finally: 375
No try/catch/finally: 332


try/catch/finally: 376
No try/catch/finally: 333


try/catch/finally: 375
No try/catch/finally: 330


try/catch/finally: 373
No try/catch/finally: 329


try/catch/finally: 373
No try/catch/finally: 330


try/catch/finally: 373
No try/catch/finally: 352


try/catch/finally: 374
No try/catch/finally: 331


try/catch/finally: 380
No try/catch/finally: 329


try/catch/finally: 374
No try/catch/finally: 334

我在一个紧循环中测试了try..catch的实际影响,它本身太小,在任何正常情况下都不会成为性能问题。

如果循环做了很少的工作(在我的测试中,我做了x++),你可以衡量异常处理的影响。带有异常处理的循环的运行时间大约长了10倍。

如果循环做一些实际的工作(在我的测试中,我调用Int32。解析方法),异常处理的影响太小,无法测量。通过交换循环的顺序,我得到了更大的差异……

try catch块对性能的影响可以忽略不计,但异常抛出可能相当大,这可能是你的同事感到困惑的地方。

本例中的结构与本米不同。它将在内部for循环中被扩展,这将导致两种情况之间不能很好地比较。

下面是更准确的比较,整个代码检查(包括变量声明)在Try/Catch块中:

        for (int j = 0; j < 10; j++)
{
Stopwatch w = new Stopwatch();
w.Start();
try {
double d1 = 0;
for (int i = 0; i < 10000000; i++) {
d1 = Math.Sin(d1);
d1 = Math.Sin(d1);
}
}
catch (Exception ex) {
Console.WriteLine(ex.ToString());
}
finally {
//d1 = Math.Sin(d1);
}
w.Stop();
Console.Write("   try/catch/finally: ");
Console.WriteLine(w.ElapsedMilliseconds);
w.Reset();
w.Start();
double d2 = 0;
for (int i = 0; i < 10000000; i++) {
d2 = Math.Sin(d2);
d2 = Math.Sin(d2);
}
w.Stop();
Console.Write("No try/catch/finally: ");
Console.WriteLine(w.ElapsedMilliseconds);
Console.WriteLine();
}

当我从本米运行原始测试代码时,我注意到Debug和Releas配置中的差异。

这个版本,我注意到在调试版本中有一个不同(实际上比其他版本更多),但在发布版本中没有任何不同。

< p > < em >结论< / em >: < br > 基于这些测试,我认为我们可以说Try/Catch 对性能有很小的影响 < p > 编辑: < br > 我尝试将循环值从10000000增加到1000000000,并在Release中再次运行以获得发布中的一些差异,结果是:

   try/catch/finally: 509
No try/catch/finally: 486


try/catch/finally: 479
No try/catch/finally: 511


try/catch/finally: 475
No try/catch/finally: 477


try/catch/finally: 477
No try/catch/finally: 475


try/catch/finally: 475
No try/catch/finally: 476


try/catch/finally: 477
No try/catch/finally: 474


try/catch/finally: 475
No try/catch/finally: 475


try/catch/finally: 476
No try/catch/finally: 476


try/catch/finally: 475
No try/catch/finally: 476


try/catch/finally: 475
No try/catch/finally: 474

你看,结果是不必然的。在某些情况下,使用Try/Catch的版本实际上更快!

try/catch对性能有影响。

但影响并不大。try/catch复杂度通常是O(1),就像一个简单的赋值一样,除了它们被放置在一个循环中。所以你必须明智地使用它们。

在这里是一个关于try/catch性能的引用(虽然没有解释它的复杂性,但它是隐含的)。来看看抛出更少的异常部分

在看到有try/catch和没有try/catch的所有统计数据后,好奇心迫使我去看后面,看看在这两种情况下生成了什么。代码如下:

c#:

private static void TestWithoutTryCatch(){
Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1));
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
// Code size       32 (0x20)
.maxstack  8
IL_0000:  nop
IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
IL_0006:  ldc.r8     1.
IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
IL_0014:  box        [mscorlib]System.Double
IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
object)
IL_001e:  nop
IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

c#:

private static void TestWithTryCatch(){
try{
Console.WriteLine("SIN(1) = {0}", Math.Sin(1));
}
catch (Exception ex){
Console.WriteLine(ex);
}
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
// Code size       49 (0x31)
.maxstack  2
.locals init ([0] class [mscorlib]System.Exception ex)
IL_0000:  nop
.try
{
IL_0001:  nop
IL_0002:  ldstr      "SIN(1) = {0}"
IL_0007:  ldc.r8     1.
IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
IL_0015:  box        [mscorlib]System.Double
IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
object)
IL_001f:  nop
IL_0020:  nop
IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
}  // end .try
catch [mscorlib]System.Exception
{
IL_0023:  stloc.0
IL_0024:  nop
IL_0025:  ldloc.0
IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
IL_002b:  nop
IL_002c:  nop
IL_002d:  leave.s    IL_002f
}  // end handler
IL_002f:  nop
IL_0030:  ret
} // end of method Program::TestWithTryCatch

我不是IL方面的专家,但我们可以看到在第4行.locals init ([0] class [mscorlib]System.Exception ex)之后创建了一个局部异常对象,这与第17行IL_0021: leave.s IL_002f之前没有try/catch的方法是一样的。如果发生异常,控件跳转到行IL_0025: ldloc.0,否则跳转到标签IL_002d: leave.s IL_002f并返回函数。

我可以安全地假设,如果没有异常发生,那么创建局部变量来保存异常对象只有和跳转指令的开销。

参见关于try/catch实现的讨论讨论try/catch块如何工作,以及一些实现如何有高开销,而一些实现有零开销。 当没有异常发生时。特别地,我认为Windows 32位的实现有很高的开销,而64位的实现没有

理论上,try/catch块不会对代码行为产生影响,除非实际发生异常。然而,在一些罕见的情况下,try/catch块的存在可能会产生重大影响,而在一些不常见但并不晦涩的情况下,这种影响可能是显而易见的。这样做的原因是给定的代码如下:

Action q;
double thing1()
{ double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
{ q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

编译器可以基于保证statement2在statement3之前执行这一事实来优化statement1。如果编译器可以识别出thing1没有副作用,并且thing2实际上没有使用x,那么它可以安全地完全省略thing1。如果thing1是昂贵的,这可能是一个主要的优化,尽管thing1是昂贵的情况也是编译器最不可能优化的。假设代码被更改:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

现在存在一个事件序列,其中statement3可以在没有执行statement2的情况下执行。即使在thing2的代码中没有任何东西可以抛出异常,另一个线程也可以使用Interlocked.CompareExchange来注意到q被清除,并将其设置为Thread.ResetAbort,然后在statement2将其值写入x之前执行Thread.Abort()。然后catch将[通过委托q]执行Thread.ResetAbort(),允许继续执行statement3。这样的事件序列当然是非常不可能的,但是编译器需要生成代码,即使在这种不可能的事件发生时也能根据规范工作。

一般来说,编译器更有可能注意到遗漏简单代码而不是复杂代码的机会,因此如果从未抛出异常,try/catch很少会对性能产生很大影响。尽管如此,在某些情况下,try/catch块的存在可能会阻止优化——如果没有try/catch的话——可以让代码运行得更快。

虽然“预防胜于处理";,从性能和效率的角度来看,我们可以选择试接比预变。考虑下面的代码:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
if (i != 0)
{
int k = 10 / i;
}
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
try
{
int k = 10 / i;
}
catch (Exception)
{


}
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

结果如下:

With Checking:  20367
With Exception: 13998

是的,try/catch将“伤害”;性能(一切都是相对的)。就浪费的CPU循环而言并没有太多,但还有其他重要的方面需要考虑:

  • 代码大小
  • 方法内联

基准

首先,让我们使用一些复杂的工具(即BenchmarkDotNet)检查速度。编译为Release (AnyCPU),在x64机器上运行。我会说没有区别,即使测试确实会告诉我们NoTryCatch()要快一点点:

|            Method |   N |     Mean |     Error |    StdDev |
|------------------ |---- |---------:|----------:|----------:|
|        NoTryCatch | 0.5 | 3.770 ns | 0.0492 ns | 0.0411 ns |
|      WithTryCatch | 0.5 | 4.060 ns | 0.0410 ns | 0.0384 ns |
| WithTryCatchThrow | 0.5 | 3.924 ns | 0.0994 ns | 0.0881 ns |

分析

一些额外的注释。

|            Method | Code size | Inlineable |
|------------------ |---------- |-----------:|
|        NoTryCatch |        12 |        yes |
|      WithTryCatch |        18 |          ? |
| WithTryCatchThrow |        18 |         no |

代码大小NoTryCatch()在代码中产生12个字节,而try/catch则增加另外6个字节。此外,无论何时编写try/catch,你都很可能有一个或多个throw new Exception("Message", ex)语句,进一步“膨胀”;代码。

这里最重要的是代码内联。在.NET中,throw关键字的存在意味着该方法永远不会被编译器内联(意味着代码更慢,但占用空间更少)。我最近彻底测试了这个事实,所以它在.NET Core中仍然有效。不确定try/catch是否遵循相同的规则。TODO: Verify!

完整的测试代码

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;


namespace TryCatchPerformance
{
public class TryCatch
{
[Params(0.5)]
public double N { get; set; }


[Benchmark]
public void NoTryCatch() => Math.Sin(N);


[Benchmark]
public void WithTryCatch()
{
try
{
Math.Sin(N);
}
catch
{
}
}


[Benchmark]
public void WithTryCatchThrow()
{
try
{
Math.Sin(N);
}
catch (Exception ex)
{
throw;
}
}
}


class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<TryCatch>();
}
}
}

我试了一个深接球。

        static void TryCatch(int level, int max)
{
try
{
if (level < max) TryCatch(level + 1, max);
}
catch
{ }
}
static void NoTryCatch(int level, int max)
{
if (level < max) NoTryCatch(level + 1, max);
}
static void Main(string[] args)
{
var s = new Stopwatch();
const int max = 10000;
s.Start();
TryCatch(0, max);
s.Stop();
Console.WriteLine("try-catch " + s.Elapsed);
s.Restart();
NoTryCatch(0, max);
s.Stop();
Console.WriteLine("no try-catch " + s.Elapsed);
}

结果:

try-catch 00:00:00.0008528
no try-catch 00:00:00.0002422