【问题标题】:Is it possible to concatenate a list of strings using only a single allocation?是否可以仅使用单个分配来连接字符串列表?
【发布时间】:2015-08-26 02:51:54
【问题描述】:

在进行了一些分析之后,我们发现我们的应用连接字符串的当前方式会导致大量的内存流失和 CPU 时间。

我们正在构建一个List<string> 的字符串来连接,它的长度约为 50 万个元素,引用价值数百兆字节的字符串。我们正在尝试优化我们应用的这一小部分,因为它似乎占用了不成比例的 CPU 和内存使用量。

我们做了很多文本处理:)

理论上,我们应该能够在单个分配和 N 个副本中执行连接 - 我们可以知道字符串中有多少个可用字符,所以它应该就像总结组件字符串的长度一样简单并分配足够的底层内存来保存结果。

假设我们从预填充的List<string> 开始,是否可以使用单个分配连接该列表中的所有字符串?

目前,我们正在使用StringBuilder 类,但是它存储了它自己的所有字符的中间缓冲区 - 所以我们有一个不断增长的块数组,每个块都存储我们提供的字符的副本它。远非理想。块数组的分配并不可怕,但最糟糕的是它分配了中间字符数组,这意味着 N 分配和副本。

我们现在能做的最好的事情是调用List<string>.ToArray()——它执行一个500k元素数组的副本——并将生成的string[]传递给string.Concat(params string[])string.Concat() 然后执行两次分配,一次将输入数组复制到内部数组中,另一次分配目标字符串的内存。

来自referencesource.microsoft.com:

    public static String Concat(params String[] values) {
        if (values == null)
            throw new ArgumentNullException("values");
        Contract.Ensures(Contract.Result<String>() != null);
        // Spec#: Consider a postcondition saying the length of this string == the sum of each string in array
        Contract.EndContractBlock();
        int totalLength=0;

        // -----------> Allocation #1 <---------
        String[] internalValues = new String[values.Length];
        
        for (int i=0; i<values.Length; i++) {
            string value = values[i];
            internalValues[i] = ((value==null)?(String.Empty):(value));
            totalLength += internalValues[i].Length;
            // check for overflow
            if (totalLength < 0) {
                throw new OutOfMemoryException();
            }
        }
        
        return ConcatArray(internalValues, totalLength);
    }

    private static String ConcatArray(String[] values, int totalLength) {

        // -----------------> Allocation #2 <---------------------
        String result =  FastAllocateString(totalLength);
        int currPos=0;

        for (int i=0; i<values.Length; i++) {
            Contract.Assert((currPos <= totalLength - values[i].Length), 
                            "[String.ConcatArray](currPos <= totalLength - values[i].Length)");

            FillStringChecked(result, currPos, values[i]);
            currPos+=values[i].Length;
        }

        return result;
    }

因此,在最好的情况下,我们有三个分配,两个用于引用组件字符串的数组,一个用于目标连接字符串。

我们可以对此进行改进吗?是否可以使用单个分配和单个字符副本循环连接 List&lt;string&gt;

编辑 1


我想总结一下目前讨论的各种方法,以及为什么它们仍然不是最理想的。我还想更具体地设置情况参数,因为我收到了很多试图绕过中心问题的问题。

...

首先,我正在使用的代码结构。共有三层:

  • 第一层是一组生成我的内容的方法。这些方法返回小的字符串对象,我将其称为“组件”字符串。这些字符串对象最终将连接成一个字符串。我没有能力修改这些方法;我必须面对他们返回字符串对象并继续前进的现实。
  • 第二层是我的代码,它调用这些内容生产者并组装输出,是这个问题的主题。我必须调用内容生产者方法,收集它们返回的字符串,并最终将返回的字符串连接成一个字符串(现实有点复杂;返回的字符串根据它们被路由输出的方式进行分区,所以我有几组大的字符串集合)。
  • 第三层是一组接受单个大字符串进行进一步处理的方法。更改该代码的界面超出了我的控制范围。

谈谈一些数字:典型的批处理运行将从内容制作者那里收集大约 500000 个字符串,代表大约 200-500 MB 的内存。我需要最有效的方法将这 500k 字符串连接成一个字符串。

