原文:bit.ly/3wSpO4o

作者:Nikita Starichenko

翻译:精致码农

大家好!今天我想和大家分享几个 .NET 的性能小贴士与基准测试。

我的系统环境:

  • BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.985
  • Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
  • .NET SDK=5.0.104

我将以百分比的形式提供基准测试结果,其中 100% 是最快的结果。

用 StringBuilder 拼接字符串

我们知道,字符串 string 是不可变的。因此,每当你拼接字符串时,就会分配一个新的字符串对象,并填充内容,最终被回收。所有这些都有昂贵开销,这就是为什么 StringBuilder 在字符串拼接时总有更好的性能。

基准测试例子:

private static StringBuilder sb = new();

[Benchmark]
public void Concat3() => ExecuteConcat(3);
[Benchmark]
public void Concat5() => ExecuteConcat(5);
[Benchmark]
public void Concat10() => ExecuteConcat(10);
[Benchmark]
public void Concat100() => ExecuteConcat(100);
[Benchmark]
public void Concat1000() => ExecuteConcat(1000); [Benchmark]
public void Builder3() => ExecuteBuilder(3);
[Benchmark]
public void Builder5() => ExecuteBuilder(5);
[Benchmark]
public void Builder10() => ExecuteBuilder(10);
[Benchmark]
public void Builder100() => ExecuteBuilder(100);
[Benchmark]
public void Builder1000() => ExecuteBuilder(1000); public void ExecuteConcat(int size)
{
string s = "";
for (int i = 0; i < size; i++)
{
s += "a";
}
} public void ExecuteBuilder(int size)
{
sb.Clear();
for (int i = 0; i < size; i++)
{
sb.Append("a");
}
}

结果:

1. 3 string concatenations - 218% (35.21 ns)
2. 3 StringBuilder concatenations - 100% (16.09 ns) 1. 5 string concatenations - 277% (66.99 ns)
2. 5 StringBuilder concatenations - 100% (24.16 ns) 1. 10 string concatenations - 379% (160.69 ns)
2. 10 StringBuilder concatenations - 100% (42.37 ns) 1. 100 string concatenations - 711% (2,796.63 ns)
2. 100 StringBuilder concatenations - 100% (393.12 ns) 1. 1000 string concatenations - 3800% (144,100.46 ns)
2. 1000 StringBuilder concatenations - 100% (3,812.22 ns)

赋予动态集合初始大小

.NET 提供了很多集合类型,比如 List<T>, Dictionary<T>, 和 HashSet<T>。所有这些集合都有动态的容量,当你添加更多的项目时,它们的大小会自动扩大。

当集合达到其大小限制时,它将分配一个新的更大的内存缓冲区,这意味着要进行额外的开销去分配容量。

基准测试例子:

[Benchmark]
public void ListDynamicCapacity()
{
List<int> list = new List<int>();
for (int i = 0; i < Size; i++)
{
list.Add(i);
}
}
[Benchmark]
public void ListPlannedCapacity()
{
List<int> list = new List<int>(Size);
for (int i = 0; i < Size; i++)
{
list.Add(i);
}
}

在第一个方法中,List 集合使用默认容量初始化,并动态扩大。在第二个方法中,初始容量被设置为它所需要的固定大小。

对于 1000 个项目,其结果是:

1. List Dynamic Capacity - 140% (2.490 us)
2. List Planned Capacity - 100% (1.774 us)

DictionaryHashSet 的测试结果是:

1. Dictionary Dynamic Capacity - 233% (20.314 us)
2. Dictionary Planned Capacity - 100% (8.702 us) 1. HashSet Dynamic Capacity - 223% (17.004 us)
2. HashSet Planned Capacity - 100% (7.624 us)

ArrayPool 用于短时大数组

数组的分配和回收的开销可能是相当昂贵的,高频地执行这些分配会增加 GC 的压力并损害性能。一个优雅的解决方案使用是 System.Buffers.ArrayPool 类,它可以在 NuGet 的 Systems.Buffers 中找到。

