【问题标题】:How can I do operations on Span<T> in parallel?如何并行对 Span<T> 进行操作?
【发布时间】:2021-06-19 03:20:02
【问题描述】:

我想同时对Span&lt;T&gt; 进行操作,但这样的事情是不合法的:

void DoSomething(Span<int> buffer, int option1, int option2)
{
   ....... 
}

void ParallelDoSomething(Span<int> buffer)
{
    var size = buffer.Length;
    Parallel.Invoke(() => DoSomething(buffer, 0, size / 2),
        () => DoSomething(buffer, size/2, size)); //not legal
}

因为编译器抱怨:不能在匿名方法、lambda 表达式、查询表达式或本地函数中使用 ref、out 或 in 参数“缓冲区”

如何在以Span&lt;T&gt; 作为参数的并行方法中执行?

【问题讨论】:

  • 作为旁注:我想知道 option1/option2 是否应该从 DoSomething() 中删除;这看起来像是应该通过切片应用的东西;附带说明一下,这样做可以提高性能:for(int i = 0 ; i &lt; span.Length; i++)foreach(var val in span) 具有 JIT 优化(边界检查省略),但 for (int i = option1; i &lt; option2; i++) 没有
  • 并行化ParallelDoSomething的调用者怎么样
  • @Mark Gravell,当然 option1/ option2 在这里无关紧要。我只是想快速更改 Mergesort 实现以使用堆栈分配 Spans 而不是数组,因为 MemoryExtensions.Sort 对我没有帮助。
  • @Charlieface,调用者没有什么可以并行化的。

标签: c# memory


【解决方案1】:

这里的问题是Span&lt;T&gt; 不能被允许进入堆 - 它在堆栈上有效 - 这意味着它不能被装箱,或用作class 上的字段(或structref struct 除外)。这排除了捕获的变量和最常见的state 参数形式。

如果您有幸将输入更改为内存,则可以捕获 内存,并在 lambda 主体中获取跨度内部

void ParallelDoSomething(Memory<int> memory)
{
    var size = memory.Length;
    Parallel.Invoke(
        () => DoSomething(memory.Span, 0, size / 2),
        () => DoSomething(memory.Span, size/2, size)
    );
}

如果你不能改变输入,你仍然可以这样做……通过作弊。您可以pin 现有跨度,创建一个覆盖该固定数据的内存,并使用 内存,就像它作为内存传入一样。这并不完全是微不足道的,因为您需要编写自己的基于指针的内存管理器实现,但是:它可以工作。这是来自 protobuf-net 的示例:https://github.com/protobuf-net/protobuf-net/blob/main/src/protobuf-net.Core/Meta/TypeModel.cs#L767-L789

或者更方便的是,固定 span 并直接捕获 指针,注意编译器通常不允许这样做(以防止指针稍后被委托使用),但是 既然我们知道时间语义,我们可以通过复制指针让它变得快乐:

unsafe void ParallelDoSomething(Span<int> span)
{
    var size = span.Length;
    fixed (int* ptr = span)
    {
        int* evil = ptr; // make the compiler happy
        Parallel.Invoke(
            () => DoSomething(new Span<int>(evil, size), 0, size / 2),
            () => DoSomething(new Span<int>(evil, size), size / 2, size)
        );
    }
}

或者如果我们想在输入点修复切片跨度:

unsafe void ParallelDoSomething(Span<int> span)
{
    var size = span.Length;
    fixed (int* ptr = span)
    {
        int* evil = ptr; // make the compiler happy
        Parallel.Invoke(
            () => DoSomething(new Span<int>(evil, size / 2));
            () => DoSomething(new Span<int>(evil + (size/2), size - (size/2));
        );
    }
}

【讨论】:

  • 为了风格和风格,您甚至可以声明一个本地的DoSomething 方法来处理跨越并隐藏evil(减少evil 在整个土地上传播的机会)。当然,如果您有很多 DoSomethings,则不太合适。 (或许多spans。)也许还值得注意的是,为了减少内存复制(这通常是引入Spans的原因)这段代码当然有很多隐藏的分配,所以对于OP,也许值得退后一步,看看我们真正想要完成的事情。
  • @JeroenMostert 是的,完全同意;我做了很多这种并行性(它驱动 Stack Overflow 的一些关键部分),但我认为我没有曾经使用过 Parallel 类型,正是因为它没有t 具有非常适合超低分配的 API 表面。我是 lambda 和本地函数上新的 static 修饰符的超级粉丝!
  • @Jeroen Mostert 是的,我正在尝试减少内存复制和分配,那么您建议使用什么来代替 Parallel?
  • 谢谢,我认为使用内存不是一个选项,因为缓冲区是堆栈分配的跨度。我害怕使用不安全的魔法将是唯一的方法。你确认了,谢谢。
  • @VladRadu:这可能是另一个问题的答案,很大程度上取决于您的实际情况。如果您处理的数据足够多以使并行性变得有趣,那么一个体面的问题是您是否真的需要Span(而不是传递Memory)。相反,如果数据不是那么多,而是用于为需要并行化的昂贵操作提供燃料,那么复制它可能不会产生很大的开销(毕竟,即使使用Task,启动线程也不便宜) .鉴于他的经验,Marc 可能也可以参与后续问题。
猜你喜欢
  • 1970-01-01
  • 2021-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-17
  • 2013-10-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多