장면 뒤에 TypedReference가 왜 있습니까? 너무 빠르고 안전합니다. 거의 마법입니다!


128

경고 :이 질문은 약간 이단 적입니다 ... 종교적 프로그래머들은 항상 좋은 관행을 따르고 있습니다. :)

TypedReference 의 사용이 왜 그렇게 암묵적으로 권장 되지 않는지 아는 사람이 있습니까?

일반적인 object포인터 가 아닌 함수를 통해 일반 매개 변수를 전달할 때 ( 값 유형이 필요한 경우 과잉 또는 느리게 사용하는 경우), 불투명 포인터가 필요할 때 등의 용도로 유용합니다. 런타임에 사양을 사용하여 배열의 요소에 빠르게 액세스해야 할 때 (를 사용하여 Array.InternalGetReference). CLR은 이러한 유형의 잘못된 사용을 허용하지 않으므로 왜 권장하지 않습니까? 안전하지 않은 것 같습니다.


내가 찾은 다른 용도 TypedReference:

C #에서 제네릭 "전문화"(유형 안전) :

static void foo<T>(ref T value)
{
    //This is the ONLY way to treat value as int, without boxing/unboxing objects
    if (value is int)
    { __refvalue(__makeref(value), int) = 1; }
    else { value = default(T); }
}

일반 포인터와 함께 작동하는 코드 작성 ( 오용하는 경우 매우 안전하지만 올바르게 사용하면 빠르고 안전합니다) :

//This bypasses the restriction that you can't have a pointer to T,
//letting you write very high-performance generic code.
//It's dangerous if you don't know what you're doing, but very worth if you do.
static T Read<T>(IntPtr address)
{
    var obj = default(T);
    var tr = __makeref(obj);

    //This is equivalent to shooting yourself in the foot
    //but it's the only high-perf solution in some cases
    //it sets the first field of the TypedReference (which is a pointer)
    //to the address you give it, then it dereferences the value.
    //Better be 10000% sure that your type T is unmanaged/blittable...
    unsafe { *(IntPtr*)(&tr) = address; }

    return __refvalue(tr, T);
}

때로는 유용한 메소드 버전의 sizeof명령어 작성 :

static class ArrayOfTwoElements<T> { static readonly Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

복싱을 피하려는 "state"매개 변수를 전달하는 메소드 작성 :

static void call(Action<int, TypedReference> action, TypedReference state)
{
    //Note: I could've said "object" instead of "TypedReference",
    //but if I had, then the user would've had to box any value types
    try
    {
        action(0, state);
    }
    finally { /*Do any cleanup needed*/ }
}

그렇다면 왜 이런 "감지 된"(문서가 부족하여) 사용 되는가? 특별한 안전상의 이유가 있습니까? 포인터와 섞이지 않으면 완벽하게 안전하고 검증 가능한 것처럼 보입니다 (어쨌든 안전하거나 검증 할 수는 없음) ...


최신 정보:

실제로 TypedReference두 배나 더 빠를 수있는 샘플 코드 :

using System;
using System.Collections.Generic;
static class Program
{
    static void Set1<T>(T[] a, int i, int v)
    { __refvalue(__makeref(a[i]), int) = v; }

    static void Set2<T>(T[] a, int i, int v)
    { a[i] = (T)(object)v; }

    static void Main(string[] args)
    {
        var root = new List<object>();
        var rand = new Random();
        for (int i = 0; i < 1024; i++)
        { root.Add(new byte[rand.Next(1024 * 64)]); }
        //The above code is to put just a bit of pressure on the GC

        var arr = new int[5];
        int start;
        const int COUNT = 40000000;

        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set1(arr, 0, i); }
        Console.WriteLine("Using TypedReference:  {0} ticks",
                          Environment.TickCount - start);
        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set2(arr, 0, i); }
        Console.WriteLine("Using boxing/unboxing: {0} ticks",
                          Environment.TickCount - start);

        //Output Using TypedReference:  156 ticks
        //Output Using boxing/unboxing: 484 ticks
    }
}

(편집 : 게시물의 마지막 버전은 디버그 버전의 코드를 사용했기 때문에 위의 벤치 마크를 편집했으며 [릴리스로 변경하는 것을 잊었습니다] GC에 부담을주지 않습니다.이 버전은 좀 더 현실적이며 내 시스템에서는 TypedReference평균 3 배 이상 빠릅니다 .)


예제를 실행하면 완전히 다른 결과가 나타납니다. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. 내가 시도하는 (시간을 측정하는 다른 방법을 포함하여) 시도하거나 관계없이 권투 / 언 박싱은 여전히 ​​시스템에서 더 빠릅니다.
Seph

