为啥 TypedReference 在幕后?它是如此快速和安全......几乎是神奇的!

Posted

技术标签:

【中文标题】为啥 TypedReference 在幕后?它是如此快速和安全......几乎是神奇的!【英文标题】:Why is TypedReference behind the scenes? It's so fast and safe... almost magical!为什么 TypedReference 在幕后?它是如此快速和安全......几乎是神奇的! 【发布时间】:2011-01-21 22:40:19 【问题描述】:

警告:这个问题有点异端......宗教程序员总是遵守良好的做法,请不要阅读。 :)

有谁知道为什么不鼓励使用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)); 
    

编写一个传递“状态”参数的方法,希望避免装箱:

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
    

(编辑:我编辑了上面的benchmark,因为上一个版本的帖子使用了debug版本的代码[我忘了改成release],对GC没有施加压力。这个版本有点多现实,在我的系统上,平均速度是TypedReference 的三倍以上。)

【问题讨论】:

当我运行你的例子时,我得到了完全不同的结果。 TypedReference: 203 ticksboxing/unboxing: 31 ticks。无论我尝试什么(包括不同的计时方式),装箱/拆箱在我的系统上仍然更快。 @Seph:我刚看到你的评论。这非常很有趣——它在 x64 上似乎更快,但在 x86 上更慢。奇怪... 我刚刚在 .NET 4.5 下的 x64 机器上测试了该基准代码。我用 Diagnostics.Stopwatch 替换了 Environment.TickCount 并用 ms 代替了滴答声。我将每个构建(x86、64、Any)运行了 3 次。三个结果中最好的结果如下: x86:205/27ms(在此构建上运行 2/3 的结果相同) x64:218/109ms Any:205/27ms(在此构建上运行 2/3 的结果相同) -all- case box/unboxing 更快。 奇怪的速度测量可能归因于以下两个事实: * (T)(object)v 实际上并没有进行堆分配。在 .NET 4+ 中,它被优化掉了。这条路径上没有分配,而且速度非常快。 * 使用 makeref 需要在堆栈上实际分配变量(而 kinda-box 方法可能会将其优化到寄存器中)。此外,通过查看时间,我认为即使使用 force-inline 标志也会损害内联。所以 kinda-box 是内联和注册的,而 makeref 进行函数调用并操作堆栈 要查看 typeref 转换的好处,让它变得不那么琐碎。例如。将基础类型转换为枚举类型 (int->DockStyle)。这个盒子是真实的,而且慢了近十倍。 【参考方案1】:

简短回答:便携性

虽然 __arglist__makeref__refvalue语言扩展,并且在 C# 语言规范中没有记录,但用于在后台实现它们的构造(vararg 调用约定、TypedReference 类型、arglistrefanytypemkanyrefrefanyval 指令)在 Vararg 库中的 CLI Specification (ECMA-335) 中完美记录。

在 Vararg 库中的定义清楚地表明它们主要是为了支持可变长度参数列表,而不是其他。变量参数列表在不需要与使用可变参数的外部 C 代码交互的平台中几乎没有用处。因此,可变参数库不是任何 CLI 配置文件的一部分。合法的 CLI 实现可能会选择不支持 Varargs 库,因为它不包含在 CLI 内核配置文件中:

4.1.6 可变参数

可变参数功能集支持可变长度参数列表和运行时类型指针。

如果省略: 任何尝试使用 vararg 调用约定或与可变参数方法关联的签名编码(参见第 II 部分)引用方法都将引发 System.NotImplementedException 异常。使用 CIL 指令 arglistrefanytypemkrefanyrefanyval 的方法将抛出 System.NotImplementedException 异常。未指定异常的确切时间。 System.TypedReference 类型不需要定义。

更新(回复GetValueDirect评论):

FieldInfo.GetValueDirectFieldInfo.SetValueDirect不是基类库的一部分。请注意,.NET Framework 类库和基类库之间存在差异。 BCL 是 CLI/C# 的一致实现唯一需要的东西,并记录在 ECMA TR/84 中。 (事实上​​,FieldInfo 本身是反射库的一部分,也不包含在 CLI 内核配置文件中)。

一旦您使用 BCL 之外的方法,您就放弃了一点可移植性(随着 Silverlight 和 MonoTouch 等非 .NET CLI 实现的出现,这变得越来越重要)。即使实现想要增加与 Microsoft .NET Framework 类库的兼容性,它也可以简单地提供 GetValueDirectSetValueDirect 采用 TypedReference 而无需使 TypedReference 由运行时特别处理(基本上,使它们等效给他们的object 同行,但没有性能优势)。

如果他们用 C# 记录它,它至少会产生一些影响:

    像任何特性一样,它可能成为新特性的障碍,特别是因为这个特性并不真正适合 C# 的设计,并且需要奇怪的语法扩展和由运行时。 C# 的所有实现都必须以某种方式实现此功能,对于根本不在 CLI 之上运行或在没有 Varargs 的 CLI 之上运行的 C# 实现来说,这不一定是微不足道的/可能的。

【讨论】:

