网站首页 > 基础教程 正文
简介
了解我们的代码性能是开发的关键部分。我们力求在保持代码可读性的同时,编写最优化和性能最佳的代码。
在这篇文章中,我将向大家展示如何测试我们代码的性能,对我们的代码进行基准测试。
什么是基准测试
基准测试是在特定条件下衡量我们的代码、应用、系统或硬件的性能。
其目标是收集精确的数据,了解系统在处理速度、内存使用、资源消耗或吞吐量等指标上的表现,并确定哪些区域的性能可以被优化。
为什么使用Stopwatch不可靠
在C#中使用Stopwatch类进行基准测试有很多问题。尽管它为测量方法或过程的经过时间提供了一种简单的方式,但它缺乏精确的基准测试所需的精确度、控制和一致性。
在我讲述这个工具的不足之前,让我们看看我们如何可以用它来完成一些非常简单的任务。
using System.Diagnostics;
// 创建一个Stopwatch实例
var sw = new Stopwatch();
// 开始计时
sw.Start();
// 运行代码
var sum = 0;
for (int i = 0; i < 100; i++)
{
sum += i * i;
Console.WriteLine(#34;{sw.ElapsedMilliseconds}");
}
// 停止计时
sw.Stop();
// 打印使用时间
Console.WriteLine(#34;Elapsed time: {sw.ElapsedMilliseconds} ms");
这将打印出每次迭代过去的毫秒数以及最后经过的毫秒数。由于这是一个短程序,我们可以通过使用ticks来转换为纳秒,如下所示:
long ticks = stopwatch.ElapsedTicks;
double nanoseconds = (ticks * 1e9) / Stopwatch.Frequency;
如果我们想快速比较两种方法或在开发过程中识别明显的性能瓶颈,使用Stopwatch可能很有用。这是一种轻量级的方法,可以初步了解可能需要优化的代码部分。
Stopwatch的弊端
- 默认情况下缺乏精确度,只能精确到大约100纳秒,这对于较小的快速微操作可能没有用。
- JIT(即时)编译 - 当代码第一次运行时,JIT编译器在运行代码之前先编译代码,这会导致延迟并错误的衡量完成的时间。后续运行的代码会稍微快一些,然而,Stopwatch并没有考虑到这一点。记住这一点,我们可以通过多次运行代码以求缓解这个问题。
- 垃圾收集(GC) - 如果在Stopwatch测量期间发生垃圾收集,记录的时间将包括GC暂停时间,这并不能反映我们的代码的实际执行时间。
这些只是使用Stopwatch测试代码性能的一些基本和最常见的缺陷,但还有其他的。
那么,最佳的方法是什么?
BenchmarkDotNet是一个在.NET中进行基准测试的库,它非常的流行,可以通过nuget进行安装。
它以以下方式克服了许多上述挑战:
- 代码预热 - 自动预热代码(通过运行代码几次)以避免与JIT相关的不准确性。
- 多次代码迭代 - 多次运行代码以分析和计算统计汇总,包括执行时间、堆内存分配等。可以配置运行代码的次数。
- 隔离环境 - 管理垃圾收集并隔离执行环境以减少外部干扰。
如何使用BenchMarkDotnet
首先,我们需要安装Nuget包。要做到这一点,我们需要在命令行/终端中运行以下命令:
dotnet add package BenchmarkDotnet
然后我们需要一些方法来进行基准测试,所以创建一个.Net 8 C#控制台应用程序,包含以下两个类文件:
// Program.cs
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<Benchmarks>();
//Benchmarks.cs
using BenchmarkDotNet.Attributes;
public class Benchmarks
{
private readonly int[] _numbers = Enumerable.Range(1, 1000).ToArray();
[Benchmark]
public int ForLoopSum()
{
int sum = 0;
for (int i = 0; i < _numbers.Length; i++)
{
sum += _numbers[i];
}
return sum;
}
[Benchmark]
public int ForeachLoopSum()
{
var sum = 0;
foreach (int number in _numbers)
{
sum += number;
}
return sum;
}
[Benchmark]
public int LinqSelect()
{
return _numbers.Sum();
}
}
以上,我们有三种不同的方法来累加一个整数数组,每种方法都以略有不同的方式进行。这是一个完美的例子,展示了基准测试如何帮助我们在代码库中选择最佳解决方案。
如何运行基准测试
要运行基准测试,我们可以在终端/命令行中运行以下命令。
dotnet build
dotnet run -c Release
然后,BenchmarkDotnet将多次运行标有 [Benchmark] 属性的方法,并将结果以易于阅读的表格形式输出,如下:
Method | Mean | Error | StdDev |
ForLoopSum | 434.2 ns | 0.40 ns | 0.31 ns |
ForeachLoopSum | 321.9 ns | 1.22 ns | 1.14 ns |
LinqSelect | 189.4 ns | 0.84 ns | 0.70 ns |
- Method - 测试中的方法名称
- Mean - 显示以纳秒计的平均时间。
- Error - 表示误差范围,告诉我们由于系统中的随机因素,“平均值”结果可能会有多大的变化。这个数字越低越好,这里我们可以看到非常小的误差范围,意味着结果是稳定的,而大的数字则意味着更多的不确定性/不可靠的结果。
- StdDev - 显示基准测试结果的一致性。低偏差分数表明在多次运行中所花费的时间非常相似,增加了可靠性。如果标准偏差很高,那就意味着方法的执行时间在多次运行之间变化很大。
如何测量内存分配
了解我们方法的运行速度是很有必要的。然而,我们的性能和优化不仅仅关于执行时间,有时我们应该确保没有内存泄漏或大量的内存被使用,尤其是在大规模的执行过程中。 我们可以将 [MemoryDiagnoser] 应用到基准测试类,这会通知基准测试库将内存统计信息包含到被测试的方法中。
当我们运行我们的基准测试时,我们得到以下输出:
Method | Mean | Error | StdDev | Allocated |
ForLoopSum | 436.8 ns | 5.32 ns | 4.98 ns | - |
ForeachLoopSum | 324.6 ns | 2.20 ns | 2.06 ns | - |
LinqSelect | 192.7 ns | 2.40 ns | 2.24 ns | - |
但等等,"Allocated"列只有一个破折号,这是为什么?结果在哪里?
像数组求和这样的简单操作通常不会分配内存,因为它们通常只使用栈内存,而BenchmarkDotNet并不以相同的方式跟踪栈内存。 但是,使用以下测试,我们可以看到如何分析内存分配:
public class MemoryBenchmark
{
[Benchmark]
public string StringConcatenation()
{
string result = "";
for (int i = 0; i < 1000; i++)
{
result += "text";
}
return result;
}
[Benchmark]
public string StringBuilderConcatenation()
{
var builder = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
{
builder.Append("text");
}
return builder.ToString();
}
}
输出:
Method | Mean | Error | StdDev | Gen0 | Allocated |
StringConcatenation | 218.930 us | 0.7230 us | 0.6409 us | 641.8457 | 3933.56 KB |
StringBuilderConcatenation | 1.645 us | 0.0034 us | 0.0030 us | 2.6875 | 16.47 KB |
这里我们有两个新的列:
- Gen0列:
Gen0列指示在每个方法的执行期间发生了多少次Gen0垃圾收集。
.Net使用一个分代垃圾收集系统,其中内存被分为三个"代"(Gen0,Gen1,和Gen2)。 - Gen0(第0代):保存短期对象,如临时变量和小的、快速丢弃的对象。Gen0收集是GC最快的类型,但仍会引入一些开销。Gen0的例子包括方法中的局部变量、临时对象,或者后面不再使用的方法调用参数。
- Gen1和Gen2:这是用于生命周期较长的对象,这些对象在Gen0收集后依然存在,如在应用程序的生命周期内保持活动的静态对象(即,单例),缓存对象或在许多操作中使用的大型集合。
- Gen0中的对象被快速但频繁地收集,而Gen2中的对象被不频繁但更费力地收集,因为它们更大或更持久。大量的Gen0收集可能是内存使用效率低下的指标,而Gen2或3的收集可能表明我们的应用程序在内存中保持了太多的长期对象。
- Allocated列:
Allocated列显示每个方法在执行期间分配的总内存。这通常以千字节(KB)为单位报告。
这些信息帮助我们了解每个方法的内存密集程度,这可能会影响性能,特别是如果该方法被频繁调用。
例如,StringBuilderConcatenation比StringConcatenation更节省内存,所以在内存使用有限制或者这个操作被频繁执行的情况下,它更可取。
我们还可以用BenchmarkDotnet测试什么?
吞吐量
- 分析每秒可以执行多少次方法的迭代。
- 表示代码的效率和可扩展性。
JIT(即时)优化影响
- 评估JIT优化对性能的影响。
- 可以测试冷启动(首次运行性能)与稳定状态性能(后续运行)。
平台和框架的差异
我们可以在不同的.NET运行时(例如,.NET 6,.NET 8,.NET Framework)上运行相同代码的基准测试,以比较是否值得将我们的应用程序升级到新的系统。
只需在应用程序的.csproj文件中更新TargetFramework节点,以指向我们希望测试的框架。
在我们的基准测试类中添加以下属性(基于目标运行时)。
[SimpleJob(runtimeMoniker: RuntimeMoniker.Net60)]
[SimpleJob(runtimeMoniker: RuntimeMoniker.Net80)]
当我们运行我们的应用程序时,我们会得到如下的输出,突出显示了.net 6和.net 8在方法上的差异。
Method | Job | Runtime | Mean | Error | StdDev |
StringConcatenation | .NET 6.0 | .NET 6.0 | 286.503 us | 3.5004 us | 3.1030 us |
StringBuilderConcatenation | .NET 6.0 | .NET 6.0 | 4.595 us | 0.0620 us | 0.0580 us |
StringConcatenation | .NET 8.0 | .NET 8.0 | 222.270 us | 1.7561 us | 1.4664 us |
StringBuilderConcatenation | .NET 8.0 | .NET 8.0 | 1.650 us | 0.0139 us | 0.0116 us |
输入参数的影响
- 支持参数化基准测试,以测试不同输入如何影响性能。
- 有助于确定最优输入范围或问题边缘案例。
我们可以这样做:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class SortBenchmark
{
[Params(10, 100, 1000)] // 数组大小
public int N;
[Params(10, 100, 1000)] // 数组最大数值
public int MaxValue;
private int[] data;
[GlobalSetup]
public void Setup()
{
data = new int[N];
var rand = new Random();
for (int i = 0; i < N; i++)
{
data[i] = rand.Next(MaxValue);
}
}
[Benchmark]
public void SortArray()
{
Array.Sort(data); // 排序
}
}
class Program
{
static void Main(string[] args)
{
// 运行 benchmark
BenchmarkRunner.Run<SortBenchmark>();
}
}
第三方库性能
使用上述技术,我们可以比较完成相同任务的不同第三方库的性能,以便在库使用上做出正确的决策。
结论
以上就是如何对我们的C#应用程序进行基准测试。使用这些方法、工具和技术的组合,可以给我们提供多样化的基准测试。 我们可以使用基准测试来改进我们的应用程序的代码库,帮助做出升级路径和方法选择的决策。
这就是这篇关于benchmarking的介绍,不知道是否对大家有帮助?欢迎大家留言告诉我^_^
猜你喜欢
- 2024-12-28 CSnakes:在.NET项目中嵌入Python代码的工具
- 2024-12-28 巧用泛型设计模式,提升代码质量新高度
- 2024-12-28 巅峰对决!Spring Boot VS .NET 6 巅峰对决之干碎龙王短剧全集完整版第5集
- 2024-12-28 基于C#开发的物联网设备通讯协议客户端终身开源免费
- 2024-12-28 C# Flurl 库浅析(一) c#folderbrowserdialog
- 2024-12-28 C# Lazy的缺点 c#的介绍
- 2024-12-28 微服务——webapi实现,脱离iis,脱离tomcat
- 2024-12-28 231.C# 跨平台服务开发 c++跨平台开发
- 2024-12-28 C# 和 .NET 开发的 10 种基本模式
- 2024-12-28 基于C# 开发的物联网设备通讯协议客户端
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)