1
@Seph : 방금 당신의 의견을 보았습니다. 그건 매우 흥미로운 - 더 빨리 64에 보인다,하지만 x86에서 느린. 이상한 ...
541686

1
방금 x64 시스템에서 .NET 4.5의 벤치 마크 코드를 테스트했습니다. Environment.TickCount를 Diagnostics.Stopwatch로 바꾸고 진드기 대신 ms로갔습니다. 각 빌드 (x86, 64, Any)를 세 번 실행했습니다. x86 : 205 / 27ms (이 빌드에서 2/3 실행의 동일한 결과) x64 : 218 / 109ms Any : 205 / 27ms (이 빌드에서 2/3 실행의 동일한 결과) -모든 경우 박스 / 언 박스가 더 빠릅니다.
kornman00

2
이상한 속도 측정은 다음 두 가지 사실에 기인 할 수 있습니다. * (T) (object) v는 실제로 힙 할당을하지 않습니다. .NET 4 이상에서는 최적화되었습니다. 이 경로에는 할당이 없으며 빠릅니다. * makeref를 사용하려면 변수가 실제로 스택에 할당되어야합니다 (종류 상자 방법은 레지스터로 최적화 할 수 있음). 또한 타이밍을 보면서 강제 인라인 플래그로도 인라인이 손상되는 것으로 가정합니다. makeref 함수 호출을 스택 운영하면서 그래서 좀-상자, 인라인 및 enregistered한다
hypersw

1
typeref 주조의 이익을 보려면 덜 사소한 것으로 만드십시오. 예를 들어 기본 형식을 열거 형 형식 ( int-> DockStyle)으로 캐스팅합니다 . 이 상자는 실제에 적용되며 거의 10 배 느립니다.
hypersw

답변:


42

짧은 대답 : 이식성 .

반면 __arglist, __makeref__refvalue되는 언어 확장 , 후드를 구현하는 데 사용되는 구조는와 C # 언어 사양 서류 미 비자 ( vararg호출 규칙, TypedReference유형, arglist, refanytype, mkanyref, 및 refanyval지침)을 완벽에 설명되어 있습니다 CLI 사양 (ECMA-335)가변 인자 라이브러리 .

Vararg 라이브러리에 정의되어 있기 때문에 가변 길이 인수 목록을 지원하는 것이지 그다지 중요하지는 않습니다. 변수 인수 목록은 varargs를 사용하는 외부 C 코드와 인터페이스 할 필요가없는 플랫폼에서 거의 사용되지 않습니다. 이러한 이유로 Varargs 라이브러리는 CLI 프로파일의 일부가 아닙니다. 합법적 CLI 구현은 Varargs 라이브러리가 CLI 커널 프로파일에 포함되지 않으므로 Varargs 라이브러리를 지원하지 않도록 선택할 수 있습니다.

4.1.6 바라 그

가변 인자 기능 세트는 가변 길이 인수 목록 및 런타임 형식의 포인터를 지원합니다.

생략 된 경우 :vararg 호출 규칙 또는 vararg 메소드와 연관된 서명 인코딩 (파티션 II 참조)을 사용하여 메소드를 참조하려고 시도 하면 System.NotImplementedException예외가 발생합니다. CIL 명령어를 사용하는 방법 arglist, refanytype, mkrefany, 및 refanyval던져된다 System.NotImplementedException예외. 예외의 정확한 타이밍은 지정되어 있지 않습니다. 유형을 System.TypedReference정의 할 필요가 없습니다.

업데이트 (댓글로 회신 GetValueDirect) :

FieldInfo.GetValueDirect되어 FieldInfo.SetValueDirect있습니다 하지 기본 클래스 라이브러리의 일부. .NET Framework 클래스 라이브러리와 기본 클래스 라이브러리에는 차이가 있습니다. BCL은 CLI / C #의 적합한 구현에 필요한 유일한 것이며 ECMA TR / 84에 문서화되어 있습니다. 실제로, FieldInfo그 자체는 Reflection 라이브러리의 일부이며 CLI 커널 프로파일에도 포함되어 있지 않습니다.

BCL 외부에서 메소드를 사용하자마자 약간의 이식성을 포기합니다 (Silverlight 및 MonoTouch와 같은 .NET CLI 이외의 구현으로 인해 점점 중요 해지고 있습니다). 구현에서 Microsoft .NET Framework 클래스 라이브러리와의 호환성을 높이고 싶더라도 런타임 에서 특수하게 처리 하지 않고 (기본적으로 성능상의 이점이없는 다른 클래스 GetValueDirect와 동등 하게) 제공 하고 SetValueDirect가져갈 수 있습니다.TypedReferenceTypedReferenceobject

