Unity中字符串拼接0GC方案

news2024/11/18 11:42:49

本文主要分析C#字符串拼接产生GC的原因,以及介绍名为ZString的库,它可以将字符串生成的内存分配为零。

在C#中,字符串拼接通常有三种方式:

  1. 直接使用+号连接;
  2. string.format;
  3. 使用StringBuilder;

下面分别细述。

故事的开始

首先,简单介绍下String类型。C# String 类型内部是“UTF-16”字节字符串。

与普通对象一样,它有一个对象头,并在堆内存中分配。同样,字符串基本上只能由“新字符串”生成。'StringBuilder.ToString','Encoding.GetString'等,最后也调用'new string'来分配一个新字符串。

即使是相同的字符串值,“new string”生成的字符串也会分配在不同的内存空间中。只有常量字符串从称为实习生池的应用程序共享空间获取固定引用。

var x = new string(new[] { 'f', 'o', 'o' });
var y = new string(new[] { 'f', 'o', 'o' });
var z = "foo";
var u = "foo";
var v = String.Intern(x);
 
// different reference: x != y != z
Console.WriteLine(Object.ReferenceEquals(x, y)); // false
Console.WriteLine(Object.ReferenceEquals(x, z)); // false
 
// same reference: z == u == v
Console.WriteLine(Object.ReferenceEquals(z, u)); // true
Console.WriteLine(Object.ReferenceEquals(z, v)); // true
 
// same value
Console.WriteLine(x == y && x == z && x == u && x == v); // true

如果你想从intern池中获取,可以使用'String.Intern'方法。Intern方法是从Intern池中获取的。如果不存在,则注册并返回其引用。由于Intern池中注册的内存无法删除,因此可能很难很好地使用它。

+拼接(String.Concat)

使用+号连接时,C# 编译器会进行专门处理,将其转换为 String.Concat。

string.Concat(object arg0, object arg1)
string.Concat(object arg0, object arg1, object arg2)
string.Concat(params object[] values)
string.Concat(string str0, string str1)
string.Concat(string str0, string str1, string str2)
string.Concat(string str0, string str1, string str2, string str3)
string.Concat(params string[] values)

不同的编译器版本处理稍有不同。例如,Visual Studio 2019 的 C# 编译器 (int x) + (string y) + (int z) 的结果将为“String.Concat(x.ToString(), y, z.ToString())”。但是,Visual Studio 2017 的 C# 编译器将是“String.Concat((object)x, y, (object)z)”,如果连接非字符串参数,将使用对象重载。因此,发生了结构装箱。

如果我们连接的字符不匹配上方的重载,比如,连接了5个字符,那么就会产生一个“params array”的分配,同样会造成额外的GC。

针对上述情况,ZString提供了最多15个参数的泛型重载,且在内部使用了“Utf16ValueStringBuilder”(在StringBuilder小节中有解释),因此几乎可以完全避免数字类型的字符串转换分配。

StringBuilder

“StringBuilder”是一个以“char[]”作为临时缓冲区的类。StringBuilder.Append()方法用于写入缓冲区,StringBuilder.ToString() 生成最终字符串。

public class SimpleStringBuilder
{
    char[] buffer;
    int offset;
 
    public void Append(string value)
    {
        value.CopyTo(0, buffer, offset, value.Length);
        offset += value.Length;
    }
 
    public override string ToString()
    {
        return new string(buffer, 0, offset);
    }
}

如果要连接多个字符串,应避免使用“+=”,因为每个“+=”都会生成一个新字符串。StringBuilder 避免生成这个临时的新字符串,而是将其复制到“char[]”。

当追加数字以及某些类型时,.NET Standard 2.0(Unity 等)和 .NET Standard 2.1(.NET Core 3.0 等)之间的行为会有所不同。

// .NET Standard 2.0
public StringBuilder Append(int value)
{
    return Append(value.ToString(CultureInfo.CurrentCulture));
}
 
// .NET Standard 2.1
public StringBuilder Append(int value)
{
    return AppendSpanFormattable(value);
}
 
