是否可以仅使用单个分配来连接字符串列表?

2024-01-10

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

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

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

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

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

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

我们现在能做的最好的事情就是打电话List<string>.ToArray()- 执行 500k 元素数组的一份副本 - 并传递结果string[] to 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<string>使用单个分配和单个字符副本循环?

Edit 1


我想总结一下迄今为止讨论的各种方法,以及为什么它们仍然不是最佳的。我还想更具体地设置情况参数,因为我收到了很多试图回避核心问题的问题。

...

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

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

谈论一些数字:典型的批处理运行将从内容生产者收集约 500000 个字符串,相当于约 200-500 MB 的内存。我需要最有效的方法将这 500k 字符串连接成一个字符串。

...

现在我想检查一下迄今为止讨论的方法。就数字而言,假设我们运行 64 位,假设我们正在收集 500000 个字符串对象,并假设字符串对象的总大小相当于 200 MB 的字符数据。另外,假设原始字符串对象的内存是没有计算在内以下分析中任何方法的总和。我做出这个假设是因为它对于任何和所有方法来说都是常见的,因为它是一个假设,即我们无法更改内容生成器的接口 - 它们返回 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

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

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

方法#3:

内容制作者 --->List<string>---->string[]---->string.Concat(string[])

  • 预先分配一个List<string>大约 500k 元素 - List 底层数组的大约 4 MB 分配(500k * 每个指针 8 字节 == 4 MB 内存)。
  • 致电所有内容制作者来收集他们的字符串。当我们将返回字符串的指针复制到 List 的底层数组中时,大约需要 4 MB 的副本。
  • Call List<string>.ToArray()获得一个string[]。大约 4 MB 的分配和复制(同样,我们实际上只是复制指针)。
  • Call 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<string>2)在使用之前没有复制该IList,那么问题就解决了。 .Net 没有提供这样的方法,因此是这个问题的主题。

Edit 2


解决方案的进展.

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

从那里,似乎可以使用以下命令获取指向新分配的字符串的指针:unsafe and fixed声明。

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

    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;
    }

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


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

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

UPDATE

请查看这个内部实现String.Join来自.NET - 它使用带有指针的不安全代码来避免多次分配。除非我遗漏了一些东西,否则您似乎可以使用列表重写它来完成您想要的事情:

    [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;
    } 

Source: 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 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

UPDATE 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* 的字符串构造函数是extern传递给底层 C++ 实现。我还没有找到该代码的可靠来源来确认,但也许这对您来说可以更快。非产品就绪代码(不检查潜在溢出、添加固定以锁定来自垃圾收集的字符串等)将从以下内容开始:

    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,以最大限度地减少应用程序的安全风险(如果您沿着这条路走)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