그들이 C #으로 문서화했다면 적어도 몇 가지 의미가 있었을 것입니다.

  1. 다른 기능과 마찬가지로, 새로운 기능의로드 블록 있습니다. 특히 C # 디자인에 적합하지 않으며 런타임시 이상한 구문 확장과 특수한 유형 전달이 필요하기 때문입니다.
  2. C #의 모든 구현은 어떻게 든이 기능을 구현해야하며 CLI에서 전혀 실행되지 않거나 Varargs없이 CLI에서 실행되지 않는 C # 구현에는 반드시 사소하거나 가능하지는 않습니다.

4
이식성에 대한 좋은 주장, +1 그러나 약 FieldInfo.GetValueDirectFieldInfo.SetValueDirect? 그것들은 BCL의 일부이며 필요한 것을 사용하기 TypedReference때문에 TypedReference언어 사양에 관계없이 기본적으로 항상 정의되어야합니까? (또 다른 참고 사항 : 키워드가 존재하지 않더라도 명령이 존재하는 한 동적으로 메소드를 방출하여 키워드에 액세스 할 수 있습니다 ... 플랫폼이 C 라이브러리와 상호 작용하는 한 이러한 키워드를 사용할 수 있습니다. C #에 키워드가 있는지 여부)
user541686

아, 또 다른 문제 : 이식성이 없어도 키워드를 문서화하지 않은 이유는 무엇입니까? 최소한 C varargs와 상호 작용할 때 필요하므로 적어도 언급 할 수 있습니까?
user541686

@Mehrdad : 허, 흥미 롭습니다. 필자는 항상 .NET 소스 의 BCL 폴더에있는 파일이 BCL 의 일부라고 생각하고 ECMA 표준화 부분에 실제로주의를 기울이지 않습니다. 이것은 매우 설득력이 있습니다 ... 한 가지 작은 것을 제외하고는 : 어디에서 사용하는 방법에 대한 문서가 없다면 CLI 사양에 (선택적) 기능을 포함시키는 것도 무의미하지 않습니까? ( TypedReference관리되는 C ++ 과 같이 하나의 언어로만 문서화 된 경우 에는 의미가 있지만, 언어로 문서화 된 문서가없고 실제로 언어를 사용할 수없는 경우에는이 기능을 정의하는 것이 왜 귀찮습니까?)
user541686

내가 @Mehrdad 의심 의 기본 동기는 내부적으로 상호 운용성 (이 기능에 대한 요구했다 [DllImport("...")] void Foo(__arglist); ) 그들은 자신의 사용을 위해 C #으로 그것을 구현했습니다. CLI의 디자인은 많은 언어의 영향을받습니다 ( "공통 언어 인프라 주석 표준"이라는 주석은이 사실을 보여줍니다.) 예기치 않은 언어를 포함하여 가능한 한 많은 언어에 적합한 런타임이되는 것이 디자인 목표였습니다 (따라서 예를 들어 가상의 관리되는 C 구현이 도움이 될 수있는 기능입니다.
Mehrdad Afshari

@Mehrdad : 아 ... 네, 꽤 설득력있는 이유입니다. 감사!
user541686

15

글쎄, 나는 Eric Lippert가 아니기 때문에 Microsoft의 동기에 대해 직접 말할 수는 없지만 추측을해야한다면 TypedReferenceet al. 솔직히 말해서 필요하지 않기 때문에 잘 문서화되어 있지 않습니다.

경우에 따라 성능 저하가 발생하더라도 이러한 기능에 대해 언급 한 모든 사용은 기능없이 수행 할 수 있습니다. 그러나 C # (및 일반적으로 .NET)은 고성능 언어로 설계되지 않았습니다. ( "Java보다 빠름"이 성능 목표라고 생각합니다.)

특정 성능 고려 사항이 제공되지 않았다고 말할 수는 없습니다. 실제로, 포인터와 같은 기능 stackalloc및 특정 최적화 된 프레임 워크 기능은 특정 상황에서 성능을 향상시키기 위해 주로 존재합니다.

타입 안전 의 주요 이점 이있는 제네릭은 또한 TypedReference박싱 및 언 박싱을 피함으로써 성능을 향상시킵니다 . 사실, 왜 당신이 이것을 선호하는지 궁금합니다.

static void call(Action<int, TypedReference> action, TypedReference state){
    action(0, state);
}

이에:

static void call<T>(Action<int, T> action, T state){
    action(0, state);
}

내가 알다시피, 단점은 전자는 JIT가 적고 메모리가 적다는 점이지만 후자는 더 친숙하고 포인터 포인터 참조를 피함으로써 약간 더 빠를 것이라고 가정합니다.

나는 전화 TypedReference및 친구 구현 세부 사항을 호출 합니다. 당신은 그것들에 대한 몇 가지 깔끔한 사용법을 지적했으며, 그것들은 탐구 할 가치가 있다고 생각하지만 구현 세부 사항에 의존하는 일반적인주의 사항이 적용됩니다. 다음 버전은 코드를 손상시킬 수 있습니다.


4
허 .. "필요하지 않아요"-다가오는 걸 봤어야했는데 :-) 그것은 사실이지만 또한 사실이 아닙니다. "필요"로 무엇을 정의합니까? 예를 들어, 확장 메소드는 실제로 "필요"합니까? 에 제네릭을 사용하는 것에 대한 귀하의 질문에 관해서는 call(): 코드가 항상 응집력이 높지 IAsyncResult.State않기 때문입니다. 제네릭을 도입하는 것이 실현 가능하지 않을 것입니다. 관련된 모든 수업 / 방법. 대답은 +1입니다. 특히 "Java보다 빠름"부분을 지적한 경우. :]
user541686

