为什么TypedReference在幕后?它是如此快速和安全……简直太神奇了!


128

警告:这个问题有点异端...宗教程序员始终恪守良好实践,请不要阅读它。:)

有谁知道为什么不鼓励使用TypedReference(隐式地,由于缺乏文档)?

我发现它有很好的用途,例如,当通过不应该是泛型的函数传递泛型参数时(object如果需要使用值类型,则使用over可能会导致过时或缓慢),需要不透明指针时;或者用于何时需要快速访问数组元素的信息,您可以在运行时找到其规格(使用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上,平均速度要快三倍以上。)


当我运行您的示例时,我得到的结果完全不同。TypedReference: 203 ticksboxing/unboxing: 31 ticks。不管我尝试什么(包括执行计时的不同方法),装箱/拆箱在我的系统上仍然更快。
Seph 2011年

1
@Seph:我刚刚看到您的评论。这有趣-在x64上似乎更快,但在x86上却慢。奇怪...
user541686

1
我刚刚在.NET 4.5的x64机器上测试了该基准代码。我用Diagnostics.Stopwatch替换了Environment.TickCount,并使用ms而不是刻度。我对每个版本(x86、64,Any)运行了三次。三种结果中最好的是以下各项:x86:205 / 27ms(在此构建中运行2/3的结果相同)x64:218 / 109ms任意:205 / 27ms(在此构建中运行2/3的结果相同) -所有情况下,装箱/拆箱速度更快。
kornman00

2
奇怪的速度测量值可能归因于以下两个事实:*(T)(object)v实际上并未进行堆分配。在.NET 4+中,已对其进行了优化。该路径上没有分配,而且该死的很快。*使用makeref要求将变量实际分配在堆栈上(而kinda-box方法可能会将其优化到寄存器中)。另外,通过查看时间,我认为即使使用force-inline标志,它也会削弱内联。因此,kinda-box会被内联和注册,而makeref会进行函数调用并操作堆栈
hypersw 2015年

1
要查看typeref转换的收益,请使其变得不那么琐碎。例如,将基础类型转换为枚举类型(int-> DockStyle)。这个盒子是真实的,并且慢了将近十倍。
hypersw

Answers:


42

简短答案:可移植性

同时__arglist__makeref__refvalue语言扩展,并在C#语言规范没有证件,用于实现它们的罩下的构建体(vararg调用约定,TypedReference类型,arglistrefanytypemkanyref,和refanyval指令)是完全在记录CLI规范(ECMA-335)在在可变参数库

通过在Vararg库中进行定义,可以很清楚地看出它们主要是为了支持可变长度的参数列表,而没有太多其他功能。变量参数列表在不需要与使用varargs的外部C代码进行接口的平台中几乎没有用。因此,Varargs库不是任何CLI配置文件的一部分。合法的CLI实现可能选择不支持Varargs库,因为它不包含在CLI内核配置文件中:

4.1.6瓦拉格

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

如果省略:尝试使用vararg调用约定或与vararg方法关联的签名编码(请参阅分区II)引用方法的任何尝试都将引发System.NotImplementedException异常。使用CIL指令的方法arglistrefanytypemkrefany,并refanyval应抛出System.NotImplementedException异常。没有指定异常的确切时间。类型System.TypedReference不需要定义。

更新(回复GetValueDirect评论):

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

一旦在BCL之外使用了一种方法,就会放弃一些可移植性(随着非.NET CLI实现的出现,如Silverlight和MonoTouch的出现,这一点变得越来越重要)。即使实现希望增加与Microsoft .NET Framework类库的兼容性,它也可以简单地提供GetValueDirectSetValueDirect接受一个,TypedReference而无需进行TypedReference运行时的特殊处理(基本上,使它们等效于它们的object对应物,而没有性能上的好处)。

如果他们用C#对其进行了记录,则至少会产生以下几点影响:

  1. 像任何功能一样,它可能会成为新功能的障碍,尤其是因为该功能实际上并不适合C#的设计,并且需要怪异的语法扩展和运行时对类型的特殊处理。
  2. C#的所有实现都必须以某种方式实现此功能,对于根本不在CLI之上运行或在没有Varargs的CLI之上运行的C#实现而言,它不一定是琐碎/可能的。

