在 .NET
平台上实现 斐波那契数列
并使用 BenchmarkDotNet
进行性能测试,是评估不同算法实现方式性能表现的一种高效且标准化的方法。通过该方式,可以对比递归、迭代、记忆化递归以及结合高性能优化技术(如 Span<T>
、Memory<T>
和 ArrayPool<T>
)的多种实现,在不同输入规模下的执行时间、内存分配和垃圾回收行为。
整个过程包括:
-
选择合适的斐波那契实现方式:
- 递归实现:直观但效率低下,尤其在大数值时存在指数级时间复杂度。
- 迭代实现:性能最优,适用于大多数生产环境。
- 记忆化递归:通过缓存减少重复计算,提升递归效率。
- 结合 ArrayPool 的记忆化递归 :避免频繁内存分配,降低
GC
压力。 - 使用
Span<T>
或Memory<T>
的实现:进一步优化内存访问效率,支持更灵活的异步或池化操作。
-
构建基准测试类 :
使用
BenchmarkDotNet
提供的[Benchmark]
特性对每个实现方法进行标注,并通过[Params]
指定多个输入值(如N = 10, 30, 40
),以模拟不同场景下的运行情况。 -
启用诊断功能 :
在基准测试类上添加
[MemoryDiagnoser]
等特性,启用内存统计功能,获取每次调用的堆内存分配信息,帮助识别潜在的性能瓶颈。 -
运行基准测试 :
使用
BenchmarkRunner.Run<T>()
启动测试,生成结构化的性能报告,包含 平均耗时(Mean)、误差范围(Error)、标准差(StdDev)、Gen0/Gen1 垃圾回收次数及内存分配量 等关键指标。 -
分析结果并优化实现 :
根据测试报告数据,判断哪种实现方式在特定场景下具有最佳性能表现。例如,迭代法通常最快且无内存分配,而结合
ArrayPool<T>
的记忆化递归则在保留递归风格的同时大幅提升了性能。
最终,这一流程不仅验证了各类斐波那契实现的实际性能差异,也为实际项目中选择合适的算法提供了可靠的数据支撑。
项目准备
- 项目环境
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Datadog.Trace.BenchmarkDotNet" Version="2.61.0" />
</ItemGroup>
</Project>
- 斐波那契数列实现
csharp
// =============================
// FibonacciSequence 斐波那契数列实现
// =============================
using System.Buffers;
namespace FibonacciSequenceTest;
internal class FibonacciSequence
{
// 递归实现(效率低)
public static long Recursive(int n)
{
if (n <= 1) return n;
return Recursive(n - 1) + Recursive(n - 2);
}
// 迭代实现(高效)
public static long Iterative(int n)
{
if (n <= 1) return n;
long a = 0, b = 1;
for (int i = 2; i <= n; i++)
{
long temp = a + b;
a = b;
b = temp;
}
return b;
}
// 带缓存的递归实现(记忆化)
public static long Memoized(int n)
{
var memo = new long[n + 1];
return FibMemo(n, memo);
}
private static long FibMemo(int n, long[] memo)
{
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
memo[n] = FibMemo(n - 1, memo) + FibMemo(n - 2, memo);
return memo[n];
}
// 使用 ArrayPool 优化的记忆化递归实现
public static long MemoizedWithPooling(int n)
{
// 从 ArrayPool 获取足够大小的缓存数组
int length = n + 1;
var memo = ArrayPool<long>.Shared.Rent(length);
try
{
return FibMemo(n, memo);
}
finally
{
// 用完后归还数组,避免内存泄漏
if (memo != null)
ArrayPool<long>.Shared.Return(memo);
}
}
// 使用 ArrayPool + Span 优化的记忆化递归实现
public static long MemoizedWithSpan(int n)
{
int length = n + 1;
var memo = ArrayPool<long>.Shared.Rent(length);
try
{
return FibMemoWithSpan(n, memo.AsSpan());
}
finally
{
if (memo != null)
ArrayPool<long>.Shared.Return(memo);
}
}
private static long FibMemoWithSpan(int n, Span<long> memo)
{
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
memo[n] = FibMemoWithSpan(n - 1, memo) + FibMemoWithSpan(n - 2, memo);
return memo[n];
}
// 使用 ArrayPool + Memory 优化的记忆化递归实现
public static long MemoizedWithMemory(int n)
{
int length = n + 1;
var memo = ArrayPool<long>.Shared.Rent(length);
try
{
return FibMemoWithMemory(n, memo.AsMemory());
}
finally
{
if (memo != null)
ArrayPool<long>.Shared.Return(memo);
}
}
private static long FibMemoWithMemory(int n, Memory<long> memo)
{
if (n <= 1) return n;
// 将 Memory<long> 转换为 Span<long>,以支持索引操作
Span<long> span = memo.Span;
if (span[n] != 0) return span[n];
span[n] = FibMemoWithMemory(n - 1, memo) + FibMemoWithMemory(n - 2, memo);
return span[n];
}
}
FibonacciSequence
测试类
csharp
// =============================
// FibonacciSequence 测试类
// =============================
using BenchmarkDotNet.Attributes;
using Datadog.Trace.BenchmarkDotNet;
namespace FibonacciSequenceTest;
[DatadogDiagnoser]
[MemoryDiagnoser]
public class FibonacciBenchmark
{
[Params(10, 30, 40)] // 测试不同的 n 值
public int N { get; set; }
[Benchmark]
public long RecursiveFibonacci() => FibonacciSequence.Recursive(N);
[Benchmark]
public long IterativeFibonacci() => FibonacciSequence.Iterative(N);
[Benchmark]
public long MemoizedFibonacci() => FibonacciSequence.Memoized(N);
[Benchmark]
public long MemoizedWithPoolingFibonacci() => FibonacciSequence.MemoizedWithPooling(N);
[Benchmark]
public long MemoizedWithSpanFibonacci() => FibonacciSequence.MemoizedWithSpan(N);
[Benchmark]
public long MemoizedWithMemoryFibonacci() => FibonacciSequence.MemoizedWithMemory(N);
}
- 使用基准测试
在 Program.cs
文件中添加如下代码:
csharp
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Datadog.Trace.BenchmarkDotNet;
namespace FibonacciSequenceTest;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, SortingBenchmark!");
var fibonacciSummary = BenchmarkRunner.Run<FibonacciBenchmark>();
}
}
启动测试
进入项目,使用 pwsh
执行如下命令:
bash
dotnet run -c Release
这段文本是一个使用 BenchmarkDotNet 工具对不同 斐波那契数列(Fibonacci)算法实现 的性能基准测试结果报告。它对比了多种实现方式在不同输入规模 N
下的执行效率、内存分配等指标。

