Tại sao TypedReference đằng sau hậu trường? Thật nhanh chóng và an toàn, gần như huyền diệu!


128

Cảnh báo: Câu hỏi này hơi dị giáo ... các lập trình viên tôn giáo luôn tuân thủ các thông lệ tốt, xin đừng đọc nó. :)

Có ai biết tại sao việc sử dụng TypedReference lại bị nản lòng như vậy (ngầm, do thiếu tài liệu) không?

Tôi đã tìm thấy những công dụng tuyệt vời cho nó, chẳng hạn như khi truyền tham số chung qua các hàm không nên chung chung (khi sử dụng objectcó thể quá mức hoặc chậm, nếu bạn cần một loại giá trị), khi bạn cần một con trỏ mờ hoặc khi bạn cần truy cập nhanh vào một phần tử của mảng, thông số kỹ thuật bạn tìm thấy trong thời gian chạy (sử dụng Array.InternalGetReference). Vì CLR thậm chí không cho phép sử dụng loại này không chính xác, tại sao nó không được khuyến khích? Nó dường như không an toàn hoặc bất cứ điều gì ...


Các ứng dụng khác tôi đã tìm thấy cho TypedReference:

Thuốc generic "Chuyên" trong C # (đây là loại an toàn):

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); }
}

Viết mã hoạt động với các con trỏ chung (điều này rất không an toàn nếu sử dụng sai, nhưng nhanh và an toàn nếu sử dụng đúng cách):

//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);
}

Viết một phiên bản phương thức của sizeofhướng dẫn, đôi khi có thể hữu ích:

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)); }
    }
}

Viết một phương thức vượt qua tham số "trạng thái" muốn tránh quyền anh:

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*/ }
}

Vậy tại sao những cách sử dụng như thế này lại "nản lòng" (do thiếu tài liệu)? Bất kỳ lý do an toàn cụ thể? Nó có vẻ hoàn toàn an toàn và có thể kiểm chứng nếu nó không bị trộn lẫn với các con trỏ (dù sao nó không an toàn hoặc có thể kiểm chứng được) ...


Cập nhật:

Mã mẫu để cho thấy rằng, thực sự, TypedReferencecó thể nhanh gấp đôi (hoặc hơn):

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
    }
}

(Chỉnh sửa: Tôi đã chỉnh sửa điểm chuẩn ở trên, vì phiên bản cuối cùng của bài đăng đã sử dụng phiên bản gỡ lỗi của mã [Tôi quên thay đổi nó để phát hành] và không gây áp lực lên GC. Phiên bản này thực tế hơn một chút, và trên hệ thống của tôi, TypedReferencetrung bình nhanh hơn gấp ba lần .)


Khi tôi chạy ví dụ của bạn, tôi nhận được kết quả hoàn toàn khác nhau. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Bất kể tôi cố gắng gì (bao gồm cả những cách khác nhau để thực hiện thời gian), quyền anh / unboxing vẫn nhanh hơn trên hệ thống của tôi.
Seph

1
@Seph: Tôi vừa thấy bình luận của bạn. Điều đó rất thú vị - dường như nhanh hơn trên x64, nhưng chậm hơn trên x86.
Thật

1
Tôi vừa kiểm tra mã điểm chuẩn trên máy x64 của tôi theo .NET 4.5. Tôi đã thay thế môi trường.TickCount bằng chẩn đoán.Stopwatch và đi với ms thay vì tick. Tôi đã chạy từng bản dựng (x86, 64, Any) ba lần. Kết quả tốt nhất trong ba kết quả là dưới dạng follws: x86: 205 / 27ms (kết quả tương tự cho 2/3 lần chạy trên bản dựng này) x64: 218 / 109ms Bất kỳ: 205 / 27ms (cùng kết quả cho 2/3 chạy trên bản dựng này) -all- trường hợp hộp / unboxing đã nhanh hơn.
kornman00

2
Các phép đo tốc độ lạ có thể được quy cho hai sự thật sau: * (T) (đối tượng) v KHÔNG thực sự phân bổ heap. Trong .NET 4+, nó được tối ưu hóa. Không có phân bổ trên con đường này, và nó rất nhanh. * Sử dụng makeref yêu cầu biến thực sự được phân bổ trên ngăn xếp (trong khi phương thức hộp loại có thể tối ưu hóa nó thành các thanh ghi). Ngoài ra, bằng cách nhìn vào thời gian, tôi giả định rằng nó làm suy yếu nội tuyến ngay cả với cờ nội tuyến. Vì vậy, kinda-box được nội tuyến và đăng ký, trong khi makeref thực hiện một cuộc gọi chức năng và vận hành ngăn xếp
hypersw