...

现在我想检查一下目前讨论的方法。为了数字,假设我们运行 64 位,假设我们正在收集 500000 个字符串对象,并假设字符串对象的总大小总计 200 兆字节的字符数据。此外,在下面的分析中,假设原始字符串对象的内存不计入任何方法的总数。我做出这个假设是因为它对任何和所有方法都必然是通用的,因为它假设我们不能改变内容生产者的接口——它们返回 500k 相对较小的完全形成的字符串对象,然后我必须接受这些对象并以某种方式连接。如上所述,我无法更改此界面。

方法#1

内容制作者 ----> StringBuilder ----> string

从概念上讲,这将是调用内容生产者,并将它们返回的字符串直接写入StringBuilder,然后再调用StringBuilder.ToString() 以获取连接的字符串。

通过分析StringBuilder 的实现,我们可以看到这样做的成本归结为 400 MB 的分配和副本:

  • 在我们从内容制作者收集输出的阶段,我们将 200 MB 的数据写入StringBuilder。我们将执行一次 200 MB 的分配来预分配 StringBuilder,然后在复制和丢弃从内容制作者返回的字符串时再分配 200 MB 的副本
  • 在我们收集了内容制作者的所有输出并形成了完整的StringBuilder 之后,我们需要调用StringBuilder.ToString()。这仅执行一次分配 (string.FastAllocateString()),然后将字符串数据从其内部缓冲区复制到字符串对象的内部存储器。

总成本:大约 400 MB 的分配和副本

方法#2

内容制作者 ---> 预分配 char[] ---> string

这个策略相当简单。假设我们大致知道我们将从生产者那里收集多少字符数据,我们可以预先分配一个 200 MB 大的char[]。然后,当我们调用内容制作者时,我们将他们返回的字符串复制到我们的char[] 中。这占 200 MB 的分配和副本。将其转换为字符串对象的最后一步是将其传递给new string(char[]) 构造函数。但是,由于字符串是不可变的,而数组不是,构造函数将复制整个数组,使其分配和复制另外 200 MB 的字符数据。

总成本:大约 400 MB 的分配和副本

方法#3:

内容制作者 ---> List&lt;string&gt; ----> string[] ----> string.Concat(string[])

  • List&lt;string&gt; 预分配为大约 500k 个元素 - 为 List 的底层数组分配大约 4 MB(500k * 每个指针 8 字节 == 4 MB 内存)。
  • 致电所有内容制作者以收集他们的字符串。大约 4 MB 的副本,因为我们将指向返回字符串的指针复制到 List 的底层数组中。
  • 致电List&lt;string&gt;.ToArray() 以获取string[]。大约 4 MB 的分配和副本(同样,我们实际上只是在复制指针)。
  • 致电string.Concat(string[]):
    • Concat 将在执行任何实际工作之前复制提供给它的数组。再次,大约 4 MB 的分配和副本。
    • 然后,Concat 将使用内部string.FastAllocateString() 特殊方法分配一个“目标”字符串对象。大约 200 MB 的分配空间。
    • 然后,Concat 会将字符串从其提供的数组的内部副本直接复制到目标中。大约 200 MB 的副本。

总成本:大约 212 MB 的分配和副本


这些方法都不是理想的,但是方法#3 非常接近。我们假设需要分配和复制的内存的绝对最小值为 200 MB(对于目标字符串),这里我们非常接近 - 212 MB。

如果有一个string.Concat 重载 1) 接受了一个 IList&lt;string&gt; 并且 2) 在使用它之前没有制作该 IList 的副本,那么问题将得到解决。 .Net 没有提供这样的方法,因此是这个问题的主题。

编辑 2


解决方案的进展

我已经对一些被黑的 IL 进行了一些测试,发现直接调用 string.FastAllocateString(n)(通常不可调用...)与调用 new string('\0', n) 一样快,并且两者都似乎 em> 分配与预期一样多的内存。

从那里,似乎可以使用unsafefixed 语句获取指向新分配的字符串的指针。