private StringBuilder AppendSpanFormattable<T>(T value)
    where T : ISpanFormattable
{
    if (value.TryFormat(RemainingCurrentChunk,
        out int charsWritten, format: default, provider: null))
    {
        m_ChunkLength += charsWritten;
        return this;
    }
    return Append(value.ToString());
}

对于 .NET Standard 2.0,它Append时调用了ToString方法。但在 .NET Standard 2.1 中,“ISpanFormattable.TryFormat”将其直接写入缓冲区,而不通过字符串。ISpanFormattable这个接口是internal的 。但是,通过检查 [ ISpanFormattable.references ],您可以看到哪种类型实现了此接口。

通过ZString可以避免添加数字类型时的字符串分配。在 .NET Standard 2.1 中,ZString 使用它们的TryFormat。在.NET Standard 2.0中,ZString使用移植的TryFormat方法。

API 本身与 StringBuilder 几乎相同。但是,它必须用“using”括起来。

// using ZString.CreateStringBuilder instead of new StringBuilder
using (var sb = ZString.CreateStringBuilder())
{
    sb.Append(enemy.Name);
    sb.Append(" Current HP:");
    sb.Append(enemy.Hp);
    sb.Append(" Current MP:");
    sb.Append(enemy.Mp);
    if (addStatus)
    {
        sb.Append(" Status:");
        sb.Append(enemy.Status);
    }
    return sb.ToString();
}

ZString.CreateStringBuilder ()方法的返回值“Utf16ValueStringBuilder”是一个结构体,所以避免了分配到StringBuilder的堆内存。此外,由于用于内部写入的“char[]”缓冲区是从ArrayPool获取的,因此避免了缓冲区分配。(这也是为什么需要通过“using”返回缓冲区。)

String.Format

由于 String.Format 的参数只能接受对象,因此会发生装箱。

// conversion of String interpolation is rewrited to following by C# compiler
$"{enemy.Name} Current Hp:{enemy.Hp} Current Mp:{enemy.Mp}";
 
// string.Object(string, object, object, object)
String.Format("{0} Current Hp:{1} Current Mp:{2}", enemy.Name, enemy.Hp, enemy.Mp);
 
// String.Format can avoid params array until 3 arguments
string string.Format(string format, object arg0)
string string.Format(string format, object arg0, object arg1)
string string.Format(string format, object arg0, object arg1, object arg2)
string string.Format(string format, params object[] args)

此外,与 StringBuilder.Append 一样,在 .NET Standard 2.0 中,也会发生字符串转换分配。

与“ZString.Concat”一样,“ZString.Format”具有最多 15 个参数的通用重载。即使在.NET Standard 2.0环境下,通过TryFormat直接转换,也能实现零分配。

终极秘诀

ZString 的内部实现是零分配。但当最后总要输出一个字符串,还是会产生GC。但是,如果适用的库具有接受字符串以外的内容的 API,则也可以避免最终的字符串生成,并且可以实现完全零分配。例如,TextMeshPro有一个名为“SetCharArray(char[] sourceText, int start, int length)”的API,可以直接给出它,并且可以避免字符串生成。

TMP_Text tmp;
 
// create StringBuilder
using(var sb = ZString.CreateStringBuilder())
{
    sb.Append("foo");
    sb.AppendLine(42);
    sb.AppendFormat("{0} {1:.###}", "bar", 123.456789);
 
    // direct write(avoid string alloc) to TextMeshPro
    tmp.SetText(sb);
 
    // SetText(Utf16ValueStringBuilder) is the same as following
    var buffer= sb.AsArraySegment();
    tmp.SetCharArray(buffer.Array, buffer.Offset, buffer.Count);
}
 
// convinient helper to use ZString.Format
tmp.SetTextFormat("Position: {0}, {1}, {2}", x, y, z);
 
// other ZString direct write utilities
.AsSpan()
.AsMemory()
.TryCopyTo(Span<char>, out int writtenChars);

参考文献:

ZString — Zero Allocation StringBuilder for .NET Core and Unity.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1476109.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