1
Để xem lợi nhuận của việc đúc typeref, làm cho nó ít tầm thường hơn. Ví dụ: tạo một kiểu cơ bản cho kiểu enum ( int-> DockStyle). Hộp này là thật, và chậm hơn gần mười lần.
hypersw

Câu trả lời:


42

Câu trả lời ngắn gọn: tính di động .

Trong khi __arglist, __makeref__refvalueđang mở rộng ngôn ngữ và không có giấy tờ trong C # Ngôn ngữ kỹ thuật, các cấu trúc sử dụng để thực hiện chúng dưới mui xe ( vararggọi quy ước, TypedReferenceloại, arglist, refanytype, mkanyref, và refanyvalhướng dẫn) được một cách hoàn hảo được ghi lại trong CLI Đặc điểm kỹ thuật (ECMA-335) trong các thư viện vararg .

Việc được định nghĩa trong Thư viện Vararg cho thấy khá rõ rằng chúng chủ yếu nhằm hỗ trợ các danh sách đối số có độ dài thay đổi và không nhiều thứ khác. Danh sách đối số biến có ít sử dụng trong các nền tảng không cần giao diện với mã C bên ngoài sử dụng varargs. Vì lý do này, thư viện Varargs không phải là một phần của bất kỳ hồ sơ CLI nào. Việc triển khai CLI hợp pháp có thể chọn không hỗ trợ thư viện Varargs vì nó không có trong hồ sơ Hạt nhân CLI:

4.1.6 Biến đổi

Bộ tính năng vararg hỗ trợ danh sách đối số độ dài thay đổi và con trỏ gõ thời gian chạy.

Nếu bị bỏ qua: Bất kỳ nỗ lực nào để tham chiếu một phương thức với varargquy ước gọi hoặc mã hóa chữ ký được liên kết với các phương thức vararg (xem Phân vùng II) sẽ ném System.NotImplementedExceptionngoại lệ. Phương pháp sử dụng các hướng dẫn CIL arglist, refanytype, mkrefany, và refanyvalsẽ ném System.NotImplementedExceptionngoại lệ. Thời gian chính xác của ngoại lệ không được chỉ định. Các loại System.TypedReferencekhông cần phải được xác định.

Cập nhật (trả lời GetValueDirectbình luận):

FieldInfo.GetValueDirectFieldInfo.SetValueDirectkhông một phần của thư viện lớp cơ sở. Lưu ý rằng có một sự khác biệt giữa Thư viện lớp .NET và Thư viện lớp cơ sở. BCL là điều duy nhất cần thiết để triển khai CLI / C # phù hợp và được ghi lại trong ECMA TR / 84 . (Trên thực tế, FieldInfobản thân nó là một phần của thư viện Reflection và điều đó cũng không có trong hồ sơ CLI Kernel).

Ngay khi bạn sử dụng một phương thức bên ngoài BCL, bạn sẽ từ bỏ một chút tính di động (và điều này ngày càng trở nên quan trọng với sự ra đời của các triển khai CLI không .NET như Silverlight và MonoTouch). Ngay cả khi một triển khai muốn tăng khả năng tương thích với Thư viện lớp Microsoft .NET Framework, nó chỉ có thể cung cấp GetValueDirectSetValueDirectthực hiện TypedReferencemà không cần TypedReferencexử lý đặc biệt bởi bộ thực thi (về cơ bản, làm cho chúng tương đương với các objectđối tác của chúng mà không có lợi ích hiệu năng).

Nếu họ đã ghi lại nó bằng C #, thì nó sẽ có ít nhất một vài hàm ý:

  1. Giống như bất kỳ tính năng nào, nó có thể trở thành vật cản đối với các tính năng mới, đặc biệt là vì tính năng này không thực sự phù hợp với thiết kế của C # và yêu cầu các phần mở rộng cú pháp kỳ lạ và bàn giao đặc biệt của loại thời gian chạy.
  2. Tất cả các triển khai của C # phải bằng cách nào đó thực hiện tính năng này và nó không nhất thiết là tầm thường / có thể đối với các triển khai C # hoàn toàn không chạy trên CLI hoặc chạy trên CLI mà không có Varargs.