可移植性的好论据,+1。但是FieldInfo.GetValueDirectFieldInfo.SetValueDirect 呢?它们是 BCL 的一部分,要使用它们,您需要 TypedReference,所以不管语言规范如何,这基本上不会强制始终定义 TypedReference 吗? (另外,另一个注意事项:即使关键字不存在,只要指令存在,您仍然可以通过动态发出方法来访问它们......所以只要您的平台与 C 库互操作,您就可以使用这些, C#是否有关键字。) 哦,还有一个问题:即使它不是可移植的,为什么他们没有记录关键字?至少,在与 C 可变参数互操作时是必要的,所以至少他们可以提到它? @Mehrdad:嗯,这很有趣。我想我一直认为 .NET 源的 BCL 文件夹中的文件是 BCL 的一部分,从未真正关注过 ECMA 标准化部分。这非常令人信服......除了一件小事:如果没有关于如何在任何地方使用它的文档,甚至在 CLI 规范中包含(可选)功能是否有点毫无意义? (如果 TypedReference 仅针对一种语言进行记录,例如托管 C++,这将是有意义的,但是如果 no 语言记录了它,那么如果没有人可以真正使用它,那为什么还要费心呢?定义功能?) @Mehrdad 我怀疑主要动机是内部需要此功能以进行互操作(例如 [DllImport("...")] void Foo(__arglist);),他们用C#实现了它供自己使用。 CLI 的设计受到许多语言的影响(注释“公共语言基础设施注释标准”证明了这一事实。)成为尽可能多的语言的合适运行时,包括不可预见的语言,绝对是一个设计目标(因此名称),这是一个特性,例如,假设的托管 C 实现可能会从中受益。 @Mehrdad:啊......是的,这是一个非常有说服力的理由。谢谢!【参考方案2】:

好吧,我不是 Eric Lippert,所以我不能直接谈论微软的动机,但如果我冒险猜测,我会说 TypedReference 等人。没有很好的记录,因为坦率地说,你不需要它们。

您提到的对这些功能的每次使用都可以在没有它们的情况下完成,尽管在某些情况下会降低性能。但是 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 和朋友的实施细节。您已经指出了它们的一些巧妙用途,我认为它们值得探索,但是依赖实现细节的通常警告也适用——下一个版本可能会破坏您的代码。

【讨论】:

嗯……“你不需要它们”——我应该预见到这一点。 :-) 这是真的,但也不是真的。你对“需要”的定义是什么?例如,扩展方法真的“需要”吗?关于您在call() 中使用泛型的问题:这是因为代码并不总是那么有凝聚力——我更多地指的是一个更像IAsyncResult.State 的示例,其中引入泛型根本不可行,因为所有突然它会为所涉及的每个类/方法引入泛型。但是,+1 的答案......尤其是指出“比 Java 更快”的部分。 :] 哦,还有一点:TypedReference 可能不会很快就会发生重大变化,因为 FieldInfo.SetValueDirect 是公开的并且可能被一些开发人员使用,取决于它。 :) 啊,但是您确实需要扩展方法来支持 LINQ。无论如何,我并不是在谈论一个很好的/需要的区别。我不会打电话给TypedReference。 (在我看来,残暴的语法和整体的笨拙使其不符合“不错”的类别。)我想说,当你真的需要在这里和那里修剪几微秒时,拥有它只是一件好事。也就是说,我正在考虑我自己的代码中的几个地方,我现在要去看看,看看我是否可以使用你指出的技术来优化它们。 @Merhdad:当时我正在研究用于进程间/主机间通信(TCP 和管道)的二进制对象序列化器/反序列化器。我的目标是让它尽可能小(就通过网络发送的字节而言)和快速(就序列化和反序列化所花费的时间而言)。我想我可能会避免使用TypedReferences 进行一些装箱和拆箱,但是 IIRC,我能够避免装箱的唯一地方 somewhere 是使用一维基元数组的元素。这里的轻微速度优势不值得它为整个项目增加的复杂性,所以我把它拿出来了。 给定delegate void ActByRef&lt;T1,T2&gt;(ref T1 p1, ref T2 p2); 类型T 的集合可以提供方法ActOnItem&lt;TParam&gt;(int index, ActByRef&lt;T,TParam&gt; proc, ref TParam param),但JITter 必须为每个值类型TParam 创建不同版本的方法。使用类型化引用将允许该方法的一个 JIT 版本处理所有参数类型。【参考方案3】:

我不知道这个问题的标题是否应该是讽刺的:long-established 一直是 TypedReference 是“真正”托管指针的缓慢、臃肿、丑陋的表亲,后者就是我们得到的使用 C++/CLI interior_ptr&lt;T&gt;,甚至是 C# 中的传统引用 (ref/out) 参数。 事实上,让TypedReference 达到每次只使用整数重新索引原始 CLR 数组的基线性能是相当困难的。

悲伤的细节是here,但谢天谢地,现在这些都不重要了……

C# 7 中的新 ref localsref return 功能现在使这个问题变得毫无意义>

这些新的语言功能在 C# 中为声明、共享和操作 true CLR 托管引用类型提供了卓越的一流支持>-在精心规定的情况下键入。

使用限制并不比以前对TypedReference 的要求更严格(性能实际上是jumping from worst to best),所以我在C# 中看不到TypedReference 的剩余可想象用例.例如,以前没有办法在 GC 堆中持久化 TypedReference,因此现在高级托管指针也是如此。

显然,TypedReference 的消亡——或者至少它几乎完全被弃用——也意味着将__makeref 扔到垃圾堆上。

【讨论】:

以上是关于为啥 TypedReference 在幕后?它是如此快速和安全......几乎是神奇的!的主要内容,如果未能解决你的问题,请参考以下文章

MSCK REPAIR TABLE 在幕后做了啥,为啥这么慢?

我是如何用2个Unix命令给SQL提速的

这种类型的图像显示是如何用 scikit-learn 完成的?

UI更新为什么一定要在UI线程里?幕后真相究竟如何?

UI更新为什么一定要在UI线程里?幕后真相究竟如何?

回调解决未处理的承诺,为啥?