I have a very large list of integers (about 2 billion elements) and a list with indices (couple thousand elements) at which I need to remove elements from the first list. My current approach is to loop over all indices in the second list, passing each to the RemoveAt() method of the first list:
indices.Sort();
indices.Reverse();
for (i = 0; i < indices.Count; i++)
{
largeList.RemoveAt(indices[i]);
}
However, it takes about 2 minutes to finish. I really need to perform this operation much faster. Is there any way of optimizing this?
I have a Intel i9X CPU with 10 cores, so maybe some way of parallel processing?
Since the order of the source list is significant, you can move each item down the list, skipping indices to be removed, and then remove the end of the list.
UPDATE: Took the .Net Core source code for RemoveAll and modified it for a indice list instead of a predicate.
UPDATE 2: Optimized to not repeat tests if possible.
UPDATE 3: Simplified as optimization having extra code proved slower in benchmarks.
Having src as the large list, and removeAtList as the indices to remove in some random order, you can do:
removeAtList.Sort();
var srcCount = src.Count;
var ralCount = removeAtList.Count;
var removeAtIndice = 1;
var freeIndex = removeAtList[0];
var current = freeIndex+1;
while (current < srcCount) {
while (removeAtIndice < ralCount && current == removeAtList[removeAtIndice]) {
++current;
++removeAtIndice;
}
if (current < srcCount)
src[freeIndex++] = src[current++];
}
src.RemoveRange(freeIndex, srcCount-freeIndex);
For a one billion element list of random integers, and a 1000 - 3000 element list of random indices, I get 1.1 ms per remove with this algorithm. Using RemoveAt, I get over 232.77 ms per remove, so this is about 200 times faster.
One way to allow this to be parallelized would be to break the list into multiple fragments; perhaps initially (arbitrarily) separate slabs of 1 million elements. As long as each slab maintains its own count, you can split the work by index into removals from different slabs (based purely on the counts), and then do the actual removal work concurrently. If you leave some spare capacity in each, you can also add elements into the middle more cheaply, as you are usually only touching one slab. Random access will be a little slower, as you may need to look at multiple slab counts to determine the correct slab, but if the slab counts are maintained in a contiguous vector (rather than against each slab), you should have excellent memory cache hit while doing it.
When you have multiple items to remove from a List, and replacing the List with a new List is not an option, the most efficient way is to use the RemoveAll method instead of the RemoveAt. The RemoveAll rearranges the internal state of the List only once, instead of doing it once per each item removed.
The RemoveAll accepts a Predicate<T> that will be invoked once for each item in the list (the large list). Unfortunately this delegate doesn't receive the index of the currently tested item. You could however depend on knowing how the RemoveAll is implemented. The source code reveals that the items are tested sequentially in ascending order. So based on this knowledge you could remove the selected indices from the list, very efficiently, with this three-liner:
var indicesSet = new HashSet<int>(indices);
int index = 0;
largeList.RemoveAll(_ => indicesSet.Contains(index++));
But you really shouldn't. This solution will break horribly if a future version of .NET comes with a different internal implementation of the RemoveAll. So consider this to be a dirty hack, and not a production-quality solution to the problem.
The method List.RemoveAt copy all next items from the removed item.
In your case, this copy 2,000 * 2,000,000,000 times each items (not really, but the true is near).
A solution is manually copy item between the removed item and the next removed item :
static void Main(string[] args)
{
var largeList = Enumerable.Range(0, 2_000_000_000).ToList();
var indices = new List<int>();
var rand = new Random();
for (var i = 0; i < 20000; i++)
{
indices.Add(rand.Next(0, largeList.Count - 1));
}
indices.Sort();
var watch = new Stopwatch();
watch.Start();
// You can convert the list to array with ToArray,
// but this duplicate the memory use.
// Or get the internal array by reflection,
// but reflection on external library isn't recommended
var largeArray = (int[])typeof(List<int>)
.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(largeList);
var current = 0;
var copyFrom = 0;
for (var i = 0; i < indices.Count; i++)
{
var copyTo = indices[i];
if (copyTo < copyFrom)
{
//In case the indice is duplicate,
//The item is already passed
continue;
}
var copyLength = copyTo - copyFrom;
Array.Copy(largeArray, copyFrom, largeArray, current, copyLength);
current += copyLength;
copyFrom = copyTo + 1;
}
//Resize the internal array
largeList.RemoveRange(largeList.Count - indices.Count, indices.Count);
watch.Stop();
Console.WriteLine(watch.Elapsed);
Console.WriteLine(largeList.Count);
}
This answer is based on other answers here - mainly, I am shifting elements up within the list, as suggested by @Vernou (in their answer) and @BACON (in comments). This one is finally performant (unlike my first few approaches), and is faster than other solutions posted so far, at least in my tests - I tried OP's setup of 2_000_000_000 entries and 2_000 indicies - runtime is under 10 seconds on my laptop (i7-8550U @ 1.8GHz, 16GB RAM):
static void FilterOutIndicies(List<int> values, List<int> sortedIndicies)
{
int sourceStartIndex = 0;
int destStartIndex = 0;
int spanLength = 0;
int skipCount = 0;
// Copy items up to last index to be skipped
foreach (var skipIndex in sortedIndicies)
{
spanLength = skipIndex - sourceStartIndex;
destStartIndex = sourceStartIndex - skipCount;
for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
{
values[destStartIndex] = values[i];
destStartIndex++;
}
sourceStartIndex = skipIndex + 1;
skipCount++;
}
// Copy remaining items (between last index to be skipped and end of list)
spanLength = values.Count - sourceStartIndex;
destStartIndex = sourceStartIndex - skipCount;
for (int i = sourceStartIndex; i < sourceStartIndex + spanLength; i++)
{
values[destStartIndex] = values[i];
destStartIndex++;
}
values.RemoveRange(destStartIndex, sortedIndicies.Count);
}