4
Đối số tốt cho tính di động, +1. Nhưng những gì về FieldInfo.GetValueDirectFieldInfo.SetValueDirect? Chúng là một phần của BCL và để sử dụng chúng mà bạn cần TypedReference , vì vậy về cơ bản, điều đó không bắt buộc TypedReferencephải luôn được xác định, bất kể thông số ngôn ngữ là gì? (Ngoài ra, một lưu ý khác: Ngay cả khi các từ khóa không tồn tại, miễn là các hướng dẫn tồn tại, bạn vẫn có thể truy cập chúng bằng các phương thức phát động động ... miễn là nền tảng của bạn giao tiếp với các thư viện C, bạn có thể sử dụng các từ khóa này, có hay không C # có từ khóa.)
user541686

Ồ, và một vấn đề khác: Ngay cả khi nó không di động, tại sao họ không ghi lại các từ khóa? Ít nhất, điều đó là cần thiết khi xen kẽ với các biến thể C, vậy ít nhất họ có thể đề cập đến nó không?
dùng541686

@Mehrdad: Huh, thật thú vị. Tôi đoán tôi luôn cho rằng các tệp trong thư mục BCL của nguồn .NET là một phần của BCL, không bao giờ thực sự chú ý đến phần tiêu chuẩn hóa ECMA. Điều này khá thuyết phục ... ngoại trừ một điều nhỏ: không phải là hơi vô nghĩa khi bao gồm tính năng (tùy chọn) trong thông số CLI, nếu không có tài liệu nào về cách sử dụng nó ở bất cứ đâu? (Sẽ có ý nghĩa nếu TypedReferenceđược ghi lại chỉ bằng một ngôn ngữ - giả sử, đã quản lý C ++ - nhưng nếu không có ngôn ngữ nào làm tài liệu đó và vì vậy nếu không ai có thể thực sự sử dụng nó, thì tại sao thậm chí còn bận tâm đến việc xác định tính năng?)
user541686

@Mehrdad Tôi nghi ngờ động lực chính là sự cần thiết của tính năng này trong nội bộ cho interop ( ví dụ [DllImport("...")] void Foo(__arglist); ) và họ đã triển khai nó trong C # để sử dụng riêng. Thiết kế của CLI bị ảnh hưởng bởi rất nhiều ngôn ngữ (chú thích "Tiêu chuẩn cơ sở hạ tầng ngôn ngữ chung" chứng minh thực tế này.) Là thời gian chạy phù hợp cho càng nhiều ngôn ngữ càng tốt, kể cả những ngôn ngữ không lường trước được, chắc chắn là mục tiêu thiết kế (do đó tên) và đây là một tính năng, ví dụ, việc triển khai C được quản lý giả định có thể có lợi từ đó.
Mehrdad Afshari

@Mehrdad: À ... ừ, đó là một lý do khá thuyết phục. Cảm ơn!
dùng541686

15

Chà, tôi không phải là Eric Lippert, vì vậy tôi không thể nói trực tiếp về động lực của Microsoft, nhưng nếu tôi muốn mạo hiểm đoán, tôi sẽ nói rằng TypedReferenceet al. không phải là tài liệu tốt bởi vì, thẳng thắn, bạn không cần chúng.

Mỗi lần sử dụng mà bạn đề cập cho các tính năng này có thể được thực hiện mà không có chúng, mặc dù ở một hình phạt hiệu suất trong một số trường hợp. Nhưng C # (và .NET nói chung) không được thiết kế để trở thành một ngôn ngữ hiệu suất cao. (Tôi đoán rằng "nhanh hơn Java" là mục tiêu hiệu suất.)

Điều đó không có nghĩa là những cân nhắc về hiệu suất nhất định chưa được chi trả. Thật vậy, các tính năng như con trỏ,stackalloc và các chức năng khung được tối ưu hóa nhất định tồn tại phần lớn để tăng hiệu suất trong các tình huống nhất định.

Generics, mà tôi muốn nói có lợi ích chính của an toàn loại, cũng cải thiện hiệu suất tương tự như TypedReferencebằng cách tránh đấm bốc và bỏ hộp. Trong thực tế, tôi đã tự hỏi tại sao bạn thích điều này:

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

đến đây:

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

Sự đánh đổi, như tôi thấy, là cái trước đòi hỏi ít JIT hơn (và, nó theo sau, ít bộ nhớ hơn), trong khi cái sau quen thuộc hơn và, tôi sẽ giả sử, nhanh hơn một chút (bằng cách tránh hội thảo con trỏ).

Tôi sẽ gọi cho TypedReferencebạn bè và chi tiết thực hiện. Bạn đã chỉ ra một số cách sử dụng gọn gàng cho chúng và tôi nghĩ rằng chúng đáng để khám phá, nhưng sự thận trọng thông thường của việc dựa vào các chi tiết triển khai áp dụng, phiên bản tiếp theo có thể phá vỡ mã của bạn.


4
Hả ... "bạn không cần chúng" - Tôi nên thấy điều đó sẽ đến. :-) Điều đó đúng nhưng cũng không đúng. Bạn định nghĩa thế nào là "cần"? Các phương thức mở rộng có thực sự "cần thiết" không, chẳng hạn? Về câu hỏi của bạn về việc sử dụng thuốc generic trong call(): Đó là vì mã không phải lúc nào cũng gắn kết - Tôi đã đề cập nhiều hơn đến một ví dụ giống như IAsyncResult.State, trong đó việc giới thiệu thuốc generic sẽ không khả thi vì đột nhiên nó sẽ giới thiệu thuốc generic cho mỗi lớp / phương pháp liên quan. +1 cho câu trả lời, mặc dù ... đặc biệt là để chỉ ra phần "nhanh hơn Java". :]
dùng541686

