2024-08-04 Rider 中使用 Benchmark 测试 C# 代码运行效率

1 运行 Benchmark

1.1 创建项目

​ 使用 Rider(2024.1.4 版本)创建新项目。注意是否勾选“将解决方案和项目放在同一目录中”,这会导致后续 Benchmark 运行目录不同。此处没有勾选。

image-20240803213728752

1.2 安装 Benchmark

  1. 点击左侧目录,打开 NuGet 包管理器。
image-20240803213954504
  1. 搜索 benchmark,找到 “BenchmarkDotNet” 并安装。
image-20240803214146076

1.3 编写测试脚本

​ 这里编写了两种判断字符串子串的方法:一种使用 Contains 方法判断,另一种使用 IndexOf 判断。

​ 通过为方法添加 [Benchmark] 特性,来标识这是我们想要测试的方法。

// See https://aka.ms/new-console-template for more information

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser(false)]
public class BenchmarkClass
{
    public string message = "Hello, World!";

    [Benchmark]
    public bool Contains() { // 测试字符串 Contains 方法
        return message.Contains("World");
    }

    [Benchmark]
    public bool IndexOf() { // 测试字符串 IndexOf 方法
        return message.IndexOf("World", StringComparison.Ordinal) != -1;
    }

    public static void Main(string[] args) {
        BenchmarkRunner.Run<BenchmarkClass>();
    }
}

1.4 运行 Benchmark

​ 打开终端。

  1. 创建项目时,如果**未勾选**“将解决方案和项目放在同一目录中”,则需要先进入项目所在目录。

    以本教程为例,此处需要先输入命令:cd .\BenchmarkSample\。

    如果**勾选**“将解决方案和项目放在同一目录中”,则已经在项目路径下,直接到第 2 步。

  2. 输入命令:dotnet run -c Release,即可运行 Benchmark 测试(Benchmark 需要在 Release 模式下才能运行)。

image-20240803215526252

​ 此处的结果比较如下:

image-20240803215950718

​ 可以看出,Contains 方法运行时间更短,效率更高。

1.5 查看运行日志

​ 以该项目为例,可以在 “BenchmarkSample -> BenchmarkSample -> BenchmarkDotNet.Artifacts” 文件夹中查看运行日志,运行结果在 results 目录下。

image-20240803220129465

2 BenchmarkDotNet 入门

以下内容参考视频 BenchmarkDotNet简易入门指南,感谢 十月的寒流

2.1 基础知识

  1. 需使用编译器优化的 Release 模式。
  2. 测试类、测试方法必须为 public。
  3. 为方法添加 [Benchmark] 特性,来标识想要测试的方法。
  4. Benchmark 运行时通过反射读取测试代码,额外开启一些进程运行。
  5. 在测试方法中,尽量避免被 JIT 优化掉的情况。
    • 例如:声明一个未被使用的变量,则 Release 模式下该声明会被优化掉。
    • 解决方法:将该变量作为方法返回值进行返回。
    • 建议:测试方法尽量都给予返回值。
  6. 一般不必创建过大的数组,除非想测试内存读取等。
    • 原因:测试目标一般是 CPU 运行算法的效率,而不是 IO 内存读取。

注意:

  1. 以下案例测试结果均为 .Net 8.0 下。
  2. 运行环境:
    • BenchmarkDotNet:v0.13.12;
    • Windows 11;
    • CPU:Intel Core i7-12700H。

2.2 案例:List 排序

​ 创建 ListOrderTest.cs 脚本,编写测试案例。将 1.3 节中的 Main() 函数内容移到 Program.cs 中。

​ 选取以下 3 种方式比较数组排序方法的运行效率:

  1. 使用 List.Sort() 直接排序;
  2. 使用 Linq.OrderBy() 排序,比较方法为 int 本身的值大小;
  3. 使用 Linq.Order() 排序。

​ 测试方法如下:

// See https://aka.ms/new-console-template for more information

using BenchmarkDotNet.Attributes;

public class ListOrderTest
{
    private List<int> _testList;

    [Benchmark]
    public List<int> ListSort() {
        var list = new List<int>(_testList);
        list.Sort();
        return list;
    }

