A fast in-memory data table for C# — a drop-in, System.Data.DataTable-friendly alternative,
verified against System.Data with differential tests and BenchmarkDotNet.
If this project has made your life easier or saved you time, you can buy me a coffee to support ongoing development. Thank you sincerely!
RCM.FastData is an in-memory data table library (FastDataTable, FastDataRow,
FastDataColumn, FastDataSet, FastDataView, relations, expression engine) with an API
modeled on System.Data.DataTable.
This repository is more than the library. It is the full verification, correction and optimization project behind it:
- the original implementation is preserved as an immutable snapshot
(
original-snapshot/2026-10-09/, SHA-256 pinned) and is never modified; - 12 confirmed defects (D1–D12) in the original were reproduced with tests, fixed in
src/RCM.FastData, and each fix is proven by unit + differential tests againstSystem.Databehavior; - the fixed/optimized version is benchmarked with BenchmarkDotNet against both
System.Dataand the immutable original.
Current status: 162/162 tests passing on both targets (net48, net8.0), Release
configuration.
FastDataTable/FastDataRow/FastDataColumn/FastDataSet/FastDataViewwith aSystem.Data-like API (internalusingaliases make it drop-in friendly)Compute(expr, filter)aggregates:SUM/AVG/MIN/MAX/COUNT/COUNT(*)withSystem.Data-equivalent typed precision (the original computed everything indouble, losingdecimal/longprecision — defect D2)Select(filter)with=,<>,>,<,>=,<=,LIKE,IS [NOT] NULL(System.DataLIKEsemantics — which is not SQL semantics), plus a per-column equality index for single-clausecol = valfilters- Primary key lookup:
Rows.Find(keys)/Rows.Contains(keys)(composite keys supported) - Relations with real foreign-key validation (
createConstraintshonored), child aggregation,ImportRow/LoadDataRow/Rows.RemoveAt - Expression engine for computed columns and filters (
FastExpressionEngine, AST-based) - Multi-target: .NET Framework 4.8 and .NET 8.0, same source
- 162 tests: 145 core (unit, P1 API, original known-defect) + 17 differential
(same table built on
System.Dataand on FastData, results compared)
BenchmarkDotNet 0.15.8, out-of-process, Workstation GC, Standard profile
(10 warmup + 15 iterations, 2 invocations). Full 102-case tables:
docs/RAPPORTO-FINALE.md §7.
net8.0, final run (102 cases, EXIT=0) — representative numbers (Mean, µs):
| Case (N = 100,000) | System.Data | Original | Fixed | Speedup vs System.Data |
|---|---|---|---|---|
| Load (build table) | 87.35 ms | 35.08 ms | 40.82 ms | 2.14× |
| Select equality-int | 6.26 µs | 1915 µs | 0.774 µs | 8.09× |
| Select equality-string | 421.6 µs | 2382 µs | 240.0 µs | 1.76× |
| Select range | 44.91 ms | 2742 µs | 2622 µs | 17.13× |
| Select compound | 50.70 ms | 2720 µs | 2562 µs | 19.79× |
Compute COUNT(Id) with filter |
42.55 ms | 2224 µs | 2764 µs | 15.39× |
row["Name"] read |
131.7 ns | 14.4 ns | 13.4 ns | 9.85× |
Rows.Find(PK) |
295.0 ns | n/a | 48.5 ns | 6.08× |
Rows.Contains(PK) |
273.6 ns | n/a | 37.0 ns | 7.40× |
n/a = not implemented in that subject (PK lookup did not exist in the original);
it is a marker, never a measurement.
Memory (per call, MemoryDiagnoser):
| Case | System.Data | Original | Fixed |
|---|---|---|---|
Cell read (row["col"], Field<T>) |
0 B | 0 B | 0 B |
Rows.Find / Rows.Contains |
152 / 128 B | n/a | 88 B |
| Select equality-int (1 match of 100,000) | 3.27 KB | 782.35 KB | 1.11 KB |
| Table memory (per 1,000 rows) | 604.45 KB | 462.42 KB | 462.24 KB |
Honest caveats, as documented in the final report:
- the fixed version is slower than the original on typed
SUMat N ≥ 10,000 — the measured cost of the D2 precision fix (declared, not a regression); - System.Data
Selectnumbers measure its warm path (it builds an implicit per-column equality index on first access; the cold cost is not measured); - the net48 final run is partial (Load, RowRead, Compute, PkLookup completed; the
Select family was interrupted — see
docs/RAPPORTO-FINALE.md§7.2/§12).
The corrected version is behavior-compatible with System.Data.DataTable/DataView for
everything tested — 162/162 tests, including 17 differential tests. Declared differences
(full matrix with [PARITA] / [DIFF-VERIF] tags in
docs/COMPATIBILITA.md):
COUNT(*)is supported by FastData, not bySystem.Data.Select(filter)preserves insertion order;System.Dataapplies an implicit sort on the filtered column (documented ADO.NET behavior).Computewithout parentheses throws a catchableNotSupportedException(the original crashed withStackOverflowException— defect D1).MIN/MAXon strings compareOrdinalIgnoreCasevs the column'sIComparable(identical ordering on ASCII, may differ with accents/case).LIKEisSystem.Datasemantics, not SQL:%/*wildcards only at the start and/or end of the pattern,_/?are literal,[c]is a single character class.- Conversions use the current culture, like
System.Data.
- .NET SDK (9.0.308 tested) with MSBuild
- .NET 8 runtime (target
net8.0) - .NET Framework 4.8 (target
net48); reference assemblies are restored automatically from NuGet (Microsoft.NETFramework.ReferenceAssemblies.net48) - Windows PowerShell 5.1 (for the helper scripts)
# environment check (SDK, runtimes, reference assemblies, BDN version)
powershell -ExecutionPolicy Bypass -File scripts\check-env.ps1
# restore / build
powershell -ExecutionPolicy Bypass -File scripts\restore.ps1
powershell -ExecutionPolicy Bypass -File scripts\build.ps1 # both targets
powershell -ExecutionPolicy Bypass -File scripts\build.ps1 -Target net8.0 # single target
# tests (unit + differential + regression; TRX in artifacts\testresults)
powershell -ExecutionPolicy Bypass -File scripts\run-tests.ps1
powershell -ExecutionPolicy Bypass -File scripts\run-tests.ps1 -Target net8.0 -Filter "FullyQualifiedName~ComputeTests"
# benchmarks (profiles: Smoke=dry, Standard=medium, Full=long, Stress=verylong)
powershell -ExecutionPolicy Bypass -File scripts\run-benchmark.ps1 -Profile Smoke -Target net8.0
powershell -ExecutionPolicy Bypass -File scripts\run-benchmark.ps1 -Profile Standard -Target net8.0
powershell -ExecutionPolicy Bypass -File scripts\run-benchmark.ps1 -Profile Standard -Target net48
# comparison (the three plan ratios: speedup vs original, speedup vs System.Data, internal improvement)
powershell -ExecutionPolicy Bypass -File scripts\compare-results.ps1 -Tag standard-net8.0Operational rule: never run build/test/benchmark in parallel (CPU contention contaminates measurements). One benchmark at a time.
RCM.FastData.sln
├── src/RCM.FastData/ the corrected/optimized library (v1.0.0)
├── src/RCM.FastData.Original/ the original as a comparison project (v0.0.0-original)
├── original-snapshot/2026-10-09/ immutable byte-identical original + SHA-256
├── shared/BenchData.cs shared test/benchmark dataset builder
├── tests/RCM.FastData.Tests/ 162 xunit tests (Core + Differential)
├── bench/RCM.FastData.Benchmarks/ BenchmarkDotNet runner (out-of-process)
├── tools/RCM.FastData.CrashProbe/ separate process for StackOverflow-risk cases
├── scripts/ PowerShell 5.1: env, build, test, benchmark, compare
└── docs/ plan, initial matrix, compatibility, final report
| File | Contents |
|---|---|
docs/PIANO.md |
operational plan, phases A–F, defects D1–D12, rules |
docs/MATRICE-INIZIALE.md |
initial test matrix (149 tests: 123 ok / 26 failures = the P0 defects), verified System.Data ground truth |
docs/COMPATIBILITA.md |
parity and declared differences, fixed version vs System.Data |
docs/RAPPORTO-FINALE.md |
final report: environment, measured results, repeat commands |
| Path | Contents |
|---|---|
artifacts/bench/bench-<target>/results/ |
BDN reports per benchmark class per target: *-report.md, *-report.csv, *-report.full.json (the two targets keep separate runs so ratios are never mixed; a run uses --artifacts=artifacts\bench\bench-<target>) |
artifacts/bench/<profile>-<target>.log |
full run log (ends with EXIT=<code> DURATION=<min>min) |
artifacts/bench/comparisons/confronto-<tag>.md |
markdown table of the three ratios per case |
artifacts/testresults/matrix-d.<target>.trx |
test report per target |
original-snapshot/2026-10-09/ |
immutable original snapshot + SHA-256 |
- Benchmarks report only measurements actually executed; a case not implemented in a
subject (e.g. PK lookup in the original, which has no
Rows.Find) is marked n/a — that is a marker, not a measurement. - The
[BENCH] RUNTIME=...marker in each log attests the runtime actually executing in the child process (net48 →mscorlib; net8.0 →System.Private.CoreLib). StackOverflow-risk cases (Computewithout parentheses on the original) run only in a separate process (tools/RCM.FastData.CrashProbe), never in the test/benchmark process.
If RCM.FastData is useful to you, consider supporting the project.
This project is developed and maintained as an open-source initiative. Contributions help support:
- New benchmark scenarios and environments
- Improvements to the verification and differential-test infrastructure
- Better documentation and reports
- Compatibility testing
- Bug fixes
- New features for the library
Every contribution is appreciated and helps keep the project growing.
Support is completely voluntary and does not affect the availability or functionality of the software.
You can also support the project by:
- ⭐ Giving the repository a Star
- 🐛 Reporting issues
- 💡 Suggesting new benchmarks or features
- 🔧 Contributing improvements
- 📖 Improving documentation
- 📢 Sharing the project with other .NET developers
Contributions are welcome via issues and pull requests. Please keep the operational rules: the original snapshot stays immutable, comparisons stay homogeneous (same runtime, schema, dataset, filter), and no benchmark reports an unmeasured number.
c# · dotnet · data-table · in-memory · database · benchmark · benchmarkdotnet · performance · xunit · fastdata
Licensed under the MIT License.
Fast in-memory data tables — verified against System.Data.