在与 Microsoft 员工进行代码审查期间,我们在 try{}
块中发现了一大段代码。她和一位 IT 代表建议这可能会影响代码的性能。事实上,他们建议大部分代码应该在 try/catch 块之外,并且只检查重要的部分。微软员工补充说,即将发布的白皮书警告不要使用不正确的 try/catch 块。
我环顾四周发现它can affect optimizations,但它似乎只适用于在作用域之间共享变量时。
我不是在问代码的可维护性,甚至不是在处理正确的异常(毫无疑问,有问题的代码需要重构)。我也不是指使用异常进行流控制,这在大多数情况下显然是错误的。这些都是重要的问题(有些更重要),但不是这里的重点。
当不抛出异常时,try/catch 块如何影响性能?
核实。
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 和不使用 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 专家,但我们可以看到,在第 17 行 IL_0021: leave.s IL_002f
之前,在第四行 .locals init ([0] class [mscorlib]System.Exception ex)
上创建了一个本地异常对象,这与没有 try/catch 的方法完全相同。如果发生异常,则控件跳转到第 IL_0025: ldloc.0
行,否则我们跳转到标签 IL_002d: leave.s IL_002f
并且函数返回。
我可以放心地假设,如果没有发生异常,那么创建局部变量以仅保存异常对象和跳转指令是开销。
不。如果 try/finally 块排除的琐碎优化实际上对您的程序有可衡量的影响,那么您可能一开始就不应该使用 .NET。
Quite comprehensive explanation of the .NET exception model.
Rico Mariani 的表演花絮:Exception Cost: When to throw and when not to
第一种成本是在代码中进行异常处理的静态成本。托管异常在这里实际上做得比较好,我的意思是静态成本可能比 C++ 中的成本低得多。为什么是这样?好吧,静态成本确实在两种地方产生:第一,try/finally/catch/throw 的实际站点,其中有这些构造的代码。其次,在无人管理的代码中,跟踪所有在抛出异常时必须销毁的对象会产生隐秘成本。有相当多的清理逻辑必须存在,而鬼鬼祟祟的部分是,即使代码本身不抛出或捕捉或以其他方式公开使用异常,仍然承担着知道如何清理自身的负担。
德米特里·扎斯拉夫斯基:
根据 Chris Brumme 的说明:在存在 catch 的情况下,JIT 未执行某些优化这一事实也存在成本
示例中的结构与 Ben M 不同。它将在内部 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();
}
当我从 Ben M 运行原始测试代码时,我注意到 Debug 和 Releas 配置的不同。
这个版本,我注意到debug版本有区别(其实比其他版本多),但是Release版本没有区别。
结论:基于这些测试,我认为我们可以说 Try/Catch 对性能的影响确实很小。
编辑:我尝试将循环值从 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
。您正在针对 10M 循环计时 12 次 try/catch entering-critical-section。循环的噪音将消除 try/catch 的任何影响。相反,如果您将 try/catch 放在紧密循环中,并与/不进行比较,您最终会得到 try/catch 的成本。 (毫无疑问,这种编码通常不是好的做法,但如果你想计算构造的开销,你就是这样做的)。如今,BenchmarkDotNet 是可靠执行时间的首选工具。
我在一个紧密的循环中测试了 try..catch
的实际影响,它本身太小,在任何正常情况下都不会成为性能问题。
如果循环所做的工作很少(在我的测试中我做了 x++
),您可以测量异常处理的影响。带有异常处理的循环运行时间大约要长十倍。
如果循环做了一些实际的工作(在我的测试中我调用了 Int32.Parse 方法),异常处理的影响太小而无法测量。通过交换循环的顺序,我得到了更大的不同......
try catch 块对性能的影响可以忽略不计,但异常抛出可能相当大,这可能是您的同事感到困惑的地方。
虽然“预防胜于处理”,但从性能和效率的角度来看,我们可以选择 try-catch 而不是 pre-variation。考虑下面的代码:
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 对性能有影响。
但影响不大。 try/catch 的复杂度通常是 O(1),就像一个简单的赋值一样,除非它们被放置在一个循环中。所以你必须明智地使用它们。
Here 是关于 try/catch 性能的参考(虽然没有解释它的复杂性,但它是隐含的)。看看 Throw Fever Exceptions 部分
理论上,除非实际发生异常,否则 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
,然后执行 {5 } 在 statement2 将其值写入 x
之前。然后 catch
将执行 Thread.ResetAbort()
[通过委托 q
],允许继续执行 statement3。这样的一系列事件当然是非常不可能发生的,但是即使发生这样的不可能事件,编译器也需要生成根据规范工作的代码。
一般来说,编译器更容易注意到忽略简单代码的机会而不是复杂代码,因此如果从不抛出异常,try/catch 很少会影响性能。尽管如此,在某些情况下,try/catch 块的存在可能会阻止优化——但对于 try/catch——本可以让代码运行得更快。
是的,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>();
}
}
}
有关 try/catch 块如何工作的讨论,以及一些实现如何在没有异常发生时具有高开销,而一些实现为零开销的讨论,请参阅 discussion on try/catch implementation。特别是,我认为 Windows 32 位实现的开销很高,而 64 位实现则没有。
我测试了一个深度尝试捕获。
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