在 .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>
的記憶化遞歸則在保留遞歸風格的同時大幅提升了性能。
最終,這一流程不僅驗證了各類斐波那契實現的實際性能差異,也為實際項目中選擇合適的算法提供了可靠的數據支撐。
項目準備
- 項目環境
<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>
- 斐波那契數列實現
// =============================
// 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
測試類
// =============================
// 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
文件中添加如下代碼:
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
執行如下命令:
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 | ? 快 | 無分配 | ? 推薦保留遞歸風格時使用 |