【unity c#】深入理解string,以及不同方式构造类与反射的性能测试(基于BenchmarkDotNet)
出这篇文章的主要一个原因就是ai回答的性能差异和实际测试完全不同,比如说是先获取构造函数再构造比Activator.CreateInstance(type)快,实际却相反
对测试结果的评价基于5.0,因为找不到unity6确切使用的net版本,根据c#9推测是net5.0
起因:看到animancer对string的描述,我开始纠结起来要不要使用,于是有此文章
省流版就是
因为字符串常量池,字面量和方法参数都是直接比较引用,而且无论"string"+"string"还是两个相同长度不同内容的字符串都是比较的引用,如果引用不同,会比较长度,只有在使用变量存字符串,并且运行时动态修改,导致引用不同
- 只要不使用变量存字符串,并且运行时动态修改,就可以放心用string
- 反射用委托
- 构造用表达式树或者Activator.CreateInstance(反正不会经常调用)
- 拼接字符串用StringBuilder
有时间纠结性能不如多做两个玩法
stringbuilder和字符串常量池都是学java时的遗产,但学c#好像没被提到过,所以在这里提一嘴
StringReference 本质是 通过字典缓存和引用比较,也就是说,如果通过动态字符串获取缓存,仍然会在取值时执行一次字符比较判断键是否相等。如果BenchmarkDotNet是可信的,那么StringReference可能 并没有起到提高效率的效果。反而因为隐式转换额外多消耗了一点
补充:
所以最终我决定放弃包装string,如果有需要扩展的就直接用【扩展方法】就好了
不过通过隐式类型转换+字典的缓存方式也让人大开眼界,留个印象说不定哪天用得上
BenchmarkDotNet可参:https://blog.csdn.net/x740073529/article/details/119934597
安装可参:https://www.cnblogs.com/WilsonPan/p/12904664.html
视频可参:https://www.bilibili.com/video/BV1DM411T7rb
简单来说就是安装包,在main入口执行,记得将运行设置为运行模式而不是调试模式:
为了8和5兼容需要删除自动生成全局using的设置:ImplicitUsings。右键,编辑项目文件可以找到
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net5.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
String
主要还是参考作用,真正真实的结果只能到unity实际测试了
结论
- 前置知识:直接用字面量(= ‘abc’)是得到字符串常量池中的引用,所以相同字面量的字符串引用地址相等。而new或者拼接动态字符串才会开辟新的内存空间
- 常量形式的
string
,哪怕是"string"+"string"的形式,也是被字符串常量池优化过的,相当于直接比较引用 - 字符串的等号应该是被重写的,调用equals,并且优先比较引用,也就时候说,只有在使用变量存字符串,并且运行时动态修改,并且长度不同,才会低效
- 查找字符串为键的字典:取hash比较,快
- 查找字符串为键的字典,取值时动态拼接字符串:多出的时间消耗约等于变量拼接常量再比较,意味着除了hash计算还有一次字符比较的操作
- 查找值类型为键的字典:遥遥领先
- 引用类型作为键:比字符串稍快
- 包装字符串为类的效果:比引用类型稍慢
- 动态拼接字符串来查找: 不缓存则较慢
- 常量字符串比较:比引用比较更快,可能有优化吧
- 不同常量不同长度字符串比较:稍慢,应该是判断了字符串长度
- 拼接常量字符串:和常量比较一样,看来是自动优化了
- 变量拼接常量再比较(normal3):很慢。这里应该是比较的字符
- 不同常量比较:快
这里是总览,命名不规范,建议和代码对照看
源码
在 .NET 5 之前,很多基础类型(例如 string)的实现是在 C++ 编写的
所以只能看net8的来参考了
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is string str))
return false;
if (this.Length != str.Length)
return false;
return EqualsHelper(this, str);
}
private static bool EqualsHelper(string strA, string strB)
{
Debug.Assert(strA != null);
Debug.Assert(strB != null);
Debug.Assert(strA.Length == strB.Length);
return SpanHelpers.SequenceEqual(
ref Unsafe.As<char, byte>(ref strA.GetRawStringData()),
ref Unsafe.As<char, byte>(ref strB.GetRawStringData()),
((uint)strA.Length) * sizeof(char));
}
补充
- 不同常量相同长度字符串:引用级
- 每个字符比较:比动态拼接快。推测是少了一系列引用比较的缘故
- 比较常量的hash:不如每个字符比较一遍
- Equals比较不同常量:比引用慢,比hash快,比较的是字符?
- Equals比较相同常量:五代比等号慢,说明等号有特殊处理
可以看到5代的Equals被8代薄纱,可能是把等号的判断移到Equals了
看其他博客说 动态拼接的比较方式为比较两个字符串的第一个字符:相等则比较第二个,实际测试并不是,比较字符串时改变差异字符的位置没有影响性能,估计是
[Benchmark]
public void xiangdengNormal7()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf" == "asdasdfasdfasdfsafasdp";
}
}
[Benchmark]
public void xiangdengNormal8()
{
string a = "asdasdfasdfasdfsafasdf";
string b = "asdasdfasdfasdfsafasdf";
for (int i = 0; i < N; i++)
{
for (int j = 0; j < a.Length; j++)
{
var t = a[j] == b[j];
}
}
}
[Benchmark]
public void xiangdengNormal9()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf".GetHashCode() == "asdasdfasdfasdfsafasdp".GetHashCode();
}
}
[Benchmark]
public void xiangdengNormal10()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf".Equals("asdasdfasdfasdfsafasdp");
}
}
[Benchmark]
public void xiangdengNormal12()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf".Equals("asdasdfasdfasdfsafasdf");
}
}
不用循环
- 这里是没用循环的结果,缓存每用上导致结果不准确,所以用hash的结果偏慢。但也反映出hash和string常量引用不同,反而丢失了常量池的优化
- 可以看到net8 比较动态字符串也快了三四倍
- 其他:模板字符串消耗更高
测试getHashCode的消耗
可以看出来字典查找的损耗就是源自于此,而且每次都是重新获取hash
[Benchmark]
public void getHash1()
{
"asdasdfasdfasdfsafasdf".GetHashCode();
}
[Benchmark]
public void getHash2()
{
stringHash.GetHashCode();
}
测试代码
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net50)] // 在Net5.0测试(unity6 => c#9 推测=> net5.0)
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)]
public class StringTest
{
// 测试数据量
private const int N = 100;
// 测试用的字符串和 StringHash 字典
private Dictionary<string, string> stringDict;
private Dictionary<int, int> intDict;
private Dictionary<YourClass, int> objDict;
private YourClass obj = new YourClass();
private Dictionary<StringHash, StringHash> stringHashDict;
private StringHash stringHash = "asdasdfasdfasdfsafasdf";
// 引用应该是相同的 用字典缓存了的
private StringHash stringHash2 = "asdasdfasdfasdfsafasdf";
private string key = "Value";
// 初始化测试数据
[GlobalSetup]
public void Setup()
{
key += "10";
stringDict = new Dictionary<string, string>(N);
stringHashDict = new Dictionary<StringHash, StringHash>(N);
intDict = new Dictionary<int, int>(N);
objDict = new Dictionary<YourClass, int>(N);
// 插入数据
objDict.Add(obj, 1); // 对象字典
for (int i = 0; i < N; i++)
{
string value = $"Value{i}";
stringDict.Add(value, value); // 字符串字典
stringHashDict.Add(value, value); // StringHash 字典
intDict.Add(i, i); // 整数字典
}
}
// 字符串字典插入性能
//[Benchmark]
//public void StringDictInsertion()
//{
// for (int i = 0; i < N; i++)
// {
// string value = $"Value{i}";
// stringDict[value] = value;
// }
//}
StringHash 字典插入性能
//[Benchmark]
//public void StringHashDictInsertion()
//{
// for (int i = 0; i < N; i++)
// {
// //string value = $"Value{i}";
// stringHashDict[new StringHash(value)] = new StringHash(value);
// }
//}
// 字符串字典查找性能
[Benchmark]
public void StringDictLookup()
{
for (int i = 0; i < N; i++)
{
string value = "Value" + i;
stringDict.TryGetValue(value, out _);
}
}
[Benchmark]
public void StringDictLookup2()
{
for (int i = 0; i < N; i++)
{
string value = "Value10";
stringDict.TryGetValue(value, out _);
}
}
[Benchmark]
public void IntDictLookup()
{
for (int i = 0; i < N; i++)
{
intDict.TryGetValue(10, out _);
}
}
[Benchmark]
public void ObjDictLookup()
{
for (int i = 0; i < N; i++)
{
objDict.TryGetValue(obj, out _);
}
}
动态字符串字典查找性能
[Benchmark]
public void StringDictLookupByDynamic()
{
for (int i = 0; i < N; i++)
{
string value = "Value";
stringDict.TryGetValue(value + "10", out _);
}
}
[Benchmark]
public void StringDictLookupByDynamic2()
{
for (int i = 0; i < N; i++)
{
stringDict.TryGetValue(key, out _);
}
}
// StringHash 字典查找性能
[Benchmark]
public void StringHashDictLookup()
{
for (int i = 0; i < N; i++)
{
//string value = $"Value{i}";
stringHashDict.TryGetValue(new StringHash("Value10"), out _);
}
}
[Benchmark]
public void StringHashDictLookup2()
{
for (int i = 0; i < N; i++)
{
//这里是缓存的
stringHashDict.TryGetValue("Value10", out _);
}
}
[Benchmark]
public void xiangdengNormal()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf" == "asdasdfasdfasdfsafasdf";
}
}
[Benchmark]
public void xiangdengNormal2()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf" == "asdasdfasdfasdf" + "safasdf";
}
}
[Benchmark]
public void xiangdengNormal3()
{
for (int i = 0; i < N; i++)
{
string a = "asdasdfasdfasdf";
var t = "asdasdfasdfasdfsafasdf" == a + "safasdf";
}
}
[Benchmark]
public void xiangdengNormal4()
{
for (int i = 0; i < N; i++)
{
var t = "asdasdfasdfasdfsafasdf" == "asdasdfasdfasdfsafasdfasfaa";
}
}
[Benchmark]
public void xiangdengNormal5()
{
for (int i = 0; i < N; i++)
{
string a = "asdasdfasdfasdfsafasdf";
string b = "asdasdfasdfasdfsafasdfasfaa";
var t = a == b;
}
}
[Benchmark]
public void xiangdengHash()
{
for (int i = 0; i < N; i++)
{
var t = stringHash == "asdasdfasdfasdfsafasdf";
}
}
[Benchmark]
public void xiangdengHash2()
{
for (int i = 0; i < N; i++)
{
var t = stringHash == "asdasdfasdfasdfsafasdfasfaa";
}
}
[Benchmark]
public void xiangdengRefrence()
{
var t = stringHash == stringHash2;
}
[Benchmark]
public void xiangdengInt()
{
var t = 123 == 123;
}
}
[Serializable]
public class StringHash : IComparable<StringHash>
{
// 字段
private readonly string name;
public int hash;
private static readonly Dictionary<string, StringHash> StringHashDic = new(256);
/// <summary>
/// 获取或添加
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static StringHash Get(string value)
{
if (value is null) return null;
if (!StringHashDic.TryGetValue(value, out var reference))
StringHashDic.Add(value, reference = new(value));
return reference;
}
// 构造函数
public StringHash(string name)
{
this.name = name;
hash = HashCode.Combine(name);
// 对于相同字符串,生成相同hash码
// hash = Animator.StringToHash(name);
}
// 重写隐式转换
// 在 string 类型和自定义的 StateHashName 类型之间进行自动转换 就可以直接=
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator StringHash(string value) => Get(value);
public static implicit operator string(StringHash value) => value?.name;
// 覆盖 ToString 方法
// 特性:尽可能将方法内联化 比委托调用快十倍
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => name;
// 重载 == 运算符
public static bool operator ==(StringHash left, StringHash right)
{
if (ReferenceEquals(left, null))
return ReferenceEquals(right, null);
return left.Equals(right);
}
// 重载 != 运算符
public static bool operator !=(StringHash left, StringHash right)
{
return !(left == right);
}
// 重写 Equals 和 GetHashCode 方法
//public override bool Equals(object obj)
//{
// if (ReferenceEquals(this, obj)) return true;
// if (obj is StringHash other)
// {
// // 检查哈希值
// if (hash != other.hash)
// return false;
// return true;
// // 哈希值相等时,进一步比较字符串内容 处理hash碰撞
// //return name == other.name;// == 对于 null 会非常安全,但 Equals 可能会抛出异常
// }
// return false;
//}
// 会被字典调用
// 如果键冲突,会调用Equals
public override int GetHashCode()
{
return hash;
}
// 比较方法
public int CompareTo(StringHash other) => name.CompareTo(other?.name);
}
构造类测试
反射经常能简化代码,但也有性能损耗,索性可以避免,委托只是其中一种方式。
- 带缓存表达式树调用构造函数(委托) : 只比正常慢了一点
- 表达式树(委托)
- 正常构造
- 通过反射获取构造方法再调用:慢了十倍以上
- 反射+强制类型转换:慢了5倍的样子
- 反射+as 类型转换:as确实比强转慢一点,但不多
- 泛型反射:和反射一样
结果: net8.0对反射似乎有不小的优化,可惜unity版本更不上
测试代码
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq.Expressions;
using System;
[MemoryDiagnoser]
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net50)] // 在Net5.0测试(unity6 => c#9 推测=> net5.0)
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)]
public class TestClass
{
private Action _delegate;
private event Action _event;
int iterations = 10000;
static void Main(string[] args)//main函数
{
// 运行 BenchmarkTest 里标记为 Benchmark 的方法,比较它们的性能
var summary = BenchmarkRunner.Run<TestClass>();
Console.WriteLine(summary);
}
private Func<YourClass> constructorDelegate;
[GlobalSetup]
public void Setup()
{
var type = typeof(YourClass);
// 获取无参数的构造函数
var constructor = type.GetConstructor(Type.EmptyTypes);
// 创建一个表达式树,表示调用构造函数的操作
var newExpression = Expression.New(constructor);
// 创建一个 Lambda 表达式,指定返回类型为 YourClass
var lambda = Expression.Lambda<Func<YourClass>>(newExpression);
// 编译表达式树,得到一个委托
constructorDelegate = lambda.Compile();
}
[Benchmark]
public void biaodashiHuancun()
{
YourClass obj = constructorDelegate();
}
[Benchmark]
public void biaodashi()
{
var type = typeof(YourClass);
// 获取无参数的构造函数
var constructor = type.GetConstructor(Type.EmptyTypes);
// 创建一个表达式树,表示调用构造函数的操作
var newExpression = Expression.New(constructor);
// 创建一个 Lambda 表达式,指定返回类型为 YourClass
var lambda = Expression.Lambda<Func<YourClass>>(newExpression);
// 编译表达式树,得到一个委托
var constructorDelegate = lambda.Compile();
YourClass obj = constructorDelegate();
}
[Benchmark]
public void Normal()
{
YourClass obj = new YourClass();
}
[Benchmark]
public void gouzao()
{
var type = typeof(YourClass);
var constructor = type.GetConstructor(Type.EmptyTypes);
YourClass obj = constructor.Invoke(null) as YourClass;
}
[Benchmark]
public void fanshe()
{
var type = typeof(YourClass); // 请替换为你的类
YourClass? obj = (YourClass)Activator.CreateInstance(type);
}
[Benchmark]
public void fansheAs()
{
var type = typeof(YourClass); // 请替换为你的类
YourClass? obj = Activator.CreateInstance(type) as YourClass;
}
[Benchmark]
public void fansheFanxing()
{
var type = typeof(YourClass);
YourClass? obj = Activator.CreateInstance<YourClass>();
}
}
//省略了字段
public class YourClass
{
public YourClass()
{
}
}
额外测试
空类测试
- 类的属性是否影响构造消耗:是
- as的消耗具体多大:可以忽略,甚至更快了
十几个字段的类
- as转换为接口和as转换为类消耗差距: 差不多
- as和is消耗:差不多
代码:
[Benchmark]
public void fanshe()
{
var type = typeof(YourClass); // 请替换为你的类
object obj = Activator.CreateInstance(type);
}
[Benchmark]
public void fansheAs()
{
var type = typeof(YourClass); // 请替换为你的类
YourClass? obj = Activator.CreateInstance(type) as YourClass;
}
[Benchmark]
public void fansheAsInterface()
{
var type = typeof(YourClass); // 请替换为你的类
IYourInterface? obj = Activator.CreateInstance(type) as IYourInterface;
}
[Benchmark]
public void TestIS()
{
var type = typeof(YourClass); // 请替换为你的类
_ = Activator.CreateInstance(type) is IYourInterface;
}
反射测试
参考:https://blog.walterlv.com/post/create-delegate-to-improve-reflection-performance.html
- 直接调用
- 通过方法(委托)调用:慢了十倍 (可能是有额外的检查)
- 通过委托调用:和通过方法差不多
- 通过缓存的反射方法:慢了百倍
- 通过反射:慢了两百倍
代码
public class Fanshe
{
private StubClass instance;
private MethodInfo method;
private Func<int, int> pureFunc;
private Func<int, int> func;
// 初始化测试数据
[GlobalSetup]
public void Setup()
{
instance = new StubClass();
method = typeof(StubClass).GetMethod(nameof(StubClass.Test), new[] { typeof(int) });
pureFunc = value => value;
// 使用反射创建一个委托
func = InstanceMethodBuilder<int, int>.CreateInstanceMethod(instance, method);
}
// 直接调用
[Benchmark]
public void DirectCall()
{
var result = instance.Test(5);
}
// 使用 Func 委托调用
[Benchmark]
public void FuncCall()
{
var result = pureFunc(5);
}
// 使用反射创建的委托调用
[Benchmark]
public void DelegateFromReflection()
{
var result = func(5);
}
// 使用缓存的反射方法调用
[Benchmark]
public void CachedReflectionCall()
{
var result = method.Invoke(instance, new object[] { 5 });
}
// 使用每次都反射查找的方法调用
[Benchmark]
public void DirectReflectionCall()
{
var result = typeof(StubClass).GetMethod(nameof(StubClass.Test), new[] { typeof(int) })
?.Invoke(instance, new object[] { 5 });
}
}