대리자 대 메서드 호출 성능
이 질문에 따라 -C #을 사용하여 메서드를 매개 변수 로 전달하고 개인적인 경험 중 일부는 C #에서 메서드를 호출하는 대신 대리자를 호출하는 성능에 대해 조금 더 알고 싶습니다.
델리게이트는 매우 편리하지만 델리게이트를 통해 많은 콜백을 수행하는 앱이 있었고 콜백 인터페이스를 사용하기 위해 이것을 다시 작성했을 때 속도가 대폭 향상되었습니다. 이것은 .NET 2.0에서 있었으므로 3과 4에서 상황이 어떻게 바뀌 었는지 잘 모르겠습니다.
델리게이트 호출은 컴파일러 / CLR에서 내부적으로 어떻게 처리되며 메서드 호출 성능에 어떤 영향을 미칩니 까?
편집 -대리자 대 콜백 인터페이스가 의미하는 바를 명확히하기 위해.
비동기 호출의 경우 내 클래스는 호출자가 구독 할 수있는 OnComplete 이벤트 및 관련 대리자를 제공 할 수 있습니다.
또는 호출자가 구현하는 OnComplete 메서드를 사용하여 ICallback 인터페이스를 만든 다음 완료시 해당 메서드를 호출하는 클래스에 등록 할 수 있습니다 (예 : Java가 이러한 작업을 처리하는 방식).
나는 그 효과를 보지 못했습니다. 나는 확실히 병목 현상을 경험 한 적이 없습니다.
다음은 대리자가 실제로 인터페이스보다 빠르다 는 것을 보여주는 매우 거칠고 준비된 벤치 마크입니다 .
using System;
using System.Diagnostics;
interface IFoo
{
int Foo(int x);
}
class Program : IFoo
{
const int Iterations = 1000000000;
public int Foo(int x)
{
return x * 3;
}
static void Main(string[] args)
{
int x = 3;
IFoo ifoo = new Program();
Func<int, int> del = ifoo.Foo;
// Make sure everything's JITted:
ifoo.Foo(3);
del(3);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < Iterations; i++)
{
x = ifoo.Foo(x);
}
sw.Stop();
Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds);
x = 3;
sw = Stopwatch.StartNew();
for (int i = 0; i < Iterations; i++)
{
x = del(x);
}
sw.Stop();
Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds);
}
}
결과 (.NET 3.5, .NET 4.0b2는 거의 동일) :
Interface: 5068
Delegate: 4404
이제 저는 대리자가 인터페이스보다 정말 빠르다 는 것을 의미하는 특별한 믿음을 가지고 있지 않습니다 . 그러나 그것은 그들이 훨씬 더 느리지 않다는 것을 상당히 확신하게합니다. 또한 이것은 델리게이트 / 인터페이스 메서드 내에서 거의 아무것도하지 않습니다. 호출 당 작업을 점점 더 많이 수행함에 따라 호출 비용은 점점 더 적은 차이를 만들 것입니다.
주의해야 할 한 가지는 단일 인터페이스 인스턴스 만 사용하는 새 델리게이트를 여러 번 만들지 않는다는 것입니다. 이로 인해 가비지 수집 등을 유발할 수 있으므로 문제 가 발생할 수 있습니다. 인스턴스 메서드를 루프 내에서 대리자로 사용하는 경우 루프 외부에서 대리자 변수를 선언하고 단일 대리자 인스턴스를 만들고 다시 사용하는 것이 더 효율적임을 알 수 있습니다. 그것. 예를 들면 :
Func<int, int> del = myInstance.MyMethod;
for (int i = 0; i < 100000; i++)
{
MethodTakingFunc(del);
}
다음보다 더 효율적입니다.
for (int i = 0; i < 100000; i++)
{
MethodTakingFunc(myInstance.MyMethod);
}
이것이 당신이 본 문제 였을까요?
CLR v 2부터 대리자 호출 비용은 인터페이스 메서드에 사용되는 가상 메서드 호출 비용과 매우 비슷합니다.
Joel Pobar 의 블로그를 참조하십시오 .
대리자가 가상 방법보다 훨씬 빠르거나 느리다는 것은 완전히 믿기지 않습니다. 델리게이트가 무시할 수있을 정도로 빨라야합니다. 낮은 수준에서 대리자는 일반적으로 다음과 같이 구현됩니다 (C 스타일 표기법을 사용하지만 이것은 단지 예시 일 뿐이므로 사소한 구문 오류는 용서하십시오).
struct Delegate {
void* contextPointer; // What class instance does this reference?
void* functionPointer; // What method does this reference?
}
대리인 호출은 다음과 같이 작동합니다.
struct Delegate myDelegate = somethingThatReturnsDelegate();
// Call the delegate in de-sugared C-style notation.
ReturnType returnValue =
(*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer);
C로 번역 된 클래스는 다음과 같습니다.
struct SomeClass {
void** vtable; // Array of pointers to functions.
SomeType someMember; // Member variables.
}
vritual 함수를 호출하려면 다음을 수행합니다.
struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer();
// Call the virtual function residing in the second slot of the vtable.
void* funcPtr = (myClass -> vtbl)[1];
ReturnType returnValue = (*((FunctionType) funcPtr))(myClass);
가상 함수를 사용할 때 함수 포인터를 얻기 위해 추가 간접 레이어를 거치는 것을 제외하고는 기본적으로 동일합니다. 그러나 최신 CPU 분기 예측기는 함수 포인터의 주소를 추측하고 함수의 주소를 찾는 것과 동시에 대상을 추측 적으로 실행하기 때문에이 추가 간접 레이어는 종종 무료입니다. 나는 타이트 루프의 가상 함수 호출이 인라인되지 않은 직접 호출보다 느리지 않다는 것을 발견했습니다 (C #가 아니라 D에 있음). 주어진 루프 실행에 대해 항상 동일한 실제 함수로 해결되는 경우 .
몇 가지 테스트를했습니다 (.Net 3.5에서 ... 나중에 .Net 4를 사용하여 집에서 확인할 것입니다). 사실 : 개체를 인터페이스로 가져온 다음 메서드를 실행하는 것이 메서드에서 대리자를 가져온 다음 대리자를 호출하는 것보다 빠릅니다.
변수가 이미 올바른 유형 (인터페이스 또는 대리자)이고 간단한 호출을 고려하면 대리자가 승리합니다.
어떤 이유로 인터페이스 메서드 (가상 메서드를 통해)를 통해 델리게이트를 가져 오는 것이 훨씬 느립니다.
그리고 Dispatches와 같이 델리게이트를 미리 저장할 수없는 경우가 있다는 점을 고려하면 인터페이스가 더 빠른 이유를 정당화 할 수 있습니다.
결과는 다음과 같습니다.
실제 결과를 얻으려면 릴리스 모드에서 컴파일하고 Visual Studio 외부에서 실행하십시오.
직통 전화 두 번 확인
00 : 00 : 00.5834988
00 : 00 : 00.5997071인터페이스 호출 확인, 모든 호출에서 인터페이스 가져 오기
00 : 00 : 05.8998212인터페이스 호출 확인, 인터페이스 한 번 가져 오기
00 : 00 : 05.3163224작업 (위임) 호출 확인, 모든 호출에서 작업 받기
00 : 00 : 17.1807980Action (위임) 호출 확인, Action 한 번 받기
00 : 00 : 05.3163224인터페이스 메서드를 통해 작업 (위임) 확인, 모든 호출에서 둘 다 가져 오기
00 : 03 : 50.7326056인터페이스 메서드에 대한 작업 (위임) 확인, 인터페이스 한 번 가져 오기, 모든 호출에서 위임
00 : 03 : 48.9141438인터페이스 메서드를 통해 작업 (위임) 확인, 둘 다 한 번 가져 오기
00 : 00 : 04.0036530
보시다시피 직접 통화는 정말 빠릅니다. 이전에 인터페이스 또는 델리게이트를 저장 한 다음 호출 만하는 것은 정말 빠릅니다. 그러나 델리게이트를 얻는 것은 인터페이스를 얻는 것보다 느립니다. 인터페이스 메서드 (또는 가상 메서드, 확실하지 않음)를 통해 델리게이트를 가져 오는 것은 정말 느립니다 (인터페이스로 객체를 가져 오는 5 초를 작업을 가져 오기 위해 동일한 작업을 수행하는 거의 4 분과 비교).
이러한 결과를 생성 한 코드는 다음과 같습니다.
using System;
namespace ActionVersusInterface
{
public interface IRunnable
{
void Run();
}
public sealed class Runnable:
IRunnable
{
public void Run()
{
}
}
class Program
{
private const int COUNT = 1700000000;
static void Main(string[] args)
{
var r = new Runnable();
Console.WriteLine("To get real results, compile this in Release mode and");
Console.WriteLine("run it outside Visual Studio.");
Console.WriteLine();
Console.WriteLine("Checking direct calls twice");
{
DateTime begin = DateTime.Now;
for (int i = 0; i < COUNT; i++)
{
r.Run();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
{
DateTime begin = DateTime.Now;
for (int i = 0; i < COUNT; i++)
{
r.Run();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking interface calls, getting the interface at every call");
{
DateTime begin = DateTime.Now;
for (int i = 0; i < COUNT; i++)
{
IRunnable interf = r;
interf.Run();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking interface calls, getting the interface once");
{
DateTime begin = DateTime.Now;
IRunnable interf = r;
for (int i = 0; i < COUNT; i++)
{
interf.Run();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking Action (delegate) calls, getting the action at every call");
{
DateTime begin = DateTime.Now;
for (int i = 0; i < COUNT; i++)
{
Action a = r.Run;
a();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking Action (delegate) calls, getting the Action once");
{
DateTime begin = DateTime.Now;
Action a = r.Run;
for (int i = 0; i < COUNT; i++)
{
a();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call");
{
DateTime begin = DateTime.Now;
for (int i = 0; i < COUNT; i++)
{
IRunnable interf = r;
Action a = interf.Run;
a();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call");
{
DateTime begin = DateTime.Now;
IRunnable interf = r;
for (int i = 0; i < COUNT; i++)
{
Action a = interf.Run;
a();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.WriteLine();
Console.WriteLine("Checking Action (delegate) over an interface method, getting both once");
{
DateTime begin = DateTime.Now;
IRunnable interf = r;
Action a = interf.Run;
for (int i = 0; i < COUNT; i++)
{
a();
}
DateTime end = DateTime.Now;
Console.WriteLine(end - begin);
}
Console.ReadLine();
}
}
}
What about the fact that delegates are containers? Doesn't the multicast ability add overhead? While we are on the subject, what if we push this container aspect a little further? Nothing forbids us, if d is a delegate, from executing d += d; or from building an arbitrarily complex directed graph of (context pointer, method pointer) pairs. Where can I find the documentation describing how this graph is traversed when the delegate is called?
참고URL : https://stackoverflow.com/questions/2082735/performance-of-calling-delegates-vs-methods
'IT Share you' 카테고리의 다른 글
Python Django Rest Framework 순서가 지정되지 않은 ObjectListWarning (0) | 2020.12.07 |
---|---|
HashSet은 삽입 순서를 유지합니까? (0) | 2020.12.07 |
Oracle JDBC 간헐적 연결 문제 (0) | 2020.12.07 |
읽기 전용 자동 구현 속성이 가능합니까? (0) | 2020.12.07 |
[]를 사용하여 인덱싱을 'System.Collections.Generic.IEnumerable <> 형식의 식에 적용 할 수 없습니다. (0) | 2020.12.07 |