    [Benchmark]
    public List<int> LinqOderBy() {
        return _testList.OrderBy(x => x).ToList();
    }
    
    [Benchmark]
    public List<int> LinqOder() {
        return _testList.Order().ToList();
    }
}

2.2.1 [GlobalSetup] 测试初始化

​ 使用 [GlobalSetup] 特性标注一个方法为测试开始前的初始化方法。本案例中,对 _testList 进行初始化,包含 100 个 0 ~ 100 的随机数。

[GlobalSetup] // 指定测试开始前的初始化方法
public void Setup() {
    _testList = new List<int>();
    var random = new Random();
    for (int i = 0; i < 100; i++) {
        _testList.Add(random.Next(0, 100));
    }
}

2.2.2 [Benchmark(Baseline = true)] 指定基准方法

​ 为 [Benchmark] 特性指定 Baseline = true 特性指定 LinqOderBy() 方法为比较基准,其他两个方法都和该方法进行比较。

[Benchmark(Baseline = true)]
public List<int> LinqOderBy() {
    return _testList.OrderBy(x => x).ToList();
}

2.2.3 [MemoryDiagnoser] 显示内存分配

​ 为测试类 ListOrderTest 添加 [MemoryDiagnoser] 特性,来显示测试方法的内存分配情况。

// See https://aka.ms/new-console-template for more information

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]  // 显示内存分配
public class ListOrderTest
{
    ...
}

2.2.4 结果分析

image-20240803233223076

​ 可以看出,ListSort() 和 LinqOder() 的效率几乎一致,远远领先 LinqOderBy() 方法。

  1. 由于指定了基准方法,因此 Ratio 和 Alloc Ratio 两栏中,LinqOderBy() 值均为 1。
  2. [MemoryDiagnoser] 添加了 Gen0、Allocated 和 Alloc Ratio 三栏,但 Gen0 一般不常用,因此可以写成 [MemoryDiagnoser(false)] 以隐藏。

完整代码:

namespace BenchmarkSample.Benchmark;

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser(false)]  // 显示内存分配
public class ListOrderTest
{
    private List<int> _testList;

    [GlobalSetup] // 指定测试开始前的初始化方法
    public void Setup() {
        _testList = new List<int>();
        var random = new Random();
        for (int i = 0; i < 100; i++) {
            _testList.Add(random.Next(0, 100));
        }
    }

    [Benchmark]
    public List<int> ListSort() {
        var list = new List<int>(_testList);
        list.Sort();
        return list;
    }

    [Benchmark(Baseline = true)]
    public List<int> LinqOderBy() {
        return _testList.OrderBy(x => x).ToList();
    }
    
    [Benchmark]
    public List<int> LinqOder() {
        return _testList.Order().ToList();
    }
}

2.3 案例:指定 List 容量初始化

​ 我们知道,List 具有扩容机制。因此,本案例比较创建 List 时,是否指定容量(capacity)对创建效率的影响。

​ 选取 3 种方法进行比较:

  1. 初始化时,不指定 capacity(默认 capacity 为 4);
  2. 初始化时,指定 capacity;
  3. 使用 Linq 中的 Enumerable.Range() 方法初始化。

​ 测试方法如下:

namespace BenchmarkSample.Benchmark;

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser(false)]
public class ListCapacityInitTest
{
    public int Count;

    [Benchmark(Baseline = true)]
    public List<int> WithoutInit() {
        var res = new List<int>();
        for (int i = 0; i < Count; i++) {
            res.Add(i);
        }
        return res;
    }
    
    [Benchmark]
    public List<int> WithInit() {
        var res = new List<int>(Count);
        for (int i = 0; i < Count; i++) {
            res.Add(i);
        }
        return res;
    }
    
    [Benchmark]
    public List<int> WithLinq() {
        return Enumerable.Range(0, Count).ToList();
    }
}

2.3.1 [Params] 多值测试

​ 为 Count 添加属性 [Params],初始化为 3 个值,分别进行测试比较。

[Params(4, 16, 130)] // 指定测试参数
public int Count;

2.3.2 结果分析

image-20240803233145228

​ 可以看出,指定 capacity 对于容量大的 List 而言运行速度更快,因为不需要频繁地进行扩容。

