diff --git a/.gitignore b/.gitignore index 0a17e0b..65cdb76 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,4 @@ FakesAssemblies/ _bin/ _Nuget/NuGet.exe snks/*.snk +BenchmarkDotNet.Artifacts/ diff --git a/benchmark/JiebaNet.ForkBench/JiebaNet.ForkBench.csproj b/benchmark/JiebaNet.ForkBench/JiebaNet.ForkBench.csproj new file mode 100644 index 0000000..f8c63a4 --- /dev/null +++ b/benchmark/JiebaNet.ForkBench/JiebaNet.ForkBench.csproj @@ -0,0 +1,31 @@ + + + + Exe + net10.0 + JiebaNet.Benchmark + JiebaNet.ForkBench + disable + disable + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/JiebaNet.UpstreamBench/JiebaNet.UpstreamBench.csproj b/benchmark/JiebaNet.UpstreamBench/JiebaNet.UpstreamBench.csproj new file mode 100644 index 0000000..e2c14fb --- /dev/null +++ b/benchmark/JiebaNet.UpstreamBench/JiebaNet.UpstreamBench.csproj @@ -0,0 +1,33 @@ + + + + Exe + net10.0 + JiebaNet.Benchmark + JiebaNet.UpstreamBench + disable + disable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..b25db14 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,28 @@ +# Benchmark + +对比本仓库(fork)与 NuGet 上的上游 [jieba.NET 0.42.2](https://www.nuget.org/packages/jieba.NET) 的性能。 + +## 布局 + +- `Shared/BenchSuite.cs` — 共享的 BenchmarkDotNet 套件(分词精确/全模式/HMM/搜索引擎、词性标注、关键词提取,共 12 个场景 + 冷启动),分别链接进两个项目。两个库的类型名完全相同,无法在单项目中同时引用,因此采用双项目布局。 +- `JiebaNet.ForkBench` — 引用本仓库源码。 +- `JiebaNet.UpstreamBench` — 引用 NuGet 包 `jieba.NET 0.42.2`,并自动把仓库 `src/Segmenter/Resources` 复制到输出目录(上游从磁盘加载词典),保证两边数据一致。 + +## 运行 + +```powershell +cd benchmark +.\run.ps1 # 双方全场景 + 对比表 +.\run.ps1 -Side fork # 只跑本仓库 +.\run.ps1 -Filter "*medium*" # 只跑部分场景(BDN 过滤器) +``` + +脚本会:串行执行双方(避免 CPU 争抢)→ 打印 BDN 原始表格 → 各测 3 次冷启动(词典/模型加载)→ 输出按场景合并的对比表(`upstream / fork` 比值 > 1 表示 fork 更快)。 + +原始结果(CSV/Markdown)在各项目的 `BenchmarkDotNet.Artifacts/results/` 目录。 + +## 说明 + +- 套件使用 `ShortRunJob`(3 次预热 + 3 次迭代)+ `MemoryDiagnoser`:适合优化前后的方向性对比;如需发布级性能数据,把 `BenchSuite.cs` 上的 `[ShortRunJob]` 移除即可使用默认 Job。 +- 新增场景:在 `Shared/BenchSuite.cs` 加 `[Benchmark]` 方法即可,两个项目自动共享。 +- `Program.cs cold` 模式用于测量真正的冷启动(BDN 的预热会把词典加载排除在统计之外)。 diff --git a/benchmark/Shared/BenchSuite.cs b/benchmark/Shared/BenchSuite.cs new file mode 100644 index 0000000..40ba1db --- /dev/null +++ b/benchmark/Shared/BenchSuite.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using JiebaNet.Analyser; +using JiebaNet.Segmenter; +using JiebaNet.Segmenter.PosSeg; + +namespace JiebaNet.Benchmark +{ + // Runs in its own (fresh) process: measures true cold start, including + // dictionary/model loading (embedded streams in this fork, files on disk upstream). + [ShortRunJob] + [MemoryDiagnoser] + public class InitBench + { + [Benchmark(Description = "Cold init + first cut")] + public string InitAndFirstCut() + { + var seg = new JiebaSegmenter(); + return string.Join("/", seg.Cut("我来到北京清华大学")); + } + } + + [ShortRunJob] + [MemoryDiagnoser] + public class BenchSuite + { + private const string ShortText = + "我来到北京清华大学,打算去附近的餐馆吃晚饭,然后再回实验室继续写代码。"; + + private string _medium; + private string _large; + private JiebaSegmenter _seg; + private PosSegmenter _pos; + private TfidfExtractor _tfidf; + private TextRankExtractor _textrank; + + [GlobalSetup] + public void Setup() + { + var input = Path.Combine(AppContext.BaseDirectory, "input"); + _medium = File.ReadAllText(Path.Combine(input, "article_social.txt")); + _large = File.ReadAllText(Path.Combine(input, "weicheng.txt")); + + _seg = new JiebaSegmenter(); + _seg.Cut("热身一下"); // trigger dictionary/model loading outside the timed region + _pos = new PosSegmenter(_seg); + _tfidf = new TfidfExtractor(_seg); + _textrank = new TextRankExtractor(); + _textrank.ExtractTags("热身一下"); + } + + [Benchmark(Description = "Cut accurate+HMM short")] + public int CutAccurateHmmShort() => _seg.Cut(ShortText).Count(); + + [Benchmark(Description = "Cut accurate no-HMM short")] + public int CutAccurateNoHmmShort() => _seg.Cut(ShortText, hmm: false).Count(); + + [Benchmark(Description = "CutAll short")] + public int CutAllShort() => _seg.Cut(ShortText, cutAll: true).Count(); + + [Benchmark(Description = "Cut accurate+HMM medium")] + public int CutAccurateHmmMedium() => _seg.Cut(_medium).Count(); + + [Benchmark(Description = "Cut accurate no-HMM medium")] + public int CutAccurateNoHmmMedium() => _seg.Cut(_medium, hmm: false).Count(); + + [Benchmark(Description = "CutAll medium")] + public int CutAllMedium() => _seg.Cut(_medium, cutAll: true).Count(); + + [Benchmark(Description = "CutForSearch medium")] + public int CutForSearchMedium() => _seg.CutForSearch(_medium).Count(); + + [Benchmark(Description = "POS cut medium")] + public int PosCutMedium() => _pos.Cut(_medium).Count(); + + [Benchmark(Description = "TF-IDF keywords medium")] + public int TfidfMedium() => _tfidf.ExtractTags(_medium).Count(); + + [Benchmark(Description = "TextRank keywords medium")] + public int TextRankMedium() => _textrank.ExtractTags(_medium).Count(); + + [Benchmark(Description = "Cut accurate+HMM large (novel)")] + public int CutAccurateHmmLarge() => _seg.Cut(_large).Count(); + + [Benchmark(Description = "CutAll large (novel)")] + public int CutAllLarge() => _seg.Cut(_large, cutAll: true).Count(); + } +} diff --git a/benchmark/Shared/Program.cs b/benchmark/Shared/Program.cs new file mode 100644 index 0000000..921336b --- /dev/null +++ b/benchmark/Shared/Program.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics; +using System.Linq; +using BenchmarkDotNet.Running; +using JiebaNet.Benchmark; +using JiebaNet.Segmenter; + +if (args.Length > 0 && args[0] == "cold") +{ + // One-shot cold start measurement: dictionary/model loading included. + // Each `dotnet run` invocation is a fresh process. + var sw = Stopwatch.StartNew(); + var seg = new JiebaSegmenter(); + var tokens = seg.Cut("我来到北京清华大学").Count(); + sw.Stop(); + Console.WriteLine($"COLD_INIT_MS={sw.ElapsedMilliseconds} tokens={tokens}"); + return; +} + +BenchmarkSwitcher.FromAssembly(typeof(BenchSuite).Assembly).Run(args); diff --git a/benchmark/run.ps1 b/benchmark/run.ps1 new file mode 100644 index 0000000..8592d5e --- /dev/null +++ b/benchmark/run.ps1 @@ -0,0 +1,111 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Run the fork-vs-upstream jieba.NET benchmark and print a combined comparison. + +.EXAMPLE + .\run.ps1 # build + run both sides + comparison table + .\run.ps1 -Side fork # only this repo's library + .\run.ps1 -Filter "*medium*" # BDN filter for a scenario subset +#> +param( + [ValidateSet("both", "fork", "upstream")] + [string]$Side = "both", + [string]$Filter = "*" +) + +$ErrorActionPreference = "Stop" +$benchmarkDir = $PSScriptRoot + +function Invoke-Bench([string]$project, [string]$label) +{ + Write-Host "" + Write-Host "===== Building $label =====" -ForegroundColor Cyan + dotnet build "$benchmarkDir\$project\$project.csproj" -c Release + if ($LASTEXITCODE -ne 0) { throw "build failed: $project" } + + Write-Host "" + Write-Host "===== Running $label (a few minutes) =====" -ForegroundColor Cyan + dotnet run --project "$benchmarkDir\$project\$project.csproj" -c Release --no-build -- --filter "$Filter" + if ($LASTEXITCODE -ne 0) { throw "benchmark failed: $project" } + + Write-Host "" + Write-Host "===== $label cold start (3 fresh processes) =====" -ForegroundColor Cyan + 1..3 | ForEach-Object { + $output = dotnet "$benchmarkDir\$project\bin\Release\net10.0\$project.dll" cold | Select-String "COLD_INIT_MS=(\d+)" + if ($output.Matches.Success) + { + "cold #{0}: {1} ms" -f $_, $output.Matches[0].Groups[1].Value + } + } +} + +function ConvertTo-Milliseconds([string]$mean) +{ + if ([string]::IsNullOrWhiteSpace($mean)) { return $null } + if ($mean -match "^([\d.,]+)\s*(\S+)$") + { + $value = [double]($Matches[1] -replace ",", "") + $unit = $Matches[2] + $micro = [string][char]0x03BC + "s" + switch ($unit) + { + "ns" { return $value / 1e6 } + "us" { return $value / 1e3 } + $micro { return $value / 1e3 } + "ms" { return $value } + "s" { return $value * 1e3 } + default { return $value } + } + } + return $null +} + +if ($Side -in @("both", "fork")) { Invoke-Bench "JiebaNet.ForkBench" "fork (this repo)" } +if ($Side -in @("both", "upstream")) { Invoke-Bench "JiebaNet.UpstreamBench" "upstream (NuGet jieba.NET 0.42.2)" } + +Write-Host "" +Write-Host "===== Raw reports =====" -ForegroundColor Cyan +foreach ($project in @("JiebaNet.ForkBench", "JiebaNet.UpstreamBench")) +{ + $report = Get-ChildItem "$benchmarkDir\$project\BenchmarkDotNet.Artifacts\results" -Filter "*BenchSuite-report-github.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($report) + { + Write-Host "--- $project ---" + Get-Content $report.FullName | Out-Host + } +} + +$forkCsv = Get-ChildItem "$benchmarkDir\JiebaNet.ForkBench\BenchmarkDotNet.Artifacts\results" -Filter "*BenchSuite-report.csv" -ErrorAction SilentlyContinue | Select-Object -First 1 +$upstreamCsv = Get-ChildItem "$benchmarkDir\JiebaNet.UpstreamBench\BenchmarkDotNet.Artifacts\results" -Filter "*BenchSuite-report.csv" -ErrorAction SilentlyContinue | Select-Object -First 1 + +if ($forkCsv -and $upstreamCsv) +{ + $fork = Import-Csv $forkCsv.FullName -Encoding UTF8 + $upstream = Import-Csv $upstreamCsv.FullName -Encoding UTF8 + + $rows = foreach ($row in $fork) + { + $other = $upstream | Where-Object { $_.Method -eq $row.Method } + if ($null -eq $other) { continue } + + $forkMs = ConvertTo-Milliseconds $row.Mean + $upstreamMs = ConvertTo-Milliseconds $other.Mean + [pscustomobject]@{ + "Scenario" = $row.Method + "fork ms" = [math]::Round($forkMs, 3) + "upstream ms" = [math]::Round($upstreamMs, 3) + "upstream / fork" = if ($forkMs -gt 0) { [math]::Round($upstreamMs / $forkMs, 2) } else { $null } + "fork alloc" = $row.Allocated + "upstream alloc" = $other.Allocated + } + } + + Write-Host "" + Write-Host "===== Comparison (BDN Mean; ratio > 1 means the fork is faster) =====" -ForegroundColor Green + $rows | Sort-Object "Scenario" | Format-Table -AutoSize | Out-Host +} +else +{ + Write-Host "Benchmark artifacts not found; run both sides to produce the comparison." -ForegroundColor Yellow +} diff --git a/jieba.NET.slnx b/jieba.NET.slnx index 1eed459..89a4983 100644 --- a/jieba.NET.slnx +++ b/jieba.NET.slnx @@ -8,4 +8,8 @@ + + + +