Is it possible to reassign a local link?

C # fallback locators are implemented using the CLR function, called managed pointers, which come with their own set of constraints, but, fortunately, immutability is not one of them. That is, in ILAsm, if you have a local variable of the type of a managed pointer, it is quite possible to change this pointer by making it a "link" to another place. (C ++ / CLI also provides this function as internal pointers .)

Reading the C # documentation on local ref sites it seems to me that C # link locators, although based on managed CLR pointers, are not roaming; if they are initialized to point to some variable, they cannot be forced to point to something else. I tried using

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

and similar designs, to no avail.

I even tried to write a small transaction related to a managed pointer in IL, it works before C #, but the CLR does not seem to like having a managed pointer in the structure, even if it never goes to heap in my use.

Do I need to resort to using IL or recursion tricks to overcome this? (I am introducing a data structure that needs to track which of the pointers have been followed, the ideal use of managed pointers.)

+7
pointers c #
source share
1 answer

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


I, too, have long been disappointed with this, and most recently came across a workable answer .

Essentially, in C # 7.2, you can now initialize ref locals using the ternary operator , and you can think of that. somewhat painful in simulating ref-local reassignment . You “pass” ref local assignments down through several variables as you move down in the lexical area of ​​your C # code.

This approach requires a lot of unconventional thinking and a lot of planning ahead. In some situations or coding scenarios, it may not be possible to predict the gamut of runtime configurations so that any conditional assignment scheme can be applied. In this case, you're out of luck. Or go to C ++ / CLI , which provides managed tracking links . The tension here is that for C #, there is a huge and undeniable gain in shortness, elegance and efficiency, which are immediately realized by introducing the usual use of managed pointers (these points are discussed below), with the degree of distortion needed to overcome the reassignment problem.

The syntax that has eluded me for so long is shown below. Or check out the link I quoted above.

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


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

This line is taken from the canonical problem case for the ref local dilemma posed by the questioner. This is code that supports reverse pointers when walking a singly linked list. The task is trivial in C / C ++ , as it should be (and is very loved by CSE101 instructors, perhaps for this reason), but it is completely painful using C # managed pointers.

Such a complaint is completely legitimate, thanks to Microsoft's own C ++ / CLI language, showing how amazing managed pointers can be in a .NET universe. Instead, most C # developers seem to just use integer indexes in arrays or, of course, full-sized native pointers with unsafe C #.

Some brief comments, for example, related to the list of links, and why you would be interested in worrying so much about these managed pointers. We assume that all nodes are actually structures in the array ( ValueType , in-situ), for example m_nodes = new Node[100]; , and each next pointer is 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 separate whole, stored separately from the records. In the following snippet, I use the new C # 7 syntax for ValueTuple . Obviously there is no problem moving forward using these whole links, but C # has traditionally not had the elegant way to maintain the link to the node from which you came from. This is a problem because one of the integers (the first) is a special case due to the fact that it is not embedded in the 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' }, }); 

In addition, apparently, each node seems to require decent processing work, but you really do not want to pay the (double) cost of executing the image (possibly the large) ValueType from its cozy array at home, and then after each shot need to take a picture. In the end, of course, the reason we use value types is to maximize performance. As I discuss in detail elsewhere on this site , structures can be extremely efficient in .NET , but only if you never pull them from your storage . This is easy to do, and it can immediately destroy your memory bus bandwidth.

The triangular approach to not raising structures simply repeats indexing the array as follows:

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

Here, each access to the ValueType field ValueType automatically dereferenced with each access. Although this “optimization” avoids the bandwidth penalty mentioned above, repeating the same array indexing operation over and over can lead to a completely different set of execution penalties. Costs (opportunities) are now associated with unreasonably spent cycles when .NET resells the supposedly invariant physical offsets or performs redundant checks of the array boundaries.

Optimizing JIT in release mode can mitigate these problems somewhat, or even significantly, by recognizing and consolidating redundancy in the code you provided, but maybe not as much as you think or hope (or, ultimately, realize that you don't want to): JIT optimization is severely limited by strict adherence to the .NET Memory Model . & lsqb; 1] which requires that whenever the storage location is publicly available, the CPU must execute the appropriate sampling sequence exactly as it was written in the code. In the previous example, this means that if ix shared by other threads before arr operations, then the JIT must make sure that the CPU actually touches the ix storage location exactly 6 times, no more, no less.

Of course, JIT cannot do anything to solve other obvious and widely recognized problems with repetitive source code, such as the previous example. In short, it is ugly, error prone, and harder to read and maintain. To illustrate this point, ☞ ... did you even notice an error that I intentionally posted in the previous code?

A cleaner version of the code shown below does not make such errors “easier”; instead, as a class, it completely eliminates them, since now there is no need for an array index variable. The ix variable is not needed in the following, since 1234 used only once. It follows that the error that I so insidiously introduced earlier cannot be extended to this example because it has no means of expression, the advantage is that what cannot exist cannot introduce an error (unlike 'that which does not exist' .. ', which, of course, may be a mistake)

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

No one will agree that this is an improvement. Therefore, ideally, we want to use managed pointers to directly read and write fields in an in situ structure. One way to do this is to write all your intense processing code as functions and properties of an instance member in ValueType itself, although for some reason it seems like many people don't like this approach. In any case, the point is now controversial with C # 7 ref locals ...

✹ ✹ ✹

Now I understand that a full explanation of the type of programming required here is probably too difficult to show with the example of toys and, therefore, is beyond the scope of the StackOverflow article. Therefore, I am going to move forward, and in order to wrap up, I will go to the section of some working code that shows a simulated remapping of a managed pointer . This is taken from a heavily modified HashSet<T> snapshot in the .NET 4.7.1 reference source [direct link] and I will 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 fragment of the sample from the working code, so I do not expect anyone to follow it, but its essence is that the 'ref' variables, denoted by v1 and v2 , are intertwined through the blocks of regions, and the ternary operator is used to coordinate their flow. For example, the sole purpose of a loop variable w is to process which variable is activated for a special case at the beginning of a traversal of a linked list (discussed earlier).

Again, this proves to be a very strange and painful limitation on the normal lightness and fluidity of modern C # . Patience, determination and, as I mentioned earlier, it takes a lot of planning ahead .



& lsqb; one.]
If you are not familiar with what is called the .NET Memory Model , I highly recommend taking a look. I believe that the power of .NET in this area is one of its most compelling features, a hidden gem and one (not so) secret superpower, which in the most fateful way confuses those who have ever been restrained friends who still adhere to 1980s era of uncoded coding. Pay attention to the epic irony: the introduction of strict restrictions on the wild or unlimited aggression of optimizing the compiler can lead to the fact that applications will have much better performance, because stronger restrictions provide reliable guarantees to developers. They, in turn, imply stronger programming abstractions or offer advanced design paradigms, in this case related to parallel systems.

For example, if you agree that blockchain programming has been languishing in the fields for decades in the native community, is it possible that the accused mob of optimizing compilers is to blame? Progress in this area of ​​specialty is easily destroyed without reliable determinism and consistency, provided by a strict and well-defined memory model, which, as already noted, is somewhat at variance with unlimited compiler optimization. Thus, restrictions mean that the field can finally be updated and grow. This was my experience in .NET, where non-blocking programming became viable, realistic, and ultimately normal everyday software.

+3
source share

All Articles