于是,一个粗略的解决方案开始出现:

    private static string Concat( List<string> list )
    {
        int concatLength = 0;

        for( int i = 0; i < list.Count; i++ )
        {
            concatLength += list[i].Length;
        }

        string newString = new string( '\0', concatLength );

        unsafe
        {
            fixed( char* ptr = newString )
            {
                ...
            }
        }

        return newString;
    }

下一个最大的障碍是实现或找到一种有效的块复制方法,例如 Buffer.BlockCopy,除了接受char* 类型的方法。

【问题讨论】:

  • 理想情况下,如果您需要不同格式的数据,您不会使用List&lt;string&gt;。为什么要这样存储?数据去哪儿了(只是到内存必须是字符串的形式)?
  • @JonathanWood - “为什么要这样存储?”因为我们有一个大型文本处理应用程序,它最终将非常大的字符串提供给外部(本机)库。请专注于提出的问题。
  • 可以使用unsafe 代码对字符串进行变异,因此您可以创建一个适当长度的字符串,然后将源字符串的字符复制到其中。但是您必须非常小心并确定自己在做什么。
  • @displayName:我倾向于不同意。我有 C 和汇编程序的背景,并在 C# 中进行了大量的字符串处理。这是我认为我擅长的问题。但是我的设置有问题。并且将跳出框框思考以更好地处理约束,这意味着理解这些约束。他不想要我的帮助,所以我不给。但这就是我需要帮助他的原因。
  • @JonathanWood:我支持你跳出框框思考。我的第一个答案是相同的证据。完全尊重您的不同意见。

标签: c# .net concatenation stringbuilder


【解决方案1】:

如果您可以在尝试执行操作之前确定连接的长度,那么在某些用例中,char 数组可以胜过字符串生成器。操作数组中的字符可以防止多次分配。

见:http://blogs.msdn.com/b/cisg/archive/2008/09/09/performance-analysis-reveals-char-array-is-better-than-stringbuilder.aspx

更新

请查看 .NET 中 String.Join 的内部实现 - 它使用带有指针的不安全代码来避免多次分配。除非我遗漏了什么,否则您似乎可以使用您的 List 重新编写它来完成您想要的:

    [System.Security.SecuritySafeCritical]  // auto-generated 
    public unsafe static String Join(String separator, String[] value, int startIndex, int count) {
        //Range check the array 
        if (value == null) 
            throw new ArgumentNullException("value");

        if (startIndex < 0)
            throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
        if (count < 0)
            throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount")); 

        if (startIndex > value.Length - count) 
            throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_IndexCountBuffer")); 
        Contract.EndContractBlock();

        //Treat null as empty string.
        if (separator == null) {
            separator = String.Empty;
        } 

        //If count is 0, that skews a whole bunch of the calculations below, so just special case that. 
        if (count == 0) { 
            return String.Empty;
        } 

        int jointLength = 0;
        //Figure out the total length of the strings in value
        int endIndex = startIndex + count - 1; 
        for (int stringToJoinIndex = startIndex; stringToJoinIndex <= endIndex; stringToJoinIndex++) {
            if (value[stringToJoinIndex] != null) { 
                jointLength += value[stringToJoinIndex].Length; 
            }
        } 

        //Add enough room for the separator.
        jointLength += (count - 1) * separator.Length;

        // Note that we may not catch all overflows with this check (since we could have wrapped around the 4gb range any number of times
        // and landed back in the positive range.) The input array might be modifed from other threads, 
        // so we have to do an overflow check before each append below anyway. Those overflows will get caught down there. 
        if ((jointLength < 0) || ((jointLength + 1) < 0) ) {
            throw new OutOfMemoryException(); 
        }

        //If this is an empty string, just return.
        if (jointLength == 0) { 
            return String.Empty;
        } 

        string jointString = FastAllocateString( jointLength );
        fixed (char * pointerToJointString = &jointString.m_firstChar) { 
            UnSafeCharBuffer charBuffer = new UnSafeCharBuffer( pointerToJointString, jointLength);

            // Append the first string first and then append each following string prefixed by the separator.
            charBuffer.AppendString( value[startIndex] ); 
            for (int stringToJoinIndex = startIndex + 1; stringToJoinIndex <= endIndex; stringToJoinIndex++) {
                charBuffer.AppendString( separator ); 
                charBuffer.AppendString( value[stringToJoinIndex] ); 
            }
            Contract.Assert(*(pointerToJointString + charBuffer.Length) == '\0', "String must be null-terminated!"); 
        }

        return jointString;
    } 

