我必须承认,通常我不会在我的程序中的 Debug 和 Release 配置之间进行切换,而且我通常会选择 Debug 配置,即使程序实际部署在客户位置也是如此。
据我所知,如果您不手动更改,这些配置之间的唯一区别是 Debug 定义了 DEBUG 常量,而 Release 则检查了 Optimize 代码 。
DEBUG
所以我的问题实际上是双重的:
这两种配置之间是否存在很大的性能差异。是否有任何特定类型的代码会在这里导致性能上的巨大差异,或者它实际上并不那么重要?
是否有任何类型的代码可以在 Debug配置下正常运行而在 Release 配置下可能会失败,或者您是否可以确定在 Debug 配置下测试并正常工作的代码在 Release 配置下也可以正常工作。
C# 编译器本身不会在 Release 构建中大量更改发出的 IL。值得注意的是,它不再发出允许您在花括号上设置断点的 NOP 操作码。最重要的是 JIT 编译器中内置的优化器。我知道它进行了以下优化:
方法内联。方法调用被注入方法的代码所取代。这是一个很大的问题,它使属性访问器基本上是免费的。
CPU 寄存器分配。局部变量和方法参数可以保持存储在 CPU 寄存器中,而不会(或不经常)存储回堆栈帧。这是一个很大的问题,值得注意的是使调试优化代码变得如此困难。并赋予 volatile 关键字一个含义。
数组索引检查消除。使用数组时的一项重要优化(所有 .NET 集合类在内部都使用数组)。当 JIT 编译器可以验证循环永远不会越界索引数组时,它将消除索引检查。大的一个。
循环展开。通过在主体中重复代码多达 4 次并减少循环来改进具有小主体的循环。降低分支成本并改进处理器的超标量执行选项。
死代码消除。像 if (false) { / … / } 这样的语句被完全消除了。这可能是由于不断的折叠和内联而发生的。其他情况是 JIT 编译器可以确定代码没有可能的副作用。这种优化使分析代码变得如此棘手。
代码提升。循环内不受循环影响的代码可以移出循环。C 编译器的优化器将花费更多时间来寻找提升的机会。然而,由于需要进行数据流分析,这是一项昂贵的优化,而且抖动无法承受时间,因此只能提升明显的案例。迫使 .NET 程序员编写更好的源代码并提升自己。
常见的子表达式消除。x = y + 4; z = y + 4; 变成z = x;在 dest[ix+1] = src[ix+1]; 这样的语句中很常见 为便于阅读而编写,没有引入辅助变量。无需牺牲可读性。
不断折叠。x = 1 + 2; 变为 x = 3;这个简单的示例被编译器提前捕获,但发生在 JIT 时间,此时其他优化使之成为可能。
复制传播。x = 一个;y = x; 变成 y = a; 这有助于寄存器分配器做出更好的决定。在 x86 抖动中这是一个大问题,因为它几乎没有可以使用的寄存器。让它选择正确的对性能至关重要。
这些非常重要的优化可以产生 很大 的不同,例如,当您分析应用程序的调试版本并将其与发布版本进行比较时。只有当代码在您的关键路径上时,这才真正重要,您编写的 5% 到 10% 的代码 实际上 会影响程序的性能。JIT 优化器不够聪明,无法预先知道什么是关键的,它只能对所有代码应用“将其转为 11”拨号。
这些优化对程序执行时间的有效结果通常会受到在其他地方运行的代码的影响。读取文件、执行 dbase 查询等。使 JIT 优化器所做的工作完全不可见。不过没关系:)
JIT 优化器是非常可靠的代码,主要是因为它已经过数百万次的测试。在您的程序的 Release 构建版本中出现问题的情况极为罕见。然而它确实发生了。x64 和 x86 抖动都存在结构问题。x86 抖动在浮点一致性方面存在问题,当浮点计算的中间值以 80 位精度保存在 FPU 寄存器中而不是在刷新到内存时被截断时,会产生细微的不同结果。