这个思想和 ThreadPool 很相似。为数组分配一个共享缓冲区,你可以重复使用,而不需要实际分配和回收它们占用的内存。基本用法是调用 ArrayPool<T>.Shared.Rent(size),这将返回一个常规数组,你可以以任何方式使用它。完成后,调用 ArrayPool<int>.Shared.Return(array) 将缓冲区返回到共享池中。

基准测试例子:

[Benchmark]
public void RegularArray()
{
int[] array = new int[ArraySize];
}
[Benchmark]
public void SharedArrayPool()
{
var pool = ArrayPool<int>.Shared;
int[] array = pool.Rent(ArraySize);
pool.Return(array);
}

ArraySize = 1000 的结果:

1. Regular Array - 2270% (440.41 ns)
2. Shared ArrayPool - 100% (19.40 ns)

结构代替类

当涉及到对象回收时,Struct 有如下几个好处:

  • 当结构类型不是类的一部分时,它们被分配在堆栈中,根本不需要垃圾回收。
  • 当结构是类(或任何引用类型)的一部分时,它们被存储在堆中。在这种情况下,它们是内联存储的,并且会随包含类型回收而回收。内联意味着该结构的数据是按原样存储的,这与引用类型相反,在引用类型中,指针被存储到堆上另一个位置。所以回收的成本要低很多。
  • 结构比引用类型占用的内存更少,因为它们没有 ObjectHeaderMethodTable

基准测试例子:

class VectorClass
{
public int X { get; set; }
public int Y { get; set; }
} struct VectorStruct
{
public int X { get; set; }
public int Y { get; set; }
} private const int ITEMS = 10000; [Benchmark]
public void WithClass()
{
VectorClass[] vectors = new VectorClass[ITEMS];
for (int i = 0; i < ITEMS; i++)
{
vectors[i] = new VectorClass();
vectors[i].X = 5;
vectors[i].Y = 10;
}
} [Benchmark]
public void WithStruct()
{
VectorStruct[] vectors = new VectorStruct[ITEMS];
// At this point all the vectors instances are already allocated with default values
for (int i = 0; i < ITEMS; i++)
{
vectors[i].X = 5;
vectors[i].Y = 10;
}
}

结果:

1. With Class - 742% (88.83 us)
2. With Struct - 100% (11.97 us)

ConcurrentQueue<T> 代替 ConcurrentBag<T>

在没有基准测试的情况下,不要使用 ConcurrentBag<T>。这个集合是为非常特殊的使用场景而设计的(当经常有项目被排队的线程删除时)。如果需要一个并发的集合队列,请选择 ConcurrentQueue<T>

基准测试例子:

private static int Size = 1000;

[Benchmark]
public void Bag()
{
ConcurrentBag<int> bag = new();
for (int i = 0; i < Size; i++)
{
bag.Add(i);
}
} [Benchmark]
public void Queue()
{
ConcurrentQueue<int> bag = new();
for (int i = 0; i < Size; i++)
{
bag.Enqueue(i);
}
}

结果:

1. ConcurrentBag - 165% (24.21 us)
2. ConcurrentQueue - 100% (14.64 us)

感谢大家阅读!

