是否可以重新分配当地的裁判? [英] Is it possible to reassign a ref local?

查看:41
本文介绍了是否可以重新分配当地的裁判?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

阅读C#文档

  ref对象引用= ref some_var;ref参考= ref other_var; 

和类似的构造都无济于事.

我什至尝试编写一个小的结构,在IL中包装一个托管指针,就C#而言,它可以工作,但是CLR似乎不喜欢在结构中具有托管指针,即使在我的结构中也是如此.用法,它永远不会进入堆.

是否真的需要借助IL或递归技巧来克服这一问题?(我正在实现一个数据结构,该结构需要跟踪遵循了哪个指针,这是对托管指针的完美使用.)

" ref-reassign "是偶然发现可行的答案.

基本上,在 C#7.2 中,您现在可以初始化 ref local 分配.

这种方法需要大量非常规的思维和大量的计划.对于某些情况或编码方案,可能无法预料运行时配置的范围,以至于任何条件分配方案都可能适用.在这种情况下,您不走运.或者,切换到 C ++/CLI ,它公开了链接.

通过三元运算符进行C#7.2 ref-local条件分配 ?:


  ref int i_node = ref(f?ref m_head:ref node.next); 

此行来自提问者在此处提出的 ref local 困境的一个典型问题案例.它来自代码,它们在行走单链列表时会保持指向后方的指针.在 C/C ++ 中,这项任务应该是微不足道的(也许由于特定的原因而受到CSE101讲师的喜爱),但使用托管指针C#却十分麻烦.

由于微软自己的 C ++/CLI 语言向我们展示了 .NET 领域中多么出色的托管指针,所以这种抱怨也是完全合法的.取而代之的是,大多数C#开发人员似乎最终只是在数组中使用整数索引,或者当然是使用 unsafe C#的完整本机指针.

关于链表漫游示例的一些简短评论,以及为什么有人会对这些托管指针产生如此大的麻烦感兴趣.我们假设所有节点实际上都是数组( ValueType in-situ )中的结构,例如 m_nodes = new Node [100]; 并且每个 next 指针都是一个整数(其在数组中的索引).

  struct节点{公开诠释,下一个;公共字符数据;公共重写String ToString()=>String.Format("{0} next:{1,2} data:{2}",ix,next,data);}; 

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

 静态(整数头,Node []节点)L =(3,新的[]{新节点{ix = 0,next = -1,data ='E'},新节点{ix = 1,next = 4,data ='B'},新节点{ix = 2,next = 0,data ='D'},新节点{ix = 3,下一个= 1,data ='A'},新节点{ix = 4,next = 2,data ='C'},}); 

此外,大概每个节点上都要做大量的处理工作,但是您真的不想付出对每个(可能是大的) ValueType 进行成像的(双重)性能成本.毕竟,我们在这里使用值类型的原因肯定是为了最大化性能.正如我在本网站上其他地方详细讨论的那样,在 .NET 中,结构可以非常高效,但前提是您 绝不要意外地将它们提"出他们的存储空间 .这很容易做到,并且可以立即破坏您的内存总线带宽.

不抬高结构的简单方法只是重复数组索引,如下所示:

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

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

发布模式下的JIT优化可以通过识别并整合所提供代码中的冗余来缓解某些问题,甚至可以极大地减轻这些问题,但可能不如您所想或希望的那样(或最终意识到您不需要):严格遵守得到广泛认可的问题具有重复的源代码,例如前面的示例.简而言之,它很丑陋,容易出错,并且难以阅读和维护.为了说明这一点,
      ☞  ...您甚至没有注意到我故意在前面的代码中放的错误?

下面显示的代码的较干净版本不会产生像容易发现"这样的错误;相反,作为一个类,它完全排除了它们,因为现在完全不需要数组索引变量了.变量 ix 在下面不需要存在,因为 1234 仅使用一次.由此得出结论,我之前如此引入的错误无法传播到此示例,因为它没有表达方式,其好处在于,不存在的错误不会引入错误(与什么不会存在...",这肯定是一个错误)

  ref节点rec = ref arr [1234];rec.a ++;rec.b ^ = rec.c;rec.d/=(rec.e + rec.f); 

没有人会不同意这是一种进步.因此,理想情况下,我们希望使用托管指针直接在结构 中读写字段.一种方法是将所有密集处理代码编写为 ValueType 本身的实例成员函数和属性,尽管出于某些原因,似乎很多人不喜欢这种方法.无论如何,现在C#7 ref locals ...

                          n         n         ✹

我现在意识到,完全解释此处所需的编程类型可能涉及太多,无法用一个玩具示例来展示,因此超出了StackOverflow文章的范围.因此,我将继续前进,以总结一下,我将介绍一些我已经显示过的 模拟托管指针重新分配 的工作代码.这取自 v1 和 v2 ,在作用域块之间交织在一起,三元运算符用于协调它们如何向下流动.例如,循环变量 w 的唯一用途是处理在特殊情况下链表遍历开始时激活了哪个变量(前面已经讨论过).

