Đây cũng không phải là một câu trả lời đầy đủ, nhưng tôi có một vài ý tưởng.
Tôi tin rằng tôi đã tìm thấy một lời giải thích tốt như chúng ta sẽ tìm thấy mà không có ai từ nhóm .NET JIT trả lời.
CẬP NHẬT
Tôi nhìn sâu hơn một chút, và tôi tin rằng tôi đã tìm ra nguồn gốc của vấn đề. Nó xuất hiện do sự kết hợp của một lỗi trong logic khởi tạo kiểu JIT và thay đổi trình biên dịch C # dựa trên giả định rằng JIT hoạt động như dự định. Tôi nghĩ rằng lỗi JIT tồn tại trong .NET 4.0, nhưng đã bị phát hiện bởi sự thay đổi trong trình biên dịch cho .NET 4.5.
Tôi không nghĩ đó beforefieldinit
là vấn đề duy nhất ở đây. Tôi nghĩ nó đơn giản hơn thế.
Loại System.String
trong mscorlib.dll từ .NET 4.0 chứa hàm tạo tĩnh:
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr ""
IL_0005: stsfld string System.String::Empty
IL_000a: ret
} // end of method String::.cctor
Trong phiên bản .NET 4.5 của mscorlib.dll, String.cctor
(hàm tạo tĩnh) vắng mặt một cách rõ rệt:
..... Không có hàm tạo tĩnh :( .....
Trong cả hai phiên bản, String
loại được trang trí bằng beforefieldinit
:
.class public auto ansi serializable sealed beforefieldinit System.String
Tôi đã cố gắng tạo một kiểu sẽ biên dịch tương tự IL (để nó có các trường tĩnh nhưng không có hàm tạo tĩnh .cctor
), nhưng tôi không thể làm được. Tất cả các loại này có một .cctor
phương thức trong IL:
public class MyString1 {
public static MyString1 Empty = new MyString1();
}
public class MyString2 {
public static MyString2 Empty = new MyString2();
static MyString2() {}
}
public class MyString3 {
public static MyString3 Empty;
static MyString3() { Empty = new MyString3(); }
}
Tôi đoán là hai điều đã thay đổi giữa .NET 4.0 và 4.5:
Đầu tiên: EE được thay đổi để nó sẽ tự động khởi tạo String.Empty
từ mã không được quản lý. Thay đổi này có lẽ được thực hiện cho .NET 4.0.
Thứ hai: Trình biên dịch đã thay đổi để nó không phát ra một hàm tạo tĩnh cho chuỗi, biết rằng nó String.Empty
sẽ được gán từ phía không được quản lý. Thay đổi này dường như đã được thực hiện cho .NET 4.5.
Dường như EE không chỉ định String.Empty
đủ sớm theo một số đường dẫn tối ưu hóa. Thay đổi được thực hiện cho trình biên dịch (hoặc bất cứ điều gì đã thay đổi để String.cctor
biến mất) mong muốn EE thực hiện phép gán này trước khi bất kỳ mã người dùng nào thực thi, nhưng có vẻ như EE không thực hiện phép gán này trước khi String.Empty
được sử dụng trong các phương thức của các lớp chung loại tham chiếu.
Cuối cùng, tôi tin rằng lỗi này là dấu hiệu của một vấn đề sâu sắc hơn trong logic khởi tạo kiểu JIT. Có vẻ như sự thay đổi trong trình biên dịch là một trường hợp đặc biệt System.String
, nhưng tôi nghi ngờ rằng JIT đã tạo ra một trường hợp đặc biệt ở đây cho System.String
.
Nguyên
Trước hết, WOW Người BCL đã rất sáng tạo với một số tối ưu hóa hiệu suất. Nhiều trong những String
phương pháp hiện nay được thực hiện bằng cách sử dụng Chủ đề tĩnh cache StringBuilder
đối tượng.
Tôi đã theo hướng dẫn đó trong một thời gian, nhưng StringBuilder
không được sử dụng trên Trim
đường dẫn mã, vì vậy tôi quyết định đó không phải là vấn đề tĩnh.
Tôi nghĩ rằng tôi đã tìm thấy một biểu hiện kỳ lạ của cùng một lỗi.
Mã này không thành công với vi phạm truy cập:
class A<T>
{
static A() { }
public A(out string s) {
s = string.Empty;
}
}
class B
{
static void Main() {
string s;
new A<object>(out s);
//new A<int>(out s);
System.Console.WriteLine(s.Length);
}
}
Tuy nhiên, nếu bạn bỏ ghi chú //new A<int>(out s);
trong Main
đó các mã làm việc tốt. Trong thực tế, nếu A
được thống nhất với bất kỳ loại tham chiếu nào, chương trình sẽ thất bại, nhưng nếu A
được thống nhất với bất kỳ loại giá trị nào thì mã không bị lỗi. Ngoài ra nếu bạn nhận xét ra hàm tạo A
tĩnh, mã không bao giờ bị lỗi. Sau khi đào sâu vào Trim
và Format
, rõ ràng vấn đề là nó Length
đang được nội tuyến và trong các mẫu này ở trên String
loại chưa được khởi tạo. Cụ thể, bên trong phần thân của hàm A
tạo, string.Empty
không được gán chính xác, mặc dù bên trong phần thân của Main
, string.Empty
được gán chính xác.
Điều đáng ngạc nhiên với tôi là việc khởi tạo kiểu String
bằng cách nào đó phụ thuộc vào việc A
có hợp nhất với loại giá trị hay không . Lý thuyết duy nhất của tôi là có một số đường dẫn mã JIT tối ưu hóa cho khởi tạo kiểu chung được chia sẻ giữa tất cả các loại và đường dẫn đó đưa ra các giả định về các loại tham chiếu BCL ("các loại đặc biệt?") Và trạng thái của chúng. Nhìn nhanh mặc dù các lớp BCL khác với public static
các trường cho thấy về cơ bản tất cả chúng đều thực hiện một hàm tạo tĩnh (ngay cả những lớp có hàm tạo rỗng và không có dữ liệu, như System.DBNull
và System.Empty
các loại giá trị BCL với public static
các trường dường như không triển khai hàm tạo tĩnh ( System.IntPtr
ví dụ) Điều này dường như chỉ ra rằng JIT đưa ra một số giả định về khởi tạo loại tham chiếu BCL.
FYI Đây là mã JITed cho hai phiên bản:
A<object>.ctor(out string)
:
public A(out string s) {
00000000 push rbx
00000001 sub rsp,20h
00000005 mov rbx,rdx
00000008 lea rdx,[FFEE38D0h]
0000000f mov rcx,qword ptr [rcx]
00000012 call 000000005F7AB4A0
s = string.Empty;
00000017 mov rdx,qword ptr [FFEE38D0h]
0000001e mov rcx,rbx
00000021 call 000000005F661180
00000026 nop
00000027 add rsp,20h
0000002b pop rbx
0000002c ret
}
A<int32>.ctor(out string)
:
public A(out string s) {
00000000 sub rsp,28h
00000004 mov rax,rdx
s = string.Empty;
00000007 mov rdx,12353250h
00000011 mov rdx,qword ptr [rdx]
00000014 mov rcx,rax
00000017 call 000000005F691160
0000001c nop
0000001d add rsp,28h
00000021 ret
}
Phần còn lại của mã ( Main
) giống hệt nhau giữa hai phiên bản.
BIÊN TẬP
Ngoài ra, IL từ hai phiên bản giống hệt nhau ngoại trừ lệnh gọi đến , A.ctor
trong B.Main()
đó IL cho phiên bản đầu tiên chứa:
newobj instance void class A`1<object>::.ctor(string&)
đấu với
... A`1<int32>...
trong lần thứ hai
Một điều cần lưu ý là mã JITed cho A<int>.ctor(out string)
: giống như trong phiên bản không chung chung.