CS8176: Iterators cannot have by-reference locals
Asked Answered
R

2

5

Is there real reason for this error in given code, or just It could go wrong in general usage where the reference would be needed across interator steps (which is not true in this case)?

IEnumerable<string> EnumerateStatic()
{
    foreach (int i in dict.Values)
    {
        ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
        int next = p.next;
        yield return p.name;
        while (next >= 0)
        {
            p = ref props[next];
            next = p.next;
            yield return p.name;
        }
    }
}

struct Prop
{
    public string name;
    public int next;
    // some more fields like Func<...> read, Action<..> write, int kind
}
Prop[] props;
Dictionary<string, int> dict;

dict is name-index map, case-insensitive
Prop.next is to point-to next node to be iterated over (-1 as terminator; because dict is case-insensitive and this linked-list was added to resolve conflicts by case-sensitive search with fallback to first).

I see two options now:

  1. Implement custom iterator/enumerator, mscs/Roslyn is just not good enough now to see well and do its job. (No blame here, I can understand, not so important feature.)
  2. Drop the optimisation and just index it twice (once for name and second time for next). Maybe the compiler will get it and produce optimal machine code anyway. (I am creating scripting engine for Unity, this really is performance critical. Maybe it just checks the bounds once and uses ref/pointer-like access with no cost next time.)

And maybe 3. (2b, 2+1/2) Just copy the struct (32B on x64, three object references and two integers, but may grow, cannot see future). Probably not good solution (I either care and write the iterator or it is as good as 2.)

What I do understand:

The ref var p cannot live after yield return, because the compiler is constructing the iterator - a state machine, the ref cannot be passed to next IEnumerator.MoveNext(). But that is not the case here.

What I do not understand:

Why is such a rule enforced, instead of trying to actually generate the iterator/enumerator to see if such ref var needs to cross the boundary (which it does not need here). Or any other way to do the job which looks doable (I do understand that what I imagine is harder to implement and expect answer to be: Roslyn folks have better things to do. Again, no offense, perfectly valid answer.)

Expected answers:

  1. Yes, maybe in the future / not worth it (create an Issue - will do if you find it worth it).
  2. There is better way (please share, I need solution).

If you want/need more context, it is for this project: https://github.com/evandisoft/RedOnion/tree/master/RedOnion.ROS/Descriptors/Reflect (Reflected.cs and Members.cs)

The reproducible example:

using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        class Test
        {
            struct Prop
            {
                public string name;
                public int next;
            }
            Prop[] props;
            Dictionary<string, int> dict;
            public IEnumerable<string> Enumerate()
            {
                foreach (int i in dict.Values)
                {
                    ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
                    int next = p.next;
                    yield return p.name;
                    while (next >= 0)
                    {
                        p = ref props[next];
                        next = p.next;
                        yield return p.name;
                    }
                }
            }
        }
        static void Main(string[] args)
        {
        }
    }
}
Regurgitate answered 3/9, 2020 at 17:33 Comment(0)
S
7

The compiler wants to rewrite iterator blocks with locals as fields, to retain the state, and you cannot have ref-types as fields. Yes, you're right that it doesn't cross the yield so technically it could probably be rewritten to re-declare it as-needed, but that makes for very complex rules for humans to remember, where simple-looking changes break the code. A blanket "no" is much easier to grok.

The workaround in this scenario (or similarly with async methods) is usually a helper method; for example:

    IEnumerable<string> EnumerateStatic()
    {
        (string value, int next) GetNext(int index)
        {
            ref var p = ref props[index];
            return (p.name, p.next);
        }
        foreach (int i in dict.Values)
        {
            (var name, var next) = GetNext(i);
            yield return name;
            while (next >= 0)
            {
                (name, next) = GetNext(next);
                yield return name;
            }
        }
    }

or

    IEnumerable<string> EnumerateStatic()
    {
        string GetNext(ref int next)
        {
            ref var p = ref props[next];
            next = p.next;
            return p.name;
        }
        foreach (int i in dict.Values)
        {
            var next = i;
            yield return GetNext(ref next);
            while (next >= 0)
            {
                yield return GetNext(ref next);
            }
        }
    }

The local function is not bound by the iterator-block rules, so you can use ref-locals.

Sotelo answered 3/9, 2020 at 18:35 Comment(3)
@Regurgitate that's up to you; I expect it would be closed as "by design", and: I'd support that closureSotelo
I expected it to be corner-case not worth pursuing now, but support that closure? Ok, I expected backlog. (P.S.: I accept, won't even try)Regurgitate
@Regurgitate because the number of cases where the compiler can prove it is limited, and it is complex for humansSotelo
N
6

the ref cannot be passed to next IEnumerator.MoveNext(). But that is not the case here.

The compiler creates a state machine class to hold the data needed at runtime to continue to the next iteration. It's that class that can't contain a ref member.

The compiler could detect that the variable is only needed in a limited scope, and not needed to be added to that state class, but as Marc says in their answer, that's an expensive feature for little added benefit. Remember, features start at -100 points. So you could ask for it, but make sure to explain its use.

For what it's worth, Marc's version is ~4% faster (according to BenchmarkDotNet) for this setup:

public class StructArrayAccessBenchmark
{
    struct Prop
    {
        public string name;
        public int next;
    }

    private readonly Prop[] _props = 
    {
        new Prop { name = "1-1", next = 1 }, // 0
        new Prop { name = "1-2", next = -1 }, // 1

        new Prop { name = "2-1", next = 3 }, // 2
        new Prop { name = "2-2", next = 4 }, // 3
        new Prop { name = "2-2", next = -1 }, // 4
    };

    readonly Dictionary<string, int> _dict = new Dictionary<string, int>
    {
        { "1", 0 },
        { "2", 2 },
    };

    private readonly Consumer _consumer = new Consumer();

    // 95ns
    [Benchmark]
    public void EnumerateRefLocalFunction() => enumerateRefLocalFunction().Consume(_consumer);

    // 98ns
    [Benchmark]
    public void Enumerate() => enumerate().Consume(_consumer);

    public IEnumerable<string> enumerateRefLocalFunction()
    {
        (string value, int next) GetNext(int index)
        {
            ref var p = ref _props[index];
            return (p.name, p.next);
        }

        foreach (int i in _dict.Values)
        {
            var (name, next) = GetNext(i);
            yield return name;

            while (next >= 0)
            {
                (name, next) = GetNext(next);
                yield return name;
            }
        }
    }

    public IEnumerable<string> enumerate()
    {
        foreach (int i in _dict.Values)
        {
            var p = _props[i];
            int next = p.next;
            yield return p.name;
            while (next >= 0)
            {
                p = _props[next];
                next = p.next; 
                yield return p.name;
            }
        }
    }

Results:

|                    Method |      Mean |    Error |   StdDev |
|-------------------------- |----------:|---------:|---------:|
| EnumerateRefLocalFunction |  94.83 ns | 0.138 ns | 0.122 ns |
|                 Enumerate |  98.00 ns | 0.285 ns | 0.238 ns |
Neron answered 3/9, 2020 at 19:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.