1
Ồ, và một điểm khác: TypedReferencecó thể sẽ không trải qua các thay đổi sớm, do FieldInfo.SetValueDirect , công khai và có thể được sử dụng bởi một số nhà phát triển, phụ thuộc vào nó. :)
dùng541686

Ah, nhưng bạn làm cần phương pháp khuyến nông, để hỗ trợ LINQ. Dù sao, tôi không thực sự nói về một sự khác biệt tốt đẹp cần có / cần phải có. Tôi sẽ không gọi TypedReferencemột trong hai. (Cú pháp tàn bạo và sự khó sử dụng nói chung không đủ tiêu chuẩn, trong suy nghĩ của tôi, từ thể loại dễ có.) Tôi nói rằng đó chỉ là một điều tốt để có khi bạn thực sự cần phải cắt một vài phần triệu giây ở đây và ở đó. Điều đó nói rằng, tôi nghĩ về một vài vị trí trong mã của riêng tôi mà tôi sẽ xem xét ngay bây giờ, để xem liệu tôi có thể tối ưu hóa chúng bằng cách sử dụng các kỹ thuật mà bạn đã chỉ ra không.
P Daddy

1
@Merhdad: Tôi đang làm việc trên một trình tuần tự hóa / giải tuần tự đối tượng nhị phân tại thời điểm để truyền thông liên tiến trình / interhost (TCP và ống). Mục tiêu của tôi là làm cho nó nhỏ hơn (về số byte được gửi qua dây) và nhanh chóng (về thời gian dành cho việc tuần tự hóa và giải tuần tự hóa) càng tốt. Tôi nghĩ rằng tôi có thể tránh một số quyền anh và bỏ hộp với TypedReferences, nhưng IIRC, nơi duy nhất tôi có thể tránh quyền anh ở đâu đó là với các yếu tố của mảng nguyên thủy một chiều. Lợi ích tốc độ nhẹ ở đây không xứng đáng với sự phức tạp mà nó đã thêm vào toàn bộ dự án, vì vậy tôi đã lấy nó ra.
P Daddy

1
Đưa ra delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);một bộ sưu tập loại Tcó thể cung cấp một phương thức ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), nhưng JITter sẽ phải tạo một phiên bản khác của phương thức cho mỗi loại giá trị TParam. Sử dụng một tham chiếu được gõ sẽ cho phép một phiên bản JITted của phương thức hoạt động với tất cả các loại tham số.
supercat

4

Tôi không thể biết liệu tiêu đề của câu hỏi này có bị mỉa mai hay không: Nó đã được thiết lập từ lâu, đó TypedReferencelà người anh em họ chậm chạp, xấu xí, xấu xí của những con trỏ được quản lý 'thật', sau này là những gì chúng ta có được với C ++ / CLI interior_ptr<T> , hoặc thậm chí các tham số theo tham chiếu ( ref/ out) truyền thống trong C # . Trên thực tế, thật khó để TypedReferenceđạt được hiệu suất cơ bản khi chỉ sử dụng một số nguyên để lập chỉ mục lại khỏi mảng CLR ban đầu mỗi lần.

Các chi tiết đáng buồn ở đây , nhưng may mắn thay, không có vấn đề nào trong số này bây giờ ...

Câu hỏi này hiện được trả lời bởi người dân địa phương mới và các tính năng trả về ref trong C # 7

Các tính năng ngôn ngữ mới này cung cấp hỗ trợ hạng nhất, nổi bật trong C # để khai báo, chia sẻ và thao tác các CLR kiểu tham chiếu được quản lý thực sự trong các tình huống được đặt trước cẩn thận.

Các hạn chế sử dụng không chặt chẽ hơn những gì đã được yêu cầu trước đây TypedReference(và hiệu suất thực sự chuyển từ xấu nhất sang tốt nhất ), vì vậy tôi thấy không có trường hợp sử dụng nào có thể hiểu được trong C # cho TypedReference. Ví dụ, trước đây không có cách nào để tồn tại một TypedReferencetrong GCđống, do đó con người cùng thực sự của con trỏ được quản lý tốt hơn bây giờ không phải là một take-away.

Và rõ ràng, sự tàn lụi của Gioror , sự mất TypedReferencegiá gần như hoàn toàn của nó, ít nhất là có nghĩa là ném __makerefvào đống rác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.