diff --git a/.github/workflows/nuget-release.yml b/.github/workflows/nuget-release.yml index eed813d..ae71b28 100644 --- a/.github/workflows/nuget-release.yml +++ b/.github/workflows/nuget-release.yml @@ -7,9 +7,9 @@ on: workflow_dispatch: inputs: version: - description: NuGet version, for example 0.1.0-preview.1 + description: NuGet version, for example 0.1.0-preview.2 required: true - default: 0.1.0-preview.1 + default: 0.1.0-preview.2 type: string publish: description: Publish to NuGet.org after all package tests pass @@ -227,9 +227,17 @@ jobs: if: github.event_name == 'push' env: GH_TOKEN: ${{ github.token }} - run: >- - gh release create '${{ github.ref_name }}' - target/nuget/packages/* - --verify-tag - --generate-notes - --title 'MiniExcel.Rust ${{ needs.prepare.outputs.version }}' + GH_REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + run: | + arguments=( + '${{ github.ref_name }}' + target/nuget/packages/* + --verify-tag + --generate-notes + --title "MiniExcel.Rust $RELEASE_VERSION" + ) + if [[ "$RELEASE_VERSION" == *-* ]]; then + arguments+=(--prerelease) + fi + gh release create "${arguments[@]}" diff --git a/README.es.md b/README.es.md index b7abad2..99fe9d0 100644 --- a/README.es.md +++ b/README.es.md @@ -52,7 +52,7 @@ v1 `1.46.0`, reutiliza sus tipos de configuración y mapping, y ejecuta mediante la biblioteca nativa de Rust las llamadas realizadas a `MiniExcelRust`. ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -115,17 +115,21 @@ Consulta la [matriz de compatibilidad](docs/compatibility.md), el [contrato de a ### Últimos Resultados NuGet La última prueba Windows x64 comparó `MiniExcel 1.46.0`, el wrapper .NET -`MiniExcel.Rust 0.1.0-preview.1` y `MiniExcel Rust 0.4.0` nativo sobre 100.000 filas x 10 columnas, +`MiniExcel.Rust 0.1.0-preview.2` y `MiniExcel Rust 0.4.0` nativo sobre un workbook con dimension +declarada de 100.000 filas x 10 columnas, con cinco procesos nuevos por runtime y escenario. Todos los valores se verificaron antes de medir. | Escenario | Runtime | Mediana | Filas/s | Primera fila | Asignación administrada | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2.598,50 ms | 38.484 | 1.051,97 ms | 2.541,93 MB | 50,93 MB | -| Cold | MiniExcel.Rust (.NET) | 1.899,42 ms | 52.648 | 450,85 ms | 289,36 MB | 49,64 MB | -| Cold | MiniExcel.Rust | 1.220,82 ms | 81.912 | 537,17 ms | n/a | 4,14 MB | -| Steady | MiniExcel | 5.997,76 ms | 50.019 | 616,40 ms | 7.625,15 MB | 52,80 MB | -| Steady | MiniExcel.Rust (.NET) | 4.202,10 ms | 71.393 | 442,59 ms | 868,05 MB | 48,79 MB | -| Steady | MiniExcel.Rust | 3.344,20 ms | 89.708 | 467,30 ms | n/a | 4,20 MB | +| Cold | MiniExcel | 2.145,90 ms | 46.600 | 38,77 ms | 1.167,07 MB | 51,26 MB | +| Cold | MiniExcel.Rust (.NET) | 1.047,67 ms | 95.450 | 12,05 ms | 107,04 MB | 44,59 MB | +| Cold | MiniExcel.Rust | 855,09 ms | 116.947 | 0,42 ms | n/a | 3,70 MB | +| Steady | MiniExcel | 4.341,55 ms | 69.100 | 3,95 ms | 3.500,58 MB | 54,74 MB | +| Steady | MiniExcel.Rust (.NET) | 2.972,94 ms | 100.910 | 5,50 ms | 321,09 MB | 48,08 MB | +| Steady | MiniExcel.Rust | 2.143,31 ms | 139.970 | 0,41 ms | n/a | 3,79 MB | + +Para una Query completa, preview.2 es 2,05 veces más rápido en Cold y 1,46 veces más rápido en +Steady que MiniExcel v1, con un 90,8 % menos de asignación administrada en ambos escenarios. Los resultados dependen de la máquina. Ejecuta `pwsh ./scripts/compare-nuget-v1-rust.ps1` con workbooks representativos; consulta la [metodología](docs/dotnet-v1-query-benchmark.md). diff --git a/README.fr.md b/README.fr.md index 0e8efd5..c994f3e 100644 --- a/README.fr.md +++ b/README.fr.md @@ -52,7 +52,7 @@ MiniExcel v1 `1.46.0`, réutilise ses types de configuration et de mapping, et transmet les appels `MiniExcelRust` à la bibliothèque native Rust. ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -115,17 +115,21 @@ Voir la [matrice de compatibilité](docs/compatibility.md), le [contrat analyse/ ### Derniers Résultats NuGet Le dernier test Windows x64 compare `MiniExcel 1.46.0`, le wrapper .NET -`MiniExcel.Rust 0.1.0-preview.1` et `MiniExcel Rust 0.4.0` natif sur 100 000 lignes x 10 colonnes, +`MiniExcel.Rust 0.1.0-preview.2` et `MiniExcel Rust 0.4.0` natif sur un workbook avec dimension +déclarée de 100 000 lignes x 10 colonnes, avec cinq processus neufs par runtime et scénario. Toutes les valeurs sont vérifiées avant la mesure. | Scénario | Runtime | Temps médian | Lignes/s | Première ligne | Allocation managée | Working set maximal | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2 598,50 ms | 38 484 | 1 051,97 ms | 2 541,93 MB | 50,93 MB | -| Cold | MiniExcel.Rust (.NET) | 1 899,42 ms | 52 648 | 450,85 ms | 289,36 MB | 49,64 MB | -| Cold | MiniExcel.Rust | 1 220,82 ms | 81 912 | 537,17 ms | n/a | 4,14 MB | -| Steady | MiniExcel | 5 997,76 ms | 50 019 | 616,40 ms | 7 625,15 MB | 52,80 MB | -| Steady | MiniExcel.Rust (.NET) | 4 202,10 ms | 71 393 | 442,59 ms | 868,05 MB | 48,79 MB | -| Steady | MiniExcel.Rust | 3 344,20 ms | 89 708 | 467,30 ms | n/a | 4,20 MB | +| Cold | MiniExcel | 2 145,90 ms | 46 600 | 38,77 ms | 1 167,07 MB | 51,26 MB | +| Cold | MiniExcel.Rust (.NET) | 1 047,67 ms | 95 450 | 12,05 ms | 107,04 MB | 44,59 MB | +| Cold | MiniExcel.Rust | 855,09 ms | 116 947 | 0,42 ms | n/a | 3,70 MB | +| Steady | MiniExcel | 4 341,55 ms | 69 100 | 3,95 ms | 3 500,58 MB | 54,74 MB | +| Steady | MiniExcel.Rust (.NET) | 2 972,94 ms | 100 910 | 5,50 ms | 321,09 MB | 48,08 MB | +| Steady | MiniExcel.Rust | 2 143,31 ms | 139 970 | 0,41 ms | n/a | 3,79 MB | + +Pour une Query complète, preview.2 est 2,05 fois plus rapide en Cold et 1,46 fois plus rapide en +Steady que MiniExcel v1, avec 90,8 % d'allocation managée en moins dans les deux scénarios. Les résultats dépendent de la machine. Relancez `pwsh ./scripts/compare-nuget-v1-rust.ps1` sur des workbooks représentatifs ; voir la [méthodologie](docs/dotnet-v1-query-benchmark.md). diff --git a/README.ja.md b/README.ja.md index 9732e1d..a5e65e5 100644 --- a/README.ja.md +++ b/README.ja.md @@ -52,7 +52,7 @@ v1 `1.46.0` に固定して依存し、その設定型と mapping 型を再利 `MiniExcelRust` の呼び出しを Rust ネイティブライブラリで実行します。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -113,18 +113,21 @@ MiniExcel::save_as("book.xlsx", &[row])?; ### 最新 NuGet 結果 -最新の Windows x64 テストでは、100,000 行 x 10 列の workbook で `MiniExcel 1.46.0`、 -.NET wrapper `MiniExcel.Rust 0.1.0-preview.1`、native `MiniExcel Rust 0.4.0` を比較しました。 +最新の Windows x64 テストでは、dimension を宣言した 100,000 行 x 10 列の workbook で +`MiniExcel 1.46.0`、.NET wrapper `MiniExcel.Rust 0.1.0-preview.2`、native `MiniExcel Rust 0.4.0` を比較しました。 runtime とシナリオごとに 5 個の新規 process を使用し、計測前にすべての値を検証しています。 | シナリオ | Runtime | 中央値 | 行/秒 | 最初の行 | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | n/a | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | n/a | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | n/a | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | n/a | 3.79 MB | + +完全な Query では、preview.2 は MiniExcel v1 より Cold で 2.05 倍、Steady で 1.46 倍高速で、 +managed allocation は両シナリオで 90.8% 少なくなりました。 結果はマシンに依存します。代表的な workbook で `pwsh ./scripts/compare-nuget-v1-rust.ps1` を実行し、 [測定方法](docs/dotnet-v1-query-benchmark.md)を参照してください。 diff --git a/README.md b/README.md index da08377..29ad280 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ v1 `1.46.0`, reuses its configuration and mapping types, and routes calls made through `MiniExcelRust` to the Rust native library. ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -115,18 +115,21 @@ See the [compatibility matrix](docs/compatibility.md), [analytics and RAG contra ### Latest NuGet Results The latest Windows x64 run compared public `MiniExcel 1.46.0`, the -`MiniExcel.Rust 0.1.0-preview.1` .NET wrapper, and native `MiniExcel Rust 0.4.0` over 100,000 rows -x 10 columns, using five fresh processes per runtime and scenario. Every row, column, and -normalized value matched before timing. +`MiniExcel.Rust 0.1.0-preview.2` .NET wrapper, and native `MiniExcel Rust 0.4.0` over a declared-dimension +workbook containing 100,000 rows x 10 columns. Five fresh processes ran per runtime and scenario; +every row, column, and normalized value matched before timing. | Scenario | Runtime | Median elapsed | Rows/s | First row | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | n/a | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | n/a | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | n/a | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | n/a | 3.79 MB | + +For full Query, preview.2 is 2.05x faster in Cold and 1.46x faster in Steady than MiniExcel v1, +with 90.8% less managed allocation in both scenarios. Results are machine-specific. Run `pwsh ./scripts/compare-nuget-v1-rust.ps1` on representative workbooks; see the [benchmark methodology](docs/dotnet-v1-query-benchmark.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 88d139e..465222d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -51,7 +51,7 @@ cargo add miniexcel 复用其配置和映射类型,并将通过 `MiniExcelRust` 发起的调用交给 Rust 原生库执行。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -112,18 +112,21 @@ MiniExcel::save_as("book.xlsx", &[row])?; ### 最新 NuGet 结果 -最新 Windows x64 测试使用 100,000 行 x 10 列工作簿,对比公开的 `MiniExcel 1.46.0`、 -`.NET` 包装器 `MiniExcel.Rust 0.1.0-preview.1` 与原生 `MiniExcel Rust 0.4.0`;每个 runtime、 -每个场景运行 5 个独立进程。计时前已逐行、逐列、逐值验证一致。 +最新 Windows x64 测试使用声明 dimension 的 100,000 行 x 10 列工作簿,对比公开的 +`MiniExcel 1.46.0`、`.NET` 包装器 `MiniExcel.Rust 0.1.0-preview.2` 与原生 +`MiniExcel Rust 0.4.0`;每个 runtime、每个场景运行 5 个独立进程。计时前已逐行、逐列、逐值验证一致。 | 场景 | Runtime | 耗时中位数 | 行/秒 | 首行延迟 | 托管分配 | 峰值工作集 | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | 不适用 | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | 不适用 | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | 不适用 | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | 不适用 | 3.79 MB | + +完整 Query 中,preview.2 在 Cold 与 Steady 场景分别比 MiniExcel v1 快 2.05 倍和 1.46 倍, +两个场景的托管分配都减少 90.8%。 结果与机器和工作簿相关。请用 `pwsh ./scripts/compare-nuget-v1-rust.ps1` 在代表性工作簿上复测; 详见[测试方法](docs/dotnet-v1-query-benchmark.zh-CN.md)。 diff --git a/README.zh-TW.md b/README.zh-TW.md index e003492..c2881b2 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -51,7 +51,7 @@ cargo add miniexcel 重用其設定與 mapping 型別,並將透過 `MiniExcelRust` 發起的呼叫交給 Rust 原生程式庫執行。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -112,18 +112,21 @@ MiniExcel::save_as("book.xlsx", &[row])?; ### 最新 NuGet 結果 -最新 Windows x64 測試使用 100,000 列 x 10 欄 workbook,比較公開的 `MiniExcel 1.46.0`、 -`.NET` 包裝器 `MiniExcel.Rust 0.1.0-preview.1` 與原生 `MiniExcel Rust 0.4.0`;每個 runtime、 -每個場景執行 5 個獨立 process。計時前已逐列、逐欄、逐值驗證一致。 +最新 Windows x64 測試使用已宣告 dimension 的 100,000 列 x 10 欄 workbook,比較公開的 +`MiniExcel 1.46.0`、`.NET` 包裝器 `MiniExcel.Rust 0.1.0-preview.2` 與原生 +`MiniExcel Rust 0.4.0`;每個 runtime、每個場景執行 5 個獨立 process。計時前已逐列、逐欄、逐值驗證一致。 | 場景 | Runtime | 耗時中位數 | 列/秒 | 首列延遲 | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | 不適用 | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | 不適用 | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | 不適用 | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | 不適用 | 3.79 MB | + +完整 Query 中,preview.2 在 Cold 與 Steady 場景分別比 MiniExcel v1 快 2.05 倍與 1.46 倍, +兩個場景的 managed allocation 都減少 90.8%。 結果會受機器與 workbook 影響。請用 `pwsh ./scripts/compare-nuget-v1-rust.ps1` 在代表性 workbook 上重測; 詳見[測試方法](docs/dotnet-v1-query-benchmark.md)。 diff --git a/benchmarks/nuget-v1-query/Program.cs b/benchmarks/nuget-v1-query/Program.cs index ada2c33..372bf4b 100644 --- a/benchmarks/nuget-v1-query/Program.cs +++ b/benchmarks/nuget-v1-query/Program.cs @@ -1,3 +1,5 @@ +using System.Buffers; +using System.Buffers.Binary; using System.Diagnostics; using System.Globalization; using System.IO.Compression; @@ -12,7 +14,8 @@ return args[0].ToLowerInvariant() switch { - "generate" => Generate(args), + "generate" => Generate(args, includeDimension: true), + "generate-no-dimension" => Generate(args, includeDimension: false), "generate-template" => GenerateTemplate(args), "verify" => Verify(args), "fingerprint" => Fingerprint(args), @@ -27,14 +30,14 @@ _ => Usage() }; -static int Generate(string[] arguments) +static int Generate(string[] arguments, bool includeDimension) { if (arguments.Length != 4 || !int.TryParse(arguments[2], out var rowCount) || rowCount < 1 || !int.TryParse(arguments[3], out var columnCount) || columnCount is < 1 or > 26) return Usage(); - CreateWorkbook(Path.GetFullPath(arguments[1]), rowCount, columnCount); + CreateWorkbook(Path.GetFullPath(arguments[1]), rowCount, columnCount, includeDimension); return 0; } @@ -287,7 +290,7 @@ static void AppendRow(IncrementalHash hash, IDictionary row) foreach (var cell in row) { AppendText(hash, cell.Key); - AppendText(hash, Normalize(cell.Value)); + AppendValue(hash, cell.Value); } } @@ -306,11 +309,54 @@ static void AppendRow(IncrementalHash hash, IDictionary row) return (rows, cells, Convert.ToHexString(hash.GetHashAndReset())); } -static void AppendText(IncrementalHash hash, string value) +static void AppendValue(IncrementalHash hash, object? value) { - var bytes = Encoding.UTF8.GetBytes(value); - hash.AppendData(BitConverter.GetBytes(bytes.Length)); - hash.AppendData(bytes); + if (value is null or DBNull) + { + AppendText(hash, "null"); + return; + } + if (value is string text) + { + AppendText(hash, text); + return; + } + if (value is ISpanFormattable spanFormattable) + { + Span characters = stackalloc char[128]; + if (spanFormattable.TryFormat(characters, out var written, default, CultureInfo.InvariantCulture)) + { + AppendCharacters(hash, characters[..written]); + return; + } + } + AppendText(hash, Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty); +} + +static void AppendText(IncrementalHash hash, string value) => AppendCharacters(hash, value.AsSpan()); + +static void AppendCharacters(IncrementalHash hash, ReadOnlySpan value) +{ + var byteCount = Encoding.UTF8.GetByteCount(value); + Span length = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(length, byteCount); + hash.AppendData(length); + + const int StackBufferSize = 256; + byte[]? rented = null; + Span bytes = byteCount <= StackBufferSize + ? stackalloc byte[StackBufferSize] + : rented = ArrayPool.Shared.Rent(byteCount); + try + { + var written = Encoding.UTF8.GetBytes(value, bytes); + hash.AppendData(bytes[..written]); + } + finally + { + if (rented is not null) + ArrayPool.Shared.Return(rented); + } } static string Normalize(object? value) => value switch @@ -320,7 +366,7 @@ static void AppendText(IncrementalHash hash, string value) _ => value.ToString() ?? string.Empty }; -static void CreateWorkbook(string path, int rows, int columns) +static void CreateWorkbook(string path, int rows, int columns, bool includeDimension) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); if (File.Exists(path)) @@ -356,7 +402,11 @@ static void CreateWorkbook(string path, int rows, int columns) var entry = archive.CreateEntry("xl/worksheets/sheet1.xml", CompressionLevel.Fastest); using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(false)); - writer.Write(""); + var endCell = $"{(char)('A' + columns - 1)}{rows}"; + writer.Write(""); + if (includeDimension) + writer.Write($""); + writer.Write(""); for (var row = 1; row <= rows; row++) { writer.Write($""); @@ -388,6 +438,7 @@ static int Usage() { Console.Error.WriteLine("Usage:"); Console.Error.WriteLine(" NuGetV1Query generate "); + Console.Error.WriteLine(" NuGetV1Query generate-no-dimension "); Console.Error.WriteLine(" NuGetV1Query generate-template "); Console.Error.WriteLine(" NuGetV1Query verify "); Console.Error.WriteLine(" NuGetV1Query fingerprint "); diff --git a/docs/dotnet-v1-query-benchmark.md b/docs/dotnet-v1-query-benchmark.md index 0292562..90c9b43 100644 --- a/docs/dotnet-v1-query-benchmark.md +++ b/docs/dotnet-v1-query-benchmark.md @@ -29,21 +29,25 @@ processes and report query time, first-row latency, managed allocation where app Use `-Rows`, `-Columns`, `-Iterations`, `-Passes`, and `-WarmupPasses` to change the load. The `NuGet Benchmark` GitHub workflow runs the same harness on Windows, Linux, and macOS for x64 and Arm64. Pass `-MiniExcelVersion 1.46.0` to pin a historical baseline for reproducible comparisons. +Generated workbooks declare their worksheet dimension by default. Pass `-OmitDimension` to measure +the fallback path that scans the worksheet before returning its first row. Content hashing uses +stack or pooled UTF-8 buffers so temporary benchmark arrays do not dominate managed allocation. #### Latest NuGet Result -The 2026-09-10 Windows x64 run used 100,000 rows x 10 columns and five fresh processes per runtime -and scenario. `MiniExcel 1.46.0`, `MiniExcel.Rust 0.1.0-preview.1` through .NET, and native +The 2026-09-10 Windows x64 run used a declared-dimension workbook with 100,000 rows x 10 columns +and five fresh processes per runtime and scenario. `MiniExcel 1.46.0`, +`MiniExcel.Rust 0.1.0-preview.2` through .NET, and native `MiniExcel Rust 0.4.0` matched every normalized value. | Scenario | Runtime | Median elapsed | Rows/s | First row | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | n/a | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | n/a | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | n/a | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | n/a | 3.79 MB | ## Fairness Controls diff --git a/docs/dotnet-v1-query-benchmark.zh-CN.md b/docs/dotnet-v1-query-benchmark.zh-CN.md index 52d5a54..21cf1b2 100644 --- a/docs/dotnet-v1-query-benchmark.zh-CN.md +++ b/docs/dotnet-v1-query-benchmark.zh-CN.md @@ -28,21 +28,25 @@ pwsh ./scripts/compare-nuget-v1-rust.ps1 可通过 `-Rows`、`-Columns`、`-Iterations`、`-Passes` 和 `-WarmupPasses` 调整压力。 GitHub 的 `NuGet Benchmark` workflow 会在 Windows、Linux、macOS 的 x64 与 Arm64 环境运行同一套测试。 如需复现历史结果,可传入 `-MiniExcelVersion 1.46.0` 固定基线版本。 +生成的工作簿默认声明 worksheet dimension;可传入 `-OmitDimension` 专门测试首行返回前必须扫描 +worksheet 的 fallback 路径。内容 hash 使用 stack 或 pooled UTF-8 buffer,避免 benchmark 临时数组 +主导托管分配数据。 #### 最新 NuGet 结果 -2026-09-10 的 Windows x64 测试使用 100,000 行 x 10 列工作簿,每个 runtime、每个场景运行 -5 个独立进程。`MiniExcel 1.46.0`、通过 .NET 调用的 `MiniExcel.Rust 0.1.0-preview.1` 与原生 +2026-09-10 的 Windows x64 测试使用声明 dimension 的 100,000 行 x 10 列工作簿,每个 runtime、 +每个场景运行 5 个独立进程。`MiniExcel 1.46.0`、通过 .NET 调用的 +`MiniExcel.Rust 0.1.0-preview.2` 与原生 `MiniExcel Rust 0.4.0` 的所有规范化值完全一致。 | 场景 | Runtime | 耗时中位数 | 行/秒 | 首行延迟 | 托管分配 | 峰值工作集 | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | 不适用 | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | 不适用 | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | 不适用 | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | 不适用 | 3.79 MB | ## 公平性控制 diff --git a/docs/i18n/README.es.md b/docs/i18n/README.es.md index a63f448..46e59da 100644 --- a/docs/i18n/README.es.md +++ b/docs/i18n/README.es.md @@ -52,7 +52,7 @@ v1 `1.46.0`, reutiliza sus tipos de configuración y mapping, y ejecuta mediante la biblioteca nativa de Rust las llamadas realizadas a `MiniExcelRust`. ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -293,17 +293,21 @@ Consulta la [matriz de compatibilidad](../compatibility.md), el [contrato de an ### Últimos Resultados NuGet La última prueba Windows x64 comparó `MiniExcel 1.46.0`, el wrapper .NET -`MiniExcel.Rust 0.1.0-preview.1` y `MiniExcel Rust 0.4.0` nativo sobre 100.000 filas x 10 columnas, +`MiniExcel.Rust 0.1.0-preview.2` y `MiniExcel Rust 0.4.0` nativo sobre un workbook con dimension +declarada de 100.000 filas x 10 columnas, con cinco procesos nuevos por runtime y escenario. Todos los valores se verificaron antes de medir. | Escenario | Runtime | Mediana | Filas/s | Primera fila | Asignación administrada | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2.598,50 ms | 38.484 | 1.051,97 ms | 2.541,93 MB | 50,93 MB | -| Cold | MiniExcel.Rust (.NET) | 1.899,42 ms | 52.648 | 450,85 ms | 289,36 MB | 49,64 MB | -| Cold | MiniExcel.Rust | 1.220,82 ms | 81.912 | 537,17 ms | n/a | 4,14 MB | -| Steady | MiniExcel | 5.997,76 ms | 50.019 | 616,40 ms | 7.625,15 MB | 52,80 MB | -| Steady | MiniExcel.Rust (.NET) | 4.202,10 ms | 71.393 | 442,59 ms | 868,05 MB | 48,79 MB | -| Steady | MiniExcel.Rust | 3.344,20 ms | 89.708 | 467,30 ms | n/a | 4,20 MB | +| Cold | MiniExcel | 2.145,90 ms | 46.600 | 38,77 ms | 1.167,07 MB | 51,26 MB | +| Cold | MiniExcel.Rust (.NET) | 1.047,67 ms | 95.450 | 12,05 ms | 107,04 MB | 44,59 MB | +| Cold | MiniExcel.Rust | 855,09 ms | 116.947 | 0,42 ms | n/a | 3,70 MB | +| Steady | MiniExcel | 4.341,55 ms | 69.100 | 3,95 ms | 3.500,58 MB | 54,74 MB | +| Steady | MiniExcel.Rust (.NET) | 2.972,94 ms | 100.910 | 5,50 ms | 321,09 MB | 48,08 MB | +| Steady | MiniExcel.Rust | 2.143,31 ms | 139.970 | 0,41 ms | n/a | 3,79 MB | + +Para una Query completa, preview.2 es 2,05 veces más rápido en Cold y 1,46 veces más rápido en +Steady que MiniExcel v1, con un 90,8 % menos de asignación administrada en ambos escenarios. Los resultados dependen de la máquina. Ejecuta `pwsh ./scripts/compare-nuget-v1-rust.ps1` con workbooks representativos; consulta la [metodología](../dotnet-v1-query-benchmark.md). diff --git a/docs/i18n/README.fr.md b/docs/i18n/README.fr.md index 46070da..0fdf590 100644 --- a/docs/i18n/README.fr.md +++ b/docs/i18n/README.fr.md @@ -52,7 +52,7 @@ MiniExcel v1 `1.46.0`, réutilise ses types de configuration et de mapping, et transmet les appels `MiniExcelRust` à la bibliothèque native Rust. ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -294,17 +294,21 @@ Voir la [matrice de compatibilité](../compatibility.md), le [contrat analyse/RA ### Derniers Résultats NuGet Le dernier test Windows x64 compare `MiniExcel 1.46.0`, le wrapper .NET -`MiniExcel.Rust 0.1.0-preview.1` et `MiniExcel Rust 0.4.0` natif sur 100 000 lignes x 10 colonnes, +`MiniExcel.Rust 0.1.0-preview.2` et `MiniExcel Rust 0.4.0` natif sur un workbook avec dimension +déclarée de 100 000 lignes x 10 colonnes, avec cinq processus neufs par runtime et scénario. Toutes les valeurs sont vérifiées avant la mesure. | Scénario | Runtime | Temps médian | Lignes/s | Première ligne | Allocation managée | Working set maximal | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2 598,50 ms | 38 484 | 1 051,97 ms | 2 541,93 MB | 50,93 MB | -| Cold | MiniExcel.Rust (.NET) | 1 899,42 ms | 52 648 | 450,85 ms | 289,36 MB | 49,64 MB | -| Cold | MiniExcel.Rust | 1 220,82 ms | 81 912 | 537,17 ms | n/a | 4,14 MB | -| Steady | MiniExcel | 5 997,76 ms | 50 019 | 616,40 ms | 7 625,15 MB | 52,80 MB | -| Steady | MiniExcel.Rust (.NET) | 4 202,10 ms | 71 393 | 442,59 ms | 868,05 MB | 48,79 MB | -| Steady | MiniExcel.Rust | 3 344,20 ms | 89 708 | 467,30 ms | n/a | 4,20 MB | +| Cold | MiniExcel | 2 145,90 ms | 46 600 | 38,77 ms | 1 167,07 MB | 51,26 MB | +| Cold | MiniExcel.Rust (.NET) | 1 047,67 ms | 95 450 | 12,05 ms | 107,04 MB | 44,59 MB | +| Cold | MiniExcel.Rust | 855,09 ms | 116 947 | 0,42 ms | n/a | 3,70 MB | +| Steady | MiniExcel | 4 341,55 ms | 69 100 | 3,95 ms | 3 500,58 MB | 54,74 MB | +| Steady | MiniExcel.Rust (.NET) | 2 972,94 ms | 100 910 | 5,50 ms | 321,09 MB | 48,08 MB | +| Steady | MiniExcel.Rust | 2 143,31 ms | 139 970 | 0,41 ms | n/a | 3,79 MB | + +Pour une Query complète, preview.2 est 2,05 fois plus rapide en Cold et 1,46 fois plus rapide en +Steady que MiniExcel v1, avec 90,8 % d'allocation managée en moins dans les deux scénarios. Les résultats dépendent de la machine. Relancez `pwsh ./scripts/compare-nuget-v1-rust.ps1` sur des workbooks représentatifs ; voir la [méthodologie](../dotnet-v1-query-benchmark.md). diff --git a/docs/i18n/README.ja.md b/docs/i18n/README.ja.md index 43940da..8dce38c 100644 --- a/docs/i18n/README.ja.md +++ b/docs/i18n/README.ja.md @@ -52,7 +52,7 @@ v1 `1.46.0` に固定して依存し、その設定型と mapping 型を再利 `MiniExcelRust` の呼び出しを Rust ネイティブライブラリで実行します。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -288,18 +288,21 @@ Workbook editing、Template、formula、formatting に依存する前に、 ### 最新 NuGet 結果 -最新の Windows x64 テストでは、100,000 行 x 10 列の workbook で `MiniExcel 1.46.0`、 -.NET wrapper `MiniExcel.Rust 0.1.0-preview.1`、native `MiniExcel Rust 0.4.0` を比較しました。 +最新の Windows x64 テストでは、dimension を宣言した 100,000 行 x 10 列の workbook で +`MiniExcel 1.46.0`、.NET wrapper `MiniExcel.Rust 0.1.0-preview.2`、native `MiniExcel Rust 0.4.0` を比較しました。 runtime とシナリオごとに 5 個の新規 process を使用し、計測前にすべての値を検証しています。 | シナリオ | Runtime | 中央値 | 行/秒 | 最初の行 | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | n/a | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | n/a | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | n/a | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | n/a | 3.79 MB | + +完全な Query では、preview.2 は MiniExcel v1 より Cold で 2.05 倍、Steady で 1.46 倍高速で、 +managed allocation は両シナリオで 90.8% 少なくなりました。 結果はマシンに依存します。代表的な workbook で `pwsh ./scripts/compare-nuget-v1-rust.ps1` を実行し、 [測定方法](../dotnet-v1-query-benchmark.md)を参照してください。 diff --git a/docs/i18n/README.zh-CN.md b/docs/i18n/README.zh-CN.md index 5147687..55bf750 100644 --- a/docs/i18n/README.zh-CN.md +++ b/docs/i18n/README.zh-CN.md @@ -51,7 +51,7 @@ cargo add miniexcel 复用其配置和映射类型,并将通过 `MiniExcelRust` 发起的调用交给 Rust 原生库执行。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -282,18 +282,21 @@ cargo run -p miniexcel-cli -- rag-export book.xlsx --header --chunk-rows 25 --fo ### 最新 NuGet 结果 -最新 Windows x64 测试使用 100,000 行 x 10 列工作簿,对比公开的 `MiniExcel 1.46.0`、 -`.NET` 包装器 `MiniExcel.Rust 0.1.0-preview.1` 与原生 `MiniExcel Rust 0.4.0`;每个 runtime、 -每个场景运行 5 个独立进程。计时前已逐行、逐列、逐值验证一致。 +最新 Windows x64 测试使用声明 dimension 的 100,000 行 x 10 列工作簿,对比公开的 +`MiniExcel 1.46.0`、`.NET` 包装器 `MiniExcel.Rust 0.1.0-preview.2` 与原生 +`MiniExcel Rust 0.4.0`;每个 runtime、每个场景运行 5 个独立进程。计时前已逐行、逐列、逐值验证一致。 | 场景 | Runtime | 耗时中位数 | 行/秒 | 首行延迟 | 托管分配 | 峰值工作集 | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | 不适用 | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | 不适用 | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | 不适用 | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | 不适用 | 3.79 MB | + +完整 Query 中,preview.2 在 Cold 与 Steady 场景分别比 MiniExcel v1 快 2.05 倍和 1.46 倍, +两个场景的托管分配都减少 90.8%。 结果与机器和工作簿相关。请用 `pwsh ./scripts/compare-nuget-v1-rust.ps1` 在代表性工作簿上复测; 详见[测试方法](../dotnet-v1-query-benchmark.zh-CN.md)。 diff --git a/docs/i18n/README.zh-TW.md b/docs/i18n/README.zh-TW.md index c7cda70..452ceeb 100644 --- a/docs/i18n/README.zh-TW.md +++ b/docs/i18n/README.zh-TW.md @@ -51,7 +51,7 @@ cargo add miniexcel 重用其設定與 mapping 型別,並將透過 `MiniExcelRust` 發起的呼叫交給 Rust 原生程式庫執行。 ```bash -dotnet add package MiniExcel.Rust --prerelease +dotnet add package MiniExcel.Rust --version 0.1.0-preview.2 ``` ```csharp @@ -282,18 +282,21 @@ Template、公式或格式功能前,請先在[相容性矩陣](../compatibilit ### 最新 NuGet 結果 -最新 Windows x64 測試使用 100,000 列 x 10 欄 workbook,比較公開的 `MiniExcel 1.46.0`、 -`.NET` 包裝器 `MiniExcel.Rust 0.1.0-preview.1` 與原生 `MiniExcel Rust 0.4.0`;每個 runtime、 -每個場景執行 5 個獨立 process。計時前已逐列、逐欄、逐值驗證一致。 +最新 Windows x64 測試使用已宣告 dimension 的 100,000 列 x 10 欄 workbook,比較公開的 +`MiniExcel 1.46.0`、`.NET` 包裝器 `MiniExcel.Rust 0.1.0-preview.2` 與原生 +`MiniExcel Rust 0.4.0`;每個 runtime、每個場景執行 5 個獨立 process。計時前已逐列、逐欄、逐值驗證一致。 | 場景 | Runtime | 耗時中位數 | 列/秒 | 首列延遲 | Managed allocation | Peak working set | | --- | --- | ---: | ---: | ---: | ---: | ---: | -| Cold | MiniExcel | 2,598.50 ms | 38,484 | 1,051.97 ms | 2,541.93 MB | 50.93 MB | -| Cold | MiniExcel.Rust (.NET) | 1,899.42 ms | 52,648 | 450.85 ms | 289.36 MB | 49.64 MB | -| Cold | MiniExcel.Rust | 1,220.82 ms | 81,912 | 537.17 ms | 不適用 | 4.14 MB | -| Steady | MiniExcel | 5,997.76 ms | 50,019 | 616.40 ms | 7,625.15 MB | 52.80 MB | -| Steady | MiniExcel.Rust (.NET) | 4,202.10 ms | 71,393 | 442.59 ms | 868.05 MB | 48.79 MB | -| Steady | MiniExcel.Rust | 3,344.20 ms | 89,708 | 467.30 ms | 不適用 | 4.20 MB | +| Cold | MiniExcel | 2,145.90 ms | 46,600 | 38.77 ms | 1,167.07 MB | 51.26 MB | +| Cold | MiniExcel.Rust (.NET) | 1,047.67 ms | 95,450 | 12.05 ms | 107.04 MB | 44.59 MB | +| Cold | MiniExcel.Rust | 855.09 ms | 116,947 | 0.42 ms | 不適用 | 3.70 MB | +| Steady | MiniExcel | 4,341.55 ms | 69,100 | 3.95 ms | 3,500.58 MB | 54.74 MB | +| Steady | MiniExcel.Rust (.NET) | 2,972.94 ms | 100,910 | 5.50 ms | 321.09 MB | 48.08 MB | +| Steady | MiniExcel.Rust | 2,143.31 ms | 139,970 | 0.41 ms | 不適用 | 3.79 MB | + +完整 Query 中,preview.2 在 Cold 與 Steady 場景分別比 MiniExcel v1 快 2.05 倍與 1.46 倍, +兩個場景的 managed allocation 都減少 90.8%。 結果會受機器與 workbook 影響。請用 `pwsh ./scripts/compare-nuget-v1-rust.ps1` 在代表性 workbook 上重測; 詳見[測試方法](../dotnet-v1-query-benchmark.md)。 diff --git a/dotnet/src/MiniExcel.Rust/MiniExcel.Rust.csproj b/dotnet/src/MiniExcel.Rust/MiniExcel.Rust.csproj index 3cc1cd4..79a8570 100644 --- a/dotnet/src/MiniExcel.Rust/MiniExcel.Rust.csproj +++ b/dotnet/src/MiniExcel.Rust/MiniExcel.Rust.csproj @@ -8,9 +8,10 @@ MiniExcelLibs MiniExcel.Rust MiniExcel.Rust - 0.1.0-preview.1 + 0.1.0-preview.2 MiniExcel.Rust MiniExcel-compatible spreadsheet APIs for .NET powered by MiniExcel for Rust. + Improves query streaming latency and allocation by fetching one initial row and reusing managed column names across native batches. Expands comparative NuGet benchmarks and separates declared-dimension from fallback worksheet scans. Mini-Software Mini-Software excel;xlsx;csv;rust;native;streaming;miniexcel diff --git a/dotnet/tests/MiniExcel.Rust.PackageTests/Program.cs b/dotnet/tests/MiniExcel.Rust.PackageTests/Program.cs index eb01100..16dc591 100644 --- a/dotnet/tests/MiniExcel.Rust.PackageTests/Program.cs +++ b/dotnet/tests/MiniExcel.Rust.PackageTests/Program.cs @@ -41,6 +41,23 @@ configuration: readConfiguration).Single(); Require(typed.Name == "Ada" && typed.Score == 42, "MiniExcel v1 attributes were not honored by typed mapping."); + var batchInputPath = Path.Combine(temporaryDirectory, "batch-input.xlsx"); + global::MiniExcelLibs.MiniExcel.SaveAs( + batchInputPath, + Enumerable.Range(1, 130) + .Select(index => new Dictionary + { + ["Name"] = $"Row {index}", + ["Score"] = index + })); + var batchRows = MiniExcelRust.Query(batchInputPath, useHeaderRow: true).ToList(); + Require(batchRows.Count == 130, "Multi-batch query returned an unexpected row count."); + Require(batchRows[64]["Name"]?.ToString() == "Row 65", "Multi-batch query lost row order."); + var firstColumnName = batchRows[0].Keys.First(); + Require( + batchRows.All(batchRow => ReferenceEquals(firstColumnName, batchRow.Keys.First())), + "Multi-batch query did not reuse managed column names."); + var outputPath = Path.Combine(temporaryDirectory, "output.xlsx"); var writeConfiguration = new OpenXmlConfiguration { diff --git a/miniexcel/examples/nuget_method_benchmark.rs b/miniexcel/examples/nuget_method_benchmark.rs index ec1f9a6..5aec7f3 100644 --- a/miniexcel/examples/nuget_method_benchmark.rs +++ b/miniexcel/examples/nuget_method_benchmark.rs @@ -52,7 +52,7 @@ fn main() -> Result<(), Box> { }); for _ in 0..warmups { if matches!(method, "query" | "query-first") { - query(&input_path, method == "query-first", None)?; + query(&input_path, method == "query-first", None, None)?; } else { run_write_method( method, @@ -68,18 +68,21 @@ fn main() -> Result<(), Box> { let mut total_rows = 0_u64; let mut total_cells = 0_u64; let mut first_row_milliseconds = None; - let mut content_hash = None; + let mut content_hasher = matches!(method, "query" | "query-first").then(Sha256::new); for _ in 0..passes { match method { "query" | "query-first" => { - let (result_rows, result_cells, hash, first_row) = - query(&input_path, method == "query-first", Some(started))?; + let (result_rows, result_cells, first_row) = query( + &input_path, + method == "query-first", + Some(started), + content_hasher.as_mut(), + )?; total_rows += result_rows; total_cells += result_cells; if first_row_milliseconds.is_none() { first_row_milliseconds = Some(first_row); } - content_hash = Some(hash); } "create" | "template" => { run_write_method( @@ -96,6 +99,7 @@ fn main() -> Result<(), Box> { } } let elapsed_milliseconds = started.elapsed().as_secs_f64() * 1000.0; + let content_hash = content_hasher.map(|hasher| format!("{:X}", hasher.finalize())); println!( "{}", serde_json::to_string(&BenchmarkResult { @@ -120,11 +124,11 @@ fn query( path: &Path, first_only: bool, started: Option, -) -> Result<(u64, u64, String, f64), Box> { + mut hasher: Option<&mut Sha256>, +) -> Result<(u64, u64, f64), Box> { let mut rows = 0_u64; let mut cells = 0_u64; let mut first_row_milliseconds = 0.0; - let mut hasher = Sha256::new(); for row in MiniExcel::query(path)? { let row = row?; if rows == 0 { @@ -133,15 +137,17 @@ fn query( } rows += 1; cells += row.len() as u64; - for (name, value) in &row { - append_text(&mut hasher, name); - append_text(&mut hasher, &normalize(value)); + if let Some(hasher) = hasher.as_deref_mut() { + for (name, value) in &row { + append_text(hasher, name); + append_text(hasher, &normalize(value)); + } } if first_only { break; } } - Ok((rows, cells, format!("{:X}", hasher.finalize()), first_row_milliseconds)) + Ok((rows, cells, first_row_milliseconds)) } fn run_write_method( @@ -204,4 +210,4 @@ fn usage() -> io::Error { io::ErrorKind::InvalidInput, "usage: nuget_method_benchmark ", ) -} \ No newline at end of file +} diff --git a/scripts/compare-nuget-v1-rust.ps1 b/scripts/compare-nuget-v1-rust.ps1 index b73d4eb..d7aa672 100644 --- a/scripts/compare-nuget-v1-rust.ps1 +++ b/scripts/compare-nuget-v1-rust.ps1 @@ -12,6 +12,8 @@ param( [ValidateRange(1, 26)] [int] $Columns = 10, + [switch] $OmitDimension, + [ValidateRange(1, 100)] [int] $Passes = 3, @@ -47,7 +49,12 @@ if ([string]::IsNullOrWhiteSpace($OutputDirectory)) { $OutputDirectory = Join-Path $repositoryRoot 'target/benchmarks/nuget-v1' } $OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) -$workbook = Join-Path $OutputDirectory "benchmark-$Rows`x$Columns.xlsx" +$workbookName = if ($OmitDimension) { + "benchmark-no-dimension-$Rows`x$Columns.xlsx" +} else { + "benchmark-$Rows`x$Columns.xlsx" +} +$workbook = Join-Path $OutputDirectory $workbookName $template = Join-Path $OutputDirectory 'template.xlsx' if ([string]::IsNullOrWhiteSpace($MiniExcelVersion)) { @@ -102,7 +109,8 @@ if ($LASTEXITCODE -ne 0) { throw 'Benchmark build failed.' } & cargo +1.85.0 build --release -p miniexcel --example nuget_method_benchmark --locked if ($LASTEXITCODE -ne 0) { throw 'Native Rust benchmark build failed.' } -& dotnet $runner generate $workbook $Rows $Columns +$generateMode = if ($OmitDimension) { 'generate-no-dimension' } else { 'generate' } +& dotnet $runner $generateMode $workbook $Rows $Columns if ($LASTEXITCODE -ne 0) { throw 'Benchmark workbook generation failed.' } & dotnet $runner verify $workbook if ($LASTEXITCODE -ne 0) { throw 'MiniExcel and MiniExcel.Rust returned different data.' } @@ -331,6 +339,7 @@ $report = [ordered]@{ RustRevision = (& git -C $repositoryRoot rev-parse HEAD).Trim() Rows = $Rows Columns = $Columns + HasWorksheetDimension = -not $OmitDimension.IsPresent WorkbookSha256 = (Get-FileHash $workbook -Algorithm SHA256).Hash.ToLowerInvariant() PackageSha256 = (Get-FileHash $package -Algorithm SHA256).Hash.ToLowerInvariant() Iterations = $Iterations @@ -353,6 +362,7 @@ $markdown.Add("- MiniExcel: $MiniExcelVersion") $markdown.Add("- MiniExcel.Rust: $MiniExcelRustVersion") $markdown.Add("- MiniExcel.Rust native: 0.4.0 ($($report.RustRevision))") $markdown.Add("- Workbook: $Rows rows x $Columns columns") +$markdown.Add("- Worksheet dimension: $(if ($OmitDimension) { 'omitted' } else { 'declared' })") $markdown.Add("- Iterations: $Iterations fresh processes per runtime and scenario") $markdown.Add('') $markdown.Add('| Method | Scenario | Runtime | Median elapsed (ms) | Rows/s | First row (ms) | Allocated (MB) | Peak working set (MB) |')