diff --git a/IntSet.sln b/IntSet.sln index fb24024..8014fcc 100644 --- a/IntSet.sln +++ b/IntSet.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36109.1 d17.14 +VisualStudioVersion = 17.14.36109.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntSet", "src\IntSet\IntSet.csproj", "{AE63B664-F383-48F8-8EEE-70FCB2169AF7}" EndProject @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решен .github\workflows\manual-publish.yml = .github\workflows\manual-publish.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntSet.Benchmarks", "src\IntSet.Benchmarks\IntSet.Benchmarks.csproj", "{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {175DFCC8-9221-4D95-81D3-42C7252227D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {175DFCC8-9221-4D95-81D3-42C7252227D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {175DFCC8-9221-4D95-81D3-42C7252227D0}.Release|Any CPU.Build.0 = Release|Any CPU + {5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 0d18665..b12480e 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,25 @@ The repository has tests that show that everything works as it should. I also have a code with benchmarks that shows superiority over HashSet in the operations of adding, deleting, and contains. I will publish it in this repository after I bring it to an acceptable form. + +## Benchmarks + +This project includes benchmarks to compare the performance and memory usage of `IntSet` against the standard `System.Collections.Generic.HashSet`. The benchmarks are implemented using [BenchmarkDotNet](https://benchmarkdotnet.org/). + +### Running the Benchmarks + +To run the benchmarks: + +1. Navigate to the benchmark project directory: + ```bash + cd src/IntSet.Benchmarks + ``` +2. Run the benchmark project: + ```bash + dotnet run -c Release + ``` + It is highly recommended to run benchmarks in `Release` configuration for accurate results. + +### Benchmark Results + +The benchmark results will be displayed in the console after the run completes. Additionally, BenchmarkDotNet will generate detailed reports (including markdown files, CSV files, and plots) in a `BenchmarkDotNet.Artifacts` directory within `src/IntSet.Benchmarks/bin/Release/netX.X/` (where `netX.X` is the target framework, e.g., `net9.0`). diff --git a/src/IntSet.Benchmarks/IntSet.Benchmarks.csproj b/src/IntSet.Benchmarks/IntSet.Benchmarks.csproj new file mode 100644 index 0000000..469bbde --- /dev/null +++ b/src/IntSet.Benchmarks/IntSet.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + diff --git a/src/IntSet.Benchmarks/Program.cs b/src/IntSet.Benchmarks/Program.cs new file mode 100644 index 0000000..a65efc4 --- /dev/null +++ b/src/IntSet.Benchmarks/Program.cs @@ -0,0 +1,19 @@ +using System; +using BenchmarkDotNet.Running; +using IntSet.Benchmarks; // Namespace where SetOperationsBenchmarks is defined + +public class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("Starting IntSet Benchmarks..."); + // To run all benchmarks from the assembly (if you have multiple benchmark classes) + // var summary = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + + // To run a specific benchmark class + var summary = BenchmarkRunner.Run(null, args); + + // You can add more summaries or configurations if needed + Console.WriteLine("IntSet Benchmarks completed."); + } +} diff --git a/src/IntSet.Benchmarks/SetOperationsBenchmarks.cs b/src/IntSet.Benchmarks/SetOperationsBenchmarks.cs new file mode 100644 index 0000000..7ec8371 --- /dev/null +++ b/src/IntSet.Benchmarks/SetOperationsBenchmarks.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using Kibnet; // Assuming IntSet is in this namespace + +namespace IntSet.Benchmarks +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + [CategoriesColumn] + // Optional: Add [SimpleJob(RuntimeMoniker.Net80)] or other job configurations if needed + public class SetOperationsBenchmarks + { + // Parameters for benchmark variations + [Params(0, 100, 10000, 1000000)] // Added 0 to test edge cases + public int Size; + + [Params("Dense", "Sparse")] + public string DataType; + + private List _dataA; + private List _dataB; // For binary set operations + + private Kibnet.IntSet _intSetA; // Fully qualify to avoid ambiguity if any + private HashSet _hashSetA; + + private Kibnet.IntSet _intSetB; // For binary set operations + private HashSet _hashSetB; // For binary set operations + + private int _itemToAdd; + private int _itemToRemove; + private int _itemToContain; + + [GlobalSetup] + public void GlobalSetup() + { + Random rand = new Random(42); // Use a fixed seed for reproducibility + if (DataType == "Dense") + { + _dataA = Enumerable.Range(0, Size).ToList(); + // Ensure _dataB is also scaled by Size for dense, and has some overlap and some difference + _dataB = Enumerable.Range(Size / 2, Size).ToList(); + } + else // Sparse + { + // Generate Size unique random numbers for sparse data for _dataA + var sparseA = new HashSet(); + while(sparseA.Count < Size) + { + sparseA.Add(rand.Next(0, Size * 10)); + } + _dataA = sparseA.ToList(); + _dataA.Sort(); // Optional: Sort if order matters for setup, though not for sets + + // Generate Size unique random numbers for sparse data for _dataB + var sparseB = new HashSet(); + while(sparseB.Count < Size) + { + // Ensure some potential overlap and difference with _dataA + sparseB.Add(rand.Next(0, Size * 10) + (Size * 5)); + } + _dataB = sparseB.ToList(); + _dataB.Sort(); // Optional + } + + // Determine items for Add, Remove, Contains operations + if (Size > 0) + { + // Item not in _dataA for Add + _itemToAdd = DataType == "Dense" ? Size : _dataA.Max() + 1; + if (_dataA.Contains(_itemToAdd)) // Ensure it's truly not in for sparse random + { + _itemToAdd = _dataA.Max() + rand.Next(1,100); + while(_dataA.Contains(_itemToAdd)) { // find one not in the set + _itemToAdd++; + } + } + + _itemToRemove = _dataA[Size / 2]; // Item in _dataA + _itemToContain = _dataA[Size / 2]; // Item in _dataA + } + else + { + _itemToAdd = 0; // Item to add to an empty set + _itemToRemove = 0; // Item to attempt to remove from an empty set + _itemToContain = 0; // Item to check in an empty set + } + } + + [IterationSetup] + public void IterationSetup() + { + // Initialize sets for each iteration to ensure a clean state + // Pass empty list if _dataA or _dataB is null (e.g. if Size is 0 and GlobalSetup didn't init them) + _intSetA = new Kibnet.IntSet(_dataA ?? new List()); + _hashSetA = new HashSet(_dataA ?? new List()); + + _intSetB = new Kibnet.IntSet(_dataB ?? new List()); + _hashSetB = new HashSet(_dataB ?? new List()); + } + + // --- Add Operation --- + [BenchmarkCategory("Add"), Benchmark] + public void IntSet_Add() => _intSetA.Add(_itemToAdd); + + [BenchmarkCategory("Add"), Benchmark] + public void HashSet_Add() => _hashSetA.Add(_itemToAdd); + + // --- Contains Operation --- + [BenchmarkCategory("Contains"), Benchmark] + public bool IntSet_Contains() => _intSetA.Contains(_itemToContain); + + [BenchmarkCategory("Contains"), Benchmark] + public bool HashSet_Contains() => _hashSetA.Contains(_itemToContain); + + // --- Remove Operation --- + [BenchmarkCategory("Remove"), Benchmark] + public bool IntSet_Remove() // Return bool for consistency with HashSet.Remove + { + if (Size > 0) return _intSetA.Remove(_itemToRemove); + return false; // Or handle as appropriate for empty set + } + + [BenchmarkCategory("Remove"), Benchmark] + public bool HashSet_Remove() + { + if (Size > 0) return _hashSetA.Remove(_itemToRemove); + return false; + } + + // --- UnionWith Operation --- + [BenchmarkCategory("Union"), Benchmark] + public void IntSet_UnionWith() => _intSetA.UnionWith(_intSetB); + + [BenchmarkCategory("Union"), Benchmark] + public void HashSet_UnionWith() => _hashSetA.UnionWith(_hashSetB); + + // --- IntersectWith Operation --- + [BenchmarkCategory("Intersection"), Benchmark] + public void IntSet_IntersectWith() => _intSetA.IntersectWith(_intSetB); + + [BenchmarkCategory("Intersection"), Benchmark] + public void HashSet_IntersectWith() => _hashSetA.IntersectWith(_hashSetB); + + // --- ExceptWith Operation --- + [BenchmarkCategory("Except"), Benchmark] + public void IntSet_ExceptWith() => _intSetA.ExceptWith(_intSetB); + + [BenchmarkCategory("Except"), Benchmark] + public void HashSet_ExceptWith() => _hashSetA.ExceptWith(_hashSetB); + + // --- SymmetricExceptWith Operation --- + [BenchmarkCategory("SymmetricExcept"), Benchmark] + public void IntSet_SymmetricExceptWith() => _intSetA.SymmetricExceptWith(_intSetB); + + [BenchmarkCategory("SymmetricExcept"), Benchmark] + public void HashSet_SymmetricExceptWith() => _hashSetA.SymmetricExceptWith(_hashSetB); + } +}