table展示子级踩坑

##elemenui中table通过row中是否有children进行判断是否展示子集&#xff0c;通过设置tree-prop的属性进行设置&#xff0c;子级的children的名字可以根据自己的子级名字进行替换&#xff0c;当然同样可以对数据处理成含有chilren的子级list。 问题&#xff1a; 1.如果是根据后…

java大数据开发面试题,完美世界java面试题

02 JVM 线程JVM内存区域JVM运行时内存垃圾回收与算法JAVA四种引用类型GC分代收集算法 VS 分区收集算法GC垃圾收集器JAVA IO/NIOJVM类加载器 03 JAVA集合 接口继承关系和实现LISTSETMAP 04 JAVA多线程并发 JAVA并发知识库JAVA线程实现/创建方式4种线程池线程生命周期&#xf…

★【递归】【链表】Leetcode 21. 合并两个有序链表

★【递归】【链表】Leetcode 21. 合并两个有序链表 解法1 &#xff1a;递归链表 简直是好题啊好题多做做 ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f388;------------------- 解法1 &#xff1a;递归链表 简直是好题啊好题多做做 >>>…

Time Travel

题目链接 解题思路 由于所有边集中的边加起来的总和至多为&#xff0c;无向图即&#xff0c;可以存下所以直接对所有边集中的边进行建边&#xff0c;同时对于每条边&#xff0c;记录其所在边集号对于每个边集&#xff0c;由大到小维护其能通过的时间点然后从1号跑最短路到当前…

javaWeb个人学习03

事务管理: 概述: 一个事务里面的操作 要么同时成功, 要么同时失败例子: 比如在根据id 删除部门的时候 当部门删除成功了 但是遇到了异常 导致下面的代码没有继续执行下去 就没法根据id删除员工的信息了 这个时候 事务就很重要了 开启回滚 或者提交事务 要么同时成功 要么同时…

【考研数学】《汤家凤 1800 》《张宇 1000 》《李永乐 660 》《李林 880 》应该如何选择?

本人数学逆袭的路上&#xff0c;深知选对一本题集对我的重要性&#xff01;&#xff01;&#xff01; 我本科期间&#xff0c;数学并不是我的强项&#xff0c;但是我却能够在考研的时候靠数学甩开别人几十分成功上岸&#xff0c;一本优秀的题集起到了关键的作用。 1800题&…

MySQL的事务与隔离级别

1. 什么是事务&#xff1f; 数据库中的事务是指对数据库执行一批操作&#xff0c;而这些操作最终要么全部执行成功&#xff0c;要么全部失败&#xff0c;不会存在部分成功的情况。这个时候就需要用到事务。 最经典的例子就是转账&#xff0c;你要给朋友小白转 1000 块钱&…

选择排序,冒泡排序,插入排序,快速排序及其优化

目录 1 选择排序 1.1 原理 1.2 具体步骤 1.3 代码实现 1.4 优化 2 冒泡排序 2.1 原理 2.2 具体步骤 2.3 代码实现 2.4 优化 3 插入排序 3.1 原理 3.2 具体步骤 3.3 代码实现 3.4 优化 4. 快速排序 4.1 原理 4.2 具体步骤 4.3 代码实现 4.4 优化 为了讲…

如何优化一个看似正常的数据库

通常DBA是不会太了解业务逻辑的&#xff0c;遇到系统中劣质的sql 一般也是以通过添加索引的方式来优化&#xff0c;但是并不是所有的sql都能通过添加索引来优化 这就需要重sql的本身来做分析&#xff0c;另外还要了解什么样的语句会不走索引&#xff01;本文通过几个简单的例子…

RK3568 android11 调试陀螺仪模块 MPU-6500

一&#xff0c;MPU6500功能介绍 1.简介 MPU6500是一款由TDK生产的运动/惯性传感器&#xff0c;属于惯性测量设备&#xff08;IMU&#xff09;的一种。MPU6500集成了3轴加速度计、3轴陀螺仪和一个板载数字运动处理器&#xff08;DMP&#xff09;&#xff0c;能够提供6轴的运动…