来源:http://www.dotnetframework.org/default.aspx/4@0/4@0/DEVDIV_TFS/Dev10/Releases/RTMRel/ndp/clr/src/BCL/System/String@cs/1305376/String@cs

更新 2

关于快速分配的好点。根据旧的 SO 帖子,您可以使用反射包装 FastAllocate(当然假设您会缓存 fastAllocate 方法引用,因此您每次都调用Invoke。也许调用的权衡比您现在正在做的更好.

var fastAllocate = typeof (string).GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
    .First(x => x.Name == "FastAllocateString");
var newString = (string)fastAllocate.Invoke(null, new object[] {20});
Console.WriteLine(newString.Length); // 20

也许另一种方法是使用不安全代码将分配复制到 char* 数组中,然后将其传递给字符串构造函数。带有 char* 的字符串构造函数是传递给底层 C++ 实现的 extern。我还没有找到该代码的可靠来源来确认,但也许这对您来说会更快。非 prod 就绪代码(不检查潜在溢出,从垃圾收集中添加固定到锁定字符串等)将以:

    public unsafe string MyConcat(List<string> values)
    {
        int index = 0;
        int totalLength = values.Sum(m => m.Length);
        char* concat = stackalloc char[totalLength + 1]; // Add additional char for null term
        foreach (var value in values)
        {
            foreach (var c in value)
            {
                concat[index] = c;
                index++;
            }
        }
        concat[index] = '\0';
        return new string(concat);
    }

现在我对此一无所知 :) 也许有人可以在这里找到一种带有编组的方法以避免不安全的代码。由于引入不安全代码需要在编译中添加不安全标志,因此请考虑将此部分添加为单独的 dll,以最大限度地降低应用程序的安全风险。

【讨论】:

  • 此方法分配的内存是理想情况下的两倍 - 一个,当我构造我自己的char[] 时,第二个,当string(char[]) 构造函数制作该char[] 的完整副本时。如果我有一个引用 200 MB 字符串的列表,使用此方法我将分配 400 MB。不幸的是,这还不够。我们最好使用List&lt;string&gt;.ToArray(),因为数组的内存本身“只有” 500k 个元素长,大约 500k * 8 = 4 MB 额外。
  • 您可能无法在 CPU 和内存使用方面取得胜利 - 可能必须根据您的服务器功能将 1 换成另一个。这在 CPU 领域可能会更好。我不知道在内存方面有更好的方法。另外,因为与 stringbuilder 不同,它不会在内部调整大小,这不会提高你的性能(即使 stringbuilder 的初始容量很大)?
  • 我添加了 String.Join 的源代码 - 如果您将其调整为直接使用 List 输入而不是字符串数组,这对您来说似乎会更好。
  • 无法使用该代码 - 无法访问 FastAllocateString(),这是问题的症结所在 - .Net 无法直接分配和构造字符串对象(它是故意这样做的,对我不利)。我正在寻找一个非常规的解决方案或不存在解决方案的答案。
  • @antiduh:我希望String.Join 提供了一种连接通过回调方法提供的已知大小的数据量的方法。由于回调方法永远无法访问正在构建的字符串,因此这样的 Join 实现可以支持“类似字符串构建器”的语义,但在正确预测总字符串长度的情况下不需要任何额外的分配。跨度>
【解决方案2】:

除非字符串的平均长度非常小,给定List&lt;String&gt;,最有效的方法是使用ToArray() 将其复制到新的String[],并将其传递给连接或连接方法。如果连接或连接方法想要在开始之前复制其数组,那么这样做可能会导致对引用数组的分配浪费,但这只会为每个字符串分配一个引用,只会有一个分配来保存字符数据,并且它的大小将正确地容纳整个字符串。

如果您自己构建数据结构,您可能会通过将String[] 初始化为估计的所需大小、自行填充并根据需要扩展它来提高效率。这将节省一次分配String[] 的数据。

