以下代码在 Visual Studio 中运行版本和在 Visual Studio 之外运行版本时给出不同的输出。我正在使用 Visual Studio 2008 并以 .NET 3.5 为目标。我也尝试过 .NET 3.5 SP1。
在 Visual Studio 外部运行时,JIT 应该启动。要么(a)我缺少的 C# 发生了一些微妙的事情,要么(b)JIT 实际上是错误的。我怀疑 JIT 是否会出错,但我已经没有其他可能性了......
在 Visual Studio 中运行时的输出:
0 0,
0 1,
1 0,
1 1,
在 Visual Studio 之外运行发布时的输出:
0 2,
0 2,
1 2,
1 2,
是什么原因?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Test
{
struct IntVec
{
public int x;
public int y;
}
interface IDoSomething
{
void Do(IntVec o);
}
class DoSomething : IDoSomething
{
public void Do(IntVec o)
{
Console.WriteLine(o.x.ToString() + " " + o.y.ToString()+",");
}
}
class Program
{
static void Test(IDoSomething oDoesSomething)
{
IntVec oVec = new IntVec();
for (oVec.x = 0; oVec.x < 2; oVec.x++)
{
for (oVec.y = 0; oVec.y < 2; oVec.y++)
{
oDoesSomething.Do(oVec);
}
}
}
static void Main(string[] args)
{
Test(new DoSomething());
Console.ReadLine();
}
}
}
这是一个 JIT 优化器错误。它正在展开内部循环,但没有正确更新 oVec.y 值:
for (oVec.x = 0; oVec.x < 2; oVec.x++) {
0000000a xor esi,esi ; oVec.x = 0
for (oVec.y = 0; oVec.y < 2; oVec.y++) {
0000000c mov edi,2 ; oVec.y = 2, WRONG!
oDoesSomething.Do(oVec);
00000011 push edi
00000012 push esi
00000013 mov ecx,ebx
00000015 call dword ptr ds:[00170210h] ; first unrolled call
0000001b push edi ; WRONG! does not increment oVec.y
0000001c push esi
0000001d mov ecx,ebx
0000001f call dword ptr ds:[00170210h] ; second unrolled call
for (oVec.x = 0; oVec.x < 2; oVec.x++) {
00000025 inc esi
00000026 cmp esi,2
00000029 jl 0000000C
当您让 oVec.y 增加到 4 时,该错误就会消失,因为展开的调用太多了。
一种解决方法是:
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 2; y++) {
oDoesSomething.Do(new IntVec(x, y));
}
}
更新:2012 年 8 月重新检查,此错误已在 4.0.30319 版本抖动中修复。但仍然存在于 v2.0.50727 抖动中。过了这么久,他们似乎不太可能在旧版本中解决这个问题。
我相信这是一个真正的 JIT 编译错误。我会把它报告给微软,看看他们怎么说。有趣的是,我发现 x64 JIT 没有同样的问题。
这是我对 x86 JIT 的阅读。
// save context
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
// put oDoesSomething pointer in ebx
00000006 mov ebx,ecx
// zero out edi, this will store oVec.y
00000008 xor edi,edi
// zero out esi, this will store oVec.x
0000000a xor esi,esi
// NOTE: the inner loop is unrolled here.
// set oVec.y to 2
0000000c mov edi,2
// call oDoesSomething.Do(oVec) -- y is always 2!?!
00000011 push edi
00000012 push esi
00000013 mov ecx,ebx
00000015 call dword ptr ds:[002F0010h]
// call oDoesSomething.Do(oVec) -- y is always 2?!?!
0000001b push edi
0000001c push esi
0000001d mov ecx,ebx
0000001f call dword ptr ds:[002F0010h]
// increment oVec.x
00000025 inc esi
// loop back to 0000000C if oVec.x < 2
00000026 cmp esi,2
00000029 jl 0000000C
// restore context and return
0000002b pop ebx
0000002c pop esi
0000002d pop edi
0000002e pop ebp
0000002f ret
这看起来像优化对我来说很糟糕......
我将您的代码复制到了一个新的控制台应用程序中。
Debug Build 使用调试器和无调试器的正确输出
使用调试器和没有调试器的正确输出
切换到 Release Build Again,两次都正确输出
同样,两次正确输出
创建了一个新的 x86 配置(我正在运行 X64 Windows 2008 并且正在使用“任何 CPU”)
Debug Build 得到正确的输出 F5 和 CTRL+F5
F5 和 CTRL+F5 都得到了正确的输出
Release Build Correct output with Debugger 没有调试器 - 得到不正确的输出
附加调试器的正确输出
没有调试器 - 得到不正确的输出
所以是 x86 JIT 错误地生成了代码。已删除有关循环重新排序等的原始文本。此处的其他一些答案已确认 JIT 在 x86 上错误地展开循环。
要解决此问题,您可以将 IntVec 的声明更改为一个类,它适用于所有风格。
认为这需要继续 MS Connect....
-1 微软!
不定期副业成功案例分享