同样,对于现代 C#的正常易用性和流畅性,事实证明这是一个非常奇怪而又受折磨的约束.耐心,决心和-正如我前面提到的-需要很多提前计划.



[ 1.]
如果您不熟悉所谓的
.NET内存模型 ,我强烈建议您看一下.我相信.NET在这一领域的实力是其最引人注目的功能之一,一个隐藏的宝石以及一个(并非如此)秘密的超级大国,这些超级大国极大地使我们这些仍坚持1980年代精神的无所不知的朋友感到尴尬裸机编码.请注意一个具有讽刺意味的讽刺意味:对编译器优化的狂野或无穷无尽的攻击施加严格限制可能最终使应用程序具有更好的性能,因为更强的约束条件为开发人员提供了可靠的保证.这些反过来暗示了更强的编程抽象性或建议了高级设计范例,在这种情况下,这些范例与并发系统相关.

例如,如果有人同意,在本机社区中,无锁编程已陷入困境.几十年来,也许应该归咎于优化编译器的无节制暴徒?没有严格和定义明确的内存模型所提供的可靠确定性和一致性,很容易破坏该专业领域的进展,如上所述,这与不受约束的编译器优化有些矛盾.因此,这里的限制意味着该领域最终可以创新和发展.这是我在.NET方面的经验,在这种情况下,无锁编程已成为一种可行的,现实的,甚至最终成为日常工作的基本日常编程工具.

C#'s ref locals are implemented using a CLR feature called managed pointers, that come with their own set of restrictions, but luckily being immutable is not one of them. I.e. in ILAsm if you have a local variable of managed pointer type, it's entirely possible to change this pointer, making it "reference" another location. (C++/CLI also exposes this feature as interior pointers.)

Reading the C# documentation on ref locals it appears to me that C#'s ref locals are, even though based on the managed pointers of CLR, not relocatable; if they are initialized to point to some variable, they cannot be made to point to something else. I've tried using

ref object reference = ref some_var;
ref reference = ref other_var;

and similar constructs, to no avail.

I've even tried to write a small struct wrapping a managed pointer in IL, it works as far as C# is concerned, but the CLR doesn't seem to like having a managed pointer in a struct, even if in my usage it doesn't ever go to the heap.

Does one really have to resort to using IL or tricks with recursion to overcome this? (I'm implementing a data structure that needs to keep track of which of its pointers were followed, a perfect use of managed pointers.)

解决方案

[edit:] "ref-reassign" is on the schedule for C# 7.3. The 'conditional-ref' workaround, which I discuss below, was deployed in C# 7.2.


I've also long been frustrated by this and just recently stumbled on a workable answer.

Essentially, in C# 7.2 you can now initialize ref locals with a ternary operator, and this can be con­triv­ed. somewhat torturously, into a simulation of ref-local reassignment. You "hand off" the ref local assignments downwards through multiple variables, as you move down in the lexical scope of your C# code.

This approach requires a great deal of unconventional thinking and a lot of planning ahead. For certain situations or coding scenarios, it may not be possible to anticipate the gamut of runtime con­figurations such that any conditional assignment scheme might apply. In this case you're out of luck. Or, switch to C++/CLI, which exposes managed tracking references. The tension here is that, for C#, the vast and indisputable gains in concision, elegance, and efficiency which are immediately realized by introducing the conventional use of managed pointers (these points are discussed fur­ther below) is frittered away with the degree of contortion required to overcome the reassignment problem.

The syntax that had eluded me for so long is shown next. Or, check the link I cited at the top.

C# 7.2 ref-local conditional assignment via ternary oerator ? :


ref int i_node = ref (f ? ref m_head : ref node.next);

This line is from a canonical problem case for the ref local dilemma that the questioner posed here. It's from code which maintains back-pointers while walking a singly-linked list. The task is trivial in C/C++, as it should be (and is quite beloved by CSE101 instructors, perhaps for that par­ticular reason)—but is entirely agonizing using managed pointers C#.

Such a complaint is entirely legitimate too, thanks to Microsoft's own C++/CLI language showing us how awesome managed pointers can be in the .NET universe. Instead, most C# developers seem to just end up using integer indices into arrays, or of course full blown native pointers with unsafe C#.

Some brief comments on the linked-list walking example, and why one would be interested in going to so much trouble over these managed pointers. We assume all of the nodes are actually structs in an array (ValueType, in-situ) such as m_nodes = new Node[100]; and each next pointer is thus an integer (its index in the array).

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

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

As shown here, the head of the list will be a standalone integer, stored apart from the records. In the next snippet, I use the new C#7 syntax for ValueTuple to do so. Obviously it's no problem to traverse forward using these integer links—but C# has traditionally lacked an elegant way to main­tain a link to the node you came from. It's a problem since one of the integers (the first one) is a special case owing to not being embedded in a Node structure.

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

Additionally, there's presumably a decent amount of processing work to do on each node, but you really don't want to pay the (double) performance costs of imaging each (possibly large) ValueType out of its cozy array home—and then having to image each one back when you're done! After all, surely the reason we're using value types here is to maximize performance. As I discuss at length elsewhere on this site, structs can be extremely efficient in .NET, but only if you never accident­ally "lift" them out of their storage. It's easy to do and it can immediately destroy your memory bus bandwidth.

The trival approach to not-lifting the structs just repeats array indexing like so:

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

Here, each ValueType field access is independently dereferenced on every access. Although this "optimization" does avoid the bandwidth penalties mentioned above, repeating the same array indexing operation over and over again can instead implicate an entirely different set of runtime penalties. The (opportunity) costs now are due to unnecessarily wasted cycles where .NET re­computes provably invariant physical offsets or performs redundant bounds checks on the array.

JIT optimizations in release-mode may mitigate these issues somewhat—or even dramatically—by recognizing and consolidating redundancy in the code you supplied, but maybe not as much as you'd think or hope (or eventually realize you don't want): JIT optimizations are strongly constrained by strict adherence to the .NET Memory Model.[1], which requires that whenever a storage location is publicly visible, the CPU must execute the relevant fetch sequence exactly as authored in the code. For the previous example, this means that if ix is shared with other threads in any way prior to the operations on arr, then the JIT must ensure that the CPU actually touches the ix storage location exactly 6 times, no more, no less.

