2008-10-11 17 views

Trả lời

42

call là để gọi các phương thức không phải ảo, tĩnh hoặc siêu lớp, tức là, mục tiêu của cuộc gọi không bị ghi đè. callvirt là để gọi các phương thức ảo (để nếu this là một lớp con ghi đè phương thức, phiên bản lớp con được gọi thay thế).

+25

Nếu tôi nhớ chính xác 'call' không kiểm tra con trỏ với null trước khi thực hiện cuộc gọi, cái gì đó' callvirt' rõ ràng là cần thiết. Đó là lý do tại sao 'callvirt' đôi khi được phát ra bởi trình biên dịch mặc dù gọi các phương thức không phải ảo là – dalle

+1

Ah, cảm ơn vì đã chỉ ra điều đó (tôi không phải là một. NET người). Các sự tương tự tôi sử dụng là call => invokespecial, và callvirt => invokevirtual, trong JVM bytecode. Trong trường hợp của JVM, cả hai lệnh kiểm tra "this" cho nullness (tôi vừa viết một chương trình thử nghiệm để kiểm tra). –

+2

Bạn có thể muốn đề cập đến sự khác biệt hiệu suất trong câu trả lời của mình, đó là lý do để có hướng dẫn 'gọi'. –

45

Khi thời gian chạy thực thi lệnh call, thực hiện cuộc gọi đến một đoạn mã (phương pháp) chính xác. Không có câu hỏi về nơi nó tồn tại. Khi IL đã được JITted, mã máy kết quả tại trang cuộc gọi là lệnh jmp vô điều kiện.

Ngược lại, lệnh callvirt được sử dụng để gọi các phương thức ảo theo cách đa hình. Vị trí chính xác của mã của phương thức phải được xác định trong thời gian chạy cho mỗi và mọi lệnh gọi. Mã JITted kết quả liên quan đến một số hướng dẫn thông qua các cấu trúc vtable. Do đó cuộc gọi chậm hơn để thực hiện, nhưng nó linh hoạt hơn ở chỗ nó cho phép các cuộc gọi đa hình.

Lưu ý rằng trình biên dịch có thể phát ra các hướng dẫn cho các phương pháp ảo call. Ví dụ:

sealed class SealedObject : object 
{ 
    public override bool Equals(object o) 
    { 
     // ... 
    } 
} 

Hãy xem xét mã gọi:

SealedObject a = // ... 
object b = // ... 

bool equal = a.Equals(b); 

Trong khi System.Object.Equals(object) là một phương pháp ảo, trong việc sử dụng này không có cách nào cho một tình trạng quá tải của các phương pháp Equals để tồn tại. SealedObject là một lớp được niêm phong và không thể có lớp con.

Vì lý do này, các lớp học sealed của .NET có thể có hiệu suất gửi phương pháp tốt hơn so với các đối tác không được niêm phong của chúng.

EDIT: Hóa ra tôi đã sai. Trình biên dịch C# không thể thực hiện bước nhảy vô điều kiện đến vị trí của phương thức vì tham chiếu của đối tượng (giá trị của this trong phương thức) có thể là rỗng. Thay vào đó nó phát ra callvirt mà không kiểm tra null và ném nếu cần thiết.

thực này giải thích một số mã kỳ lạ Tôi tìm thấy trong khuôn khổ NET sử dụng Reflector:

if (this==null) // ... 

Có thể cho một trình biên dịch để phát ra mã xác minh được rằng có một giá trị null cho con trỏ this (local0), chỉ csc không làm điều này.

Vì vậy, tôi đoán call chỉ được sử dụng cho các phương thức và cấu trúc tĩnh lớp.

Với thông tin này, bây giờ dường như với tôi rằng sealed chỉ hữu ích cho bảo mật API. Tôi tìm thấy another question dường như không có lợi ích về hiệu suất để niêm phong các lớp học của bạn.

CHỈNH SỬA 2: Có nhiều điều hơn thế này. Ví dụ đoạn mã sau đưa ra một lệnh call:

new SealedObject().Equals("Rubber ducky"); 

Rõ ràng trong trường hợp này không có cơ hội mà các trường hợp đối tượng có thể là null.

Điều thú vị là, trong một DEBUG xây dựng, đoạn code sau phát ra callvirt:

var o = new SealedObject(); 
o.Equals("Rubber ducky"); 

này là bởi vì bạn có thể thiết lập một breakpoint trên dòng thứ hai và sửa đổi các giá trị của o. Trong bản phát hành, tôi tưởng tượng cuộc gọi sẽ là call thay vì callvirt.

Rất tiếc, máy tính của tôi hiện không hoạt động, nhưng tôi sẽ thử nghiệm với điều này khi nó lại hoạt động trở lại.

+2