另一种方法是分配一个String[8192][],然后为每个字符串数组分配一个String[8192]。全部完成后,您将确切知道需要传递给Concat 方法的String[] 大小,以便您可以创建一个精确大小的数组。这种方法需要更多的分配,但只有最终的String[]String 本身需要在大对象堆上进行。

【讨论】:

    【解决方案3】:

    很遗憾你给自己施加的限制。它的结构非常块状,很难让任何流程进行。例如,如果您不期望 IList,而只期望 IEnumerable,那么您也许可以让内容的生产者 变得更容易。不仅如此,您还可以通过仅在需要时使用字符串来使您的处理受益 - 并且仅在它们产生时使用。

    这会让你走上一些不错的异步之路。

    另一端,他们​​让你一次发送到整个东西。这很难。

    但是话虽如此,而且由于您要一遍又一遍地运行它,等等...我想知道您是否无法创建字符串缓冲区或字节缓冲区或 StringBuilder 或其他任何东西 - 并在两者之间重用它执行 - 一次分配最大怪物(或根据需要逐步重新分配它) - 并且不要让 gc 拥有它。字符串构造函数将一遍又一遍地复制它 - 但这是每个周期的一次分配。如果你运行这么多,你会让机器变热,那么它可能值得一试。在不久的过去,我已经准确地做出了这种权衡(但我没有 5gb 可以扼杀)。起初感觉很脏 - 但哇哦 - 吞吐量大声说话!

    另外,虽然您的原生 API 需要一个字符串,但您可以对它撒谎 - 让它认为您正在给它一个字符串。您很可能在最后传递带有空字符的缓冲区 - 或长度 - 取决于 API 的细节。我认为有一两个评论者谈到了这一点。在这种情况下,您可能需要在调用大 ol' 字符串的本机使用者期间固定缓冲区。

    如果是这种情况,您只能一次性分配缓冲区,重复复制到其中,仅此而已。它可能会在您提出的最佳情况下进行。

    【讨论】:

    • 来吧,伙计...帖子的标题是“是否可以仅使用单个分配来连接字符串列表?”。这就是我想要解决的问题,我已经说得很清楚了。这在理论上是可能的,所以让我们让它在实践中成为可能,即使它需要一些艰苦的思考和一点点魔法。我没有可能重组它周围的部分,它们不是我拥有的部分。我得到字符串,我连接字符串,我发送字符串。
    • 是的,我知道 - 我冒着投反对票的风险建议不是“答案”的东西,但我已经看到很多这样的问题,其中正确的答案不是问题的答案。由于您提供了如此多的背景知识,因此似乎偏离另一个方向的答案至少可能会引发一些思考。一般来说,一些开箱即用的东西是值得赞赏的——但如果你不喜欢它,我会明白的。没有冒犯的意思。据我所知,您之前已经考虑过所有这些 - 但以防万一 - 也许其他人会遇到同样的问题(或类似问题)。
    【解决方案4】:

    我已经实现了一种方法,可以将一个 List 连接成一个字符串,该字符串只执行一次分配。

    以下代码在 .Net 4.6 下编译 - Block.MemoryCopy 直到 4.6 才添加到 .Net。

    “不安全”的实现:

    public static unsafe class FastConcat
    {
        public static string Concat( IList<string> list )
        {
            string destinationString;
            int destLengthChars = 0;
    
            for( int i = 0; i < list.Count; i++ )
            {
                destLengthChars += list[i].Length;
            }
    
            destinationString = new string( '\0', destLengthChars );
    
            unsafe
            {
                fixed( char* origDestPtr = destinationString )
                {
                    char* destPtr = origDestPtr; // a pointer we can modify.
                    string source;
    
                    for( int i = 0; i < list.Count; i++ )
                    {
                        source = list[i];
    
                        fixed( char* sourcePtr = source )
                        {
                            Buffer.MemoryCopy(
                                sourcePtr,
                                destPtr,
                                long.MaxValue,
                                source.Length * sizeof( char )
                            );
                        }
    
                        destPtr += source.Length;
                    }
                }
            }
    
            return destinationString;
        }
    
    }
    

    竞争实现是以下“安全”实现:

    public static string Concat( IList<string> list )
    {
        return string.Concat( list.ToArray() )
    }
    

    内存消耗

    • “不安全”实现只执行一次分配和零次临时分配。 List&lt;string&gt; 直接连接成一个新分配的 string 对象。
    • “安全”实现需要两个列表副本 - 一个在我调用 ToArray() 将其传递给 string.Concat 时,另一个在 string.Concat 执行其自己的数组内部副本时。

    当连接 500k 元素列表时,“安全”string.Concat 方法在 64 位进程中准确分配了 8 MB 的额外内存,我通过在内存监视器中运行测试驱动程序确认了这一点。这是我们对安全实现执行的数组副本所期望的。

    CPU 性能

    对于小型工作集,不安全的实现似乎赢了大约 25%。

    测试驱动程序已通过编译 64 位、通过 NGEN 将程序安装到本机映像缓存中并在未加载的工作站上从调试器外部运行来进行测试。

    来自我的测试驱动程序,带有一个小型工作集(500k 字符串,每个 2-10 个字符长):

    Unsafe Time: 17.266 ms
    Unsafe Time: 18.419 ms
    Unsafe Time: 16.876 ms
    
    Safe Time: 21.265 ms
    Safe Time: 21.890 ms
    Safe Time: 24.492 ms
    

    不安全的平均值:17.520 毫秒。安全平均值:22.549 毫秒。安全比不安全需要大约 25% 的时间。这可能是由于安全实现必须做的额外工作,分配临时数组。

    ...

    来自我的带有大型工作集的测试驱动程序(500k 字符串,每个 500-800 个字符长):

    Unsafe Time: 498.122 ms
    Unsafe Time: 513.725 ms
    Unsafe Time: 515.016 ms
    
    Safe Time: 487.456 ms
    Safe Time: 499.508 ms
    Safe Time: 512.390 ms
    

    如您所见,大字符串的性能差异大致为零,可能是因为时间主要是原始副本。

    结论

    如果您不关心数组副本,则安全实现非常容易实现,并且与不安全实现大致一样快。如果您想在内存使用方面做到绝对完美,请使用 unsafe 实现。


    我附上了我用于测试工具的代码:

    class PerfTestHarness
    {
        private List<string> corpus;
    
        public PerfTestHarness( List<string> corpus )
        {
            this.corpus = corpus;
    
            // Warm up the JIT
    
            // Note that `result` is discarded. We reference it via 'result[0]' as an 
            // unused paramater to my prints to be absolutely sure it doesn't get 
            // optimized out. Cheap hack, but it works.
            string result;
    
            result = FastConcat.Concat( this.corpus );
            Console.WriteLine( "Fast warmup done", result[0] );
    
            result = string.Concat( this.corpus.ToArray() );
            Console.WriteLine( "Safe warmup done", result[0] );
    
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    
        public void PerfTestSafe()
        {
            Stopwatch watch = new Stopwatch();
            string result;
    
            GC.Collect();
            GC.WaitForPendingFinalizers();
    
            watch.Start();
            result = string.Concat( this.corpus.ToArray() );
            watch.Stop();
    
            Console.WriteLine( "Safe Time: {0:0.000} ms", watch.Elapsed.TotalMilliseconds, result[0] );
            Console.WriteLine( "Memory usage: {0:0.000} MB", Environment.WorkingSet / 1000000.0 );
            Console.WriteLine();
        }
    
        public void PerfTestUnsafe()
        {
            Stopwatch watch = new Stopwatch();
            string result;
    
            GC.Collect();
            GC.WaitForPendingFinalizers();
    
            watch.Start();
            result = FastConcat.Concat( this.corpus );
            watch.Stop();
    
            Console.WriteLine( "Unsafe Time: {0:0.000} ms", watch.Elapsed.TotalMilliseconds, result[0] );
            Console.WriteLine( "Memory usage: {0:0.000} MB", Environment.WorkingSet / 1000000.0 );
            Console.WriteLine();
        }
    }
    

    【讨论】:

      【解决方案5】:

      StringBuilder 旨在有效地连接字符串。它没有其他用途。
      使用设置初始容量的构造函数:

        int totalLength = CalcTotalLength();
      
        // sufficient capacity 
        StringBuilder sb = new StringBuilder(totalLength);
      

      但是你说连StringBuilder都分配了中间内存,你想做得更好……

      这些是不寻常的要求,因此您需要编写一个适合您情况的函数(创建一个适当大小的 char[],然后将其填充)。我敢肯定,你是有能力的。

      【讨论】:

      • 你如何将char[] 变成string 对象? new string(char[]) 执行提供的数组的副本,在我的例子中,它是 200 MB 内存的副本。
      • 复制 200 MB 并不是世界末日。但假设它是……那么你真的需要一种不同的编程语言(例如 C++)或升级的 PC。
      • "复制 200MB.." 这就是这篇文章的重点。我正在开发一个能够产生大量文本输出的大型内容制作系统,我们正在批量处理非常大的文本块。我不是一次复制 200 MB,而是一次又一次地复制它。您的回答和 cmets 对对话没有任何帮助。
      • 也许你是对的。或者也许我告诉过你解决方案... C++
      • 不接受更改实现的语言。
      【解决方案6】:

      我的前两个答案现在已经包含在问题中。这是我高度依赖情况,但很有用 -

      第三个答案

      如果在所有这些 MB 的字符串中你得到很多相同的字符串,那么更聪明的方法是使用两个字典,一个是 Dictionary&lt;int, int&gt; 来存储 position 和字符串的“Id”那个位置,而另一个位置是Dictionary&lt;int, int&gt;,用于存储“Id”和原始字符串[]中实际字符串的索引。

      巧合的是,我正在尝试做的事情已经在 C# 中实现了。有点像这样……

      如果确实有很多相同的字符串,那么String Interning 有用吗?如果大量匹配字符串来自内容制作者,则可以保证节省大量 200 MB 目标。

      什么是 String.Intern?

      当您在 C# 中使用字符串时,CLR 会做一些聪明的事情,称为 字符串实习。这是一种存储任何字符串的副本的方法。如果你 最终有一百个——或者更糟糕的是,一百万个——具有相同的字符串 值,占用所有存储相同的内存是一种浪费 一遍又一遍地串起来。字符串实习是一种解决方法。 CLR 维护一个称为实习池的表,其中包含一个 对每个文字字符串的单个唯一引用 在程序运行时以编程方式声明或创建。和 .NET Framework 为您提供了两种有用的方法来与 实习生池:String.Intern() 和 String.IsInterned()。

      String.Intern() 的工作方式非常简单。你通过它 单个字符串作为参数。如果该字符串已经在实习生中 池,它返回对该字符串的引用。如果它还没有在 实习生池,它添加它并返回您传递的相同引用 进入它。

      使用String Interning的方法在链接中有说明。为了这个答案的完整性,我可以在此处添加代码,但前提是您认为这些解决方案很有用。

      【讨论】:

      • -1 没有阅读问题。我知道如何使用 StringBuilder;让调用代码在 StringBuilder 中收集其字符串是在回避这个问题。问题:我们从List&lt;string&gt; 开始。是否可以在一次分配中将其转换为string
      • 请查看我的编辑以分析您的策略。
      • 此外,string.Join(string, IEnumerable&lt;string&gt;) 在底层使用了StringBuilder,将其放入“双重分配和复制”存储桶中。它实际上比这更糟糕,因为它要使用一个需要多次增长的 StringBuilder。它必须使用 StringBuilder,并且必须以这种方式使用它,因为通过将列表作为 IEnumerable 传递,我丢失了信息 - 有多少元素,并且可以随意直接访问每个元素。
      • 我不认为字符串实习是要走的路,特别是因为这个系统有数据不断地流过它(一批接一批)。我也怀疑实习池是否愿意存储数百兆字节的字符串数据。此外,所有 interning 所做的只是缓存字符串对象 - 在此处讨论的许多技术中,字符串对象的身份不会被保留 - 复制到字符缓冲区等。
      • @antiduh:只要您没有返回它们的串联,字符串就会保持活动状态并因此被缓存。只要代码不崩溃,pool肯定会存储数据,不管多少MB。
      猜你喜欢
      • 2011-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-09-23
      • 2021-04-23
      • 1970-01-01
      • 2012-02-21
      • 2017-05-05
      相关资源
      最近更新 更多