五个 .NET 性能小贴士的更多相关文章

  1. android性能小贴士 翻译

    转自http://developer.android.com/training/articles/perf-tips.html 性能小贴士: 这篇文档主要一些微优化可以提升应用程序性能,但是这些改变不 ...

  2. Elasticsearch 学习之提升性能小贴士

    小贴士1:规划索引.分片 以及集群增长情况 ES使得创建大量索引和超大量分片非常地容易,但更重要的是理解每个索引和分片都是一笔开销.如果拥有太多的索引或分片,单单是管理负荷就会影响到ES集群的性能,潜 ...

  3. 小贴士——提高PHP程序在NGINX代理服务器的性能

    NGINX本身就是面向最大性能的代理服务器,因此在使用NGINX,并没有性能调整的配置工作.但是却有很多选项可用于定制NGINX的行为,利用底层硬件和操作系统. 下面将介绍用于提供PHP在NGINX的 ...

  4. jprofiler8使用小贴士

    说明:本文的小贴士是针对jprofiler8的,其他版本上可能有不适用的地方 贴士一:使用jpenable监控,无需增加jvm参数和重启 贴士一:使用jpenable监控,无需增加jvm参数和重启 j ...

  5. Kafka设计解析(五)Kafka性能测试方法及Benchmark报告

    转载自 技术世界,原文链接 Kafka设计解析(五)- Kafka性能测试方法及Benchmark报告 摘要 本文主要介绍了如何利用Kafka自带的性能测试脚本及Kafka Manager测试Kafk ...

  6. Angular2 小贴士 Name

    Angular2 正式版已经发布了一个月了,我也是通过各种方式在进行验证是否可以满足我们的需求,今天我就发现了一个问题.现在我们来一起说明一下,这个可能不算是bug,而应该需要我们记住就可以了. 我们 ...

  7. 【小贴士】虚拟键盘与fixed带给移动端的痛!

    前言 今天来公司的主要目的就是研究虚拟键盘与fixed的问题,期间因为同事问起闭包与事件委托(阻止冒泡)相关问题,便穿插了一篇别的: [小贴士]工作中的”闭包“与事件委托的”阻止冒泡“,有兴趣的朋友可 ...

  8. SVN小贴士

    我辛辛苦苦写的到哪里了? SVN小贴士SVN服务器上的代码项目组公用,你的每一个提交都会体现给项目组每个人,所以提交要慎重,要注意避免代码冲突,使用SVN小贴士: 1.提前宣布开发计划,保持项目组成员 ...

  9. 初识bd时的一些技能小贴士

    既然小豆腐如此给力,而且充分的利用主动学习的优势,已经有了迅速脑补,压倒式的优势,不过这只是表面而已,一切才刚刚开始,究竟鹿死谁手,还有待验证. 以上可以看到,小豆腐为什么拼命的要teach我们了么, ...

随机推荐

  1. C++中头文件和实现文件的关系

    头文件相当于是声明的集合,包括头文件的语句#Include实质为程序代码的宏替换. 编译阶段将函数和变量登记在符号表,链接时将各种函数的入口地址在其中查找到来调用,解引用.

  2. eclipse左边的工程列表窗口不见了解决方案

    解决eclipse左边的工程列表窗口看不到工程目录的方法: Window->Show View->Project Explorer(如果没有Project Explorer选项,则Wind ...

  3. mybatis学习——映射器(mappers)

    在定义 SQL 映射语句之前,我们需要告诉 MyBatis 到哪里去找到这些语句. 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射 ...

  4. 【NX二次开发】Block UI 超级截面

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  5. 腾讯TencentOS 十年云原生的迭代演进之路

    导语 TencentOS Server (又名 Tencent Linux 简称 Tlinux) 是腾讯针对云的场景研发的 Linux 操作系统,提供了专门的功能特性和性能优化,为云服务器实例中的应用 ...

  6. 01 . Varnish简介,原理,配置缓存

    简介 Varnish是高性能开源的反向代理服务器和HTTP缓存服务器,其功能与Squid服务器相似,都可以用来做HTTP缓存.可以安装 varnish 在任何web前端,同时配置它缓存内容.与传统的 ...

  7. Golang学习(用代码来学习) - 第二篇

    type Circle struct { radius float64 desc string } //定义结构体里面的方法 func (c Circle) getArea() float64 { r ...

  8. Spring @Transaction 注解是如何执行事务的?

    前言 相信小伙伴一定用过 @Transaction 注解,那 @Transaction 背后的秘密又知道多少呢? Spring 是如何开启事务的?又是如何进行提交事务和关闭事务的呢? 画图猜测 在开始 ...

  9. Docker启动PostgreSQL时创建多个数据库

    1 前言 在文章<Docker启动PostgreSQL并推荐几款连接工具>中我们介绍如何通过Docker来启动PostgreSQL,但只有一个数据库,如果想要创建多个数据库在同一个Dock ...

  10. 一次性讲清楚spring中bean的生命周期之一:getSingleton方法

    要想讲清楚spring中bean的生命周期,真的是不容易,以AnnotationConfigApplicationContext上下文为基础来讲解bean的生命周期,AnnotationConfigA ...