C# Substring方法的危险性?

12

最近我一直在了解Java substring方法的一些缺陷,特别是与内存有关的问题,以及Java如何保留对原始字符串的引用。具有讽刺意味的是,我正在开发一个服务器应用程序,它每秒钟使用C# .Net的substring实现多次。这让我想到...

  1. C# (.Net) string.Substring是否存在内存问题?
  2. string.Substring的性能如何?是否有更快的方法基于起始/结束位置来拆分字符串?

8
每分钟50次对我来说似乎并不是很大的负担。每秒数百到数千次会非常强烈,但一秒钟一次左右呢? - jball
@jball:现在大约每秒钟一次,但随着服务器负载的增加,子字符串的使用率也会增加。 - caesay
2
重点不在于CPU使用率会很高——如果它是一个运行多天并在长字符串上调用Substring的服务器应用程序,如果.NET遭受相同的问题,则在那段时间内仍然可能“泄漏”大量内存。 - Peter
2
@Tommy,我在评论你的第二个关于更快获取子字符串的问题。内存泄漏是一个值得注意的问题,但像.Substring这样的核心框架操作应该被认为是高效的,直到你看到实际的减速并将其追踪到该操作。 - jball
1
这个问题是由Eric Lippert本人回答的:https://dev59.com/yHE95IYBdhLWcg3wEpro - Mikhail Poda
NFlath.com上题为“String.substring的危险性”的文章链接已失效。 - Ishmael
9个回答

18

查看 .NET 的 String.Substring 实现,一个子字符串不与原始字符串共享内存。

private unsafe string InternalSubString(int startIndex, int length, bool fAlwaysCopy)
{
    if (((startIndex == 0) && (length == this.Length)) && !fAlwaysCopy)
    {
        return this;
    }

    // Allocate new (separate) string
    string str = FastAllocateString(length);

    // Copy chars from old string to new string
    fixed (char* chRef = &str.m_firstChar)
    {
        fixed (char* chRef2 = &this.m_firstChar)
        {
            wstrcpy(chRef, chRef2 + startIndex, length);
        }
    }
    return str;
}

1
如果它不与原始内存共享,则您是在说GC实际上将收集原始字符串,并且不会泄漏内存? - caesay
5
如果你不保持对原始字符串的引用,那么它会被垃圾回收。 - snarf
3
除非它在字符串常量池中。 - Yuriy Faktorovich
1
确实如此。但这通常意味着字符串要么是编译时的文字,要么是您特意将字符串设置为内部字符串。 - snarf
1
我正在寻找相反的东西,即与原始字符串共享内存的子字符串(以提高速度)。 - ctrl-alt-delor

3
每次使用substring时,您都会创建一个新的字符串实例-它必须将字符从旧字符串复制到新字符串,并进行相关的新内存分配-不要忘记这些是unicode字符。 这可能是好事或坏事-在某些情况下,您无论如何都想在某个地方使用这些字符。 根据您要做什么,您可能需要自己的方法,仅查找您稍后可以使用的字符串中的正确索引。

2

仅提供另一种观点。

内存不足(大多数情况下)并不意味着您已经使用完所有内存。它意味着您的内存已经被分段,下一次您想要分配一块内存时,系统无法找到连续的内存块来适应您的需求。

频繁的分配/释放会导致内存碎片化。由于您进行的操作类型,GC可能无法及时进行碎片整理。我知道.NET中的Server GC在进行内存整理方面非常出色,但是您可以通过编写糟糕的代码来饥饿(防止GC进行收集)系统。


1
我看不到你能够阻止垃圾回收机制的运行。当垃圾回收机制发现存在过多的"内存压力"时,会进行内存回收操作。但是据我所知,这个检查是在分配内存时进行的,因此你不能在不让垃圾回收机制运行的情况下进行内存分配(据我所知,在垃圾回收期间,所有线程都将被暂停)。 - snarf

1

尝试并测量经过的毫秒总是很好的。

Stopwatch watch = new Stopwatch();
watch.Start();
// run string.Substirng code
watch.Stop();
watch.ElapsedMilliseconds();

我一直在使用 ANTS Profiler 解决gc相关问题。想知道是否有更好的选择? - jebberwocky

