是否可以重新分配本地的参考?

Joh*_*ous 9 .net c# pointers

C#的ref本地是使用称为托管指针的CLR功能实现的,它具有自己的一组限制,但幸运的是,不可变的不是其中之一.即如果你有一个托管指针类型的局部变量,那么在ILAsm中,完全有可能改变这个指针,使其成为"引用"另一个位置.(C++/CLI还将此功能公开为内部指针.)

在ref本地读取C#文档时,我觉得C#的ref本地是,即使基于CLR的托管指针,也不可重定位; 如果它们被初始化为指向某个变量,则不能使它们指向别的东西.我试过用了

ref object reference = ref some_var;
ref reference = ref other_var;
Run Code Online (Sandbox Code Playgroud)

和类似的结构,无济于事.

我甚至试图在IL中编写一个包含托管指针的小结构,就C#而言它是有效的,但是CLR似乎不喜欢在结构中有一个托管指针,即使在我的使用中它没有永远不会去堆.

是否真的不得不诉诸于使用IL或者递归来解决这个问题?(我正在实现一个数据结构,需要跟踪它的哪些指针被跟踪,完美使用托管指针.)

Gle*_*den 5

[编辑:] " ref-reassign "是C#7.3的时间表.我在下面讨论的'conditional-ref'解决方法是在C#7.2中部署的.


我也一直对此感到沮丧,最近偶然发现了一个可行的答案.

基本上,在C#7.2中,您现在可以使用三元运算符初始化ref本地,这可以设计.有点折磨,进入反地方重新分配的模拟.当您在C#代码的词法范围内向下移动时,您可以通过多个变量向下"移交" ref本地分配.

这种方法需要大量的非常规思维和未来的大量计划.对于某些情况或编码方案,可能无法预测运行时配置的范围,使得任何条件分配方案可能适用.在这种情况下,你运气不好.或者,切换到C++/CLI,它公开托管跟踪引用.这里的紧张局势是,对于C#而言,通过引入传统的托管指针使用(这些点将在下面进一步讨论)立即实现的简洁,优雅和高效的巨大且无可争辩的收益随着所需的扭曲程度而消失.克服重新分配问题.

接下来会显示我已经躲过很长时间的语法.或者,查看我在顶部引用的链接.

C#7.2通过三元曝光器进行ref-local条件赋值 ? :


ref int i_node = ref (f ? ref m_head : ref node.next);
Run Code Online (Sandbox Code Playgroud)

这一行来自一个典型的问题案例,ref local提出了提问者在这里提出的两难问题.它来自代码,它在行走单链表时维护后向指针.这个任务在C/C++中是微不足道的,因为它应该是(并且非常受CSE101教师的喜爱,也许是因为这个特殊原因) - 但是使用托管指针C#完全令人痛苦.

由于微软自己的C++/CLI语言向我们展示了.NET Universe中有多么棒的托管指针,因此这样的投诉也是完全合法的.相反,大多数C#开发人员似乎最终只使用整数索引到数组中,或者当然使用unsafeC#完全成熟的本地指针.

关于链表行走示例的一些简短评论,以及为什么人们有兴趣在这些托管指针上遇到这么多麻烦.我们假设所有的节点实际上是结构中的阵列(ValueType,原位),例如m_nodes = new Node[100];,并且每个next指针是这样的整数(其在阵列中的索引).

struct Node
{
    public int ix, next;
    public char data;

    public override String ToString() => 
              String.Format("{0}  next: {1,2}  data: {2}", ix, next, data);
};
Run Code Online (Sandbox Code Playgroud)

如此处所示,列表的头部将是一个独立的整数,与记录分开存储.在接下来的片断,我用的是新的C#7语法ValueTuple这样做.显然,使用这些整数链接遍历是没有问题的 - 但是C#传统上缺乏一种优雅的方式来维护与您来自的节点的链接.这是一个问题,因为其中一个整数(第一个)是一个特殊情况,因为它没有嵌入Node结构中.

static (int head, Node[] nodes) L =
    (3,
    new[]
    {
        new Node { ix = 0, next = -1, data = 'E' },
        new Node { ix = 1, next =  4, data = 'B' },
        new Node { ix = 2, next =  0, data = 'D' },
        new Node { ix = 3, next =  1, data = 'A' },
        new Node { ix = 4, next =  2, data = 'C' },
    });
Run Code Online (Sandbox Code Playgroud)

另外,在每个节点上可能需要进行大量的处理工作,但是你真的不想支付ValueType从其舒适的阵列家中成像每个(可能很大)的(双倍)性能成本,然后不得不进行成像.当你完成后,每一个回来!毕竟,我们在这里使用价值类型的原因当然是为了最大限度地提高性能.正如我在本网站的其他地方详细讨论的那样,结构可以非常高效.NET,但前提是你永远不会意外地将它们从存储中"抬起".它很容易做到,它可以立即破坏你的内存总线带宽.