以下是关键内容的通俗解释:
📊 表格结构说明
列名 | 含义 |
---|---|
Method |
测试的方法名称(不同的 Fibonacci 实现) |
N |
输入参数,表示求第 N 个斐波那契数 |
Mean |
平均耗时(单位:纳秒 ns 或 微秒 μs) |
Error |
置信区间的一半(99.9% 置信度) |
StdDev |
标准差,衡量运行时间波动 |
Median |
中位数,排除极端值后的时间 |
Gen0 |
Gen0 垃圾回收次数(每千次操作) |
Allocated |
每次操作分配的托管内存大小 |
🧪 被测试的斐波那契实现方法
方法名 | 实现方式 | 是否推荐 |
---|---|---|
RecursiveFibonacci |
普通递归(无优化) | ❌ 不推荐 |
IterativeFibonacci |
迭代法(最高效) | ✅ 强烈推荐 |
MemoizedFibonacci |
使用数组缓存的记忆化递归 | ⚠️ 可用但有内存分配 |
MemoizedWithPoolingFibonacci |
使用 ArrayPool<long> 缓存数组优化 |
✅ 推荐 |
MemoizedWithSpanFibonacci |
使用 ArrayPool<long> + Span<long> + 缓存 |
✅ 推荐 |
MemoizedWithMemoryFibonacci |
使用 ArrayPool<long> + Memory<long> + 缓存 |
✅ 推荐 |
📈 性能对比分析(按 N 分组)
当 N = 10
时:
方法 | 平均耗时 | 内存分配 |
---|---|---|
RecursiveFibonacci | 251.435 ns | - |
IterativeFibonacci | 7.234 ns | - |
MemoizedFibonacci | 63.627 ns | 112 B |
MemoizedWithPoolingFibonacci | 18.526 ns | - |
MemoizedWithSpanFibonacci | 21.416 ns | - |
MemoizedWithMemoryFibonacci | 20.367 ns | - |
📌 结论:
- 迭代法最快(仅 7ns)
- 普通递归较慢
- 使用池化或 Span/ Memory 优化后的记忆化递归显著优于普通递归
当 N = 30
时:
方法 | 平均耗时 | 内存分配 |
---|---|---|
RecursiveFibonacci | 3,372,317 ns(3.37ms) | - |
IterativeFibonacci | 26.832 ns | - |
MemoizedFibonacci | 301.255 ns | 272 B |
MemoizedWithPoolingFibonacci | 18.624 ns | - |
MemoizedWithSpanFibonacci | 19.883 ns | - |
MemoizedWithMemoryFibonacci | 24.130 ns | - |
📌 结论:
- 普通递归性能急剧下降(指数级增长)
- 其他优化方法依然保持稳定低耗时
- 迭代法仍是最快
当 N = 40
时:
方法 | 平均耗时 | 内存分配 |
---|---|---|
RecursiveFibonacci | 416,127,408 ns(约 416ms) | - |
IterativeFibonacci | 35.565 ns | - |
MemoizedFibonacci | 436.763 ns | 352 B |
MemoizedWithPoolingFibonacci | 18.548 ns | - |
MemoizedWithSpanFibonacci | 19.698 ns | - |
MemoizedWithMemoryFibonacci | 20.206 ns | - |
📌 结论:
- 普通递归已变得不可接受
- 所有优化版本仍保持微秒级响应
- 迭代法依旧最优
✅ 总结与建议
特性 | 推荐实现 |
---|---|
最快实现 | IterativeFibonacci (迭代法) |
最节省内存 | MemoizedWithPoolingFibonacci (结合 ArrayPool) |
支持异步和长期持有 | MemoizedWithMemoryFibonacci |
保留递归风格又兼顾性能 | MemoizedWithPoolingFibonacci 或 MemoizedWithSpanFibonacci |
📝 小结
方法 | 性能 | 内存 | 是否推荐 |
---|---|---|---|
普通递归 | ❌ 极慢 | 低 | 否 |
迭代法 | ✅ 极快 | 无分配 | ✅ 强烈推荐 |
记忆化递归 | ⚠️ 中等 | 高 | 一般 |
记忆化 + ArrayPool/Span/Memory | ✅ 快 | 无分配 | ✅ 推荐保留递归风格时使用 |