1
在使用 subString 时,可能会遇到 Java 内存泄漏的情况。但是,通过使用复制构造函数(即形如 "new String(String)" 的调用)实例化一个新的 String 对象,可以轻松解决这个问题。通过这种方式,您可以丢弃所有对原始字符串的引用(如果这实际上是一个问题,原始字符串可能相当大),并仅在内存中保留您需要的部分。
理论上讲,JVM 可以更聪明地压缩 String 对象(如上所建议的),但现在我们只能用现有的方法来完成工作。
至于 C#,正如已经说过的,这个问题不存在。

0

在开发过程中,您可以使用以下代码对内存进行分析:

bool forceFullCollection = false;

Int64 valTotalMemoryBefore = System.GC.GetTotalMemory(forceFullCollection);

//call String.Substring

Int64 valTotalMemoryAfter = System.GC.GetTotalMemory(forceFullCollection);

Int64 valDifferenceMemorySize = valTotalMemoryAfter - valTotalMemoryBefore;

关于参数forceFullCollection:“如果forceFullCollection参数为true,则此方法在返回之前会等待短时间,同时系统收集垃圾并完成对象。间隔的持续时间是由已完成的垃圾回收周期数和循环之间恢复的内存量变化所确定的内部指定限制。垃圾回收器不能保证收集所有不可访问的内存。” GC.GetTotalMemory Method

祝你好运!;)


0

我记得Java中的字符串是存储实际字符以及起始位置和长度。

这意味着子字符串可以共享相同的字符(因为它们是不可变的),只需要维护单独的起始位置和长度。

所以我不确定您在Java字符串方面的内存问题是什么。


关于您编辑发布的那篇文章,我认为这似乎不是什么大问题。

除非您有制作大字符串并留下小子字符串的习惯,否则这对内存几乎没有影响。

即使您有一个10M的字符串并且制作了400个子字符串,您只使用了那个10M的基础字符数组 - 它并没有制作400个该子字符串的副本。唯一的内存影响是每个子字符串对象的起始/长度位。

作者似乎在抱怨他们将一个巨大的字符串读入内存,然后只想要其中的一部分,但整个字符串都被保留了下来 - 我的建议是他们可能需要重新考虑如何处理他们的数据 :-)

将这称为Java bug也是牵强附会。Bug是指不符合规格的东西。这是一个故意的设计决策,旨在提高性能,因为您不理解事物的工作原理而导致内存不足并不是bug,IMNSHO。而且这绝对不是内存泄漏。


在那篇文章的评论中,有一个可能是好的建议,即GC可以通过压缩未使用的字符串来更积极地回收它们。

这不是第一次GC时想做的事情,因为它相对昂贵。然而,在每个其他GC操作都无法回收足够空间的情况下,您可以这样做。

不幸的是,这几乎肯定意味着底层的char数组需要记录所有引用它的字符串对象,以便它既能找出哪些位是未使用的修改所有字符串对象的起始和长度字段。

这本身可能会引入不可接受的性能影响,而且如果您的内存如此短缺以至于这成为问题,您甚至可能无法为较小版本的字符串分配足够的空间。

我认为,如果内存不足,我可能更喜欢维护这种char数组到字符串映射,以使这种GC水平成为可能,而是希望将该内存用于我的字符串。


既然有一个完全可接受的解决方法,而且好的程序员应该知道他们选择的语言的怪癖,我认为作者是正确的 - 这个问题不会被修复。

这不是因为Java开发人员太懒,而是因为这不是一个问题。

你可以自由地实现与C#相匹配的字符串方法(除了某些特定的场景外,它们不共享底层数据)。这将解决你的内存问题,但代价是性能下降,因为每次调用子字符串时都必须复制数据。就像IT(和生活)中的大多数事情一样,这是一个权衡。


1
我不同意"因为你不知道如何工作而导致内存耗尽不是错误"的说法。substring的文档说明:返回一个新字符串,它是此字符串的子字符串。它没有提供任何提示,表明返回的字符串会在内存中固定原始字符串。因此,文档应该清楚地说明实际行为,或者应该避免这种"优化"。由你决定 - 要么文档有缺陷,要么实现有问题。开发人员不应该必须检查此类方法的内部实现才能正确使用它们。 - LBushkin

0

CLR(因此C#)对Substring的实现不会保留源字符串的引用,因此它不具有Java字符串的“内存泄漏”问题。


0
大多数字符串问题都是由于String是不可变的。当您需要进行大量字符串操作时,应使用StringBuilder类:

http://msdn.microsoft.com/en-us/library/2839d5h5(VS.71).aspx

请注意,真正的问题在于内存分配,而不是 CPU,尽管过多的内存分配会占用 CPU...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接