是否可以仅使用单个分配来连接字符串列表? 的相关文章

  • 如何在C++中实现模板类协变?

    是否可以以这样一种方式实现类模板 如果模板参数相关 一个对象可以转换为另一个对象 这是一个展示这个想法的例子 当然它不会编译 struct Base struct Derived Base template
  • FFMPEG Seeking 带来音频伪影

    我正在使用 ffmpeg 实现音频解码器 在读取音频甚至搜索已经可以工作时 我无法找到一种在搜索后清除缓冲区的方法 因此当应用程序在搜索后立即开始读取音频时 我没有任何工件 avcodec flush buffers似乎对内部缓冲区没有任何
  • SSH 主机密钥指纹与模式 C# WinSCP 不匹配

    我尝试通过 WinSCP 使用 C 连接到 FTPS 服务器 但收到此错误 SSH 主机密钥指纹 与模式不匹配 经过大量研究 我相信这与密钥的长度有关 当使用 服务器和协议信息 下的界面进行连接时 我从 WinSCP 获得的密钥是xx xx
  • 使用 Microsoft Graph API 订阅 Outlook 推送通知时出现 400 错误请求错误

    我正在尝试使用 Microsoft Graph API 创建订阅以通过推送通知获取 Outlook 电子邮件 mentions 我在用本文档 https learn microsoft com en us graph api subscri
  • 为什么 POSIX 允许在只读模式下超出现有文件结尾 (fseek) 进行搜索

    为什么寻找文件结尾很有用 为什么 POSIX 让我们像示例中那样在以只读方式打开的文件中进行查找 c http en cppreference com w c io fseek http en cppreference com w c io
  • 写入和读取文本文件 - C# Windows 通用平台应用程序 Windows 10

    有用 但在显示任何内容之前 您必须在文本框中输入内容 我想那是因为我使用了 TextChanged 事件处理程序 如果我希望它在没有用户交互的情况下显示文本文件的内容 我应该使用哪个事件处理程序 因此 我想在按下按钮时将一些数据写入 C W
  • 如何针对 Nancy 中的 Active Directory 进行身份验证?

    这是一篇过时的文章 但是http msdn microsoft com en us library ff650308 aspx paght000026 step3 http msdn microsoft com en us library
  • 基于范围的 for 循环中的未命名循环变量?

    有没有什么方法可以不在基于范围的 for 循环中 使用 循环变量 同时也避免编译器发出有关未使用它的警告 对于上下文 我正在尝试执行以下操作 我启用了 将警告视为错误 并且我不想进行像通过在某处毫无意义地提及变量来强制 使用 变量这样的黑客
  • 在 ASP.Net Core 2.0 中导出到 Excel

    我曾经使用下面的代码在 ASP NET MVC 中将数据导出到 Excel Response AppendHeader content disposition attachment filename ExportedHtml xls Res
  • 我的 strlcpy 版本

    海湾合作委员会 4 4 4 c89 我的程序做了很多字符串处理 我不想使用 strncpy 因为它不会终止 我不能使用 strlcpy 因为它不可移植 只是几个问题 我怎样才能让我的函数正常运行 以确保它完全安全稳定 单元测试 这对于生产来
  • 更改窗口的内容 (WPF)

    我创建了一个简单的 WPF 应用程序 它有两个 Windows 用户在第一个窗口中填写一些信息 然后单击 确定 这会将他们带到第二个窗口 这工作正常 但我试图将两个窗口合并到一个窗口中 这样只是内容发生了变化 我设法找到了这个更改窗口内容时
  • .NET 选项将视频文件流式传输为网络摄像头图像

    我有兴趣开发一个应用程序 它允许我从 xml 构建视频列表 包含视频标题 持续时间等 并将该列表作为我的网络摄像头流播放 这意味着 如果我要访问 ustream tv 或在实时通讯软件上激活我的网络摄像头 我的视频播放列表将注册为我的活动网
  • 检查 url 是否指向文件或页面

    我们需要以下内容 如果文件确实是文件 则从 URL 下载该文件 否则 如果它是一个页面 则什么也不做 举个简单的例子 我有以下命令来下载文件 My Computer Network DownloadFile http www wired c
  • 在 URL 中发送之前对特殊字符进行百分比编码

    我需要传递特殊字符 如 等 Facebook Twitter 和此类社交网站的 URL 为此 我将这些字符替换为 URL 转义码 return valToEncode Replace 21 Replace 23 Replace 24 Rep
  • char指针或char变量的默认值是什么[重复]

    这个问题在这里已经有答案了 下面是我尝试打印 char 变量和指针的默认值 值的代码 但无法在控制台上看到它 它是否有默认值或只是无法读取 ASCII 范围 include
  • 如何构建印度尼西亚电话号码正则表达式

    这些是一些印度尼西亚的电话号码 08xxxxxxxxx 至少包含 11 个字符长度 08xxxxxxxxxxx 始终以 08 开头 我发现这个很有用 Regex regex new Regex 08 0 9 0 9 0 9 0 9 0 9
  • 如何在内存中存储分子?

    我想将分子存储在内存中 这些可以是简单的分子 Methane CH4 C H bond length 108 7 pm H H angle 109 degrees But also more complex molecules like p
  • 在 ASP.NET 中将事件冒泡为父级

    我已经说过 ASP NET 中的层次结构 page user control 1 user control 2 control 3 我想要做的是 当控件 3 它可以是任何类型的控件 我一般都想这样做 让用户用它做一些触发回发的事情时 它会向
  • 如何在 C# 中播放在线资源中的 .mp3 文件?

    我的问题与此非常相似question https stackoverflow com questions 7556672 mp3 play from stream on c sharp 我有音乐网址 网址如http site com aud
  • C++ 成员函数中的“if (!this)”有多糟糕?

    如果我遇到旧代码if this return 在应用程序中 这种风险有多严重 它是一个危险的定时炸弹 需要立即在应用程序范围内进行搜索和销毁工作 还是更像是一种可以悄悄留在原处的代码气味 我不打算writing当然 执行此操作的代码 相反

随机推荐