4
关于可移植性的很好论据,+ 1。但是,我们FieldInfo.GetValueDirectFieldInfo.SetValueDirect?它们是BCL的一部分,要使用它们,您需要 TypedReference,因此TypedReference,不管语言规范如何,基本上不是始终要强制定义它们吗?(另外,请注意:即使关键字不存在,只要存在指令,您仍然可以通过动态发出方法来访问它们……只要平台与C库互操作,您就可以使用它们,是否C#具有关键字。)
user541686 2011年

哦,还有另一个问题:即使它不是便携式的,为什么他们没有记录关键字呢?至少,在与C varargs互操作时有必要,因此至少他们可以提到它?
user541686 2011年

@Mehrdad:嗯,这很有趣。我猜想我一直以为.NET源代码的BCL文件夹中的文件是BCL的一部分,从不真正关注ECMA标准化部分。这非常有说服力……除了一件小事情:如果没有关于如何在任何地方使用它的文档,甚至在CLI规范中包括(可选)功能是否也毫无意义?(如果TypedReference仅针对一种语言(例如托管C ++)进行了文档记录是有意义的,但是如果没有语言对它进行文档记录,那么如果没有人可以真正使用它,那为什么还要麻烦定义功能?)
user541686

@Mehrdad我怀疑主要动机是内部需要互操作(例如 [DllImport("...")] void Foo(__arglist);)使用此功能,并且他们在C#中实现了自用。CLI的设计受许多语言的影响(注释“公共语言基础结构注释标准”证明了这一事实。)确保尽可能多的语言(包括不可预见的语言)成为合适的运行时绝对是设计目标(因此,名称),例如,假设的托管C实现可能会从中受益。
Mehrdad Afshari

@Mehrdad:啊...是的,这是一个令人信服的理由。谢谢!
user541686 2011年

15

好吧,我不是埃里克·利珀特(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和朋友介绍实施细节。您已经为它们指出了一些巧妙的用法,我认为它们值得探讨,但是通常需要依靠实现细节进行警告,下一个版本可能会破坏您的代码。


4
嗯...“您不需要它们”-我应该已经看到了。:-)是的,但事实也不是。您如何定义“需求”?例如,真的需要“扩展方法”吗?关于在以下情况下使用泛型的问题call():这是因为代码并不总是那么具有凝聚力-我指的是更多类似的示例IAsyncResult.State,其中引入泛型根本不可行,因为突然之间它将引入泛型。每个涉及的课程/方法。不过,答案是+1,尤其是指出“比Java更快”的部分。:]
user541686

1
哦,还有一点:考虑到FieldInfo.SetValueDirect是公开的,并且可能被某些开发人员使用,因此TypedReference可能不会在短期内进行重大更改。:)
user541686

嗯,但是您确实需要扩展方法来支持LINQ。无论如何,我并不是真正在谈论“有需要/有需要”的区别。我什么都不会打电话TypedReference。(在我看来,糟糕的语法和整体上的笨拙使它不符合“很好”的范畴。)我想说,当您真的需要在这里和那里微调几微秒时,这是一件好事。就是说,我想到了自己的代码中的几个地方,现在我要去看看,看看是否可以使用您所指出的技术来优化它们。
P Daddy

1
@Merhdad:我当时正在使用二进制对象序列化器/反序列化器进行进程间/主机间通信(TCP和管道)。我的目标是使它尽可能小(就通过网络发送的字节而言)和快(就序列化和反序列化所花费的时间而言)。我以为我可能会避免使用TypedReferences进行装箱和拆箱,但是IIRC,我唯一能够避免在某处装箱的地方是使用基元的一维数组。此处略微的速度优势并不值得将其添加到整个项目中的复杂性,因此我选择了它。
P Daddy

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版本与所有参数类型一起使用。
2014年

4

我无法弄清楚这个问题的标题是否应该被讽刺:长期以来,这TypedReference就是“真正的”托管指针的缓慢,肿,丑陋的表亲,后者是我们使用C ++ / CLI获得的 interior_ptr<T>,或者甚至C#中的传统按引用(ref/ out)参数。实际上,仅使用整数重新索引原始CLR数组就很难达到基准性能。TypedReference

可悲的细节在这里,但值得庆幸的是,现在这些都不重要了...

现在,C#7中的新ref局部变量ref返回功能可以解决这个问题。

这些新的语言功能在C#中提供了杰出的一流支持,用于在精心设计的情况下声明,共享和处理真正的CLR 托管引用类型

使用限制并不比以前的要求严格TypedReference(并且性能从最差到最好),因此我看不到C#中剩余的可用用例TypedReference。例如,以前没有办法将a持久化TypedReferenceGC堆中,因此高级托管指针现在也是如此。

显然,这种方式的消亡(TypedReference或至少几乎完全被淘汰)也意味着__makeref垃圾堆。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.