Thuộc tính kín chắc chắn nhanh hơn khi tìm kiếm chúng thông qua sự phản chiếu, nhưng khác hơn thế, tôi không biết về bất kỳ lợi ích nào khác mà bạn chưa đề cập đến. – TraumaPony

11

Vì lý do này, lớp được niêm phong của .NET có thể có hiệu suất gửi phương pháp tốt hơn so với các đối tác không được niêm phong của chúng.

Thật không may điều này không đúng. Callvirt làm một điều khác mà làm cho nó hữu ích. Khi một đối tượng có một phương thức gọi là callvirt, nó sẽ kiểm tra xem đối tượng có tồn tại hay không và nếu không ném NullReferenceException. Cuộc gọi chỉ đơn giản là sẽ nhảy đến vị trí bộ nhớ ngay cả khi tham chiếu đối tượng không có, và cố gắng thực hiện các byte trong vị trí đó.

Điều này có nghĩa là callvirt luôn được sử dụng bởi trình biên dịch C# (không chắc chắn về VB) cho các lớp và cuộc gọi luôn được sử dụng cho các cấu trúc (vì chúng không bao giờ được rỗng hoặc phân lớp).

Sửa Để đối phó với Drew Noakes bình luận: Có vẻ như bạn có thể nhận được biên dịch để phát ra một cuộc gọi cho bất kỳ lớp học, nhưng chỉ trong trường hợp rất cụ thể như sau:

public class SampleClass 
{ 
    public override bool Equals(object obj) 
    { 
     if (obj.ToString().Equals("Rubber Ducky", StringComparison.InvariantCultureIgnoreCase)) 
      return true; 

     return base.Equals(obj); 
    } 

    public void SomeOtherMethod() 
    { 
    } 

    static void Main(string[] args) 
    { 
     // This will emit a callvirt to System.Object.Equals 
     bool test1 = new SampleClass().Equals("Rubber Ducky"); 

     // This will emit a call to SampleClass.SomeOtherMethod 
     new SampleClass().SomeOtherMethod(); 

     // This will emit a callvirt to System.Object.Equals 
     SampleClass temp = new SampleClass(); 
     bool test2 = temp.Equals("Rubber Ducky"); 

     // This will emit a callvirt to SampleClass.SomeOtherMethod 
     temp.SomeOtherMethod(); 
    } 
} 

LƯU Ý Các lớp học không phải được niêm phong để làm việc này.

Vì vậy, nó trông giống như các trình biên dịch sẽ phát ra một cuộc gọi nếu tất cả những điều này là đúng:

  • Cuộc gọi phương pháp là ngay lập tức sau khi tạo đối tượng
  • Phương pháp này không được thực hiện trong một lớp cơ sở
+0

Điều đó có ý nghĩa. Tôi đã nghiên cứu CIL và đưa ra giả định về hành vi của trình biên dịch. Cảm ơn cho thanh toán bù trừ này lên. Tôi sẽ cập nhật câu trả lời của mình. –

+1

Thực ra tôi không tin bạn đúng 100% ở đây. Tôi đã cập nhật bài đăng của mình (chỉnh sửa 2). –

+0

Xin chào Cameron. Bạn có thể làm rõ liệu phân tích mã này có được thực hiện trên bản phát hành không? –

5

Theo MSDN:

Call:

Lệnh gọi gọi phương thức được chỉ định bởi bộ mô tả phương thức được truyền bằng lệnh. Bộ mô tả phương thức là một mã thông báo siêu dữ liệu cho biết phương thức gọi ... Mã thông báo siêu dữ liệu mang đầy đủ thông tin để xác định xem cuộc gọi có phải là phương thức tĩnh hay không, phương pháp dụ, phương pháp ảo hoặc hàm toàn cục. Trong tất cả các trường hợp, địa chỉ đích được xác định hoàn toàn từ bộ mô tả phương pháp (ngược lại với lệnh Callvirt để gọi các phương thức ảo, nơi địa chỉ đích cũng phụ thuộc vào kiểu thời gian chạy của tham chiếu mẫu được đẩy trước Callvirt).

CallVirt:

Các hướng dẫn callvirt gọi là phương pháp cuối-bound trên một đối tượng. Tức là, phương thức được chọn dựa trên kiểu thời gian chạy của obj thay vì lớp thời gian biên dịch được hiển thị trong con trỏ phương thức.Callvirt có thể được sử dụng để gọi cả phương thức ảo và cá thể.

Vì vậy, về cơ bản, các tuyến đường khác nhau được thực hiện để gọi phương pháp dụ của một đối tượng, overriden hay không:

Gọi: biến ->biến của loại đối tượng -> Phương pháp

callvirt: biến -> trường hợp đối tượng -> đối tượng loại đối tượng -> phương pháp