计算机网络——IPV4数字报

1. IPv4数据报的结构 本结构遵循的是RFC 791规范&#xff0c;介绍了一个IPv4数据包头部的不同字段。 1.1 IPv4头部 a. 版本&#xff08;Version&#xff09;&#xff1a;指明了IP协议的版本&#xff0c;IPv4表示为4。 b. 头部长度&#xff08;IHL, Internet Header Length&…

web组态软件

1、强大的画面显示web组态功能 2、良好的开放性。 开放性是指组态软件能与多种通信协议互联&#xff0c;支持多种硬件设备&#xff0c;向上能与管理层通信&#xff0c;实现上位机和下位机的双向通信。 3、丰富的功能模块。 web组态提供丰富的控制功能库&#xff0c;满足用户的测…

回归预测 | Matlab实现OOA-HKELM鱼鹰算法优化混合核极限学习机多变量回归预测

回归预测 | Matlab实现OOA-HKELM鱼鹰算法优化混合核极限学习机多变量回归预测 目录 回归预测 | Matlab实现OOA-HKELM鱼鹰算法优化混合核极限学习机多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现OOA-HKELM鱼鹰算法优化混合核极限学习机多变量…

《互联网的世界》第二讲-最短路径优先

昨天讲 dns 时讲过&#xff0c;“你问一个当地人最近的厕所在哪&#xff0c;路人给你一个地址…”&#xff0c;可是只有地址还不够&#xff0c;如何到达那里呢&#xff1f;这是本节的内容。 自然的方式是&#xff0c;一边走一边问&#xff0c;根据路人的指示继续一边走一边问…

pikachu之xss获取键盘记录

前备知识 跨域 跨域&#xff08;Cross-Origin&#xff09;是指在互联网中&#xff0c;浏览器为了保护用户信息安全而实施的一种安全策略——同源策略&#xff08;Same-Origin Policy&#xff09;&#xff0c;即浏览器禁止一个域上的文档或者脚本&#xff08;如通过JavaScript发…

单片机复位按键电路、唤醒按键电路

目录 单片机复位按键 外部手动复位 单片机复位按键电路 复位按键电路1 复位按键电路2 单片机唤醒按键 单片机唤醒按键电路 单片机复位按键 单片机复位&#xff1a;简单来说&#xff0c;复位引脚就是有复位信号&#xff0c;就是从头开始执行程序 本质&#xff1a;就是靠…

Linux内核适配 (一)

我们的产品包含多个内核驱动模块,随着Linux内核的不断演进,既有的驱动代码可能因为使用了一些被新版本内核所废弃的函数或者数据结构,导致不能编译通过,或者运行时出错。这时,我们就需要修改我们的驱动代码,以便其能在新版本的内核上正常工作,这个过程通常被称为「适配」…

【机器学习】线性回归模型(Linear Regression)

&#x1f338;博主主页&#xff1a;釉色清风&#x1f338;文章专栏&#xff1a;机器学习&#x1f338;今日语录:温柔的一半是知识&#xff0c;没有知识的涵养撑不起你想要的风骨。 ☘️0文章预览 本系列文章主要是根据吴恩达老师的机器学习课程以及自己的理解整合而成&#xf…

【GO开发工程师】grpc进阶#golang

【GO开发工程师】grpc进阶#golang 推荐个人主页&#xff1a;席万里的个人空间 文章目录 【GO开发工程师】grpc进阶#golang1、protobuf2、grpc2.1、gRPC 的 Metadata机制2.2、grpc拦截器 1、protobuf syntax "proto3"; // 指定使用的 protobuf 版本为 proto3 import…

react-JSX基本使用

1.目标 能够知道什么是JSX 能够使用JSX创建React元素 能够在JSX中使用JS表达式 能够使用JSX的条件渲染和列表渲染 能够给JSX添加样式 2.目录 JSX的基本使用 JSX中使用JS表达式 JSX的条件渲染 JSX的列表渲染 JSX的样式处理 3.JSX的基本使用 3.1 createElement()的问题 A. …