非托管内存中的可变字符串可用于托管空间
Posted
技术标签:
【中文标题】非托管内存中的可变字符串可用于托管空间【英文标题】:Mutable String in unmanaged memory useable in managed space 【发布时间】:2021-07-03 01:30:41 【问题描述】:注意:我的案例是在一个旧 API 的生态系统中,它只适用于字符串,没有现代 .NET 添加。
所以我非常需要没有分配的可变字符串。字符串每 X 毫秒更新一次,因此您可以在几分钟内计算出它可以产生多少垃圾(StringBuilder 甚至根本不接近相关)。我目前的方法是预先分配 fixed 大小的字符串并通过固定、直接写入字符以及在容量达到时静默掉落或抛出来对其进行变异。
这很好用。分配的字符串是长期存在的,因此最终 GC 会将其提升到 Gen2 并且固定不会对其造成太大影响,从而最大限度地减少开销。但是有 2 个主要的问题:
-
因为字符串是固定的,我必须用
\0
填充它,虽然到目前为止这对所有默认的 NET/MONO 功能和第 3 方的东西都很好,但无法知道当字符串在 len 中为 1024 时其他东西会如何反应, 但最后 100 个是 \0
我无法调整它的大小,因为这会导致分配。我可以一次分配一次蓝月亮,但由于字符串是相当动态的,我无法确定它何时会尝试进一步扩展或缩小。我可以使用 "expand only" 方法,这样我只在需要扩展时分配,但是,这有填充开销的缺点(如果字符串扩展为 5k 个字符,但下一个字符串只是 3k - 2k字符将被填充以获得额外的周期)以及额外的内存使用。我不确定 GC 对 mchuge 的感觉如何,通常在 Gen2 中而不是在 LOH 中固定字符串。 另一种方法是池化可重复使用的字符串对象,但是,这具有更高的内存和 GC 开销 + 查找开销。
由于目标字符串必须存在相当长的时间,我正在考虑通过字节缓冲区将其移动到Unmanaged memory。这将消除 GC 的负担(固定惩罚开销),并且我可以以比托管堆更低的成本重新调整大小/重新分配。
我很难理解的是 - 我怎样才能切片分配的非托管缓冲区的特定部分并将其包装为正常的网络字符串以在托管空间/代码中使用?比如,将它传递给Console.WriteLine
或一些在屏幕上绘制 UI 标签并接受字符串的第三方库。这甚至可行吗?
附:据我所知,NET5 的计划(我认为将在 NET6 中最终确定)您将不再能够改变字符串之类的东西(在运行时被阻止或未定义的故障)。他们的解决方案似乎是 POH,这基本上就是我所描述的,具有相同的限制。
【问题讨论】:
"GC 会将其提升到 Gen2 并且固定不会对它造成太大影响,从而最大限度地减少开销。" - 这不一定是真的。如果字符串在 gen0 中时被固定,那么 GC 可能会决定完全避免提升它!如果它被提升,它仍然会妨碍压缩。如果它在 在 gen2 中被固定,那么您可能是对的。 我真的鼓励你不要走改变字符串的路线——有很多事情假设字符串不会改变。你不能摆脱一个字符数组吗? .NET 字符串以长度开头,因此您不能将 char 数组的任意位分割成字符串,因为它上面没有适当的标头。 如果可以的话,我会在内存中保留一个char[]
,它是变异的。当您需要将其转换为字符串以传递给 Console.WriteLine
等时(我假设您不是每 X 毫秒都这样做!),使用 string.Create
将您的 char[]
的相关部分复制到一个新的字符串。
不,我有一个只接受字符串的旧 API/生态系统。如果我有能力使用 char 数组或 Span 这个问题就不存在了。您提到的所有这些建议我已经多次提及,它们在这个特定情况下根本不相关。
对,但由于您没有提及它们,人们会建议它们。我不是读心术!
【参考方案1】:
我怎样才能切片分配的非托管缓冲区的特定部分并将其包装为正常的网络字符串以在托管空间/代码中使用
据我所知,这是不可能的。 .Net 有自己的方式来定义对象(对象头等),您不能将某些任意内存区域视为 .net 对象。固定和更改字符串似乎很危险,因为字符串旨在是不可变的,并且有些事情可能无法正常工作(例如,使用字符串作为字典键)。
正确的方法是(正如 Canton7 提到的)使用 char[]
缓冲区和 Span<char>
/ Memory<char>
对字符串进行切片。当传递给其他方法时,您可以将字符串的一部分转换为实际的字符串对象。当调用像Console.WriteLine
或 UI 方法这样的方法时,分配字符串对象的开销与正在发生的其他事情相比是无关紧要的。
如果您有只接受 string
的旧代码,您要么需要接受由此带来的限制,要么重写代码以接受内存/跨度表示。
我强烈建议进行分析,看看这是否是频繁分配的实际问题。只要字符串适合小对象堆(SOH,即小于 87kb)并且没有提升到第 2 代,开销可能不会很大。 SOH 上的分配速度很快,运行第 0 代 GC 的时间不会直接与分配的数量成比例。所以每隔几毫秒更新一次可能并不可怕。如果你在谈论微秒,我会更担心。
【讨论】:
以上是关于非托管内存中的可变字符串可用于托管空间的主要内容,如果未能解决你的问题,请参考以下文章
可变的StringBuffer类和StringBuilder类