如何使用 Span<T> 和 stackalloc 创建临时小列表
Posted
技术标签:
【中文标题】如何使用 Span<T> 和 stackalloc 创建临时小列表【英文标题】:How to Span<T> and stackalloc to create a temporary small list 【发布时间】:2020-10-12 21:03:49 【问题描述】:我正在阅读一些用 C 编写的代码的描述,这些代码由于在堆栈而不是堆上分配临时数组以用于非常热的循环而提高了速度。 (它被描述为类似于 SBO 优化)。有问题的对象类似于List<T>
,因为它只是一个顶部具有一些基本便利功能的数组。它分配一小部分内存供使用,如果列表扩展超过数组的大小,它会在堆上分配一个新数组,复制数据并更新指针。
我想在 C# 中做同样的事情,但我不确定如何完成它,因为我想将它保存在 safe
上下文中,所以我不能使用指针来更新数据引用,如果它已扩展,并且Span<int>
没有隐式转换为int[]
。具体来说:
stackalloc
内存在方法退出时被释放,所以我不确定是否有比给它一个 Span 字段并在使用它的方法内创建后分配它更简单的方法来使用这样的结构。
如何在不更改面向公众的界面的情况下巧妙地在使用不同类型(Span 和 int[])的支持字段之间切换?
【问题讨论】:
【参考方案1】:在 C 和 C++ 语言中,开发人员定义了将在哪个内存中实例化对象:堆栈或堆。 在 C# 中,它由数据类型的作者确定。 您可以使用 Span 和指针来实现您的目标。 https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netcore-3.1。 但我不建议您这样做,因为您的代码不安全。这意味着当您不再需要此类对象时,CLR 赋予您管理它的所有责任,至少清理内存。当 C# 开发人员想要优化真正的大数据集合时,通常会使用这样的技巧,这会在堆中分配大量内存。 如果它仍然是您正在寻找的 - 那么,C# 可能不是最好的选择。 更重要的是,如果你有一个大集合,并且你找到了如何将它放入堆栈内存的方法 - 你很容易遇到 ***Exception。
【讨论】:
对于所有与性能相关的任务来说,“C# 可能不是最佳选择”这种陈词滥调会不会死掉......?【参考方案2】:我设法想出了一个解决方案,不确定它是否是最好的实现,但它似乎有效。我也有几个选择。
注意:仅当您有一个需要创建临时数组并且被频繁调用的函数非常 时,这对提高速度很有用。切换到堆分配对象的能力只是一种备用,以防您超出缓冲区。
选项 1 - 使用 Span 和 stackalloc
如果你正在构建到 .NET Core 2.1 或更高版本,.NET Standard 2.1 或更高版本,或者可以使用 NuGet 来使用System.Memory package,解决方案真的很简单。
使用ref struct
代替类而不是类(这是有Span<T>
字段所必需的,并且两者都不能离开声明它们的方法。如果您需要一个长期存在的类,那么没有理由尝试在堆栈上分配,因为无论如何您都必须将其移动到堆中。)
public ref struct SmallList
private Span<int> data;
private int count;
//...
然后添加所有列表功能。 Add()
、Remove()
等。在 Add 或任何可能扩展列表的函数中,添加检查以确保不会超出范围。
if (count == data.Length)
int[] newArray = new int[data.Length * 2]; //double the capacity
Array.Copy(data.ToArray(), 0, new_array, 0, cap);
data = new_array; //Implicit cast! Easy peasy!
Span<T>
可以用于堆栈分配的内存,但它也可以指向堆分配的内存。因此,如果您不能保证您的列表总是足够小以适合堆栈,那么上面的 sn-p 为您提供了一个很好的回退,它不应该频繁发生而导致明显的问题。如果是,要么增加初始堆栈分配大小(在合理范围内,不要溢出!),或者使用其他解决方案,如数组池。
使用结构只需要一个额外的行和一个构造函数,该构造函数需要一个跨度来分配给data
字段。不确定是否有办法一次性完成所有操作,但这很容易:
Span<int> span = stackalloc int[32];
SmallList list = new SmallList(span);
如果您需要在嵌套函数中使用它(这是我的问题的一部分),您只需将其作为参数传递,而不是让嵌套函数返回一个列表。
void DoStuff(SmallList results) /* do stuff */
DoStuff(list);
//use results...
选项 2:数组池
System.Memory 包还包括ArrayPool
类,它允许您存储一个小数组池,您的类/结构可以取出这些数组,而不会打扰垃圾收集器。根据用例,这具有相当的速度。它还有一个好处,那就是它适用于必须超越单一方法的类。如果您不能使用System.Memory
,也可以自己编写。
选项 3:指针
你可以用指针和其他unsafe
代码做这样的事情,但问题是技术上询问safe
代码。我只是希望我的清单是完整的。
选项 4:没有 System.Memory
如果您像我一样使用 Unity / Mono,则在 at least 2021 之前,您无法使用 System.Memory 和相关功能。这让您可以推出自己的解决方案。数组池实现起来相当简单,并且可以避免垃圾分配。堆栈分配的数组有点复杂。
幸运的是,someone has already done it,特别是考虑到 Unity。链接的页面很长,但包括演示概念的示例代码和代码生成工具,该工具可以针对您的确切用例创建SmallBuffer
类。基本思想是只创建一个带有您索引的各个变量的结构,就好像它们是一个数组一样。
更新:我尝试了这两种解决方案,在我的情况下,数组池比 SmallBuffer 稍快(而且容易得多),所以记得分析!
【讨论】:
以上是关于如何使用 Span<T> 和 stackalloc 创建临时小列表的主要内容,如果未能解决你的问题,请参考以下文章
C# 7.2 中的 Span<T> 和 Memory<T> 有啥区别?
Span<T> 和朋友不在 .NET Native UWP 应用程序中工作