【问题标题】:Avoiding copies of large struct parameter when composing iterator methods编写迭代器方法时避免复制大型结构参数
【发布时间】:2021-06-14 22:11:28
【问题描述】:

我有一个从分析中知道的大型结构,复制起来很昂贵。我正在使用 in 关键字传递这个结构的实例,效果很好。

现在我想将此作为参数传递给迭代器方法,该方法本身将值传递给其他迭代器方法 - 但in 是不允许的,这意味着每次传递给方法时都会复制该值。

这里的上下文是一个包含视频游戏保存状态的结构,迭代器方法是一个“加载”方法,它在 Unity 游戏引擎中将保存数据的处理分散到多个帧(它使用迭代器实现协程) .加载方法比较复杂,需要分解成几种方法。

例子:

struct SaveData{
    // large data
}

// Async loading - can spread processing across frames (yay!) but copies lots of data (boo!)

IEnumerator LoadAsync(SaveData saveData) {// wish I could use 'in' here!
    // use some part of saveData
    yield return;
    // use more of saveData
    yield return InnerLoad(saveData); // wish I could use 'in' here!
}

IEnumerator InnerLoadAsync(SaveData saveData) {// wish I could use 'in' here!
    // use saveData
    yield return;
}


// Synchronous loading - very efficient (yay!) but blocks, causing an unacceptably long delay (boo!)

void LoadSynchronous(in SaveData saveData){
    // use some part of saveData
    // use more of saveData
    InnerLoadSynchronous(in saveData);
}

void InnerLoadSynchronous(in SaveData saveData){
    // use saveData
}

我理解为什么一般情况下 in 不允许用于迭代器(例如,如果迭代器/协程比值的所有者寿命长怎么办?) - 所以我明白为什么最外层的迭代器函数需要副本。但是对于内部调用,由于它们是用yield return 调用的,所以内部迭代器不会比内部更持久,所以似乎应该有某种方法可以使用in

这里有没有我遗漏的语言功能,或者我可以使用一个很好的模式来解决它?我认为用外部类包装类型会起作用,但它看起来有点乱,当然仍然需要一份副本,因为我不能有 refin 成员。

【问题讨论】:

  • 如果它这么大,并且你到处都通过引用传递它,你基本上把它当作存储在托管堆以外的某个地方的引用类型。为什么不能转换为引用类型?这一切都在尖叫“值类型是错误的选择。”
  • 如果您需要它是一个值类型,您必须将您的Load 方法实现为适当的状态机,而不是依赖编译器为您生成状态机。然后,您可以在每次调用时将 in SaveData 传递给状态机的 MoveNext 方法。
  • @madreflection - “存储在托管堆以外的地方”是一个非常有用的说法!你是对的,因为它太大了,所以把它放在堆栈上是很疯狂的。所以很明显,无论如何我应该将它包装在一个引用类型中,我可以在没有'in'的情况下传递它。这并不像在任何地方都使用引用类型那么简单——外部 SaveData 结构将包含许多部分,如果它们都是引用类型,这将导致堆碎片(这是一个真正观察到的问题,因此我为什么这么喜欢结构!)。我的目标是使用少量的大内存区域。
  • @canton7 谢谢!我根本没有考虑过这一点——我会检查 Unity 是否支持接受 MoveNext 参数的协程,或者我是否可以自己调用 IEnumerator 方法。我感觉是 Unity 调用了MoveNext,但可能有办法绕过它。
  • Unity 确实调用了MoveNext,并且没有办法让 C# 编译器生成一个迭代器方法,该方法具有一个接受任何参数的MoveNext 方法。我认为没有必要将SaveData 放在包装类中:为什么不把它变成一个类呢?没有人建议它的所有字段也都变成类。

标签: c# iterator


【解决方案1】:

但是对于内部调用,由于它们是通过 yield return 调用的,因此内部迭代器不会比内部迭代器更持久,所以似乎应该有一些方法可以使用。

你错过了一些东西。举个简单的例子:

public class C
{
    public static void Main()
    {
        var enumerator = Outer(3);

        Console.WriteLine("Enumerating 1");
        enumerator.MoveNext();

        Console.WriteLine("Enumerating 2");
        enumerator.MoveNext();

        var innerEnumerator = (IEnumerator)enumerator.Current;

        Console.WriteLine("Enumerating Inner 1");
        innerEnumerator.MoveNext();
    }
    
    public static IEnumerator Outer(int i)
    {
        yield return null;
        Console.WriteLine("Yielding Inner");
        yield return Inner(i);
    }
    
    public static IEnumerator Inner(int i)
    {
        Console.WriteLine($"Inner {i}");
        yield break;   
    }
}

打印出来:

Enumerating 1
Enumerating 2
Yielding Inner
Enumerating Inner 1
Inner 3

(SharpLab).

如您所见,Inner 并未立即枚举。编译器生成的Inner 实现将编译器生成的IEnumerable 返回给Outer 的调用者,直到调用者显式调用MoveNextInner 的主体才被执行。

但是,Inner 被调用得更早。编译器生成的Inner 的实现完全执行,并在上面的Yielding Inner 之后返回生成的IEnumerator。所以Inner 需要将变量i 存储在编译器生成的类中的某个位置,这就是为什么它不能是in

【讨论】:

  • 这很有趣,谢谢!不知何故,我多年来一直在使用 IEnumerator,但没有考虑到第一次调用实际上并没有执行任何代码!我刚刚检查了 Unity 做了什么,结果发现它在启动协程时总是调用一次 MoveNext,这可能是我不了解迭代器的原因。无论如何 - 鉴于您的回答,似乎在语言中这是不可能的,我将不得不通过包装我的类型来解决它。
  • 这是一个众所周知的陷阱,意味着如果你抛出例如ArgumentExceptions 来自您的迭代器方法,除非有人真正尝试枚举它,否则它们不会被抛出,这很糟糕。这就是为什么大多数IEnumerable-returning 方法会进行参数检查,然后进行return Inner(args) 其中Inner 是另一种使用yield return 的方法,以确保在调用out 方法时运行参数检查,而不是当 IEnumerable 第一次被枚举时
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
  • 2021-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多