From 10ae7448759d056ddb380463ebc58be40025e13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 5 Feb 2023 17:58:29 +0100 Subject: [PATCH] RecyclableList.Contains reworked for performance improvements. 69% vs List.Contains --- .../RecyclableCollectionsBenchmarks.List.cs | 12 + ...bleCollectionsBenchmarks.RecyclableList.cs | 20 +- Recyclable.Collections/RecyclableList.cs | 225 +++++++++++++----- .../RecyclableListTests.cs | 47 ++++ 4 files changed, 245 insertions(+), 59 deletions(-) diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs index 2996574..ee76fc0 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.List.cs @@ -111,5 +111,17 @@ namespace Recyclable.Collections.Benchmarks var data = TestObjectsAsList; DoNothing(data.Count); } + + [Benchmark(Baseline = true)] + public void List_Contains() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = 100_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[i])); + } + } } } diff --git a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs index b661bc2..6ca2add 100644 --- a/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs +++ b/Recyclable.Collections.Benchmarks/RecyclableCollectionsBenchmarks.RecyclableList.cs @@ -4,21 +4,21 @@ namespace Recyclable.Collections.Benchmarks { public partial class RecyclableCollectionsBenchmarks : BenchmarkBase { - [Benchmark] + //[Benchmark] public void RecyclableList_Create() { using var list = new RecyclableList(); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableList_Create_WithCapacity() { using var list = new RecyclableList(minBlockSize: 102_400, expectedItemsCount: TestObjectCount); DoNothing(list); } - [Benchmark] + //[Benchmark] public void RecyclableList_Add() { var data = TestObjects; @@ -30,7 +30,7 @@ namespace Recyclable.Collections.Benchmarks } } - [Benchmark] + //[Benchmark] public void RecyclableList_Add_WithCapacity() { var data = TestObjects; @@ -77,5 +77,17 @@ namespace Recyclable.Collections.Benchmarks var data = _testRecyclableList; DoNothing(data.LongCount); } + + [Benchmark] + public void RecyclableList_Contains() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = 100_000; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Contains(data[i])); + } + } } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index ca70c76..00cbd24 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -433,7 +433,7 @@ namespace Recyclable.Collections } } - _longCount = targetBlockIdx * blockSize + targetItemIdx; + _longCount = (targetBlockIdx * blockSize) + targetItemIdx; _lastBlockIndex = targetBlockIdx; _nextItemIndex = targetItemIdx; return; @@ -510,6 +510,7 @@ namespace Recyclable.Collections _longCount = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(T item) { if (_longCount == 0) @@ -517,101 +518,215 @@ namespace Recyclable.Collections return false; } - int midPoint = _blockSize / 2; - int lastBlockIndex = _lastBlockIndex; - int itemIndex0 = 0; - int itemIndex1 = midPoint; + int blockSize = _blockSize; + int itemIndex = 0; int blockIndex = 0; + int lastBlockIndex = 0; Span memoryBlocksSpan = new(_memoryBlocks); Span blockArraySpan = new(memoryBlocksSpan[blockIndex]); if (item != null) { - while (blockIndex <= lastBlockIndex) + while (blockIndex < lastBlockIndex) { - if (item.Equals(blockArraySpan[itemIndex0++])) + if (item.Equals(blockArraySpan[itemIndex])) { return true; } - if (item.Equals(blockArraySpan[itemIndex1++])) + if (itemIndex + 1 < blockSize && item.Equals(blockArraySpan[itemIndex + 1])) + { + return true; + } + + if (itemIndex + 2 < blockSize && item.Equals(blockArraySpan[itemIndex + 2])) + { + return true; + } + + if (itemIndex + 3 < blockSize && item.Equals(blockArraySpan[itemIndex + 3])) { return true; } - if (itemIndex0 == midPoint) + if (itemIndex + 4 < blockSize && item.Equals(blockArraySpan[itemIndex + 4])) + { + return true; + } + + if (itemIndex + 5 < blockSize && item.Equals(blockArraySpan[itemIndex + 5])) + { + return true; + } + + if (itemIndex + 6 < blockSize && item.Equals(blockArraySpan[itemIndex + 6])) + { + return true; + } + + if (itemIndex + 7 < blockSize && item.Equals(blockArraySpan[itemIndex + 7])) + { + return true; + } + + itemIndex += 8; + if (itemIndex >= blockSize) { blockIndex++; - if (blockIndex == lastBlockIndex) - { - int nextItemIndex = _nextItemIndex; - if (nextItemIndex == 0) - { - return false; - } - - midPoint = (nextItemIndex / 2) + (nextItemIndex % 2); - } - blockArraySpan = new(memoryBlocksSpan[blockIndex]); - itemIndex0 = 0; - itemIndex1 = midPoint; + itemIndex = 0; } } + + blockArraySpan = new(memoryBlocksSpan[lastBlockIndex]); + var nextItemIndex = _nextItemIndex; + while (itemIndex < nextItemIndex) + { + if (item.Equals(blockArraySpan[itemIndex])) + { + return true; + } + + if (itemIndex + 1 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 1])) + { + return true; + } + + if (itemIndex + 2 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 2])) + { + return true; + } + + if (itemIndex + 3 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 3])) + { + return true; + } + + if (itemIndex + 4 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 4])) + { + return true; + } + + if (itemIndex + 5 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 5])) + { + return true; + } + + if (itemIndex + 6 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 6])) + { + return true; + } + + if (itemIndex + 7 < nextItemIndex && item.Equals(blockArraySpan[itemIndex + 7])) + { + return true; + } + + itemIndex += 8; + } } else { while (blockIndex < lastBlockIndex) { - if (blockArraySpan[itemIndex0++] == null) + if (blockArraySpan[itemIndex] == null) { return true; } - if (blockArraySpan[itemIndex1++] == null) + if (itemIndex + 1 < blockSize && blockArraySpan[itemIndex + 1] == null) { return true; } - if (itemIndex0 == midPoint) + if (itemIndex + 2 < blockSize && blockArraySpan[itemIndex + 2] == null) + { + return true; + } + + if (itemIndex + 3 < blockSize && blockArraySpan[itemIndex + 3] == null) + { + return true; + } + + if (itemIndex + 4 < blockSize && blockArraySpan[itemIndex + 4] == null) + { + return true; + } + + if (itemIndex + 5 < blockSize && blockArraySpan[itemIndex + 5] == null) + { + return true; + } + + if (itemIndex + 6 < blockSize && blockArraySpan[itemIndex + 6] == null) + { + return true; + } + + if (itemIndex + 7 < blockSize && blockArraySpan[itemIndex + 7] == null) + { + return true; + } + + itemIndex += 8; + if (itemIndex >= blockSize) { blockIndex++; - if (blockIndex == lastBlockIndex) - { - if (_nextItemIndex == 0) - { - return false; - } - - midPoint = ((_nextItemIndex - 1) / 2) + ((_nextItemIndex - 1) % 2); - } - blockArraySpan = new(memoryBlocksSpan[blockIndex]); - itemIndex0 = 0; - itemIndex1 = 0; + itemIndex = 0; } } + + blockArraySpan = new(memoryBlocksSpan[lastBlockIndex]); + var nextItemIndex = _nextItemIndex; + while (itemIndex < nextItemIndex) + { + if (blockArraySpan[itemIndex] == null) + { + return true; + } + + if (itemIndex + 1 < nextItemIndex && blockArraySpan[itemIndex + 1] == null) + { + return true; + } + + if (itemIndex + 2 < nextItemIndex && blockArraySpan[itemIndex + 2] == null) + { + return true; + } + + if (itemIndex + 3 < nextItemIndex && blockArraySpan[itemIndex + 3] == null) + { + return true; + } + + if (itemIndex + 4 < nextItemIndex && blockArraySpan[itemIndex + 4] == null) + { + return true; + } + + if (itemIndex + 5 < nextItemIndex && blockArraySpan[itemIndex + 5] == null) + { + return true; + } + + if (itemIndex + 6 < nextItemIndex && blockArraySpan[itemIndex + 6] == null) + { + return true; + } + + if (itemIndex + 7 < nextItemIndex && blockArraySpan[itemIndex + 7] == null) + { + return true; + } + + itemIndex += 8; + } } return false; - - //for (memoryBlockIndex = 0; memoryBlockIndex < lastBlockIndex; memoryBlockIndex++) - //{ - // //T? found = Array.Find(memoryBlocksSpan[memoryBlockIndex], x => comparer.Equals(x, item)); - // //if (found != null) - // //{ - // // return true; - // //} - - // blockArraySpan = new(memoryBlocksSpan[memoryBlockIndex]); - // for (itemIndex = 0; itemIndex < mainBlockSize; itemIndex++) - // { - // if (comparer.Equals(blockArraySpan[itemIndex], item)) - // { - // return true; - // } - // } - //} } public void CopyTo(T[] array, int arrayIndex) => _memoryBlocks.CopyTo(0, _blockSize, _longCount, array, arrayIndex); diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 7cff822..9a1fdc4 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -85,6 +85,53 @@ namespace Recyclable.CollectionsTests _ = list.LongCount.Should().Be(0); } + [Fact] + public void ContainsShouldFindAllItems() + { + // Prepare + var testData = _testData; + using var list = NewRecyclableList; + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Act + foreach (var item in testData) + { + // Validate + _ = list.Contains(item).Should().BeTrue(); + } + } + + [Fact] + public void ContainsShouldNotFindNonExistingItems() + { + // Prepare + var testData = _testData; + using var list = NewRecyclableList; + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Act + foreach (var item in testData) + { + // Validate + _ = list.Contains(-item).Should().BeFalse(); + } + + // Validate + _ = list.Contains(0).Should().BeFalse(); + } + + [Fact] + public void ContainsShouldNotFindAnythingWhenListEmpty() + { + // Prepare + using var list = new RecyclableList(); + + // Validate + _ = list.Contains(0).Should().BeFalse(); + _ = list.Contains(1).Should().BeFalse(); + } + + [Fact] public void CopyToShouldCopyAllItems() {