Of course the JIT can do nothing to address the other obvious and widely-acknowledged problem with repetitive source code such as the previous example. In short, it's ugly, bug-prone, and harder to read and maintain. To illustrate this point,
              ☞   ...did you even notice the bug I intentionally put in the preceding code?

The cleaner version of the code shown next doesn't make bugs like this "easier to spot;" in­stead, as a class, it precludes them en­tirely, since there's now no need for an array-in­dexing variable at all. Variable ix doesn't need exist in the following, since 1234 is used only once. It follows that the bug I so deviously introduced earlier cannot be propagated to this example because it has no means of expression, the benefit being that what can't exist can't introduce a bug (as opposed to 'what does not exist...', which most certainly could be a bug)

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

Nobody would disagree that this is an improvement. So ideally we want to use managed pointers to directly read and write fields in the structure in situ. One way to do this is to write all of your in­ten­sive processing code as instance member functions and properties in the ValueType itself, though for some reason it seems that many people don't like this approach. In any case, the point is now moot with C#7 ref locals...

                                                    ✹                   ✹                   ✹

I'm now realizing that fully explaining the type of programming required here is probably too in­volved to show with a toy example and thus beyond the scope of a StackOverflow article. So I'm going to jump ahead and in order to wrap up I'll drop in a section of some working code I have showing simulated managed pointer reassignment. This is taken from a heavily modified snap­shot of HashSet<T> in the .NET 4.7.1 reference source[direct link], and I'll just show my version without much explanation:

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

This is just an arbitrary sample fragment from the working code so I don't expect anyone to follow it, but the gist of it is that the 'ref' variables, designated v1 and v2, are intertwined across scope blocks and the ternary operator is used to coordinate how they flow down. For example, the only purpose of the loop variable w is to handle which variable gets activated for the special case at the start of the linked-list traversal (discussed earlier).

Again, it turns out to be a very bizarre and tortured constraint on the normal ease and fluidity of modern C#. Patience, determination, and—as I mentioned earlier—a lot of planning ahead is required.



[1.]
If you're not familiar with what's called the .NET Memory Model, I strongly suggest taking a look. I believe .NET's strength in this area is one of its most compelling features, a hidden gem and the one (not-so-)secret superpower that most fatefully em­barrasses those ever-strident friends of ours who yet adhere to the 1980's-era ethos of bare-metal coding. Note an epic irony: imposing strict limits on wild or unbounded aggression of compiler optimization may end up enabling apps with much better performance, because stronger constraints expose re­liable guarantees to developers. These, in turn imply stronger programming abstractions or suggest advanced design paradigms, in this case relevant to concurrent systems.

For example, if one agrees that, in the native community, lock-free programming has languished in the margins for decades, perhaps the unruly mob of optimizing compilers is to blame? Progress in this specialty area is easily wrecked without the reliable determinism and consistency provided by a rigorous and well-defined memory model, which, as noted, is somewhat at odds with unfettered compiler optimization. So here, constraints mean that the field can at last innovate and grow. This has been my experience in .NET, where lock-free programming has become a viable, realistic—and eventually, mundane—basic daily programming vehicle.

这篇关于是否可以重新分配当地的裁判?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