2

một điều có lẽ đáng thêm vào câu trả lời trước là, có vẻ là chỉ có một mặt như thế nào "IL gọi là" thực sự thực hiện, và hai khuôn mặt để thực hiện "cuộc gọi IL".

Thực hiện thiết lập mẫu này.

public class Test { 
     public int Val; 
     public Test(int val) 
      { Val = val; } 
     public string FInst() // note: this==null throws before this point 
      { return this == null ? "NO VALUE" : "ACTUAL VALUE " + Val; } 
     public virtual string FVirt() 
      { return "ALWAYS AN ACTUAL VALUE " + Val; } 
    } 
    public static class TestExt { 
     public static string FExt (this Test pObj) // note: pObj==null passes 
      { return pObj == null ? "NO VALUE" : "VALUE " + pObj.Val; } 
    } 

Thứ nhất, cơ thể CIL của FInst() và FEXT() là 100% giống hệt nhau, opcode-to-opcode (ngoại trừ một tuyên bố "dụ" và người kia "tĩnh") - tuy nhiên, FInst() sẽ được gọi với "callvirt" và FExt() với "call".

Thứ hai, FInst() và FVirt() cả hai sẽ được gọi với "callvirt" - mặc dù một là ảo nhưng người kia không phải là - nhưng nó không phải là "cùng callvirt" mà thực sự sẽ nhận được để thực hiện.

Đây là những gì gần xảy ra sau khi JITting:

pObj.FExt(); // IL:call 
    mov   rcx, <pObj> 
    call  (direct-ptr-to) <TestExt.FExt> 

    pObj.FInst(); // IL:callvirt[instance] 
    mov   rax, <pObj> 
    cmp   byte ptr [rax],0 
    mov   rcx, <pObj> 
    call  (direct-ptr-to) <Test.FInst> 

    pObj.FVirt(); // IL:callvirt[virtual] 
    mov   rax, <pObj> 
    mov   rax, qword ptr [rax] 
    mov   rax, qword ptr [rax + NNN] 
    mov   rcx, <pObj> 
    call  qword ptr [rax + MMM] 

Sự khác biệt duy nhất giữa "gọi" và "callvirt [Ví dụ]" là "callvirt [Ví dụ]" cố ý cố gắng truy cập một byte từ * pObj trước nó gọi con trỏ trực tiếp của hàm thể hiện (để có thể ném một ngoại lệ "ngay tại đó và sau đó").

Vì vậy, nếu bạn đang khó chịu bởi số lượng thời gian mà bạn phải viết là "kiểm tra phần" của

var d = GetDForABC (a, b, c); 
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E; 

Bạn không thể đẩy "if (này == null) return SOME_DEFAULT_E;" xuống vào ClassD.GetE() chính nó (như là "IL callvirt [dụ]" ngữ nghĩa cấm bạn làm điều này) nhưng bạn tự do để đẩy nó vào .GetE() nếu bạn di chuyển .GetE() đến một phần mở rộng hoạt động ở đâu đó (như ngữ nghĩa "gọi IL" cho phép - nhưng than ôi, mất quyền truy cập vào các thành viên riêng tư, v.v.)

Điều đó nói rằng, "callvirt [instance]" có nhiều điểm chung là với "call" so với "callvirt [virtual]", vì sau này có thể phải thực hiện một phép ba hướng để tìm địa chỉ của hàm của bạn. (gián tiếp để typedef cơ sở, sau đó đến căn-vtab-hay-một số giao diện, sau đó đến khe thực tế)

Hope this helps, Boris

1

Chỉ cần thêm vào các câu trả lời ở trên, tôi nghĩ rằng sự thay đổi có được thực hiện lâu trở lại như vậy mà Callvirt IL hướng dẫn sẽ được tạo ra cho tất cả các phương pháp dụ và lệnh Call IL sẽ được tạo ra cho các phương pháp tĩnh.

tham khảo:

Pluralsight khóa học "C Internals # Ngôn ngữ - Phần 1 bởi Bart De Smet (video - Gọi hướng dẫn và gọi ngăn xếp trong CLR IL in a Nutshell)

và cũng https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/

+0

'cuộc gọi' cũng được sử dụng cho các cuộc gọi cơ bản. Ngoài ra, tôi tin rằng trình biên dịch roslyn sẽ tạo ra các lệnh 'call' cho các phương thức kín/phi ảo nếu nó có thể xác định một cách nhỏ gọn rằng đối tượng sẽ không bao giờ rỗng (ví dụ: [các cuộc gọi được tạo ra từ toán tử điều kiện rỗng] (http: // stackoverflow.com/questions/34535000/call-instead-of-callvirt-in-case-of-the-new-c-sharp-6-null-check)). –