전화 및 Callvirt
CIL 좀 "Call"과 "Callvirt"의 차이점은 무엇입니까?
call
비가 상, 정적 또는 수퍼 클래스 메서드를 호출하기위한 것입니다. 즉, 호출 대상이 재정의 대상이 아닙니다. callvirt
가상 메서드를 호출하기위한 것입니다 ( this
이 메서드를 재정의하는 하위 클래스 인 경우 하위 클래스 버전이 대신 호출 됨).
실행이 경우 call
를 사용할 때 정확한 코드 (메소드)를 호출합니다. 그것은 어디에 존재하는지에 존재하지 않는다.
IL이 JIT 처리 호출 사이트의 결과 기계 코드는 무조건
jmp
명령입니다.
대조적 으로이에서는 callvirt
다형성 방식으로 가상 메서드를 호출하는 데 사용됩니다. 메소드 코드의 정확한 위치는 모든 호출에 대해 실행해야합니다. 결과 JITted 코드에는 vtable 구조를 포함하는 일부 간접적 인 작업이 포함됩니다. 따라서 호출은 실행 속도가 느리지 만 다형성 호출을 허용한다는 점에서 더 유연합니다.
컴파일러는 call
가상 메서드에 대한 명령을 사용할 수 있습니다 . 예를 들면 :
sealed class SealedObject : object
{
public override bool Equals(object o)
{
// ...
}
}
코드 호출을 고려하십시오.
SealedObject a = // ...
object b = // ...
bool equal = a.Equals(b);
하지만 System.Object.Equals(object)
가상의 방법이며, 이는 사용에의 방법이 없다 Equals
존재하는 방법. SealedObject
봉인 된 클래스는 상속 된 클래스입니다.
NET의 sealed
클래스는 봉인되지 않은 클래스보다 더 나은 메소드 디스패치 성능을 수 있습니다.
편집 : 내가 틀렸다는 것이 밝혀졌습니다. 개체의 참조 ( this
메서드 내 값 )가 null 일 수 있으므로 C # 컴파일러는 메서드의 위치로 무조건 점프 할 수 없습니다 . 대신 callvirt
null 검사를 수행하고 필요한 경우 throw합니다.
이것은 실제로 Reflector를 사용하여 .NET 프레임 워크에서 사용 기괴한 코드를 설명합니다.
if (this==null) // ...
컴파일러가 this
포인터 (local0)에서 null 값을 가진 가능한 가능한 코드를 생성 할 수 있습니다.
그래서 call
클래스 정적 메소드와 사용되는 것 입니다.
이 정보를 감안할 때 sealed
API 보안에만 유용한 시청 . 클래스 봉인에 성능상의 이점이 없음을 시사하는 다른 질문 을 발견했습니다 .
편집 2 : 보기보다 이것에 더 많은 것이 있습니다. 를 들어 다음 예 코드는 call
명령어를 내 보냅니다 .
new SealedObject().Equals("Rubber ducky");
명백한 경우 개체 인스턴스가 null 일 가능성은 없습니다.
흥미롭게도 DEBUG 빌드에서 다음 코드가 방출 callvirt
됩니다.
var o = new SealedObject();
o.Equals("Rubber ducky");
두 번째 줄에 중단 점을 설정하고 값을 수 있기 때문입니다 o
. 릴리스에서 나는 전화가 될 것 상상 call
보다는 callvirt
.
불행히도 내 PC는 현재 작동하지 않지만 다시 작동하면 실험 할 것입니다.
NET의 봉인 된 클래스는 봉인되지 않은 클래스보다 더 나은 메서드 디스패치 성능을 얻을 수 있습니다.
불행히도 이것은 사실이 아닙니다. Callvirt는 그것을 유용하게 만드는 또 다른 일을합니다. 가상 객체에 메서드가 호출 될 때 호출 될 경우 가상 객체가 존재하는지 확인하고 NullReferenceException을 발생합니다. 호출은 메모리 참조가없는 경우에도 점프하고 해당 위치에서 바이트를 실행합니다.
이것이 의미하는 바는 callvirt는 항상 C # 컴파일러 (VB에 대해 확실하지 않음)에서 클래스에 사용하고 호출은 항상 사용하는 것입니다 (널 또는 하위 클래스가 될 수 없기 때문에).
Drew Noakes의 의견에 대한 응답으로 편집 : 예, 컴파일러가 모든 클래스에 대한 호출을 생성 할 수있는 것 같지만 다음과 같은 매우 구체적인 경우에만 해당됩니다.
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();
}
}
참고이 작업을 위해 클래스를 봉인 할 필요는 없습니다.
따라서 다음 사항이 모두 참이면 컴파일러가 호출을 내 보냅니다.
- 메서드 호출은 생성합니다.
- 메서드가 기본 클래스에서 구현되지 않습니다.
MSDN에 따르면 :
전화 :
호출 명령어는 명령어와 함께 전달 된 메서드 설명자가 나타내는 메서드를 호출합니다. 메서드 설명자는 호출 할 메서드를 나타내는 메타 데이터 토큰입니다 ... 메타 데이터 토큰은 호출이 정적 메서드, 인스턴스 메서드, 가상 메서드 또는 전역 함수에 대한 것인지 여부를 확인하는 데 충분한 정보를 전달합니다. 이러한 모든 경우에 대상 주소는 전적으로 메서드 설명자에서 결정됩니다 (이를 가상 메서드를 호출하기위한 Callvirt 명령어와 대조됩니다. 여기서 대상 주소는 Callvirt 이전에 푸시 된 인스턴스 참조의 런타임 유형에도 종 속됨).
CallVirt :
callvirt 명령어는 객체에 대해 후기 바인딩 된 메서드를 호출합니다. 즉 , 메서드 포인터에서 볼 수있는 컴파일 타임 클래스가 아닌 obj의 런타임 유형을 기반으로 메서드가 선택 됩니다. Callvirt는 가상 및 인스턴스 메서드를 모두 호출하는 데 사용할 수 있습니다.
따라서 기본적으로 객체의 인스턴스 메소드를 호출하기 위해 다른 경로가 사용됩니다.
호출 : 변수-> 변수의 유형 객체-> 메소드
CallVirt : 변수-> 객체 인스턴스-> 객체의 유형 객체-> 메소드
이전 답변에 추가 할 가치가있는 한 가지는 "IL call"이 실제로 실행되는 방식에 대해 한 면만 있고 "IL callvirt"가 실행되는 방법에 대해 두면이있는 것 같습니다.
이 샘플 설정을 사용하십시오.
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; }
}
첫째, FInst () 및 FExt ()의 CIL 본문은 100 % 동일하며 opcode-to-opcode (하나는 "인스턴스"로 선언되고 다른 하나는 "정적"으로 선언된다는 점 제외)입니다. 그러나 FInst ()는 다음과 같이 호출됩니다. "callvirt"및 "call"이있는 FExt ().
둘째, FInst ()와 FVirt ()는 둘 다 "callvirt"로 호출됩니다. 하나는 가상이지만 다른 하나는 그렇지 않더라도 실제로 실행되는 것은 "동일한 callvirt"가 아닙니다.
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]
"call"과 "callvirt [instance]"의 유일한 차이점은 "callvirt [instance]"가 인스턴스 함수의 직접 포인터를 호출하기 전에 의도적으로 * pObj에서 1 바이트에 액세스하려고 시도한다는 것입니다 (예외를 발생시키기 위해 " 바로 거기에 ").
따라서 "확인 부분"을 작성해야하는 횟수에 짜증이 난다면
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;
"if (this == null) return SOME_DEFAULT_E;"를 푸시 할 수 없습니다. ClassD.GetE () 자체로 내려가지만 ( "IL callvirt [instance]"시맨틱이이 작업을 금지하므로) .GetE ()를 확장 함수로 어딘가로 이동하면 .GetE ()로 자유롭게 푸시 할 수 있습니다. ( "IL 호출"의미 체계가 허용하지만 아쉽게도 개인 구성원에 대한 액세스 권한을 잃는 등)
즉, "callvirt [instance]"의 실행은 "callvirt [virtual]"보다 "call"과 더 많은 공통점이 있습니다. 후자는 함수의 주소를 찾기 위해 삼중 간접 명령을 실행해야 할 수도 있기 때문입니다. (typedef base, base-vtab-or-some-interface, 실제 슬롯에 대한 간접 지정)
도움이 되었기를 바랍니다, Boris
위의 답변에 추가하면 Callvirt IL 명령이 모든 인스턴스 메서드에 대해 생성되고 Call IL 명령이 정적 메서드에 대해 생성되도록 오래 전에 변경되었다고 생각합니다.
참조 :
Pluralsight 과정 "C # Language Internals-Part 1 by Bart De Smet (동영상-CLR IL의 호출 지침 및 호출 스택)
또한 https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/
참조 URL : https://stackoverflow.com/questions/193939/call-and-callvirt
'ProgramingTip' 카테고리의 다른 글
++ x % = 10은 C ++에서 잘 정의되어 있습니까? (0) | 2020.12.29 |
---|---|
유창한 인터페이스가 데메테르의 법칙을 경유하고 있습니까? (0) | 2020.12.29 |
파일 / 디렉토리의 내용을 모니터링 하시겠습니까? (0) | 2020.12.29 |
MySQL에서 누계 계산 (0) | 2020.12.29 |
읽기 메모리 장벽과 인증서를 이해하는 방법 (0) | 2020.12.29 |