1
오, 그리고 또 다른 요점 : 공개되어 있고 일부 개발자가 사용 하는 FieldInfo.SetValueDirect 가 그것에 의존 한다는 점을 감안할 때 TypedReference아마도 빠른 변화를 겪지 않을 것입니다 . :)
user541686

아, 그러나 LINQ를 지원하려면 확장 방법 필요합니다. 어쨌든, 나는 정말로 좋은 행동과 필요한 행동의 차이점에 대해 이야기하고 있지 않습니다. 나는 TypedReference그들 중 하나를 호출하지 않을 것 입니다. (사악한 구문과 전반적인 불안감으로 인해 마음에 들지 않는 카테고리에서 실격을 풀 수 있습니다.) 여기저기서 몇 마이크로 초를 다듬어야 할 때 주변에 두는 것이 좋습니다. 즉, 내 코드에서 몇 가지 장소를 생각할 것입니다. 지금 살펴볼 기술을 사용하여 최적화 할 수 있는지 확인하십시오.
P Daddy

1
@Merhdad : 프로세스 간 / 호스트 간 통신 (TCP 및 파이프)을 위해 이진 개체 직렬 변환기 / 디시리얼라이저를 작업하고있었습니다. 저의 목표는 가능한 작게 (와이어를 통해 전송 된 바이트 수로), 빠르게 (직렬화 및 직렬화 해제에 소비 된 시간으로) 만드는 것이 었습니다. 나는 TypedReferences로 일부 권투 및 언 박싱을 피할 수 있다고 생각 했지만 IIRC, 어딘가 에서 권투를 피할 수있는 유일한 장소 는 1 차원 배열의 기본 요소의 요소 뿐이었습니다 . 여기서 약간의 속도 이점은 전체 프로젝트에 추가 된 복잡성의 가치가 없었으므로 제거했습니다.
P 아빠

1
주어진 delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);유형의 콜렉션이 T메소드를 제공 할 수 ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param)있지만 JITter는 모든 값 유형에 대해 다른 버전의 메소드를 작성해야합니다 TParam. 유형이 지정된 참조를 사용하면 하나의 JITted 버전의 메소드가 모든 매개 변수 유형에 대해 작동 할 수 있습니다.
supercat

4

나는이 질문의 제목은 비꼬는 있어야 여부를 알아낼 수 없습니다 : 된 전통TypedReference'사실'관리 포인터의 느린, 비 대한, 추악한 사촌이며, 후자의 존재 우리가 무엇을 얻을 C ++ / CLI interior_ptr<T> , 또는 C #의 전통적인 참조 기준 ( ref/ out) 매개 변수 조차도 . 실제로 매번 원래 CLR 배열을 다시 색인화하기 위해 정수를 사용하는 기준 성능에 도달하는 것은 매우 어렵 습니다.TypedReference

슬픈 세부 사항은 여기 있지만 고맙게도 현재는 중요하지 않습니다 ...

이 질문은 이제 새에 의해 논쟁을 렌더링 심판 주민 C # 7 ref return 기능에

이러한 새로운 언어 기능은 일류의 탁월한 지원을 제공합니다. 은 신중하게 예측 된 상황에서 진정한 CLR 관리 형 참조 유형 을 선언, 공유 및 조작 할 수 있도록 C # 합니다.

사용 제한은 이전에 요구 된 것보다 엄격하지 않습니다 TypedReference(그리고 성능은 문자 그대로 최악에서 최고로 점프합니다) 그래서에 남아 생각할 유스 케이스 볼) C 번호 를 들어 TypedReference. 예를 들어, 이전을 지속 할 수있는 방법이 없었다 TypedReference에서 GC우수한 관리 포인터의 진정한 같은 존재가 지금은 테이크 아웃되지 않도록, 힙.

그리고 명백하게도, TypedReference거의 완전한 지원 중단 의 소멸은__makeref 정크 힙을 합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.