Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,4 @@ FakesAssemblies/
_bin/
_Nuget/NuGet.exe
snks/*.snk
BenchmarkDotNet.Artifacts/
31 changes: 31 additions & 0 deletions benchmark/JiebaNet.ForkBench/JiebaNet.ForkBench.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>JiebaNet.Benchmark</RootNamespace>
<AssemblyName>JiebaNet.ForkBench</AssemblyName>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Analyser\Analyser.csproj" />
<ProjectReference Include="..\..\src\Segmenter\Segmenter.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\Program.cs" Link="Program.cs" />
<Compile Include="..\Shared\BenchSuite.cs" Link="BenchSuite.cs" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\test\Segmenter.Tests\Resources\article_social.txt" Link="input\article_social.txt" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\test\Segmenter.Tests\Resources\围城.txt" Link="input\weicheng.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
33 changes: 33 additions & 0 deletions benchmark/JiebaNet.UpstreamBench/JiebaNet.UpstreamBench.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>JiebaNet.Benchmark</RootNamespace>
<AssemblyName>JiebaNet.UpstreamBench</AssemblyName>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="jieba.NET" Version="0.42.2" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\Program.cs" Link="Program.cs" />
<Compile Include="..\Shared\BenchSuite.cs" Link="BenchSuite.cs" />
</ItemGroup>

<!-- Upstream jieba.NET loads its dictionaries from a Resources folder next to
the application, so provide it from the repo. -->
<ItemGroup>
<None Include="..\..\src\Segmenter\Resources\**" Link="Resources\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\test\Segmenter.Tests\Resources\article_social.txt" Link="input\article_social.txt" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\test\Segmenter.Tests\Resources\围城.txt" Link="input\weicheng.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -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 的预热会把词典加载排除在统计之外)。
90 changes: 90 additions & 0 deletions benchmark/Shared/BenchSuite.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
20 changes: 20 additions & 0 deletions benchmark/Shared/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
111 changes: 111 additions & 0 deletions benchmark/run.ps1
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions jieba.NET.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
<Project Path="test/NativeAotSmokeTest/NativeAotSmokeTest.csproj" />
<Project Path="test/Segmenter.Tests/Segmenter.Tests.csproj" />
</Folder>
<Folder Name="/benchmark/">
<Project Path="benchmark/JiebaNet.ForkBench/JiebaNet.ForkBench.csproj" />
<Project Path="benchmark/JiebaNet.UpstreamBench/JiebaNet.UpstreamBench.csproj" />
</Folder>
</Solution>
Loading