扩展Eric Petroelje 的答案。
如果我们如下重写程序(行为相同,但避免使用 lambda 函数使反汇编更容易阅读),我们可以对其进行反汇编,看看“缓存寄存器中的字段值”实际上意味着什么
class Foo
{
public bool Complete; // { get; set; }
}
class Program
{
static Foo foo = new Foo();
static void ThreadProc()
{
bool toggle = false;
while (!foo.Complete) toggle = !toggle;
Console.WriteLine("Thread done");
}
static void Main()
{
var t = new Thread(ThreadProc);
t.Start();
Thread.Sleep(1000);
foo.Complete = true;
t.Join();
}
}
我们得到以下行为:
Foo.Complete is a Field | Foo.Complete is a Property
x86-RELEASE | loops forever | completes
x64-RELEASE | completes | completes
在 x86 版本中,CLR JIT 将 while(!foo.Complete) 编译为以下代码:
Complete 是一个字段:
004f0153 a1f01f2f03 mov eax,dword ptr ds:[032F1FF0h] # Put a pointer to the Foo object in EAX
004f0158 0fb64004 movzx eax,byte ptr [eax+4] # Put the value pointed to by [EAX+4] into EAX (this basically puts the value of .Complete into EAX)
004f015c 85c0 test eax,eax # Is EAX zero? (is .Complete false?)
004f015e 7504 jne 004f0164 # If it is not, exit the loop
# start of loop
004f0160 85c0 test eax,eax # Is EAX zero? (is .Complete false?)
004f0162 74fc je 004f0160 # If it is, goto start of loop
最后两行是问题。如果 eax 为零,那么它只会在一个无限循环中说“EAX 为零吗?”,而无需任何代码更改 eax 的值!
Complete 是一个属性:
00220155 a1f01f3a03 mov eax,dword ptr ds:[033A1FF0h] # Put a pointer to the Foo object in EAX
0022015a 80780400 cmp byte ptr [eax+4],0 # Compare the value at [EAX+4] with zero (is .Complete false?)
0022015e 74f5 je 00220155 # If it is, goto 2 lines up
这实际上看起来像更好的代码。虽然 JIT 已将属性 getter(否则您会看到一些call
指令转到其他函数)内联到一些简单的代码中以Complete
直接读取该字段,因为它不允许缓存变量,当它生成循环时,它会重复读取一遍又一遍的记忆,而不仅仅是毫无意义地读取寄存器
在 x64 版本中,64 位 CLR JIT 将 while(!foo.Complete) 编译成这段代码
Complete 是一个字段:
00140245 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table
0014024f 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX
00140252 0fb64808 movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX
00140256 85c9 test ecx,ecx # Is ECX zero ? (is the .Complete field false?)
00140258 751b jne 00140275 # If nonzero/true, exit the loop
0014025a 660f1f440000 nop word ptr [rax+rax] # Do nothing!
# start of loop
00140260 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table
0014026a 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX
0014026d 0fb64808 movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX
00140271 85c9 test ecx,ecx # Is ECX Zero ? (is the .Complete field true?)
00140273 74eb je 00140260 # If zero/false, go to start of loop
Complete 是一个属性
00140250 48b8d82fe11200000000 mov rax,12E12FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table
0014025a 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX
0014025d 0fb64008 movzx eax,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in EAX
00140261 85c0 test eax,eax # Is EAX 0 ? (is the .Complete field false?)
00140263 74eb je 00140250 # If zero/false, go to the start
64 位 JIT 对属性和字段都做同样的事情,除了当它是一个字段时,它会“展开”循环的第一次迭代——这基本上if(foo.Complete) { jump past the loop code }
出于某种原因在它前面放了一个。
在这两种情况下,它在处理属性时都与 x86 JIT 做类似的事情:
- 将方法内联到直接内存读取 - 它不缓存它,并且每次都重新读取值
我不确定 64 位 CLR 是否不允许像 32 位一样将字段值缓存在寄存器中,但如果是这样,那就不用担心了。也许它会在未来?
无论如何,这说明了行为是如何依赖于平台并可能发生变化的。我希望它有帮助:-)