diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cb6fd1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,240 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = tab +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = when_on_single_line:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_operators = when_on_single_line:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = when_multiline:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_diagnostic.HAA0303.severity = silent +dotnet_diagnostic.HAA0401.severity = silent +dotnet_diagnostic.HAA0501.severity = silent +dotnet_diagnostic.HAA0502.severity = silent +dotnet_diagnostic.HAA0601.severity = silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_diagnostic.CA2007.severity = error \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac9b19b..857336e 100644 --- a/.gitignore +++ b/.gitignore @@ -298,3 +298,6 @@ __pycache__/ *.btm.cs *.odx.cs *.xsd.cs +.vscode/launch.json +.vscode/tasks.json +/source/nuget.config diff --git a/LICENSE b/LICENSE index 8864d4a..d21a0d2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2017 +Copyright (c) 2020 electricessence (Oren F.) All rights reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Open.Disposable.ObjectPools.sln b/Open.Disposable.ObjectPools.sln new file mode 100644 index 0000000..6de8138 --- /dev/null +++ b/Open.Disposable.ObjectPools.sln @@ -0,0 +1,42 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31717.71 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Disposable.ObjectPools.Benchmarking", "benchmarking\Open.Disposable.ObjectPools.Benchmarking.csproj", "{8404875C-ED46-4057-8D97-598B4D7B8040}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Disposable.ObjectPools", "source\Open.Disposable.ObjectPools.csproj", "{7DF5DA35-AB11-479D-BD87-27461AC3F5A8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Disposable.ObjectPools.Tests", "tests\Open.Disposable.ObjectPools.Tests.csproj", "{507DBB8A-A662-4D16-A375-48470A578B50}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4E2B0ECD-63D2-42CF-91D2-53917748B856}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8404875C-ED46-4057-8D97-598B4D7B8040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8404875C-ED46-4057-8D97-598B4D7B8040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8404875C-ED46-4057-8D97-598B4D7B8040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8404875C-ED46-4057-8D97-598B4D7B8040}.Release|Any CPU.Build.0 = Release|Any CPU + {7DF5DA35-AB11-479D-BD87-27461AC3F5A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DF5DA35-AB11-479D-BD87-27461AC3F5A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DF5DA35-AB11-479D-BD87-27461AC3F5A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DF5DA35-AB11-479D-BD87-27461AC3F5A8}.Release|Any CPU.Build.0 = Release|Any CPU + {507DBB8A-A662-4D16-A375-48470A578B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {507DBB8A-A662-4D16-A375-48470A578B50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {507DBB8A-A662-4D16-A375-48470A578B50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {507DBB8A-A662-4D16-A375-48470A578B50}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {86DED392-9553-4DB9-9EC3-BEBE4F470390} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 51153b5..5781a52 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # Open.Disposable.ObjectPools A set of variations on ObjectPool implementations with differing underlying collections. + +[![NuGet](https://img.shields.io/nuget/v/Open.Disposable.ObjectPools.svg)](https://www.nuget.org/packages/Open.Disposable.ObjectPools/) diff --git a/benchmarking/Benchmark.cs b/benchmarking/Benchmark.cs index e423ab3..98e0379 100644 --- a/benchmarking/Benchmark.cs +++ b/benchmarking/Benchmark.cs @@ -1,63 +1,90 @@ using Open.Diagnostics; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; -namespace Open.Disposable.ObjectPools +#if DEBUG +using System.Diagnostics; +#endif + +namespace Open.Disposable.ObjectPools; + +public class Benchmark : BenchmarkBase>> + where T : class { - public class Benchmark : BenchmarkBase>> - where T : class + public Benchmark(uint size, uint repeat, Func> poolFactory) : base(size, repeat, poolFactory) + { + // Because some pools do comparison checks on values, we have have unique instances/values. + _items = new T[(int)size]; + var pool = Param(); + Parallel.For(0, TestSize, i => _items[i] = pool.Take()); + } + + readonly T[] _items; + + protected override IEnumerable TestOnceInternal() { - public Benchmark(uint size, uint repeat, Func> poolFactory) : base(size,repeat,poolFactory) + var pool = Param() ?? throw new NullReferenceException(); + using var _ = pool as IDisposable; + //yield return TimedResult.Measure("Take From Empty (In Parallel)", () => + //{ + // Parallel.For(0, TestSize, i => _items[i] = pool.Take()); + //}); + + yield return TimedResult.Measure("Give To (In Parallel)", () => { - // Because some pools do comparison checks on values, we have have unique instances/values. - _items = new T[(int)size]; - } + // ReSharper disable once AccessToDisposedClosure + Parallel.For(0, TestSize, i => pool.Give(_items[i])); +#if DEBUG + if (pool is DefaultObjectPool) return; + var count = pool.Count; + Debug.Assert(pool is OptimisticArrayObjectPool || count == TestSize, $"Expected {TestSize}, acutal count: {count}"); +#endif + }); - readonly T[] _items; + yield return TimedResult.Measure("Mixed 90%-Take/10%-Give (In Parallel)", () => + { + Parallel.For(0, TestSize, i => + { + if (i % 10 == 0) + pool.Give(_items[i]); + else + _items[i] = pool.Take(); + }); + }); - protected override IEnumerable TestOnceInternal() + yield return TimedResult.Measure("Mixed 50%-Take/50%-Give (In Parallel)", () => { - using (var pool = Param()) + Parallel.For(0, TestSize, i => { + if (i % 2 == 0) + _items[i] = pool.Take(); + else + pool.Give(_items[i]); + }); + }); - yield return TimedResult.Measure("Take From Empty (In Parallel)", () => - { - Parallel.For(0, TestSize, i => _items[i] = pool.Take()); - }); - - yield return TimedResult.Measure("Give To (In Parallel)", () => - { - Parallel.For(0, TestSize, i => pool.Give(_items[i])); - }); - - yield return TimedResult.Measure("Mixed Read/Write (In Parallel)", () => - { - Parallel.For(0, TestSize, i => - { - if (i % 2 == 0) - _items[i] = pool.Take(); - else - pool.Give(_items[i]); - }); - }); - - yield return TimedResult.Measure("Empty Pool (.TryTake())", () => - { - while (pool.TryTake() != null) { - // remaining++; - } - }); - } - } - - public static TimedResult[] Results(uint size, uint repeat, Func> poolFactory) + yield return TimedResult.Measure("Mixed 10%-Take/90%-Give (In Parallel)", () => { - return (new Benchmark(size, repeat, poolFactory)).Result; - } + Parallel.For(0, TestSize, i => + { + if (i % 10 == 0) + _items[i] = pool.Take(); + else + pool.Give(_items[i]); + }); + }); + + //if (pool is DefaultObjectPool) yield break; + //yield return TimedResult.Measure("Empty Pool (.TryTake())", () => + //{ + // while (pool.TryTake() is not null) + // { + // // remaining++; + // } + //}); } + + public static TimedResult[] Results(uint size, uint repeat, Func> poolFactory) => (new Benchmark(size, repeat, poolFactory)).Result; } diff --git a/benchmarking/BenchmarkResult.csv b/benchmarking/BenchmarkResult.csv index b43069c..3747579 100644 --- a/benchmarking/BenchmarkResult.csv +++ b/benchmarking/BenchmarkResult.csv @@ -1,34 +1,35 @@ -Batch,Pool Type," From Empty (In Parallel)"," To (In Parallel)",d Read/Write (In Parallel),y Pool (.TryTake()),L, -Repeat 200000 for size 4,QueueObjectPool,00:00:00.8064961,00:00:00.7379976,00:00:00.7372226,00:00:00.0912138,00:00:02.3729301, -Repeat 200000 for size 4,ConcurrentQueueObjectPool,00:00:00.8392163,00:00:00.8090963,00:00:00.8090662,00:00:00.1057385,00:00:02.5631173, -Repeat 200000 for size 4,OptimisticArrayObjectPool,00:00:00.8582319,00:00:00.7316954,00:00:00.7462951,00:00:00.0322101,00:00:02.3684325, -Repeat 80000 for size 10,QueueObjectPool,00:00:00.4891898,00:00:00.4641127,00:00:00.4325487,00:00:00.0887330,00:00:01.4745842, -Repeat 80000 for size 10,ConcurrentQueueObjectPool,00:00:00.4774220,00:00:00.4532694,00:00:00.4577644,00:00:00.1030334,00:00:01.4914892, -Repeat 80000 for size 10,OptimisticArrayObjectPool,00:00:00.5075819,00:00:00.3865528,00:00:00.4139942,00:00:00.0178275,00:00:01.3259564, -Repeat 24000 for size 50,QueueObjectPool,00:00:00.2172210,00:00:00.4786407,00:00:00.2498973,00:00:00.1187320,00:00:01.0644910, -Repeat 24000 for size 50,ConcurrentQueueObjectPool,00:00:00.2130298,00:00:00.2258805,00:00:00.2090524,00:00:00.1446890,00:00:00.7926517, -Repeat 24000 for size 50,OptimisticArrayObjectPool,00:00:00.3218326,00:00:00.1749609,00:00:00.1948959,00:00:00.0136129,00:00:00.7053023, -Repeat 16000 for size 100,QueueObjectPool,00:00:00.1933122,00:00:01.1807893,00:00:00.3700023,00:00:00.1532743,00:00:01.8973781, -Repeat 16000 for size 100,ConcurrentQueueObjectPool,00:00:00.1885421,00:00:00.2231642,00:00:00.1945119,00:00:00.1892646,00:00:00.7954828, -Repeat 16000 for size 100,OptimisticArrayObjectPool,00:00:00.4629300,00:00:00.1296838,00:00:00.2026568,00:00:00.0155329,00:00:00.8108035, -Repeat 25600 for size 250,QueueObjectPool,00:00:00.5119740,00:00:03.6755755,00:00:03.0652345,00:00:00.5573299,00:00:07.8101139, -Repeat 25600 for size 250,ConcurrentQueueObjectPool,00:00:00.5032784,00:00:00.6586173,00:00:00.5488527,00:00:00.7357870,00:00:02.4465354, -Repeat 25600 for size 250,OptimisticArrayObjectPool,00:00:03.0809331,00:00:00.3488741,00:00:00.7180123,00:00:00.0531776,00:00:04.2009971, -Repeat 9600 for size 1000,QueueObjectPool,00:00:00.4419158,00:00:02.4529980,00:00:02.3464286,00:00:00.6892199,00:00:05.9305623, -Repeat 9600 for size 1000,ConcurrentQueueObjectPool,00:00:00.4453723,00:00:00.6651569,00:00:00.5769710,00:00:00.8922154,00:00:02.5797156, -Repeat 9600 for size 1000,OptimisticArrayObjectPool,00:00:13.4891871,00:00:00.2492917,00:00:01.1461595,00:00:00.0718376,00:00:14.9564759, -,OptimisticArrayObjectPool,00:00:03.0074967,00:00:00.3465589,00:00:00.7164117,00:00:00.0529356,00:00:04.1234029, -Repeat 25600 for size 250,ChannelObjectPool,00:00:04.8289131,00:00:05.0525342,00:00:02.7992174,00:00:00.4744166,00:00:13.1550813, -Repeat 9600 for size 1000,QueueObjectPool,00:00:00.4429800,00:00:02.5445855,00:00:02.5696949,00:00:00.7169192,00:00:06.2741796, -Repeat 9600 for size 1000,ConcurrentQueueObjectPool,00:00:00.4447846,00:00:00.6793450,00:00:00.5848884,00:00:00.9255401,00:00:02.6345581, -Repeat 9600 for size 1000,OptimisticArrayObjectPool,00:00:13.5246195,00:00:00.2524943,00:00:01.1490467,00:00:00.0721550,00:00:14.9983155, -Repeat 9600 for size 1000,ChannelObjectPool,00:00:03.0021715,00:00:03.3152862,00:00:01.8869001,00:00:00.5896046,00:00:08.7939624, -1264,00:00:00.4388072,00:00:00.0009975,00:00:01.1470602, -Repeat 4800 for size 4000,QueueObjectPool,00:00:00.7134197,00:00:03.7582117,00:00:03.1855656,00:00:01.2950620,00:00:08.9522590, -Repeat 4800 for size 4000,ConcurrentQueueObjectPool,00:00:00.7232603,00:00:01.2258140,00:00:01.0573101,00:00:01.6096397,00:00:04.6160241, -Repeat 4800 for size 4000,OptimisticArrayObjectPool,00:00:00.6968814,00:00:00.3086700,00:00:00.5845562,00:00:00.0007690,00:00:01.5908766, -Repeat 4800 for size 4000,InterlockedArrayObjectPool,00:00:00.6864968,00:00:00.3074012,00:00:00.6233171,00:00:00.0007314,00:00:01.6179465, -Repeat 2400 for size 8000,QueueObjectPool,00:00:00.7021946,00:00:03.5501399,00:00:02.5203758,00:00:01.2936158,00:00:08.0663261, -Repeat 2400 for size 8000,ConcurrentQueueObjectPool,00:00:00.7088997,00:00:01.2280982,00:00:01.0299925,00:00:01.5836229,00:00:04.5506133, -Repeat 2400 for size 8000,OptimisticArrayObjectPool,00:00:00.6956117,00:00:00.2895694,00:00:00.5699170,00:00:00.0004110,00:00:01.5555091, -Repeat 2400 for size 8000,InterlockedArrayObjectPool,00:00:00.6782009,00:00:00.2871359,00:00:00.6188317,00:00:00.0003863,00:00:01.5845548, +Batch,Pool Type," To (In Parallel)",d 90%-Take/10%-Give (In Parallel),d 50%-Take/50%-Give (In Parallel),d 10%-Take/90%-Give (In Parallel),L, +Repeat 2400000 for size 4,Microsoft.Extensions.ObjectPool.DefaultObjectPool,00:00:03.8980333,00:00:03.9366455,00:00:03.9072529,00:00:03.8850033,00:00:15.6269350, +Repeat 2400000 for size 4,ConcurrentQueueObjectPoolSlim,00:00:03.8494058,00:00:03.9230416,00:00:03.8901577,00:00:03.8867848,00:00:15.5493899, +Repeat 2400000 for size 4,ConcurrentQueueObjectPool,00:00:03.9883122,00:00:04.0076765,00:00:03.9682549,00:00:03.9925014,00:00:15.9567450, +Repeat 2400000 for size 4,OptimisticArrayObjectPool,00:00:03.9568135,00:00:04.2170893,00:00:04.0558332,00:00:03.9863018,00:00:16.2160378, +Repeat 2400000 for size 4,InterlockedArrayObjectPool,00:00:03.9797846,00:00:04.2078410,00:00:04.0431276,00:00:03.9695552,00:00:16.2003084, +Repeat 960000 for size 10,Microsoft.Extensions.ObjectPool.DefaultObjectPool,00:00:02.4087556,00:00:02.5079212,00:00:02.4920962,00:00:02.4665527,00:00:09.8753257, +Repeat 960000 for size 10,ConcurrentQueueObjectPoolSlim,00:00:02.4513135,00:00:02.6173013,00:00:02.5037361,00:00:02.5337465,00:00:10.1060974, +Repeat 960000 for size 10,ConcurrentQueueObjectPool,00:00:02.5503897,00:00:02.6621958,00:00:02.5548883,00:00:02.5976682,00:00:10.3651420, +Repeat 960000 for size 10,OptimisticArrayObjectPool,00:00:02.4579233,00:00:02.7069571,00:00:02.4949206,00:00:02.4763234,00:00:10.1361244, +Repeat 960000 for size 10,InterlockedArrayObjectPool,00:00:02.5692140,00:00:02.7507886,00:00:02.5367601,00:00:02.5870651,00:00:10.4438278, +Repeat 288000 for size 50,Microsoft.Extensions.ObjectPool.DefaultObjectPool,00:00:01.3692996,00:00:01.3587399,00:00:01.0988709,00:00:01.2363235,00:00:05.0632339, +Repeat 288000 for size 50,ConcurrentQueueObjectPoolSlim,00:00:01.4405049,00:00:01.4442130,00:00:01.2083265,00:00:01.2814624,00:00:05.3745068, +Repeat 288000 for size 50,ConcurrentQueueObjectPool,00:00:01.5758473,00:00:01.5021273,00:00:01.3193568,00:00:01.3970673,00:00:05.7943987, +Repeat 288000 for size 50,OptimisticArrayObjectPool,00:00:01.0780899,00:00:01.2999000,00:00:01.1287828,00:00:01.1037828,00:00:04.6105555, +Repeat 288000 for size 50,InterlockedArrayObjectPool,00:00:01.1225109,00:00:01.2954306,00:00:01.1756768,00:00:01.1486593,00:00:04.7422776, +Repeat 192000 for size 100,Microsoft.Extensions.ObjectPool.DefaultObjectPool,00:00:01.8633666,00:00:01.6135337,00:00:01.0777082,00:00:01.4542875,00:00:06.0088960, +Repeat 192000 for size 100,ConcurrentQueueObjectPoolSlim,00:00:01.9024162,00:00:01.6790522,00:00:01.2862753,00:00:01.5210385,00:00:06.3887822, +Repeat 192000 for size 100,ConcurrentQueueObjectPool,00:00:02.0542650,00:00:01.7484990,00:00:01.4418580,00:00:01.6489656,00:00:06.8935876, +Repeat 192000 for size 100,OptimisticArrayObjectPool,00:00:00.8471340,00:00:01.0865546,00:00:00.9761332,00:00:00.8651384,00:00:03.7749602, +Repeat 192000 for size 100,InterlockedArrayObjectPool,00:00:00.9431637,00:00:01.1474065,00:00:01.0727406,00:00:00.9714928,00:00:04.1348036, +Repeat 153600 for size 250,Microsoft.Extensions.ObjectPool.DefaultObjectPool,00:00:04.0471422,00:00:03.2658468,00:00:01.8253285,00:00:03.0743397,00:00:12.2126572, +Repeat 153600 for size 250,ConcurrentQueueObjectPoolSlim,00:00:04.0982231,00:00:03.2845860,00:00:02.1882361,00:00:03.0712649,00:00:12.6423101, +Repeat 153600 for size 250,ConcurrentQueueObjectPool,00:00:04.1283099,00:00:03.2866676,00:00:02.3430258,00:00:03.1057312,00:00:12.8637345, +Repeat 153600 for size 250,OptimisticArrayObjectPool,00:00:00.8952769,00:00:01.2805030,00:00:01.2960246,00:00:01.0256400,00:00:04.4974445, +Repeat 153600 for size 250,InterlockedArrayObjectPool,00:00:01.0063385,00:00:01.3870075,00:00:01.4698480,00:00:01.1912251,00:00:05.0544191, +0:02.0767858,00:00:00.6488593,00:00:09.9478764, +Repeat 12800 for size 2000,ConcurrentStackObjectPool,00:00:00.6708805,00:00:02.5628201,00:00:04.7978301,00:00:04.7563902,00:00:03.4723738,00:00:00.6475781,00:00:16.9078728, +Repeat 12800 for size 2000,OptimisticArrayObjectPool,00:00:00.7184635,00:00:07.2485577,00:00:07.4304114,00:00:01.1244839,00:00:04.7572699,00:00:15.8850338,00:00:37.1642202, +Repeat 12800 for size 2000,InterlockedArrayObjectPool,00:00:01.2202062,00:00:19.5837353,00:00:08.9766760,00:00:01.0202604,00:00:12.8356061,00:01:23.7957059,00:02:07.4321899, +0:00:00.7628823,00:00:01.6182833,00:00:01.4624384,00:00:00.8960254,00:00:04.7396294, +Repeat 14400 for size 500,ConcurrentQueueObjectPool,00:00:00.8285180,00:00:00.9902911,00:00:00.8006946,00:00:01.1172107,00:00:03.7367144, +Repeat 14400 for size 500,ConcurrentStackObjectPool,00:00:00.6978419,00:00:02.2585037,00:00:02.4760289,00:00:02.8394301,00:00:08.2718046, +Repeat 14400 for size 500,OptimisticArrayObjectPool,00:00:01.0721065,00:00:05.6089585,00:00:00.5478712,00:00:22.2804453,00:00:29.5093815, +Repeat 14400 for size 500,InterlockedArrayObjectPool,00:00:01.1338229,00:00:12.8073240,00:00:00.7613453,00:00:32.4547747,00:00:47.1572669, diff --git a/benchmarking/BenchmarkResult.txt b/benchmarking/BenchmarkResult.txt index 92bf94a..b0427c4 100644 --- a/benchmarking/BenchmarkResult.txt +++ b/benchmarking/BenchmarkResult.txt @@ -1,150 +1,195 @@ -Repeat 200000 for size 4 +Repeat 2400000 for size 4 ------------------------------------ -QueueObjectPool......................................... -00:00:00.8064961 Take From Empty (In Parallel) -00:00:00.7379976 Give To (In Parallel) -00:00:00.7372226 Mixed Read/Write (In Parallel) -00:00:00.0912138 Empty Pool (.TryTake()) -00:00:02.3729301 TOTAL - -ConcurrentQueueObjectPool............................... -00:00:00.8392163 Take From Empty (In Parallel) -00:00:00.8090963 Give To (In Parallel) -00:00:00.8090662 Mixed Read/Write (In Parallel) -00:00:00.1057385 Empty Pool (.TryTake()) -00:00:02.5631173 TOTAL - -OptimisticArrayObjectPool............................... -00:00:00.8582319 Take From Empty (In Parallel) -00:00:00.7316954 Give To (In Parallel) -00:00:00.7462951 Mixed Read/Write (In Parallel) -00:00:00.0322101 Empty Pool (.TryTake()) -00:00:02.3684325 TOTAL +Microsoft.Extensions.ObjectPool.DefaultObjectPool....... +00:00:03.8980333 Give To (In Parallel) +00:00:03.9366455 Mixed 90%-Take/10%-Give (In Parallel) +00:00:03.9072529 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.8850033 Mixed 10%-Take/90%-Give (In Parallel) +00:00:15.6269350 TOTAL - -Repeat 80000 for size 10 ------------------------------------- - -QueueObjectPool......................................... -00:00:00.4891898 Take From Empty (In Parallel) -00:00:00.4641127 Give To (In Parallel) -00:00:00.4325487 Mixed Read/Write (In Parallel) -00:00:00.0887330 Empty Pool (.TryTake()) -00:00:01.4745842 TOTAL +ConcurrentQueueObjectPoolSlim........................... +00:00:03.8494058 Give To (In Parallel) +00:00:03.9230416 Mixed 90%-Take/10%-Give (In Parallel) +00:00:03.8901577 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.8867848 Mixed 10%-Take/90%-Give (In Parallel) +00:00:15.5493899 TOTAL ConcurrentQueueObjectPool............................... -00:00:00.4774220 Take From Empty (In Parallel) -00:00:00.4532694 Give To (In Parallel) -00:00:00.4577644 Mixed Read/Write (In Parallel) -00:00:00.1030334 Empty Pool (.TryTake()) -00:00:01.4914892 TOTAL +00:00:03.9883122 Give To (In Parallel) +00:00:04.0076765 Mixed 90%-Take/10%-Give (In Parallel) +00:00:03.9682549 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.9925014 Mixed 10%-Take/90%-Give (In Parallel) +00:00:15.9567450 TOTAL OptimisticArrayObjectPool............................... -00:00:00.5075819 Take From Empty (In Parallel) -00:00:00.3865528 Give To (In Parallel) -00:00:00.4139942 Mixed Read/Write (In Parallel) -00:00:00.0178275 Empty Pool (.TryTake()) -00:00:01.3259564 TOTAL +00:00:03.9568135 Give To (In Parallel) +00:00:04.2170893 Mixed 90%-Take/10%-Give (In Parallel) +00:00:04.0558332 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.9863018 Mixed 10%-Take/90%-Give (In Parallel) +00:00:16.2160378 TOTAL + +InterlockedArrayObjectPool.............................. +00:00:03.9797846 Give To (In Parallel) +00:00:04.2078410 Mixed 90%-Take/10%-Give (In Parallel) +00:00:04.0431276 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.9695552 Mixed 10%-Take/90%-Give (In Parallel) +00:00:16.2003084 TOTAL -Repeat 24000 for size 50 +Repeat 960000 for size 10 ------------------------------------ -QueueObjectPool......................................... -00:00:00.2172210 Take From Empty (In Parallel) -00:00:00.4786407 Give To (In Parallel) -00:00:00.2498973 Mixed Read/Write (In Parallel) -00:00:00.1187320 Empty Pool (.TryTake()) -00:00:01.0644910 TOTAL +Microsoft.Extensions.ObjectPool.DefaultObjectPool....... +00:00:02.4087556 Give To (In Parallel) +00:00:02.5079212 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.4920962 Mixed 50%-Take/50%-Give (In Parallel) +00:00:02.4665527 Mixed 10%-Take/90%-Give (In Parallel) +00:00:09.8753257 TOTAL + +ConcurrentQueueObjectPoolSlim........................... +00:00:02.4513135 Give To (In Parallel) +00:00:02.6173013 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.5037361 Mixed 50%-Take/50%-Give (In Parallel) +00:00:02.5337465 Mixed 10%-Take/90%-Give (In Parallel) +00:00:10.1060974 TOTAL ConcurrentQueueObjectPool............................... -00:00:00.2130298 Take From Empty (In Parallel) -00:00:00.2258805 Give To (In Parallel) -00:00:00.2090524 Mixed Read/Write (In Parallel) -00:00:00.1446890 Empty Pool (.TryTake()) -00:00:00.7926517 TOTAL +00:00:02.5503897 Give To (In Parallel) +00:00:02.6621958 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.5548883 Mixed 50%-Take/50%-Give (In Parallel) +00:00:02.5976682 Mixed 10%-Take/90%-Give (In Parallel) +00:00:10.3651420 TOTAL OptimisticArrayObjectPool............................... -00:00:00.3218326 Take From Empty (In Parallel) -00:00:00.1749609 Give To (In Parallel) -00:00:00.1948959 Mixed Read/Write (In Parallel) -00:00:00.0136129 Empty Pool (.TryTake()) -00:00:00.7053023 TOTAL +00:00:02.4579233 Give To (In Parallel) +00:00:02.7069571 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.4949206 Mixed 50%-Take/50%-Give (In Parallel) +00:00:02.4763234 Mixed 10%-Take/90%-Give (In Parallel) +00:00:10.1361244 TOTAL + +InterlockedArrayObjectPool.............................. +00:00:02.5692140 Give To (In Parallel) +00:00:02.7507886 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.5367601 Mixed 50%-Take/50%-Give (In Parallel) +00:00:02.5870651 Mixed 10%-Take/90%-Give (In Parallel) +00:00:10.4438278 TOTAL -Repeat 16000 for size 100 +Repeat 288000 for size 50 ------------------------------------ -QueueObjectPool......................................... -00:00:00.1933122 Take From Empty (In Parallel) -00:00:01.1807893 Give To (In Parallel) -00:00:00.3700023 Mixed Read/Write (In Parallel) -00:00:00.1532743 Empty Pool (.TryTake()) -00:00:01.8973781 TOTAL +Microsoft.Extensions.ObjectPool.DefaultObjectPool....... +00:00:01.3692996 Give To (In Parallel) +00:00:01.3587399 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.0988709 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.2363235 Mixed 10%-Take/90%-Give (In Parallel) +00:00:05.0632339 TOTAL + +ConcurrentQueueObjectPoolSlim........................... +00:00:01.4405049 Give To (In Parallel) +00:00:01.4442130 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.2083265 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.2814624 Mixed 10%-Take/90%-Give (In Parallel) +00:00:05.3745068 TOTAL ConcurrentQueueObjectPool............................... -00:00:00.1885421 Take From Empty (In Parallel) -00:00:00.2231642 Give To (In Parallel) -00:00:00.1945119 Mixed Read/Write (In Parallel) -00:00:00.1892646 Empty Pool (.TryTake()) -00:00:00.7954828 TOTAL +00:00:01.5758473 Give To (In Parallel) +00:00:01.5021273 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.3193568 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.3970673 Mixed 10%-Take/90%-Give (In Parallel) +00:00:05.7943987 TOTAL OptimisticArrayObjectPool............................... -00:00:00.4629300 Take From Empty (In Parallel) -00:00:00.1296838 Give To (In Parallel) -00:00:00.2026568 Mixed Read/Write (In Parallel) -00:00:00.0155329 Empty Pool (.TryTake()) -00:00:00.8108035 TOTAL +00:00:01.0780899 Give To (In Parallel) +00:00:01.2999000 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.1287828 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.1037828 Mixed 10%-Take/90%-Give (In Parallel) +00:00:04.6105555 TOTAL + +InterlockedArrayObjectPool.............................. +00:00:01.1225109 Give To (In Parallel) +00:00:01.2954306 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.1756768 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.1486593 Mixed 10%-Take/90%-Give (In Parallel) +00:00:04.7422776 TOTAL -Repeat 25600 for size 250 +Repeat 192000 for size 100 ------------------------------------ -QueueObjectPool......................................... -00:00:00.5119740 Take From Empty (In Parallel) -00:00:03.6755755 Give To (In Parallel) -00:00:03.0652345 Mixed Read/Write (In Parallel) -00:00:00.5573299 Empty Pool (.TryTake()) -00:00:07.8101139 TOTAL +Microsoft.Extensions.ObjectPool.DefaultObjectPool....... +00:00:01.8633666 Give To (In Parallel) +00:00:01.6135337 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.0777082 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.4542875 Mixed 10%-Take/90%-Give (In Parallel) +00:00:06.0088960 TOTAL + +ConcurrentQueueObjectPoolSlim........................... +00:00:01.9024162 Give To (In Parallel) +00:00:01.6790522 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.2862753 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.5210385 Mixed 10%-Take/90%-Give (In Parallel) +00:00:06.3887822 TOTAL ConcurrentQueueObjectPool............................... -00:00:00.5032784 Take From Empty (In Parallel) -00:00:00.6586173 Give To (In Parallel) -00:00:00.5488527 Mixed Read/Write (In Parallel) -00:00:00.7357870 Empty Pool (.TryTake()) -00:00:02.4465354 TOTAL +00:00:02.0542650 Give To (In Parallel) +00:00:01.7484990 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.4418580 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.6489656 Mixed 10%-Take/90%-Give (In Parallel) +00:00:06.8935876 TOTAL OptimisticArrayObjectPool............................... -00:00:03.0809331 Take From Empty (In Parallel) -00:00:00.3488741 Give To (In Parallel) -00:00:00.7180123 Mixed Read/Write (In Parallel) -00:00:00.0531776 Empty Pool (.TryTake()) -00:00:04.2009971 TOTAL +00:00:00.8471340 Give To (In Parallel) +00:00:01.0865546 Mixed 90%-Take/10%-Give (In Parallel) +00:00:00.9761332 Mixed 50%-Take/50%-Give (In Parallel) +00:00:00.8651384 Mixed 10%-Take/90%-Give (In Parallel) +00:00:03.7749602 TOTAL + +InterlockedArrayObjectPool.............................. +00:00:00.9431637 Give To (In Parallel) +00:00:01.1474065 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.0727406 Mixed 50%-Take/50%-Give (In Parallel) +00:00:00.9714928 Mixed 10%-Take/90%-Give (In Parallel) +00:00:04.1348036 TOTAL -Repeat 9600 for size 1000 +Repeat 153600 for size 250 ------------------------------------ -QueueObjectPool......................................... -00:00:00.4419158 Take From Empty (In Parallel) -00:00:02.4529980 Give To (In Parallel) -00:00:02.3464286 Mixed Read/Write (In Parallel) -00:00:00.6892199 Empty Pool (.TryTake()) -00:00:05.9305623 TOTAL +Microsoft.Extensions.ObjectPool.DefaultObjectPool....... +00:00:04.0471422 Give To (In Parallel) +00:00:03.2658468 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.8253285 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.0743397 Mixed 10%-Take/90%-Give (In Parallel) +00:00:12.2126572 TOTAL + +ConcurrentQueueObjectPoolSlim........................... +00:00:04.0982231 Give To (In Parallel) +00:00:03.2845860 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.1882361 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.0712649 Mixed 10%-Take/90%-Give (In Parallel) +00:00:12.6423101 TOTAL ConcurrentQueueObjectPool............................... -00:00:00.4453723 Take From Empty (In Parallel) -00:00:00.6651569 Give To (In Parallel) -00:00:00.5769710 Mixed Read/Write (In Parallel) -00:00:00.8922154 Empty Pool (.TryTake()) -00:00:02.5797156 TOTAL +00:00:04.1283099 Give To (In Parallel) +00:00:03.2866676 Mixed 90%-Take/10%-Give (In Parallel) +00:00:02.3430258 Mixed 50%-Take/50%-Give (In Parallel) +00:00:03.1057312 Mixed 10%-Take/90%-Give (In Parallel) +00:00:12.8637345 TOTAL OptimisticArrayObjectPool............................... -00:00:13.4891871 Take From Empty (In Parallel) -00:00:00.2492917 Give To (In Parallel) -00:00:01.1461595 Mixed Read/Write (In Parallel) -00:00:00.0718376 Empty Pool (.TryTake()) -00:00:14.9564759 TOTAL +00:00:00.8952769 Give To (In Parallel) +00:00:01.2805030 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.2960246 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.0256400 Mixed 10%-Take/90%-Give (In Parallel) +00:00:04.4974445 TOTAL + +InterlockedArrayObjectPool.............................. +00:00:01.0063385 Give To (In Parallel) +00:00:01.3870075 Mixed 90%-Take/10%-Give (In Parallel) +00:00:01.4698480 Mixed 50%-Take/50%-Give (In Parallel) +00:00:01.1912251 Mixed 10%-Take/90%-Give (In Parallel) +00:00:05.0544191 TOTAL diff --git a/benchmarking/ConsoleReport.cs b/benchmarking/ConsoleReport.cs index b331fff..c92de46 100644 --- a/benchmarking/ConsoleReport.cs +++ b/benchmarking/ConsoleReport.cs @@ -2,18 +2,16 @@ using System.IO; using System.Text; -namespace Open.Disposable.ObjectPools +namespace Open.Disposable.ObjectPools; + +public class ConsoleReport(TextWriter output = null) + : Diagnostics.BenchmarkConsoleReport>>(ITERATIONS, output, Benchmark.Results) + where T : class { - public class ConsoleReport : Open.Diagnostics.BenchmarkConsoleReport>> - where T : class - { - const int ITERATIONS = 100000; - public ConsoleReport(TextWriter output = null) : base(ITERATIONS, output, (c,r,p)=> Benchmark.Results(c, r, p)) - { - } + const int ITERATIONS = 100000; - public ConsoleReport(StringBuilder output) : this (new StringWriter(output)) - { - } + public ConsoleReport(StringBuilder output) + : this(new StringWriter(output)) + { } } diff --git a/benchmarking/DefaultObjectPool.cs b/benchmarking/DefaultObjectPool.cs new file mode 100644 index 0000000..966b26c --- /dev/null +++ b/benchmarking/DefaultObjectPool.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.ObjectPool; +using System; +using System.Diagnostics.CodeAnalysis; + +#nullable enable + +namespace Open.Disposable.ObjectPools; + +public class DefaultObjectPool(Func factory, int capacity = 64) + : Microsoft.Extensions.ObjectPool.DefaultObjectPool(new Policy(factory), capacity), IObjectPool + where T : class +{ + class Policy(Func factory) : IPooledObjectPolicy + { + private readonly Func _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + + public T Create() => _factory(); + + public bool Return(T obj) + // recycler?.Invoke(obj); + => true; + } + + public int Capacity { get; } + + public int Count => throw new NotImplementedException(); + + [SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize")] + public void Dispose() + { + } + + public T Generate() => factory(); + + public void Give(T item) => Return(item); + + public T Take() => Get(); + + public bool TryTake([NotNullWhen(true)] out T item) + { + item = Get(); + return true; + } + + public T? TryTake() => Get(); +} diff --git a/benchmarking/GlobalSuppressions.cs b/benchmarking/GlobalSuppressions.cs new file mode 100644 index 0000000..f90df4c --- /dev/null +++ b/benchmarking/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "", Scope = "member", Target = "~M:Open.Disposable.ObjectPools.DefaultObjectPool`1.Dispose")] diff --git a/benchmarking/Open.Disposable.ObjectPools.Benchmarking.csproj b/benchmarking/Open.Disposable.ObjectPools.Benchmarking.csproj index 0dac186..95c63dd 100644 --- a/benchmarking/Open.Disposable.ObjectPools.Benchmarking.csproj +++ b/benchmarking/Open.Disposable.ObjectPools.Benchmarking.csproj @@ -2,9 +2,18 @@ Exe - netcoreapp1.1 + net9.0 Open.Disposable 1.0.0 + IDE0130; + + + + latest + + + + latest @@ -12,8 +21,9 @@ - - + + + diff --git a/benchmarking/Program.cs b/benchmarking/Program.cs index 8051a10..6e8f256 100644 --- a/benchmarking/Program.cs +++ b/benchmarking/Program.cs @@ -1,13 +1,13 @@ -using Open.Disposable; -using Open.Disposable.ObjectPools; -using Open.Text.CSV; +using Open.Text.CSV; using System; using System.IO; using System.Text; -class Program +namespace Open.Disposable.ObjectPools.Benchmarks; + +static class Program { - static void Main(string[] args) + static void Main() { Console.Write("Initializing..."); @@ -21,26 +21,41 @@ static void Main(string[] args) * 3) A sync locked Queue is faster than a sync locked LinkedList. * 4) ConcurrentQueue seems to be the overall winner when dealing with pools larger than 100 but is the clear loser for very small sizes. */ - + // Start with a baseline... - report.AddBenchmark("QueueObjectPool", // Note, that this one isn't far off from the following in peformance. - count => () => QueueObjectPool.Create((int)count * 2)); + //report.AddBenchmark("QueueObjectPool", // Note, that this one isn't far off from the following in peformance. + // count => () => QueueObjectPool.Create((int)count * 2)); + + report.AddBenchmark("Microsoft.Extensions.ObjectPool.DefaultObjectPool", + count => () => new DefaultObjectPool(() => new object(), (int)count * 2)); - // The two contenders... - report.AddBenchmark("ConcurrentQueueObjectPool", // Note, that this one isn't far off from the following in peformance, but definitely is faster than LinkedListObjectPool and the rest. + report.AddBenchmark("ConcurrentQueueObjectPoolSlim", + count => () => ConcurrentQueueObjectPoolSlim.Create((int)count * 2)); + + report.AddBenchmark("ConcurrentQueueObjectPool", count => () => ConcurrentQueueObjectPool.Create((int)count * 2)); + + //report.AddBenchmark("ConcurrentStackObjectPool", + // count => () => ConcurrentStackObjectPool.Create((int)count * 2)); + report.AddBenchmark("OptimisticArrayObjectPool", count => () => OptimisticArrayObjectPool.Create((int)count * 2)); + + // Is ineveitably slower than the above but should be enabled for testing code changes. + report.AddBenchmark("InterlockedArrayObjectPool", + count => () => InterlockedArrayObjectPool.Create((int)count * 2)); + report.Pretest(200, 200); // Run once through first to scramble/warm-up initial conditions. Console.SetCursorPosition(0, Console.CursorTop); - report.Test(4, 8); - report.Test(10, 8); - report.Test(50, 12); - report.Test(100, 16); - report.Test(250, 64); - report.Test(1000, 96); + const int loopMultiple = 12; + report.Test(4, 8 * loopMultiple); + report.Test(10, 8 * loopMultiple); + report.Test(50, 12 * loopMultiple); + report.Test(100, 16 * loopMultiple); + report.Test(250, 32 * loopMultiple); + //report.Test(2000, 64 * loopMultiple); File.WriteAllText("./BenchmarkResult.txt", sb.ToString()); using (var fs = File.OpenWrite("./BenchmarkResult.csv")) @@ -55,5 +70,4 @@ static void Main(string[] args) Console.WriteLine("(press any key when finished)"); Console.ReadKey(); } - -} \ No newline at end of file +} diff --git a/source/Array/InterlockedArrayObjectPool.cs b/source/Array/InterlockedArrayObjectPool.cs index 185af46..909a324 100644 --- a/source/Array/InterlockedArrayObjectPool.cs +++ b/source/Array/InterlockedArrayObjectPool.cs @@ -1,101 +1,111 @@ /* Based on Roslyn's ObjectPool */ using System; -using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Threading; -namespace Open.Disposable +namespace Open.Disposable; + +/// +/// An extremely fast ObjectPool when the capacity is in the low 100s. +/// +public class InterlockedArrayObjectPool + : ObjectPoolBase + where T : class { - /// - /// An extremely fast ObjectPool when the capacity is in the low 100s. - /// - /// - public class InterlockedArrayObjectPool : ObjectPoolBase - where T : class + public InterlockedArrayObjectPool( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base(factory, recycler, disposer, capacity) + => Pool = new ReferenceContainer[capacity - 1]; + + public InterlockedArrayObjectPool( + Func factory, + int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) { } + + protected Memory> Pool; + + // Sets a limit on what has been stored yet to prevent over searching the array unnecessarily.. + protected int MaxStored; + protected const int MaxStoredIncrement = 5; // Instead of every one. + + public override int Count { - - public InterlockedArrayObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) + get { - AllowPocket = true; - Pool = new Element[capacity - 1]; - } + var p = Pool.Span; + int count = PocketCount; + foreach(var e in p) + if (e.Value is not null) count++; - public InterlockedArrayObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { + return count; } + } - Element[] Pool; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool Store(ReadOnlySpan> p, T item, int index) + => p[index].TrySave(item); - [DebuggerDisplay("{Value,nq}")] - protected struct Element - { - T _value; - internal bool Save(ref T value) - { - return _value != null - && null == Interlocked.CompareExchange(ref _value, value, null); - } - - internal T TryRetrieve() - { - var item = _value; - return (item != null - && item == Interlocked.CompareExchange(ref _value, null, item)) - ? item - : null; - } - } + protected override bool Receive(T item) + { + var elements = Pool; + var len = elements.Length; + var span = elements.Span; - protected override bool Receive(T item) + for (var i = 0; i < len; i++) { - var elements = Pool; - var len = elements?.Length ?? 0; - - for (int i = 0; i < len; i++) - { - if (elements[i].Save(ref item)) - return true; - } - - return false; + if (!Store(span, item, i)) continue; + var m = MaxStored; + if (i >= m) Interlocked.CompareExchange(ref MaxStored, m + MaxStoredIncrement, m); + return true; } - protected override T TryTakeInternal() - { - // We missed getting the first item or it wasn't there. - var elements = Pool; - var len = elements?.Length ?? 0; - - for (int i = 0; i < len; i++) - { - var item = elements[i].TryRetrieve(); - if (item != null) return item; - } + return false; + } - return null; - } + protected override T? TryRelease() + { + // We missed getting the first item or it wasn't there. + var elements = Pool.Span; + var len = elements.Length; - protected override void OnDispose(bool calledExplicitly) + for (var i = 0; i < len && i < MaxStored; i++) { - Pool = null; + var item = elements[i].TryRetrieve(); + if (item is not null) return item; } + return null; } - public static class InterlockedArrayObjectPool + protected override void OnDispose() { - public static InterlockedArrayObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new InterlockedArrayObjectPool(factory, capacity); - } - - public static InterlockedArrayObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } + base.OnDispose(); + Pool = Array.Empty>(); + MaxStored = 0; } } + +public static class InterlockedArrayObjectPool +{ + public static InterlockedArrayObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); + + public static InterlockedArrayObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); + + public static InterlockedArrayObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); + + public static InterlockedArrayObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); + + public static InterlockedArrayObjectPool CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); + + public static InterlockedArrayObjectPool CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); +} diff --git a/source/Array/OptimisticArrayObjectPool.cs b/source/Array/OptimisticArrayObjectPool.cs index b2ccce3..bc1c121 100644 --- a/source/Array/OptimisticArrayObjectPool.cs +++ b/source/Array/OptimisticArrayObjectPool.cs @@ -1,117 +1,52 @@ /* Based on Roslyn's ObjectPool */ using System; -using System.Diagnostics; -using System.Threading; +using System.Runtime.CompilerServices; -namespace Open.Disposable -{ - /// - /// An extremely fast ObjectPool when the capacity is in the low 100s. - /// - /// - public class OptimisticArrayObjectPool : ObjectPoolBase - where T : class - { - - public OptimisticArrayObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - AllowPocket = false; - Pool = new Element[capacity - 1]; - } - - public OptimisticArrayObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { - } - - - [DebuggerDisplay("{Value,nq}")] - protected struct Element - { - internal T Value; - } - - T _firstItem; - Element[] Pool; +namespace Open.Disposable; - protected override bool Receive(T item) - { - // First see if optimisically we can store in _firstItem; - if (_firstItem == null) - { - _firstItem = item; - return true; - } - // Else iterate to find an empty slot. - else - { - var elements = Pool; - var len = elements?.Length ?? 0; - - for (int i = 0; i < len; i++) - { - var e = elements[i]; - // As suggested by Roslyn's implementation, don't worry about interlocking here. It's okay if a few get loose. - if (e.Value == null) - { - e.Value = item; - return true; - } - } - - return false; - } - } +/// +/// An extremely fast ObjectPool when the capacity is in the low 100s. +/// +public class OptimisticArrayObjectPool + : InterlockedArrayObjectPool + where T : class +{ + public OptimisticArrayObjectPool( + Func factory, + Action? recycler, + int capacity = DEFAULT_CAPACITY) + : base(factory, recycler, null /* disposer not applicable here */, capacity) { } - protected override T TryTakeInternal() - { - T item = _firstItem; + public OptimisticArrayObjectPool( + Func factory, + int capacity = DEFAULT_CAPACITY) + : this(factory, null, capacity) { } - // First check and see if we actually were able to get the first item. - if (item != null && item == Interlocked.CompareExchange(ref _firstItem, null, item)) - return item; + // As suggested by Roslyn's implementation, don't worry about interlocking here. It's okay if a few get loose. - // We missed getting the first item or it wasn't there. - var elements = Pool; - var len = elements?.Length ?? 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override bool SaveToPocket(T item) + => Pocket.SetIfNull(item); - for (int i = 0; i < len; i++) - { - var e = elements[i]; - item = e.Value; - if (item != null) - { - if (item == Interlocked.CompareExchange(ref e.Value, null, item)) - { - return item; - } - } - } + // As suggested by Roslyn's implementation, don't worry about interlocking here. It's okay if a few get loose. - return null; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override bool Store(ReadOnlySpan> p, T item, int index) + => p[index].SetIfNull(item); +} - protected override void OnDispose(bool calledExplicitly) - { - Pool = null; - } +public static class OptimisticArrayObjectPool +{ + public static OptimisticArrayObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); - } + public static OptimisticArrayObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); - public static class OptimisticArrayObjectPool - { - public static OptimisticArrayObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new OptimisticArrayObjectPool(factory, capacity); - } + public static OptimisticArrayObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, capacity); - public static OptimisticArrayObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } - } + public static OptimisticArrayObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); } diff --git a/source/Channels/ChannelObjectPool.cs b/source/Channels/ChannelObjectPool.cs new file mode 100644 index 0000000..a9c3694 --- /dev/null +++ b/source/Channels/ChannelObjectPool.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Channels; + +namespace Open.Disposable +{ + public sealed class ChannelObjectPool : ObjectPoolBase + where T : class + { + public ChannelObjectPool(Func factory, Action recycler, Action disposer, int capacity = DEFAULT_CAPACITY) + : base(factory, recycler, disposer, capacity) + { + Pool = Channel.CreateBounded(new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.DropWrite }); + } + + public ChannelObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) + { } + + Channel Pool; + + public override int Count => -1; + + protected override void OnDispose(bool calledExplicitly) + { + Pool?.Writer.TryComplete(); + Pool = null; + } + + protected override bool Receive(T item) + => Pool?.Writer.TryWrite(item) ?? false; + + protected override T TryRelease() + { + T item = null; + Pool?.Reader.TryRead(out item); + return item; + } + } + + + public static class ChannelObjectPool + { + public static ChannelObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class + { + return new ChannelObjectPool(factory, capacity); + } + + + public static ChannelObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() + { + return Create(() => new T(), capacity); + } + + public static ChannelObjectPool Create(Func factory, bool autoRecycle, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable + { + Action recycler = null; + if (autoRecycle) recycler = Recycler.Recycle; + return new ChannelObjectPool(factory, recycler, null, capacity); + } + + public static ChannelObjectPool Create(bool autoRecycle, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() + { + return Create(() => new T(), autoRecycle, capacity); + } + } +} diff --git a/source/Channels/ChanneledRecycler.cs b/source/Channels/ChanneledRecycler.cs new file mode 100644 index 0000000..756cd1a --- /dev/null +++ b/source/Channels/ChanneledRecycler.cs @@ -0,0 +1,108 @@ +using System; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Open.Disposable +{ + /// + /// This class is provided as an asynchronous queue for recycling instead of using a recycle delegate with an object pool and calling GiveAsync() which could pile up unnecessarily. + /// So if recycling an object takes extra time, this might be a good way to toss objects away and not have to worry about the heavy cost as they will one by one be processed back into the target pool. + /// + public sealed class ChanneledRecycler : RecyclerBase + where T : class + { + // Something else could be used and could be more performant. + // But it's an ideal interface for what's needed. And the point is that the recyler should not take up too much cpu time. + Channel _bin; + + internal ChanneledRecycler( + IObjectPool target, + Action recycleFunction, + ushort limit = Constants.DEFAULT_CAPACITY) : base(target, recycleFunction) + { + var b = _bin = Channel.CreateBounded(new BoundedChannelOptions(limit) + { + FullMode = BoundedChannelFullMode.DropWrite + }); + + async Task ProcessAsync() + { + var reader = _bin.Reader; + while (Target != null + && await reader.WaitToReadAsync().ConfigureAwait(false)) + { + while (Target != null + && reader.TryRead(out T item)) + { + recycleFunction(item); + Target?.Give(item); + } + } + } + + Completion = ProcessAsync().ContinueWith( + t => t.IsCompleted + ? b.Reader.Completion + : t) + .Unwrap(); + } + + internal ChanneledRecycler( + ushort limit, + IObjectPool pool, + Action recycleFunction) : this(pool, recycleFunction, limit) + { + + } + + public override bool Recycle(T item) + => _bin?.Writer.TryWrite(item) ?? false; + + protected override void OnCloseRequested() + => _bin?.Writer.Complete(); + + protected override void OnDispose(bool calledExplicitly) + { + _bin?.Writer.Complete(); + _bin = null; + Target = null; + } + } + + public static class ChanneledRecycler + { + public static ChanneledRecycler CreateRecycler( + this IObjectPool pool, + Action recycleFunction, + ushort limit = Constants.DEFAULT_CAPACITY) + where T : class + { + return new ChanneledRecycler(pool, recycleFunction, limit); + } + + public static ChanneledRecycler CreateRecycler( + this IObjectPool pool, + ushort limit, + Action recycleFunction) + where T : class + { + return new ChanneledRecycler(pool, recycleFunction, limit); + } + + public static void Recycle(IRecyclable r) + { + r.Recycle(); + } + + public static ChanneledRecycler CreateRecycler( + this IObjectPool pool, + ushort limit = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable + { + return new ChanneledRecycler(pool, Recycle, limit); + } + + } + + +} diff --git a/source/Collection/CollectionWrapperObjectPool.cs b/source/Collection/CollectionWrapperObjectPool.cs index 2b72fd3..6ce6815 100644 --- a/source/Collection/CollectionWrapperObjectPool.cs +++ b/source/Collection/CollectionWrapperObjectPool.cs @@ -1,78 +1,69 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Open.Disposable -{ - public class CollectionWrapperObjectPool : TrimmableObjectPoolBase - where T : class - where TCollection : class, ICollection - { - public CollectionWrapperObjectPool(TCollection pool, Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - Pool = pool; - } +namespace Open.Disposable; - public CollectionWrapperObjectPool(TCollection pool, Func factory, int capacity = DEFAULT_CAPACITY) - : this(pool, factory, null, capacity) - { - } - - protected TCollection Pool; +public class CollectionWrapperObjectPool + : TrimmableGenericCollectionObjectPoolBase + where T : class + where TCollection : class, ICollection +{ + public CollectionWrapperObjectPool( + TCollection pool, + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY, + bool countTrackingEnabled = true) + : base(pool, factory, recycler, disposer, capacity, countTrackingEnabled) { } - public override int Count => Pool?.Count ?? 0; + public CollectionWrapperObjectPool( + TCollection pool, + Func factory, + int capacity = DEFAULT_CAPACITY, + bool countTrackingEnabled = true) + : this(pool, factory, null, null, capacity, countTrackingEnabled) { } - protected override bool Receive(T item) + protected override bool Receive(T item) + { + var p = Pool; + if (p is null) return false; + lock (SyncRoot) { - var p = Pool; - if (p != null) - { - lock (p) - { - // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. - // The lock operation should be quick enough to not pile up too many items. - p.Add(item); - return true; - } - } - - return false; + // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + // The lock operation should be quick enough to not pile up too many items. + p.Add(item); } + return true; + } - protected override T TryTakeInternal() - { - retry: - - var p = Pool; - var item = p?.FirstOrDefault(); - if (item != null) - { - /* Removing the first item is typically horribly inefficient but we can't make assumptions about the implementation here. - * It's a trade off between potentially iterating the entire collection before removing the last item, or relying on the underlying implementation. - * This implementation is in place for reference more than practice. Sub classes should override. */ + protected override T? TryRelease() + { + retry: - bool wasRemoved = false; - lock (p) wasRemoved = p.Remove(item); - if (!wasRemoved) goto retry; - } + var p = Pool; + var item = p?.FirstOrDefault(); + if (item is null) return null; + /* Removing the first item is typically horribly inefficient but we can't make assumptions about the implementation here. + * It's a trade off between potentially iterating the entire collection before removing the last item, or relying on the underlying implementation. + * This implementation is in place for reference more than practice. Sub classes should override. */ - return item; - } + bool wasRemoved; + lock (SyncRoot) wasRemoved = p!.Remove(item); + if (!wasRemoved) goto retry; - protected override void OnDispose(bool calledExplicitly) - { - Pool = null; - } + return item; } +} - public class CollectionWrapperObjectPool : CollectionWrapperObjectPool> - where T : class - { - public CollectionWrapperObjectPool(ICollection pool, Func factory, int capacity = DEFAULT_CAPACITY) : base(pool, factory, capacity) - { - } - } +public class CollectionWrapperObjectPool + : CollectionWrapperObjectPool> + where T : class +{ + public CollectionWrapperObjectPool( + ICollection pool, + Func factory, + int capacity = DEFAULT_CAPACITY) + : base(pool, factory, capacity) { } } diff --git a/source/Collection/ConcurrentBagObjectPool.cs b/source/Collection/ConcurrentBagObjectPool.cs deleted file mode 100644 index 5a06559..0000000 --- a/source/Collection/ConcurrentBagObjectPool.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Concurrent; - -namespace Open.Disposable -{ - public sealed class ConcurrentBagObjectPool : TrimmableObjectPoolBase - where T : class - - { - - public ConcurrentBagObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - Pool = new ConcurrentBag(); - } - - public ConcurrentBagObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { - } - - ConcurrentBag Pool; - - public override int Count => Pool?.Count ?? 0; - - protected override bool Receive(T item) - { - var p = Pool; - if (p == null) return false; - p.Add(item); // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. - return true; - } - - protected override T TryTakeInternal() - { - var p = Pool; - if (p == null) return null; - p.TryTake(out T item); - return item; - } - - protected override void OnDispose(bool calledExplicitly) - { - Pool = null; - } - - } - - public static class ConcurrentBagObjectPool - { - public static ConcurrentBagObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new ConcurrentBagObjectPool(factory, capacity); - } - - public static ConcurrentBagObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } - } -} diff --git a/source/Collection/ConcurrentQueueObjectPool.cs b/source/Collection/ConcurrentQueueObjectPool.cs index 1211877..a21436d 100644 --- a/source/Collection/ConcurrentQueueObjectPool.cs +++ b/source/Collection/ConcurrentQueueObjectPool.cs @@ -1,65 +1,65 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; +using System.Runtime.CompilerServices; -namespace Open.Disposable +namespace Open.Disposable; + +public class ConcurrentQueueObjectPool + : TrimmableCollectionObjectPoolBase> + where T : class { - public sealed class ConcurrentQueueObjectPool : TrimmableObjectPoolBase - where T : class - { + public ConcurrentQueueObjectPool( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base(new ConcurrentQueue(), factory, recycler, disposer, capacity) { } - public ConcurrentQueueObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - Pool = new ConcurrentQueue(); - } + public ConcurrentQueueObjectPool( + Func factory, + int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) { } - public ConcurrentQueueObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { - - } + /* + * NOTE: ConcurrentQueue is very fast and will perform quite well without using the 'Pocket' feature. + * Benchmarking reveals that mixed read/writes (what really matters) are still faster with the pocket enabled so best to keep it so. + */ - ConcurrentQueue Pool; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override bool Receive(T item) + { + var p = Pool; + if (p is null) return false; + p.Enqueue(item); // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + return true; + } - public override int Count => Pool?.Count ?? 0; - - protected override bool Receive(T item) - { - var p = Pool; - if (p == null) return false; - p.Enqueue(item); // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. - return true; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override T? TryRelease() + { + var p = Pool; + if (p is null) return null; + p.TryDequeue(out var item); + return item; + } +} +public static class ConcurrentQueueObjectPool +{ + public static ConcurrentQueueObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); - protected override T TryTakeInternal() - { - var p = Pool; - if (p == null) return null; - p.TryDequeue(out T item); - return item; - } + public static ConcurrentQueueObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); - protected override void OnDispose(bool calledExplicitly) - { - Pool = null; - } + public static ConcurrentQueueObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); - } + public static ConcurrentQueueObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); - public static class ConcurrentQueueObjectPool - { - public static ConcurrentQueueObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new ConcurrentQueueObjectPool(factory, capacity); - } + public static ConcurrentQueueObjectPool CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); - public static ConcurrentQueueObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } - } + public static ConcurrentQueueObjectPool CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); } diff --git a/source/Collection/ConcurrentQueueObjectPoolSlim.cs b/source/Collection/ConcurrentQueueObjectPoolSlim.cs new file mode 100644 index 0000000..8ddbffe --- /dev/null +++ b/source/Collection/ConcurrentQueueObjectPoolSlim.cs @@ -0,0 +1,41 @@ +using System; + +namespace Open.Disposable; + +public sealed class ConcurrentQueueObjectPoolSlim + : ConcurrentQueueObjectPoolSlimBase + where T : class +{ + public ConcurrentQueueObjectPoolSlim( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY - 10) + : base(factory, recycler, disposer, capacity) { } + + public ConcurrentQueueObjectPoolSlim( + Func factory, + int capacity = DEFAULT_CAPACITY - 10) + : this(factory, null, null, capacity) { } +} + +public static class ConcurrentQueueObjectPoolSlim +{ + public static ConcurrentQueueObjectPoolSlim Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); + + public static ConcurrentQueueObjectPoolSlim Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); + + public static ConcurrentQueueObjectPoolSlim CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); + + public static ConcurrentQueueObjectPoolSlim CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); + + public static ConcurrentQueueObjectPoolSlim CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); + + public static ConcurrentQueueObjectPoolSlim CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); +} diff --git a/source/Collection/ConcurrentQueueObjectPoolSlimBase.cs b/source/Collection/ConcurrentQueueObjectPoolSlimBase.cs new file mode 100644 index 0000000..24483fa --- /dev/null +++ b/source/Collection/ConcurrentQueueObjectPoolSlimBase.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace Open.Disposable; + +public abstract class ConcurrentQueueObjectPoolSlimBase + : CountTrackedObjectPoolBase + where T : class +{ + protected ConcurrentQueueObjectPoolSlimBase( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY - 10) + : base(factory, recycler, disposer, capacity) + => Pool = new(); + + ConcurrentQueue Pool; + + /* + * NOTE: ConcurrentQueue is very fast and will perform quite well without using the 'Pocket' feature. + * Benchmarking reveals that mixed read/writes (what really matters) are still faster with the pocket enabled so best to keep it so. + */ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override bool Receive(T item) + { + var p = Pool; + if (p is null) return false; + p.Enqueue(item); // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override T? TryRelease() + { + var p = Pool; + if (p is null) return null; + p.TryDequeue(out var item); + return item; + } + + protected override void OnDispose() + { + base.OnDispose(); + Pool = null!; + } +} diff --git a/source/Collection/ConcurrentStackObjectPool.cs b/source/Collection/ConcurrentStackObjectPool.cs new file mode 100644 index 0000000..bc01239 --- /dev/null +++ b/source/Collection/ConcurrentStackObjectPool.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Concurrent; + +namespace Open.Disposable; + +public sealed class ConcurrentStackObjectPool + : TrimmableCollectionObjectPoolBase> + where T : class +{ + public ConcurrentStackObjectPool( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base(new ConcurrentStack(), factory, recycler, disposer, capacity) { } + + public ConcurrentStackObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) { } + + protected override bool Receive(T item) + { + var p = Pool; + if (p is null) return false; + p.Push(item); // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + return true; + } + + protected override T? TryRelease() + { + var p = Pool; + if (p is null) return null; + p.TryPop(out var item); + return item; + } +} + +public static class ConcurrentStackObjectPool +{ + public static ConcurrentStackObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); + + public static ConcurrentStackObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); + + public static ConcurrentStackObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); + + public static ConcurrentStackObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); + + public static ConcurrentStackObjectPool CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); + + public static ConcurrentStackObjectPool CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); +} diff --git a/source/Collection/LinkedListObjectPool.cs b/source/Collection/LinkedListObjectPool.cs deleted file mode 100644 index b64a565..0000000 --- a/source/Collection/LinkedListObjectPool.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Open.Disposable -{ - public class LinkedListObjectPool : CollectionWrapperObjectPool> - where T : class - { - public LinkedListObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(new LinkedList(), factory, recycler, capacity) - { - } - - public LinkedListObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { - } - - - protected override T TryTakeInternal() - { - var p = Pool; - if (p == null) return null; - T item; - lock(p) - { - var node = p.Last; - if (node == null) return null; - item = node.Value; - p.RemoveLast(); - } - - return item; - } - - } - - public static class LinkedListObjectPool - { - public static LinkedListObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new LinkedListObjectPool(factory, capacity); - } - - public static LinkedListObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } - } -} diff --git a/source/Collection/QueueObjectPool.cs b/source/Collection/QueueObjectPool.cs index 19e705b..ce0756e 100644 --- a/source/Collection/QueueObjectPool.cs +++ b/source/Collection/QueueObjectPool.cs @@ -1,77 +1,74 @@ using System; using System.Collections.Generic; -using System.Text; -namespace Open.Disposable +namespace Open.Disposable; + +public sealed class QueueObjectPool + : TrimmableCollectionObjectPoolBase> + where T : class { - public sealed class QueueObjectPool : TrimmableObjectPoolBase - where T : class - { + public QueueObjectPool( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base( + new Queue(Math.Min(DEFAULT_CAPACITY, capacity)) /* Very very slight speed improvment when capacity is initially set. */, + factory, recycler, disposer, capacity, false) + { } - public QueueObjectPool(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - Pool = new Queue(capacity); // Very very slight speed improvment when capacity is set. - } + public QueueObjectPool( + Func factory, + int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) { } - public QueueObjectPool(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) + protected override bool Receive(T item) + { + var p = Pool; + if (p is not null) { - + // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + // The lock operation should be quick enough to not pile up too many items. + lock (SyncRoot) p.Enqueue(item); + return true; } - Queue Pool; - - public override int Count => Pool?.Count ?? 0; + return false; + } - protected override bool Receive(T item) + protected override T? TryRelease() + { + var p = Pool; + if (p is not null && p.Count != 0) { - var p = Pool; - if (p!=null) + lock (SyncRoot) { - // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. - // The lock operation should be quick enough to not pile up too many items. - lock (p) p.Enqueue(item); - return true; + if (p.Count != 0) + return p.Dequeue(); } - - return false; } - protected override T TryTakeInternal() - { - var p = Pool; - if (p!=null && p.Count != 0) - { - lock (p) - { - if (p.Count!=0) - return p.Dequeue(); - } + return null; + } +} - } +public static class QueueObjectPool +{ + public static QueueObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); - return null; - } + public static QueueObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); - protected override void OnDispose(bool calledExplicitly) - { - Pool = null; - } - } + public static QueueObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); - public static class QueueObjectPool - { - public static QueueObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) - where T : class - { - return new QueueObjectPool(factory, capacity); - } + public static QueueObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); - public static QueueObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) - where T : class, new() - { - return Create(() => new T(), capacity); - } - } + public static QueueObjectPool CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); + + public static QueueObjectPool CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); } diff --git a/source/Collection/StackObjectPool.cs b/source/Collection/StackObjectPool.cs new file mode 100644 index 0000000..1cdfd96 --- /dev/null +++ b/source/Collection/StackObjectPool.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; + +namespace Open.Disposable; + +public sealed class StackObjectPool + : TrimmableCollectionObjectPoolBase> + where T : class +{ + public StackObjectPool( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base( + new Stack(Math.Min(DEFAULT_CAPACITY, capacity)) /* Very very slight speed improvment when capacity is set. */, + factory, recycler, disposer, capacity, false) + { } + + public StackObjectPool( + Func factory, + int capacity = DEFAULT_CAPACITY) + : this(factory, null, null, capacity) { } + + protected override bool Receive(T item) + { + var p = Pool; + if (p is not null) + { + // It's possible that the count could exceed MaxSize here, but the risk is negligble as a few over the limit won't hurt. + // The lock operation should be quick enough to not pile up too many items. + lock (SyncRoot) p.Push(item); + return true; + } + + return false; + } + + protected override T? TryRelease() + { + var p = Pool; + if (p is null || p.Count == 0) + return null; + + lock (SyncRoot) + { + if (p.Count != 0) + return p.Pop(); + } + + return null; + } +} + +public static class StackObjectPool +{ + public static StackObjectPool Create(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class => new(factory, capacity); + + public static StackObjectPool Create(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, new() => Create(() => new T(), capacity); + + public static StackObjectPool CreateAutoRecycle(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(factory, Recycler.Recycle, null, capacity); + + public static StackObjectPool CreateAutoRecycle(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable, new() => CreateAutoRecycle(() => new T(), capacity); + + public static StackObjectPool CreateAutoDisposal(Func factory, int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable => new(factory, null, d => d.Dispose(), capacity); + + public static StackObjectPool CreateAutoDisposal(int capacity = Constants.DEFAULT_CAPACITY) + where T : class, IDisposable, new() => CreateAutoDisposal(() => new T(), capacity); +} diff --git a/source/Constants.cs b/source/Constants.cs index 0477a39..343a47e 100644 --- a/source/Constants.cs +++ b/source/Constants.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace Open.Disposable; -namespace Open.Disposable +internal static class Constants { - internal static class Constants - { - internal const int DEFAULT_CAPACITY = 65535; - } + internal const int DEFAULT_CAPACITY = 64; // Should accomodate all object pools without decreasing their effectiveness. } diff --git a/source/CountTrackedObjectPoolBase.cs b/source/CountTrackedObjectPoolBase.cs new file mode 100644 index 0000000..2b6f7f7 --- /dev/null +++ b/source/CountTrackedObjectPoolBase.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Open.Disposable; + +public abstract class CountTrackedObjectPoolBase + : ObjectPoolBase + where T : class +{ + protected CountTrackedObjectPoolBase( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + : base(factory, recycler, disposer, capacity) { } + + int _count; + /// + public override int Count => _count; + + protected override bool CanReceive => _count < MaxSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnReleased() => Interlocked.Decrement(ref _count); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnReceived() => Interlocked.Increment(ref _count); +} diff --git a/source/IObjectPool.cs b/source/IObjectPool.cs index d4864ab..e4993fb 100644 --- a/source/IObjectPool.cs +++ b/source/IObjectPool.cs @@ -1,119 +1,182 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Diagnostics.Contracts; using System.Threading.Tasks; -namespace Open.Disposable +namespace Open.Disposable; + +public interface IObjectPool + where T : class { - public interface IObjectPool : IDisposable + /// + /// Defines the maximum at which the pool can grow. + /// + int Capacity { get; } + + /// + /// Directly calls the underlying factory that generates the items. (No pool interaction.) + /// + T Generate(); + + /// + /// Receives an item and adds it to the pool. Ignores null references.
+ /// WARNING: The item is considered 'dead' but resurrectable so be sure not to hold on to the item's reference. + ///
+ /// The item to give up to the pool. + void Give(T item); + + /// + /// If the pool has an item currently avaialable, removes it from the pool and provides it as the out parameter. + /// + /// The item to return if available. Will be null if none avaialable. + /// True if an item is provided. +#if NETSTANDARD2_1 + bool TryTake([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? item); +#else + bool TryTake(out T? item); +#endif + + /// + /// If the pool has an item currently avaialable, removes it from the pool and returns it. + /// + /// The item to return if available. Will be null if none avaialable. + T? TryTake(); + + /// + /// If the pool has an item currently avaialable, removes it from the pool and returns it. + /// If none is available, it generates one. + /// + /// An item removed from the pool or generated. Should never be null. + T Take(); + + /// + /// Total number of items in the pool.
+ /// Depending on the implementation could be an O(n) operation and should only be used for debugging. + ///
+ int Count { get; } +} + +public static class ObjectPoolExtensions +{ + /// + /// Receives items and iteratively adds them to the pool.
+ /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. + ///
+ /// The pool to give to. + /// The items to give up to the pool. + public static void Give(this IObjectPool target, IEnumerable items) + where T : class + { + if (target is null) throw new ArgumentNullException(nameof(target)); + Contract.EndContractBlock(); + + if (items is null) return; + foreach (var i in items) + target.Give(i); + } + + /// + /// Receives items and iteratively adds them to the pool.
+ /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. + ///
+ /// The pool to give to. + /// The first item to give up to the pool. + /// The second item to give up to the pool. + /// The remaining items to give up to the pool. + public static void Give(this IObjectPool target, T item1, T item2, params T[] items) + where T : class + { + if (target is null) throw new ArgumentNullException(nameof(target)); + Contract.EndContractBlock(); + + target.Give(item1); + target.Give(item2); + target.Give(items); + } + + /// + /// Provides a disposable RecycleHelper that contains an item from the pool.
+ /// Will be returned to the pool once .Dispose() is called. + ///
+ /// The object type from the pool. + /// The object pool. + /// A RecycleHelper containing an item from the pool. + public static RecycleHelper Rent(this IObjectPool source) + where T : class => new(source); + + /// + /// Provides an item from the pool and returns it when the action sucessfully completes. + /// + /// The object type from the pool. + /// The object pool. + /// The action to execute. + public static void Rent(this IObjectPool source, Action action) where T : class { - /// - /// Defines the maximum at which the pool can grow. - /// - int Capacity { get; } - - /// - /// Directly calls the underlying factory that generates the items. (No pool interaction.) - /// - T Generate(); - - /// - /// Receives an item and adds it to the pool. Ignores null references. - /// WARNING: The item is considered 'dead' but resurrectable so be sure not to hold on to the item's reference. - /// - /// The item to give up to the pool. - /// An optional action exectue on the item only if it's possible to return to the pool. - void Give(T item); - - /// - /// Asynchronously receives an item and adds it to the pool. Ignores null references. - /// WARNING: The item is considered 'dead' but resurrectable so be sure not to hold on to the item's reference. - /// - /// The item to give up to the pool. - /// An optional action exectue on the item only if it's possible to return to the pool. - Task GiveAsync(T item); - - /// - /// If the pool has an item currently avaialable, removes it from the pool and provides it as the out parameter. - /// - /// The item to return if available. Will be null if none avaialable. - /// True if an item is provided. - bool TryTake(out T item); - - /// - /// If the pool has an item currently avaialable, removes it from the pool and returns it. - /// - /// The item to return if available. Will be null if none avaialable. - T TryTake(); - - - /// - /// If the pool has an item currently avaialable, removes it from the pool and returns it. - /// If none is available, it generates one. - /// - /// An item removed from the pool or generated. Should never be null. - T Take(); - - /// - /// Awaits an available item from the pool. If none are available it generates one. - /// - /// An item removed from the pool or generated. - Task TakeAsync(); + if (source is null) throw new ArgumentNullException(nameof(source)); + if (action is null) throw new ArgumentNullException(nameof(action)); + Contract.EndContractBlock(); + + var item = source.Take(); + action(item); + source.Give(item); } - public static class ObjectPoolExtensions + /// + /// Provides an item from the pool and returns it when the action sucessfully completes. + /// + /// The object type from the pool. + /// The object pool. + /// The action to execute. + /// The value from the action. + public static TResult Rent(this IObjectPool source, Func action) + where T : class { - /// - /// Receives items and iteratively adds them to the pool. - /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. - /// - /// The items to give up to the pool. - public static void Give(this IObjectPool target, IEnumerable items) - where T : class - { - if (items != null) - foreach (var i in items) - target.Give(i); - } - - /// - /// Receives items and iteratively adds them to the pool. - /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. - /// - /// The first item to give up to the pool. - /// The second item to give up to the pool. - /// The remaining items to give up to the pool. - public static void Give(this IObjectPool target, T item1, T item2, params T[] items) - where T : class - { - target.Give(item1); - target.Give(item2); - target.Give(items); - } - - /// - /// Asynchronously receives items and iteratively adds them to the pool. - /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. - /// - /// The items to give up to the pool. - public static Task GiveAsync(this IObjectPool target, IEnumerable items) - where T : class - { - return Task.Run(() => target.Give(items)); - } - - /// - /// Asynchronously receives items and iteratively adds them to the pool. - /// WARNING: These items are considered 'dead' but resurrectable so be sure not to hold on to their reference. - /// - /// The first item to give up to the pool. - /// The second item to give up to the pool. - /// The remaining items to give up to the pool. - public static Task GiveAsync(this IObjectPool target, T item1, T item2, params T[] items) - where T : class - { - return Task.Run(() => target.Give(item1, item2, items)); - } + if (source is null) throw new ArgumentNullException(nameof(source)); + if (action is null) throw new ArgumentNullException(nameof(action)); + Contract.EndContractBlock(); + + var item = source.Take(); + var result = action(item); + source.Give(item); + return result; + } + + /// + /// Provides an item from the pool and returns it when the action sucessfully completes. + /// + /// The object type from the pool. + /// The object pool. + /// The action to execute. + public static async ValueTask RentAsync(this IObjectPool source, Func action) + where T : class + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (action is null) throw new ArgumentNullException(nameof(action)); + Contract.EndContractBlock(); + + var item = source.Take(); + await action(item).ConfigureAwait(false); + source.Give(item); + } + + /// + /// Provides an item from the pool and returns it when the action sucessfully completes. + /// + /// The object type from the pool. + /// The object pool. + /// The action to execute. + /// The value from the action. + public static async ValueTask RentAsync(this IObjectPool source, Func> action) + where T : class + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (action is null) throw new ArgumentNullException(nameof(action)); + Contract.EndContractBlock(); + + var item = source.Take(); + var result = await action(item).ConfigureAwait(false); + source.Give(item); + return result; } } diff --git a/source/IRecyclable.cs b/source/IRecyclable.cs new file mode 100644 index 0000000..f192ea1 --- /dev/null +++ b/source/IRecyclable.cs @@ -0,0 +1,9 @@ +namespace Open.Disposable; + +public interface IRecyclable +{ + /// + /// Signals the item should be recycled. + /// + void Recycle(); +} diff --git a/source/IRecycler.cs b/source/IRecycler.cs new file mode 100644 index 0000000..034309f --- /dev/null +++ b/source/IRecycler.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace Open.Disposable; + +public interface IRecycler + : IDisposable + where T : class +{ + /// + /// Recycles the item. + /// + bool Recycle(T item); + + /// + /// Closes the recycling. No more should be recycled. + /// + Task Close(); +} diff --git a/source/ITrimmableObjectPool.cs b/source/ITrimmableObjectPool.cs index 946b73f..913dcf9 100644 --- a/source/ITrimmableObjectPool.cs +++ b/source/ITrimmableObjectPool.cs @@ -1,12 +1,9 @@ -using System; +namespace Open.Disposable; -namespace Open.Disposable +public interface ITrimmableObjectPool { - public interface ITrimmableObjectPool - { - event ObjectPoolResizeEvent GivenTo; - event ObjectPoolResizeEvent TakenFrom; + event ObjectPoolResizeEvent Received; + event ObjectPoolResizeEvent Released; - void TrimTo(int targetSize); - } + void TrimTo(int targetSize); } diff --git a/source/ObjectPoolAutoTrimmer.cs b/source/ObjectPoolAutoTrimmer.cs index a1a1bea..3506075 100644 --- a/source/ObjectPoolAutoTrimmer.cs +++ b/source/ObjectPoolAutoTrimmer.cs @@ -1,85 +1,87 @@ using Open.Threading.Tasks; using System; +using System.Diagnostics.CodeAnalysis; -namespace Open.Disposable +namespace Open.Disposable; + +[SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Micro-optimization for retrieving this value as read-only is slightly slower")] +[SuppressMessage("Roslynator", "RCS1169:Make field read-only.")] +class ObjectPoolAutoTrimmer + : DisposableBase { - public class ObjectPoolAutoTrimmer : DisposableBase - { - ITrimmableObjectPool _pool; - ActionRunner _trimmer; - - // micro-optimization for retrieving this value as read-only is slightly slower. - ushort _trimmedSize; - TimeSpan _trimDelay; - - /// - /// Max size that trimming will allow. - /// - public readonly ushort TrimmedSize; - - /// - /// Time to wait/defer trimming. Default is 500 milliSeconds. - /// - public readonly TimeSpan TrimDelay; - - /// - /// Constructs an auto-trimming ObjectPool helper. - /// - /// The governable object pool to maintain. - /// The target size to limit to after a half second timeout. Allowing the pool to still grow to the max size until the trim occurs. - /// The amount of time to wait/defer trimming. - public ObjectPoolAutoTrimmer( - ushort trimmedSize, - ITrimmableObjectPool pool, - TimeSpan? trimDelay = null) - { - _pool = pool ?? throw new ArgumentNullException("pool"); + ITrimmableObjectPool _pool; + ActionRunner _trimmer; - TrimmedSize = _trimmedSize = trimmedSize; - TrimDelay = _trimDelay = trimDelay ?? TimeSpan.FromMilliseconds(500); + // Micro-optimization for retrieving this value as read-only is slightly slower. + ushort _trimmedSize; + TimeSpan _trimDelay; - _trimmer = new ActionRunner(TrimInternal); + /// + /// Max size that trimming will allow. + /// + // ReSharper disable once NotAccessedField.Global + public readonly ushort TrimmedSize; - pool.GivenTo += Target_GivenTo; - pool.TakenFrom += Target_TakenFrom; + /// + /// Time to wait/defer trimming. Default is 500 milliSeconds. + /// + // ReSharper disable once NotAccessedField.Global + public readonly TimeSpan TrimDelay; - if (pool is DisposableBase d) - { - if (d.IsDisposed) throw new ArgumentException("Cannot trim for an object pool that is already disposed."); - d.BeforeDispose += Pool_BeforeDispose; - } - } + /// + /// Constructs an auto-trimming ObjectPool helper. + /// + /// The target size to limit to after a half second timeout. Allowing the pool to still grow to the max size until the trim occurs. + /// The governable object pool to maintain. + /// The amount of time to wait/defer trimming. + public ObjectPoolAutoTrimmer( + ushort trimmedSize, + ITrimmableObjectPool pool, + TimeSpan? trimDelay = null) + { + _pool = pool ?? throw new ArgumentNullException(nameof(pool)); - protected virtual void Target_GivenTo(int newSize) + if (pool is DisposableBase d) { - if (newSize>=0 && newSize > _trimmedSize) - _trimmer?.Defer(_trimDelay, false); + if (d.WasDisposed) throw new ArgumentException("Cannot trim for an object pool that is already disposed."); + d.BeforeDispose += Pool_BeforeDispose; } - protected virtual void Target_TakenFrom(int newSize) - { - if (newSize >= 0 && newSize <= _trimmedSize) - _trimmer?.Cancel(); - } + TrimmedSize = _trimmedSize = trimmedSize; + TrimDelay = _trimDelay = trimDelay ?? TimeSpan.FromMilliseconds(500); - void Pool_BeforeDispose(object sender, EventArgs e) - { - Dispose(); - } + _trimmer = new ActionRunner(TrimInternal); - protected virtual void TrimInternal() - { + pool.Received += Target_GivenTo; + pool.Released += Target_TakenFrom; + } + + protected virtual void Target_GivenTo(int newSize) + { + if (newSize >= 0 && newSize > _trimmedSize) + _trimmer?.Defer(_trimDelay, false); + } + + protected virtual void Target_TakenFrom(int newSize) + { + if (newSize >= 0 && newSize <= _trimmedSize) _trimmer?.Cancel(); - _pool?.TrimTo(_trimmedSize); - } + } - protected override void OnDispose(bool calledExplicitly) - { - DisposeOf(ref _trimmer); + void Pool_BeforeDispose(object? sender, EventArgs e) => Dispose(); - var target = Nullify(ref _pool); - target.GivenTo -= Target_GivenTo; - target.TakenFrom -= Target_TakenFrom; - } + protected virtual void TrimInternal() + { + _trimmer?.Cancel(); + _pool?.TrimTo(_trimmedSize); + } + + protected override void OnDispose() + { + DisposeOf(ref _trimmer); + + var target = Nullify(ref _pool); + target.Received -= Target_GivenTo; + target.Released -= Target_TakenFrom; } } diff --git a/source/ObjectPoolBase.cs b/source/ObjectPoolBase.cs index 19ee4f2..7a78c7a 100644 --- a/source/ObjectPoolBase.cs +++ b/source/ObjectPoolBase.cs @@ -1,159 +1,133 @@ using System; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; +using System.Runtime.CompilerServices; -namespace Open.Disposable -{ - public abstract class ObjectPoolBase : DisposableBase, IObjectPool - where T : class - { - protected const int DEFAULT_CAPACITY = Constants.DEFAULT_CAPACITY; - - protected ObjectPoolBase(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - { - if (capacity < 1) - throw new ArgumentOutOfRangeException("capacity", capacity, "Must be at least 1."); - Factory = factory ?? throw new ArgumentNullException("factory"); - MaxSize = capacity; - Recycler = recycler; - } - - protected ObjectPoolBase(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { +namespace Open.Disposable; - } - - protected Action Recycler; - protected int MaxSize; - public int Capacity => MaxSize; +public abstract class ObjectPoolBase + : DisposableBase, IObjectPool + where T : class +{ + protected const int DEFAULT_CAPACITY = Constants.DEFAULT_CAPACITY; - // Read-only because if Take() is called after disposal, this still facilitates returing an object. - // Allow the GC to do the final cleanup after dispose. - protected readonly Func Factory; + protected ObjectPoolBase( + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY) + { + if (capacity < 1) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "Must be at least 1."); + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + MaxSize = capacity; + Recycler = recycler; + OnDiscarded = disposer; + } - public T Generate() - { - return Factory(); - } + // Not read-only due to quirk where read-only is slower than not. + protected int MaxSize; + public int Capacity => MaxSize; - protected virtual bool CanReceive => true; + /// + /// Total number of items in the pool. + /// + /// + public abstract int Count { get; } + protected int PocketCount => Pocket.Value is null ? 0 : 1; - protected bool PrepareToReceive(T item) - { - if (item == null) return false; + protected readonly Action? Recycler; // Before entering the pool. + protected readonly Action? OnDiscarded; // When not able to be used. - if (!CanReceive) return false; - var r = Recycler; - if (r != null) - { - r(item); - if (!CanReceive) return false; - } + // Read-only because if Take() is called after disposal, this still facilitates returing an object. + // Allow the GC to do the final cleanup after dispose. + protected readonly Func Factory; - return true; - } + public T Generate() => Factory(); - // Contract should be that no item can be null here. - protected abstract bool Receive(T item); + // ReSharper disable once UnassignedField.Global + protected ReferenceContainer Pocket; // Default struct constructs itself. - protected virtual void OnGivenTo() - { + #region Receive (.Give(T item)) + protected virtual bool CanReceive => true; // A default of true is acceptable, enabling the Receive method to do the actual deciding. - } - protected void OnGivenTo(bool wasGiven) - { - if (wasGiven) OnGivenTo(); - } + protected bool PrepareToReceive(T item) + { + if (!CanReceive) return false; - public void Give(T item) - { - if (PrepareToReceive(item) - && (GaveToPocket(ref item) || Receive(item))) - OnGivenTo(); - } + Debug.Assert(item is not null); + var r = Recycler; + if (r is null) return true; - protected virtual Task GiveInternalAsync(T item) - { - return Task.Run(() => Receive(item)); - } + r(item!); + // Did the recycle take so long that the state changed? + return CanReceive; + } - public Task GiveAsync(T item) - { - // We need to pre-check CanReceive because excessive tasks could build up if not. - if (item == null || !CanReceive) return Task.CompletedTask; - - return GiveAsyncConditional(item); - } + // Contract should be that no item can be null here. + protected abstract bool Receive(T item); - async Task GiveAsyncConditional(T item) - { - if(PrepareToReceive(item) - && (GaveToPocket(ref item) || await GiveInternalAsync(item))) - OnGivenTo(); - } + protected virtual void OnReceived() + { + } - public virtual T Take() + /// + public void Give(T item) + { + if (item is null) return; + if (PrepareToReceive(item) + && (SaveToPocket(item) + || Receive(item))) { - return TryTake() ?? Factory(); + OnReceived(); } - - protected virtual Task TakeAsyncInternal() + else { - return Task.Run((Func)Take); + OnDiscarded?.Invoke(item); } + } + #endregion - public Task TakeAsync() - { - // See if there's one available already. - if (TryTake(out T firstTry)) - return Task.FromResult(firstTry); + #region Release (.Take()) + /// + public virtual T Take() => TryTake() ?? Factory(); - return TakeAsyncInternal(); - } + /// +#if NETSTANDARD2_1_OR_GREATER + public bool TryTake([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? item) +#else + public bool TryTake(out T? item) +#endif + => (item = TryTake()) is not null; - public bool TryTake(out T item) - { - item = TryTake(); - return item != null; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool SaveToPocket(T item) => Pocket.TrySave(item); - protected bool AllowPocket = true; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected T? TakeFromPocket() => Pocket.TryRetrieve(); - protected T Pocket; - protected T TakeFromPocket() - { - if (!AllowPocket || Pocket == null) return null; - return Interlocked.Exchange(ref Pocket, null); - } + protected abstract T? TryRelease(); - protected bool GaveToPocket(ref T item) - { - if (!AllowPocket || Pocket != null) return false; - item = Interlocked.Exchange(ref Pocket, item); - return item == null; - } + /// + public T? TryTake() + { + var item = TakeFromPocket() ?? TryRelease(); + if (item is not null) OnReleased(); + return item; + } - protected abstract T TryTakeInternal(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void OnReleased() + { + } + #endregion - public T TryTake() - { - var item = TakeFromPocket() ?? TryTakeInternal(); - if (item != null) OnTakenFrom(); - return item; - } + protected override void OnBeforeDispose() => MaxSize = 0; - protected virtual void OnTakenFrom() - { - } - protected void OnTakenFrom(bool wasTaken) - { - if (wasTaken) OnTakenFrom(); - } + protected override void OnDispose() + { + if (OnDiscarded is null) return; - protected override void OnBeforeDispose() - { - MaxSize = 0; - } + T? d; + while ((d = TryRelease()) is not null) OnDiscarded(d); } } diff --git a/source/ObjectPoolCompatibilityExtensions.cs b/source/ObjectPoolCompatibilityExtensions.cs new file mode 100644 index 0000000..89a146c --- /dev/null +++ b/source/ObjectPoolCompatibilityExtensions.cs @@ -0,0 +1,16 @@ +namespace Open.Disposable.ObjectPoolCompatibility; + +/// +/// Provided as means for compatiibility with object pools +/// that have the same interface as the Microsoft Extensions version. +/// +public static class ObjectPoolCompatibilityExtensions +{ + /// + public static T Get(this IObjectPool pool) + where T : class => pool.Take(); + + /// + public static void Return(this IObjectPool pool, T item) + where T : class => pool.Give(item); +} diff --git a/source/ObjectPoolResizeEvent.cs b/source/ObjectPoolResizeEvent.cs index faf66c1..78659d9 100644 --- a/source/ObjectPoolResizeEvent.cs +++ b/source/ObjectPoolResizeEvent.cs @@ -1,6 +1,3 @@ -using System; +namespace Open.Disposable; -namespace Open.Disposable -{ - public delegate void ObjectPoolResizeEvent(int newSize); -} +public delegate void ObjectPoolResizeEvent(int newSize); diff --git a/source/Open.Disposable.ObjectPools.csproj b/source/Open.Disposable.ObjectPools.csproj index ae9f78a..6e14fe1 100644 --- a/source/Open.Disposable.ObjectPools.csproj +++ b/source/Open.Disposable.ObjectPools.csproj @@ -1,34 +1,66 @@  - - netcoreapp1.1 - Open.Disposable - A set of variations on ObjectPool implementations with differing underlying collections. + + netstandard2.0;netstandard2.1;net9.0 + latest + enable + true + Open.Disposable + True + electricessence + + A set of variations on ObjectPool implementations with differing underlying collections. -Part of the "Open" set of libraries. - electricessence - electricessence - - https://github.com/electricessence/Open.Disposable.ObjectPools/blob/master/LISCENSE.md - https://github.com/electricessence/Open.Disposable.ObjectPools/blob/master/LISCENSE.md - https://github.com/electricessence/Open.Disposable.ObjectPools/ - https://github.com/electricessence/Open.Disposable.ObjectPools/ - git - objectpool, dotnet, dotnetcore, cs, idisposable, threadsafe, thread-safe - True - 1.0.4 - 1.1.0.0 - 1.1.0.0 - Removed pools that are simply too slow for real world use and migrated to separate branch since they are simply not worth - + Part of the "Open" set of libraries. + + © electricessence (Oren F.) All rights reserved. + https://github.com/Open-NET-Libraries/Open.Disposable.ObjectPools/ + https://github.com/Open-NET-Libraries/Open.Disposable.ObjectPools/ + git + objectpool;idisposable;thread safe + 3.0.2 + + MIT + true + true + snupkg + logo.png + README.md + IDE0290;CA1510;IDE0130;IDE0301; + - - - + + + + + + True + \ + + + True + + + - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/source/RecycleHelper.cs b/source/RecycleHelper.cs new file mode 100644 index 0000000..98882cc --- /dev/null +++ b/source/RecycleHelper.cs @@ -0,0 +1,37 @@ +using System; + +namespace Open.Disposable; + +/// +/// Allows for the 'using' syntax to be used with any object to return it to the pool. +/// +public struct RecycleHelper : IDisposable + where T : class +{ + private readonly IObjectPool _pool; + + private RecycleHelper(IObjectPool pool, T item) + { + _pool = pool; + _item = item; + } + + public RecycleHelper(IObjectPool pool) + : this(pool ?? throw new ArgumentNullException(nameof(pool)), pool.Take()) + { + } + + private T? _item; + public readonly T Item + => _item ?? throw new ObjectDisposedException(GetType().ToString()); + + public void Dispose() + { + var i = _item; + _item = null; + if (i is null) return; + _pool.Give(i); + } + + public static implicit operator T(RecycleHelper helper) => helper.Item; +} diff --git a/source/Recycler.cs b/source/Recycler.cs index 115b0b0..43813f5 100644 --- a/source/Recycler.cs +++ b/source/Recycler.cs @@ -1,75 +1,79 @@ using System; +using System.Threading.Channels; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -namespace Open.Disposable +namespace Open.Disposable; + +/// +public class Recycler : RecyclerBase + where T : class { - /// - /// This class is provided as an asynchronous queue for recycling instead of using a recycle delegate with an object pool and calling GiveAsync() which could pile up unnecessarily. - /// So if recycling an object takes extra time, this might be a good way to toss objects away and not have to worry about the heavy cost as they will one by one be processed back into the target pool. - /// - public class Recycler : DisposableBase - where T : class + Channel _bin; + + internal Recycler( + IObjectPool target, + Action recycleFunction, + ushort limit = Constants.DEFAULT_CAPACITY) : base(target, recycleFunction) { - IObjectPool _target; - ActionBlock _bin; + _bin = Channel.CreateBounded(limit); + Completion = Processor(recycleFunction); + } - public Recycler( - IObjectPool target, - Action recycleFunction, - ushort limit = ushort.MaxValue) + async Task Processor(Action recycleFunction) + { + var bin = _bin; + do { - if(recycleFunction==null) throw new ArgumentNullException("recycleFunction"); - _target = target ?? throw new ArgumentNullException("target"); - - _bin = new ActionBlock(item => - { - if (_target != null) - { - recycleFunction(item); - _target?.Give(item); - } - }); - - if (target is DisposableBase d) + while (bin.Reader.TryRead(out var item)) { - if (d.IsDisposed) throw new ArgumentException("Cannot recycle for an object pool that is already disposed."); - d.BeforeDispose += Pool_BeforeDispose; - // Could possibly dispose before this line somewhere... But that's just nasty. :P + recycleFunction(item); + Target?.Give(item); } } + while (await bin.Reader.WaitToReadAsync().ConfigureAwait(false)); + } - public Recycler( - ushort limit, - IObjectPool pool, - Action recycleFunction) : this(pool, recycleFunction, limit) - { - - } + internal Recycler( + ushort limit, + IObjectPool pool, + Action recycleFunction) : this(pool, recycleFunction, limit) { } - void Pool_BeforeDispose(object sender, EventArgs e) - { - Dispose(); - } + /// + public override bool Recycle(T item) => _bin?.Writer.TryWrite(item) ?? false; - public bool Recycle(T item) - { - return _bin?.Post(item) ?? false; - } + protected override void OnCloseRequested() => _bin?.Writer.Complete(); - public Task Close() - { - var b = _bin; - if (b == null) return Task.CompletedTask; - b.Complete(); - return b.Completion; - } + protected override void OnDispose() + { + base.OnDispose(); - protected override void OnDispose(bool calledExplicitly) - { - _bin.Complete(); - _bin = null; - _target = null; - } + _bin.Writer.TryComplete(); + _bin = null!; } } + +public static class Recycler +{ + public static Recycler CreateRecycler( + this IObjectPool pool, + Action recycleFunction, + ushort limit = Constants.DEFAULT_CAPACITY) + where T : class => new(pool, recycleFunction, limit); + + public static Recycler CreateRecycler( + this IObjectPool pool, + ushort limit, + Action recycleFunction) + where T : class => new(pool, recycleFunction, limit); + + private static Action? _recycleDelegate; + // A predefined delegate instead of a method to avoid additional allocations. + public static Action Recycle + => _recycleDelegate ??= (IRecyclable r) + => (r ?? throw new ArgumentNullException(nameof(r))).Recycle(); + + public static Recycler CreateRecycler( + this IObjectPool pool, + ushort limit = Constants.DEFAULT_CAPACITY) + where T : class, IRecyclable => new(pool, Recycle, limit); +} diff --git a/source/RecyclerBase.cs b/source/RecyclerBase.cs new file mode 100644 index 0000000..c46a24b --- /dev/null +++ b/source/RecyclerBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading.Tasks; + +namespace Open.Disposable; + +/// +/// This class is provided as an asynchronous queue for recycling instead of using a recycle delegate with an object pool and calling GiveAsync() which could pile up unnecessarily. +/// So if recycling an object takes extra time, this might be a good way to toss objects away and not have to worry about the heavy cost as they will one by one be processed back into the target pool. +/// +// ReSharper disable once InheritdocConsiderUsage +public abstract class RecyclerBase : DisposableBase, IRecycler + where T : class +{ + protected IObjectPool Target; + + protected RecyclerBase( + IObjectPool target, + Action recycleFunction) + { + if (recycleFunction is null) throw new ArgumentNullException(nameof(recycleFunction)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + Contract.EndContractBlock(); + + if (target is not DisposableBase d) return; + if (d.WasDisposed) throw new ArgumentException("Cannot recycle for an object pool that is already disposed."); + d.BeforeDispose += Pool_BeforeDispose; + // Could possibly dispose before this line somewhere... But that's just nasty. :P + } + + void Pool_BeforeDispose(object? sender, EventArgs e) => Dispose(); + + public abstract bool Recycle(T item); + + protected abstract void OnCloseRequested(); + + // ReSharper disable once MemberCanBeProtected.Global + public Task Completion { get; protected set; } = Task.CompletedTask; + + /// + public Task Close() + { + OnCloseRequested(); + return Completion; + } + + protected override void OnDispose() + { + OnCloseRequested(); + Target = null!; + } +} diff --git a/source/ReferenceContainer.cs b/source/ReferenceContainer.cs new file mode 100644 index 0000000..f2b47fa --- /dev/null +++ b/source/ReferenceContainer.cs @@ -0,0 +1,66 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Open.Disposable; + +public interface IReferenceContainer + where T : class +{ + int Capacity { get; } + bool SetIfNull(T value); + bool TrySave(T value); + T? TryRetrieve(); +} + +[DebuggerDisplay("{Value,nq}")] +public struct ReferenceContainer : IReferenceContainer + where T : class +{ + public readonly int Capacity => 1; + + T? _value; + + /// + /// The value contained. + /// + public T? Value + { + readonly get => _value; + set => _value = value; + } + + /// + /// Sets the value if it is currently null without interlocking. + /// + /// true if the value was set; otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool SetIfNull(T value) + { + if (_value is not null) return false; + _value = value; + return true; + } + + /// + /// Tries to atomically store the value. + /// + /// true if the value was set; otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySave(T value) + => _value is null && Interlocked.CompareExchange(ref _value, value, null) == null; + + /// + /// Tries to atomically retrieve the value. + /// + /// The value retrieved; otherwise null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T? TryRetrieve() + { + var item = _value; + return (item is not null + && item == Interlocked.CompareExchange(ref _value, null, item)) + ? item + : null; + } +} diff --git a/source/SharedPools.cs b/source/SharedPools.cs new file mode 100644 index 0000000..810c253 --- /dev/null +++ b/source/SharedPools.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Open.Disposable; + +public sealed class SharedPool( + Func factory, + Action? recycler, + int capacity) + : OptimisticArrayObjectPool(factory, recycler, capacity) + where T : class +{ + [Obsolete("Shared pools do not support disposal.")] + public new void Dispose() + => throw new NotSupportedException("Shared pools cannot be disposed."); + + protected override void OnDispose() + => throw new NotSupportedException("Shared pools cannot be disposed."); +} + +public static class ListPool +{ + /// + /// A shared object pool for use with lists. + /// The list is cleared after being returned. + /// The max size of the pool is 64 which should suffice for most use cases. + /// + public static readonly SharedPool> Shared = Create(); + + /// + /// Creates an object pool for use with lists. + /// The list is cleared after being returned. + /// + public static SharedPool> Create(int capacity = Constants.DEFAULT_CAPACITY) => new(() => [], h => + { + h.Clear(); + if (h.Capacity > 16) h.Capacity = 16; + }, capacity); + + /// + /// Provides a disposable RecycleHelper that contains an item from the pool.
+ /// Will be returned to the pool once .Dispose() is called. + ///
+ /// A RecycleHelper containing an item from the pool. + public static RecycleHelper> Rent() => Shared.Rent(); +} + +public static class HashSetPool +{ + /// + /// A shared object pool for use with hash-sets. + /// The hash-set is cleared after being returned. + /// The max size of the pool is 64 which should suffice for most use cases. + /// + public static readonly SharedPool> Shared = Create(); + + /// + /// Creates an object pool for use with hash-sets. + /// The hash-set is cleared after being returned. + /// + public static SharedPool> Create(int capacity = Constants.DEFAULT_CAPACITY) + => new(() => [], h => h.Clear(), capacity); + + /// + public static RecycleHelper> Rent() => Shared.Rent(); +} + +public static class StringBuilderPool +{ + /// + /// A shared object pool for use with StringBuilders. + /// The StringBuilder is cleared after being returned. + /// The max size of the pool is 64 which should suffice for most use cases. + /// + public static readonly SharedPool Shared = Create(); + + /// + /// Creates an object pool for use with StringBuilders. + /// The StringBuilder is cleared after being returned. + /// + public static SharedPool Create(int capacity = Constants.DEFAULT_CAPACITY) + => new(() => new(), sb => sb.Clear(), capacity); + + /// + /// Provides a StringBuilder to be used for processing and finalizes by calling .ToString() and returning the value. + /// + /// If either the pool or action are null. + public static string RentToString(this IObjectPool pool, Action action) + { + if (pool is null) throw new ArgumentNullException(nameof(pool)); + if (action is null) throw new ArgumentNullException(nameof(action)); + + var sb = pool.Take(); + action(sb); + var result = sb.ToString(); + pool.Give(sb); + return result; + } + + /// If the action is null. + /// + public static string RentToString(Action action) + => Shared.RentToString(action); + + /// + /// Provides a disposable RecycleHelper that contains a StringBuilder from the pool.
+ /// Will be returned to the pool once .Dispose() is called. + ///
+ /// A RecycleHelper containing a StringBuilder from the pool. + public static RecycleHelper Rent() => Shared.Rent(); +} + +public static class DictionaryPool + where TKey : notnull +{ + /// + /// A shared object pool for use with dictionaries. + /// The dictionary is cleared after being returned. + /// The max size of the pool is 64 which should suffice for most use cases. + /// + public static readonly SharedPool> Shared = Create(); + + /// + /// Creates an object pool for use with dictionaries. + /// The dictionary is cleared after being returned. + /// + public static SharedPool> Create(int capacity = Constants.DEFAULT_CAPACITY) + => new(() => [], h => h.Clear(), capacity); + + /// + /// Provides a disposable RecycleHelper that contains an item from the pool.
+ /// Will be returned to the pool once .Dispose() is called. + ///
+ /// A RecycleHelper containing a item from the pool. + public static RecycleHelper> Rent() => Shared.Rent(); +} diff --git a/source/TrimmableCollectionObjectPoolBase.cs b/source/TrimmableCollectionObjectPoolBase.cs new file mode 100644 index 0000000..73b1626 --- /dev/null +++ b/source/TrimmableCollectionObjectPoolBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Open.Disposable; + +/* + * There are two class varations here because ICollection and ICollection do not overlap. + * 'Count' is the common property used in both classes. + */ + +public abstract class TrimmableCollectionObjectPoolBase + : TrimmableObjectPoolBase + where T : class + where TCollection : class, ICollection +{ + protected TrimmableCollectionObjectPoolBase( + TCollection pool, + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY, + bool countTrackingEnabled = true) + : base(factory, recycler, disposer, capacity, countTrackingEnabled) + => Pool = pool ?? throw new ArgumentNullException(nameof(pool)); + + protected TCollection Pool; + + /// + public override int Count => (Pool?.Count ?? 0) + PocketCount; + + protected override void OnDispose() + { + base.OnDispose(); + Pool = null!; + } +} + +public abstract class TrimmableGenericCollectionObjectPoolBase + : TrimmableObjectPoolBase + where T : class + where TCollection : class, ICollection +{ + protected TrimmableGenericCollectionObjectPoolBase( + TCollection pool, + Func factory, + Action? recycler, + Action? disposer, + int capacity = DEFAULT_CAPACITY, + bool countTrackingEnabled = true) + : base(factory, recycler, disposer, capacity, countTrackingEnabled) + => Pool = pool ?? throw new ArgumentNullException(nameof(pool)); + + protected TCollection Pool; + + /// + public override int Count => (Pool?.Count ?? 0) + PocketCount; + + protected override void OnDispose() + { + //base.OnDispose(); // Do not call because the following is more optimized. + + var p = Pool; + Pool = null!; + + if (OnDiscarded is not null) + { + foreach (var item in p) + OnDiscarded(item); + } + + p.Clear(); + } +} diff --git a/source/TrimmableObjectPoolBase.cs b/source/TrimmableObjectPoolBase.cs index 24bee7d..f37ee39 100644 --- a/source/TrimmableObjectPoolBase.cs +++ b/source/TrimmableObjectPoolBase.cs @@ -1,77 +1,92 @@ using System; using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; -namespace Open.Disposable +namespace Open.Disposable; + +[DebuggerDisplay("Count = {Count}")] +public abstract class TrimmableObjectPoolBase : ObjectPoolBase, ITrimmableObjectPool + where T : class { - [DebuggerDisplay("Count = {Count}")] - public abstract class TrimmableObjectPoolBase : ObjectPoolBase, ITrimmableObjectPool - where T : class - { + protected TrimmableObjectPoolBase(Func factory, Action? recycler, Action? disposer, int capacity, bool countTrackingEnabled = true) + : base(factory, recycler, disposer, capacity) => _countTrackingEnabled = countTrackingEnabled; +#if NET9_0_OR_GREATER + protected readonly Lock SyncRoot = new(); +#else + protected readonly object SyncRoot = new(); +#endif - public TrimmableObjectPoolBase(Func factory, Action recycler, int capacity = DEFAULT_CAPACITY) - : base(factory, recycler, capacity) - { - } + int _count; + readonly bool _countTrackingEnabled; // When true this enables tracking the number of entries entering and exiting the pool instead of calling '.Count'. - public TrimmableObjectPoolBase(Func factory, int capacity = DEFAULT_CAPACITY) - : this(factory, null, capacity) - { - } + protected int CountInternal => _countTrackingEnabled ? _count : Count; - /// - /// Total number of items in the pool. - /// - public abstract int Count { get; } + protected override bool CanReceive => CountInternal < MaxSize; - protected override bool CanReceive => Count < MaxSize; + /// + /// Signal for when an item was taken (actually removed) from the pool. + /// + public event ObjectPoolResizeEvent? Released; + protected void OnReleased(int newSize) + { + Debug.Assert(newSize > -2, $"newSize: {newSize}, _count: {_count}"); // Should never get out of control. It may go negative temporarily but should be 100% accounted for. + Released?.Invoke(newSize); + } - /// - /// Signal for when an item was taken (actually removed) from the pool. - /// - public event ObjectPoolResizeEvent TakenFrom; - protected void OnTakenFrom(int newSize) - { - TakenFrom?.Invoke(newSize); - } - protected override void OnTakenFrom() - { - OnTakenFrom(Count); - } - - /// - /// Signal for when an item was given (actually accepted) to the pool. - /// - public event ObjectPoolResizeEvent GivenTo; - protected void OnGivenTo(int newSize) - { - GivenTo?.Invoke(newSize); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnReleased() => OnReleased(_countTrackingEnabled ? Interlocked.Decrement(ref _count) : Count); - protected override void OnGivenTo() - { - OnGivenTo(Count); - } + /// + /// Signal for when an item was given (actually accepted) to the pool. + /// + public event ObjectPoolResizeEvent? Received; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void OnReceived(int newSize) => Received?.Invoke(newSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnReceived() => OnReceived(_countTrackingEnabled ? Interlocked.Increment(ref _count) : Count); + + /// + public virtual void TrimTo(int targetSize) + { + if (targetSize < 0) return; // Possible upstream math hiccup or default -1. Silently dismiss. + var count = CountInternal; + + if (count <= targetSize) return; - public virtual void TrimTo(int targetSize) + var i = 0; // Prevent an possiblility of indefinite loop. + for ( + var attempts = count - targetSize; + i < attempts; + i++) { - if (targetSize < 0) return; // Possible upstream math hiccup or default -1. Silently dismiss. - int count = Count; - - int i = 0; // Prevent an possiblility of indefinite loop. - for ( - var attempts = count - targetSize; - i < attempts && count > targetSize; - i++) - { - if (TryTakeInternal() == null) - break; + var e = TryRelease(); + if (e is null) break; - count = Count; + if (_countTrackingEnabled) + { + Interlocked.Decrement(ref _count); + //var c = Interlocked.Decrement(ref _count); + //Debug.Assert(c >= 0); } - if (i != 0) OnTakenFrom(count); + OnDiscarded?.Invoke(e); } + if (i != 0) OnReleased(CountInternal); } + + //protected ReferenceContainer Pocket2; // Default struct constructs itself. + + //protected override bool SaveToPocket(T item) + // => Pocket.TrySave(item) || Pocket2.TrySave(item); + + //protected override T TakeFromPocket() + // => Pocket.TryRetrieve() ?? Pocket2.TryRetrieve(); + + //protected override int PocketCount => + // base.PocketCount + (Pocket2.Value is null ? 0 : 1); } diff --git a/source/logo.png b/source/logo.png new file mode 100644 index 0000000..473ec87 Binary files /dev/null and b/source/logo.png differ diff --git a/tests/ObjectPoolSmokeTests.cs b/tests/ObjectPoolSmokeTests.cs index c94226f..2c940c4 100644 --- a/tests/ObjectPoolSmokeTests.cs +++ b/tests/ObjectPoolSmokeTests.cs @@ -1,45 +1,36 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; -namespace Open.Disposable +namespace Open.Disposable; + +[TestClass] +public class ObjectPoolSmokeTests { - [TestClass] - public class ObjectPoolSmokeTests + class IdContainer { - class IdContainer - { - public int ID; - } - - [TestMethod] - public void BufferBlockObjectPool_FactoryTest() - { - int i = 0; - var pool = BufferBlockObjectPool.Create(()=>new IdContainer { ID = ++i }); - Assert.AreEqual(1, pool.Take().ID); - } + public int ID; + } - [TestMethod] - public void ConcurrentBagObjectPool_FactoryTest() - { - int i = 0; - var pool = ConcurrentBagObjectPool.Create(() => new IdContainer { ID = ++i }); - Assert.AreEqual(1, pool.Take().ID); - } + [TestMethod] + public void OptimisticArrayObjectPool_FactoryTest() + { + var i = 0; + var pool = OptimisticArrayObjectPool.Create(() => new IdContainer { ID = ++i }); + Assert.AreEqual(1, pool.Take().ID); + } - [TestMethod] - public void OptimisticArrayObjectPool_FactoryTest() - { - int i = 0; - var pool = OptimisticArrayObjectPool.Create(() => new IdContainer { ID = ++i }); - Assert.AreEqual(1, pool.Take().ID); - } + [TestMethod] + public void ListPool_RecycleTest() + { + var pool = ListPool.Shared; + var list = pool.Take(); + list.Add(1); + pool.Give(list); + Assert.AreEqual(list, pool.Take()); + Assert.AreEqual(0, list.Count); - [TestMethod] - public void LinkedListObjectPool_FactoryTest() - { - int i = 0; - var pool = LinkedListObjectPool.Create(() => new IdContainer { ID = ++i }); - Assert.AreEqual(1, pool.Take().ID); - } +#pragma warning disable CS0618 // Type or member is obsolete + Assert.ThrowsException(pool.Dispose); +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/tests/Open.Disposable.ObjectPools.Tests.csproj b/tests/Open.Disposable.ObjectPools.Tests.csproj index 917c23b..73cc3c8 100644 --- a/tests/Open.Disposable.ObjectPools.Tests.csproj +++ b/tests/Open.Disposable.ObjectPools.Tests.csproj @@ -1,14 +1,22 @@  - netcoreapp1.1 + net9.0 Open.Disposable + + latest + + + + latest + + - - - + + +