​ 而使用 Linq 方法则更有普适性,推荐使用该方法初始化 List。

完整代码:

namespace BenchmarkSample.Benchmark;

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser(false)]
public class ListCapacityInitTest
{
    [Params(4, 16, 130)] // 指定测试参数
    public int Count;

    [Benchmark(Baseline = true)]
    public List<int> WithoutInit() {
        var res = new List<int>();
        for (int i = 0; i < Count; i++) {
            res.Add(i);
        }
        return res;
    }
    
    [Benchmark]
    public List<int> WithInit() {
        var res = new List<int>(Count);
        for (int i = 0; i < Count; i++) {
            res.Add(i);
        }
        return res;
    }
    
    [Benchmark]
    public List<int> WithLinq() {
        return Enumerable.Range(0, Count).ToList();
    }
}

2.4 案例:翻转字符串

​ 对比翻转字符串的 3 种方式:

  1. 使用 Linq 中 string.Reverse().ToArray() 翻转为字符数组,再创建新 string;
  2. 使用 StringBuilder 辅助,手动翻转;
  3. 使用 string.ToCharArray() 转为字符数组,翻转后再创建新 string。

​ 测试方法如下:

namespace BenchmarkSample.Benchmark;

using System.Text;
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser(false)]
public class StringReverseTest
{
    private string _testStr = "Hello World!";

    [Benchmark]
    public string Linq() {
        return new string(_testStr.Reverse().ToArray());
    }

    [Benchmark]
    public string StringBuilder() {
        var sb = new StringBuilder();
        for (int i = _testStr.Length - 1; i >= 0; i--) {
            sb.Append(_testStr[i]);
        }
        return sb.ToString();
    }

    [Benchmark]
    public string CharArray() {
        var array = _testStr.ToCharArray();
        Array.Reverse(array);
        return new string(array);
    }
}

2.4.1 [Orderer] [RankColumn] 结果排序显示

​ 为测试类 StringReverseTest 添加特性 [Orderer] 以将结果排序显示,通过指定参数来选择排序方式。

​ 同时,添加特性 [RankColumn] 以显示排名列。

[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)] // 结果排序显示,从快到慢
[RankColumn]                                   // 显示排名列
public class StringReverseTest
{
    ...
}

2.4.2 结果分析

image-20240803233105812

​ 可以看出,string.ToCharArray() 方法效率最高,但一般推荐使用 StringBuilder,因为性能足够,且更为泛用。不出意料,Linq 方法效率最低。

完整代码:

namespace BenchmarkSample.Benchmark;

using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;

[MemoryDiagnoser(false)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)] // 结果排序显示,从快到慢
[RankColumn]                                   // 显示排名列
public class StringReverseTest
{
    private string _testStr = "Hello World!";

    [Benchmark]
    public string Linq() {
        return new string(_testStr.Reverse().ToArray());
    }

    [Benchmark]
    public string StringBuilder() {
        var sb = new StringBuilder();
        for (int i = _testStr.Length - 1; i >= 0; i--) {
            sb.Append(_testStr[i]);
        }
        return sb.ToString();
    }

    [Benchmark]
    public string CharArray() {
        var array = _testStr.ToCharArray();
        Array.Reverse(array);
        return new string(array);
    }
}

3 其他 Tip

3.1 [SimpleJob] 不同 .Net 版本测试

​ 使用特性 [SimpleJob] 指定比较的版本。

[MemoryDiagnoser(false)]
[SimpleJob(RuntimeMoniker.Net70)] // 比较 .Net7.0 和 8.0
[SimpleJob(RuntimeMoniker.Net80)]
public class CSharpVersionTest
{
    ...
}

3.2 [IterationSetup] 迭代初始化

​ 每次迭代前,使用特性 [IterationSetup] 指定初始化方法。

[IterationSetup]
public void IterationSetup() { }

3.3 [Arguments] 传参测试

​ 使用特性 [Arguments] 为有参方法传递测试参数。

​ 一次 [Arguments] 即测试一组样例,可与 [Params] 排列进行组合测试。

[Benchmark]
[Arguments(100, 0.5f)]
[Arguments(200, 2.5f)]
public void ArgumentsFunc(int a, float b) { }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔗理苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值