Skip to content

Commit 46e9fa3

Browse files
feat: Add benchmarks for IntSet vs HashSet<int>
This commit introduces a new benchmark project (`IntSet.Benchmarks`) to compare the performance and memory allocation of `Kibnet.IntSet` against `System.Collections.Generic.HashSet<int>` for common set operations. The following operations are benchmarked: - Add - Remove - Contains - UnionWith - IntersectWith - ExceptWith - SymmetricExceptWith Benchmarks are parameterized for different set sizes (0, 100, 10k, 1M elements) and data distributions (Dense, Sparse). The `BenchmarkDotNet` library is used to run the benchmarks and generate reports. Instructions on how to run the benchmarks and locate the results have been added to the `README.md` file.
1 parent e0439ba commit 46e9fa3

5 files changed

Lines changed: 232 additions & 0 deletions

File tree

IntSet.sln

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решен
1313
.github\workflows\manual-publish.yml = .github\workflows\manual-publish.yml
1414
EndProjectSection
1515
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F0B2B966-A713-4E71-9324-91826F62E022}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntSet.Benchmarks", "src\IntSet.Benchmarks\IntSet.Benchmarks.csproj", "{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}"
19+
EndProject
1620
Global
1721
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1822
Debug|Any CPU = Debug|Any CPU
@@ -27,11 +31,18 @@ Global
2731
{175DFCC8-9221-4D95-81D3-42C7252227D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
2832
{175DFCC8-9221-4D95-81D3-42C7252227D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
2933
{175DFCC8-9221-4D95-81D3-42C7252227D0}.Release|Any CPU.Build.0 = Release|Any CPU
34+
{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35+
{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Release|Any CPU.ActiveCfg = Release|Any CPU
37+
{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74}.Release|Any CPU.Build.0 = Release|Any CPU
3038
EndGlobalSection
3139
GlobalSection(SolutionProperties) = preSolution
3240
HideSolutionNode = FALSE
3341
EndGlobalSection
3442
GlobalSection(ExtensibilityGlobals) = postSolution
3543
SolutionGuid = {831885E0-1B4E-4287-BDEB-135D87533880}
3644
EndGlobalSection
45+
GlobalSection(NestedProjects) = preSolution
46+
{5CC7E42D-3CEE-4626-B90C-9BFB59D05D74} = {F0B2B966-A713-4E71-9324-91826F62E022}
47+
EndGlobalSection
3748
EndGlobal

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,25 @@ The repository has tests that show that everything works as it should.
3939

4040
I also have a code with benchmarks that shows superiority over HashSet in the operations of adding, deleting, and contains.
4141
I will publish it in this repository after I bring it to an acceptable form.
42+
43+
## Benchmarks
44+
45+
This project includes benchmarks to compare the performance and memory usage of `IntSet` against the standard `System.Collections.Generic.HashSet<int>`. The benchmarks are implemented using [BenchmarkDotNet](https://benchmarkdotnet.org/).
46+
47+
### Running the Benchmarks
48+
49+
To run the benchmarks:
50+
51+
1. Navigate to the benchmark project directory:
52+
```bash
53+
cd src/IntSet.Benchmarks
54+
```
55+
2. Run the benchmark project:
56+
```bash
57+
dotnet run -c Release
58+
```
59+
It is highly recommended to run benchmarks in `Release` configuration for accurate results.
60+
61+
### Benchmark Results
62+
63+
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., `net8.0`).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="BenchmarkDotNet" Version="0.15.0" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\IntSet\IntSet.csproj" />
16+
</ItemGroup>
17+
18+
</Project>

src/IntSet.Benchmarks/Program.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using BenchmarkDotNet.Running;
3+
using IntSet.Benchmarks; // Namespace where SetOperationsBenchmarks is defined
4+
5+
public class Program
6+
{
7+
public static void Main(string[] args)
8+
{
9+
Console.WriteLine("Starting IntSet Benchmarks...");
10+
// To run all benchmarks from the assembly (if you have multiple benchmark classes)
11+
// var summary = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
12+
13+
// To run a specific benchmark class
14+
var summary = BenchmarkRunner.Run<SetOperationsBenchmarks>(args);
15+
16+
// You can add more summaries or configurations if needed
17+
Console.WriteLine("IntSet Benchmarks completed.");
18+
}
19+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Configs;
6+
using BenchmarkDotNet.Jobs;
7+
using Kibnet; // Assuming IntSet is in this namespace
8+
9+
namespace IntSet.Benchmarks
10+
{
11+
[MemoryDiagnoser]
12+
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
13+
[CategoriesColumn]
14+
// Optional: Add [SimpleJob(RuntimeMoniker.Net80)] or other job configurations if needed
15+
public class SetOperationsBenchmarks
16+
{
17+
// Parameters for benchmark variations
18+
[Params(0, 100, 10000, 1000000)] // Added 0 to test edge cases
19+
public int Size;
20+
21+
[Params("Dense", "Sparse")]
22+
public string DataType;
23+
24+
private List<int> _dataA;
25+
private List<int> _dataB; // For binary set operations
26+
27+
private Kibnet.IntSet _intSetA; // Fully qualify to avoid ambiguity if any
28+
private HashSet<int> _hashSetA;
29+
30+
private Kibnet.IntSet _intSetB; // For binary set operations
31+
private HashSet<int> _hashSetB; // For binary set operations
32+
33+
private int _itemToAdd;
34+
private int _itemToRemove;
35+
private int _itemToContain;
36+
37+
[GlobalSetup]
38+
public void GlobalSetup()
39+
{
40+
Random rand = new Random(42); // Use a fixed seed for reproducibility
41+
if (DataType == "Dense")
42+
{
43+
_dataA = Enumerable.Range(0, Size).ToList();
44+
// Ensure _dataB is also scaled by Size for dense, and has some overlap and some difference
45+
_dataB = Enumerable.Range(Size / 2, Size).ToList();
46+
}
47+
else // Sparse
48+
{
49+
// Generate Size unique random numbers for sparse data for _dataA
50+
var sparseA = new HashSet<int>();
51+
while(sparseA.Count < Size)
52+
{
53+
sparseA.Add(rand.Next(0, Size * 10));
54+
}
55+
_dataA = sparseA.ToList();
56+
_dataA.Sort(); // Optional: Sort if order matters for setup, though not for sets
57+
58+
// Generate Size unique random numbers for sparse data for _dataB
59+
var sparseB = new HashSet<int>();
60+
while(sparseB.Count < Size)
61+
{
62+
// Ensure some potential overlap and difference with _dataA
63+
sparseB.Add(rand.Next(0, Size * 10) + (Size * 5));
64+
}
65+
_dataB = sparseB.ToList();
66+
_dataB.Sort(); // Optional
67+
}
68+
69+
// Determine items for Add, Remove, Contains operations
70+
if (Size > 0)
71+
{
72+
// Item not in _dataA for Add
73+
_itemToAdd = DataType == "Dense" ? Size : _dataA.Max() + 1;
74+
if (_dataA.Contains(_itemToAdd)) // Ensure it's truly not in for sparse random
75+
{
76+
_itemToAdd = _dataA.Max() + rand.Next(1,100);
77+
while(_dataA.Contains(_itemToAdd)) { // find one not in the set
78+
_itemToAdd++;
79+
}
80+
}
81+
82+
_itemToRemove = _dataA[Size / 2]; // Item in _dataA
83+
_itemToContain = _dataA[Size / 2]; // Item in _dataA
84+
}
85+
else
86+
{
87+
_itemToAdd = 0; // Item to add to an empty set
88+
_itemToRemove = 0; // Item to attempt to remove from an empty set
89+
_itemToContain = 0; // Item to check in an empty set
90+
}
91+
}
92+
93+
[IterationSetup]
94+
public void IterationSetup()
95+
{
96+
// Initialize sets for each iteration to ensure a clean state
97+
// Pass empty list if _dataA or _dataB is null (e.g. if Size is 0 and GlobalSetup didn't init them)
98+
_intSetA = new Kibnet.IntSet(_dataA ?? new List<int>());
99+
_hashSetA = new HashSet<int>(_dataA ?? new List<int>());
100+
101+
_intSetB = new Kibnet.IntSet(_dataB ?? new List<int>());
102+
_hashSetB = new HashSet<int>(_dataB ?? new List<int>());
103+
}
104+
105+
// --- Add Operation ---
106+
[BenchmarkCategory("Add"), Benchmark]
107+
public void IntSet_Add() => _intSetA.Add(_itemToAdd);
108+
109+
[BenchmarkCategory("Add"), Benchmark]
110+
public void HashSet_Add() => _hashSetA.Add(_itemToAdd);
111+
112+
// --- Contains Operation ---
113+
[BenchmarkCategory("Contains"), Benchmark]
114+
public bool IntSet_Contains() => _intSetA.Contains(_itemToContain);
115+
116+
[BenchmarkCategory("Contains"), Benchmark]
117+
public bool HashSet_Contains() => _hashSetA.Contains(_itemToContain);
118+
119+
// --- Remove Operation ---
120+
[BenchmarkCategory("Remove"), Benchmark]
121+
public bool IntSet_Remove() // Return bool for consistency with HashSet.Remove
122+
{
123+
if (Size > 0) return _intSetA.Remove(_itemToRemove);
124+
return false; // Or handle as appropriate for empty set
125+
}
126+
127+
[BenchmarkCategory("Remove"), Benchmark]
128+
public bool HashSet_Remove()
129+
{
130+
if (Size > 0) return _hashSetA.Remove(_itemToRemove);
131+
return false;
132+
}
133+
134+
// --- UnionWith Operation ---
135+
[BenchmarkCategory("Union"), Benchmark]
136+
public void IntSet_UnionWith() => _intSetA.UnionWith(_intSetB);
137+
138+
[BenchmarkCategory("Union"), Benchmark]
139+
public void HashSet_UnionWith() => _hashSetA.UnionWith(_hashSetB);
140+
141+
// --- IntersectWith Operation ---
142+
[BenchmarkCategory("Intersection"), Benchmark]
143+
public void IntSet_IntersectWith() => _intSetA.IntersectWith(_intSetB);
144+
145+
[BenchmarkCategory("Intersection"), Benchmark]
146+
public void HashSet_IntersectWith() => _hashSetA.IntersectWith(_hashSetB);
147+
148+
// --- ExceptWith Operation ---
149+
[BenchmarkCategory("Except"), Benchmark]
150+
public void IntSet_ExceptWith() => _intSetA.ExceptWith(_intSetB);
151+
152+
[BenchmarkCategory("Except"), Benchmark]
153+
public void HashSet_ExceptWith() => _hashSetA.ExceptWith(_hashSetB);
154+
155+
// --- SymmetricExceptWith Operation ---
156+
[BenchmarkCategory("SymmetricExcept"), Benchmark]
157+
public void IntSet_SymmetricExceptWith() => _intSetA.SymmetricExceptWith(_intSetB);
158+
159+
[BenchmarkCategory("SymmetricExcept"), Benchmark]
160+
public void HashSet_SymmetricExceptWith() => _hashSetA.SymmetricExceptWith(_hashSetB);
161+
}
162+
}

0 commit comments

Comments
 (0)