不解除结构的trival方法只重复数组索引,如下所示:

int ix = 1234;
arr[ix].a++;
arr[ix].b ^= arr[ix].c;
arr[ix].d /= (arr[lx].e + arr[ix].f);
Run Code Online (Sandbox Code Playgroud)

这里,每次ValueType访问时都会独立地取消引用每个字段访问.虽然这种"优化"确实避免了上面提到的带宽损失,但是一遍又一遍地重复相同的数组索引操作可能会涉及一组完全不同的运行时惩罚.(机会)成本现在是由于不必要地浪费了周期,其中.NET重新计算可证明不变的物理偏移或对阵列执行冗余边界检查.

通过识别和整合您提供的代码中的冗余,发布模式中的JIT优化可以在某种程度上甚至是显着地缓解这些问题,但可能没有您想象或希望的那么多(或者最终意识到您想要):严格遵守.NET内存模型严格限制JIT优化.[1],这要求每当存储位置公开可见时,CPU必须完全按照代码中的编写执行相关的提取序列.对于前面的示例,这意味着如果ix在操作开始之前以任何方式与其他线程共享arr,那么JIT必须确保CPU实际上恰好触摸ix存储位置6次,不多也不少.

当然,JIT 无法解决重复源代码(例如前面的示例)中另一个明显且广为人知的问题.简而言之,它很丑陋,容易出错,而且难以阅读和维护.为了说明这一点,
              ☞   ...你是否注意到我故意在前面的代码中添加的错误?

接下来显示的代码的清洁版本不会使这样的错误"更容易发现;" 相反,作为一个类,它完全排除了它们,因为现在根本不需要数组索引变量.ix以下不需要变量,因为1234只使用一次.因此,我之前如此狡猾地介绍的错误无法传播到这个例子,因为它没有表达方式,好处是不存在的东西不能引入错误(而不是' 存在的东西.. .',肯定可能是一个bug)

ref Node rec = ref arr[1234];
rec.a++;
rec.b ^= rec.c;
rec.d /= (rec.e + rec.f);
Run Code Online (Sandbox Code Playgroud)

没有人会不同意这是一个改进.理想情况下,我们希望使用托管指针直接在原位读取和写入结构中的字段.实现此目的的一种方法是将所有密集处理代码编写为实例成员函数和属性ValueType本身,但由于某种原因,似乎很多人不喜欢这种方法.无论如何,关于C#7 反对本地人的观点现在没有意义......

                                                    ✹✹✹

我现在意识到,完全解释这里所需的编程类型可能过于涉及玩具示例,因此超出了StackOverflow文章的范围.所以我要跳过去,为了结束,我将放入一些工作代码的部分,我已经展示了模拟的托管指针重新分配.这是从参考源[直接链接]中经过大量修改的快照HashSet<T>中获取的,我只是在没有太多解释的情况下显示我的版本:.NET 4.7.1

int v1 = m_freeList;

for (int w = 0; v1 != -1; w++)
{
    ref int v2 = ref (w == 0 ? ref m_freeList : ref m_slots[v1].next);

    ref Slot fs = ref m_slots[v2];

    if (v2 >= i)
    {
        v2 = fs.next;
        fs = default(Slot);
        v1 = v2;
    }
    else
        v1 = fs.next;
}
Run Code Online (Sandbox Code Playgroud)

这只是来自工作代码的任意样本片段,所以我不希望任何人遵循它,但它的要点是'ref'变量,指定v1v2跨范围块交织,并且三元运算符用于协调它们如何流下来.例如,循环变量的唯一目的w是处理在链表遍历开始时针对特殊情况激活哪个变量(前面已讨论过).

再次,它证明了对现代C#的正常简易性和流动性的一种非常奇怪和折磨的限制.耐心,决心和 - 如前所述 - 需要进行大量的计划.



[1.]
如果你不熟悉所谓的.NET内存模型,我强烈建议你看看.我相信.NET在这个领域的实力是其最引人注目的特征之一,一个隐藏的宝石和一个(不那么)秘密的超级大国,它最让我们这些曾经坚持20世纪80年代时代精神的那些非常尖锐的朋友感到尴尬.裸机编码.请注意一个史诗般的讽刺:对编译器优化的狂野或无限侵略施加严格限制可能最终使应用程序具有更好的性能,因为更强的约束会为开发人员提供可靠的保证.这些反过来意味着更强的编程抽象或建议高级设计范例,在这种情况下与并发系统相关.

例如,如果有人认为,在原生社区中,无锁编程已经在边缘徘徊了几十年,那么优化编译器的不规则暴徒可能应该受到指责吗?如果没有严格且定义明确的内存模型提供的可靠的确定性和一致性,这个专业领域的进展很容易破坏,如上所述,这与不受约束的编译器优化有些不一致.因此,限制意味着该领域最终可以创新和发展.这是我在.NET方面的经验,无锁编程已经成为一种可行的,现实的,最终是平凡的日常编程工具.