-
Notifications
You must be signed in to change notification settings - Fork 9
[5.4] - RavenDB-24810 & RavenDB-24815 #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5.4] - RavenDB-24810 & RavenDB-24815 #77
Conversation
…y and use the the unmanaged array for it
src/Lucene.Net/Util/HybridArray.cs
Outdated
| if (length < 0) | ||
| throw new ArgumentOutOfRangeException(nameof(length)); | ||
|
|
||
| if (length * sizeof(T) > LOH_THRESHOLD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't make everything unmanaged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using the ArrayPool will be more cost-effective for small allocations. For long arrays, using this threshold will accommodate 8,192 elements, while for TermInfo (which is 32 bytes), it will contain up to 2,048 elements. Smaller segments with fewer cached terms are more likely to be merged frequently than larger segments, which creates more allocation and deallocation cycles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that you looking at memory only, but the virtual method calls, etc are also costly.
why not do everything in unmanaged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the virtual method calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And you added two branches for each index? And if statemen ton each op.
Why is that something that we want to do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sth that we can check, but in general I think that calling AllocHGlobal and FreeHGlobal for each of those each time will have a much bigger penalty. Smaller allocations should be managed and come from a pool IMO in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying data between managed and unmanaged memory isn't for free. Also we have more context here as @grisha-kotler pointed in this first answer. So for me it seems reasonable to use unmanaged only for bigger allocations (since we're trying to solve LOH compaction issue).
Regarding additional if making two branches, sometimes we tried to optimize code to make it branchless but not always it resulted in better performance.
I think the decision should be made based on perf testing results instead of relaying on intuition. It's too important code path. Note that it will require quite comprehensive testing, with variety of different scenarios.
| private class AnonymousClassDocValues:DocValues | ||
| { | ||
| public AnonymousClassDocValues(int end, int[] arr, ReverseOrdFieldSource enclosingInstance) | ||
| public AnonymousClassDocValues(int end, IArray<int> arr, ReverseOrdFieldSource enclosingInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could avoid boxing here by using TArray where TArray : IArray<int>
and during construction:
if (arr.GetType() == typeof(ManagedArray<int>))
return new AnonymousClassDocValues<ManagedArray<int>>(end, (ManagedArray<int>)arr, this);
return new AnonymousClassDocValues<UnmanagedArray<int>>(end, (UnmanagedArray<int>)arr, this);3e602e2 to
c90f8ff
Compare
src/Lucene.Net/Util/HybridArray.cs
Outdated
|
|
||
| if (length * sizeof(T) > LOH_THRESHOLD) | ||
| { | ||
| _ptr = UnmanagedStringArray.Segment.AllocateMemory(_elementSize * _length, type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While agree this is good and beneficial to move it to unmanaged memory, I'd like to make a note here for us to consider. I think this is big change for us which potentially might be source of different problems.
I'm thinking about potential issues related to:
- unmanaged heap fragmentation
- long lived unmanaged allocations - since it's going to be kept by a cache
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've introduced a UseOnlyManagedArray flag to give us precise control over this behavior and mitigate the risks.
For 32-bit: You are absolutely right. Unmanaged heap fragmentation can become a serious issue due to the limited virtual address space. To prevent this, we will set UseOnlyManagedArray = true in our 32-bit builds, completely avoiding unmanaged allocations and the associated risks.
For 64-bit: This is far less of a concern. The vast virtual address space in 64-bit processes makes it highly unlikely for fragmentation to cause allocation failures. The performance benefits of avoiding the LOH outweigh the negligible risk of fragmentation here.
Regading the long-lived unmanaged allocations, this is a deliberate trade-off. While these allocations might be long-lived because they are held by a cache, it is preferable to have them in the unmanaged heap rather than the LOH.
src/Lucene.Net/Util/HybridArray.cs
Outdated
| if (length < 0) | ||
| throw new ArgumentOutOfRangeException(nameof(length)); | ||
|
|
||
| if (length * sizeof(T) > LOH_THRESHOLD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying data between managed and unmanaged memory isn't for free. Also we have more context here as @grisha-kotler pointed in this first answer. So for me it seems reasonable to use unmanaged only for bigger allocations (since we're trying to solve LOH compaction issue).
Regarding additional if making two branches, sometimes we tried to optimize code to make it branchless but not always it resulted in better performance.
I think the decision should be made based on perf testing results instead of relaying on intuition. It's too important code path. Note that it will require quite comprehensive testing, with variety of different scenarios.
src/Lucene.Net/Index/ArrayHolder.cs
Outdated
| using (_indexPointers) | ||
| using (_termInfos) | ||
| { | ||
| OnArrayHolderDisposed?.Invoke(_managedAllocations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously we called it before actually releasing other resources but now we do it before. It that okay? (just sanity check, I don't know who uses OnArrayHolderDisposed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OnArrayHolderDisposed is used for monitor the managed memory usage.
I moved it after the disposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
| // We'll return the old StringIndexCache and let the caller decide if he wants to dispose it sooner | ||
|
|
||
| var copy = _stringIndexCache; | ||
| var disposable = new DisposableAction(() => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just pass _stringIndexCache, _longCache, _doubleCache and dispose them explicitly there? So this way we won't need to allocate and use Action.
DisposableAction is private so we can make it's implementation more specific to what we deal with here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant here was to have DisposableAction(StringIndexCache stringIndexCache, LongCache longCache, DoubleCache doubleCache) instead of DisposableAction(Action action) so we don't need to do the allocation of Action
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
…use only the ManagedArray
…nlined helper method.
…dispose them explicitly
c90f8ff to
00c6627
Compare
Consists of 2 changes:
High usage of LOH memory - Replace arrays used in the term cache and for sorting that exceed
64KBwith unmanaged arrays. This reduces the LOH size significantly. Although this cache is long-lived, it can be disposed of when no longer needed, which would leave gaps in the LOH and cause fragmentation. Moving to unmanaged memory eliminates this fragmentation risk entirely.https://issues.hibernatingrhinos.com/issue/RavenDB-24810/High-usage-of-LOH-memory-in-Lucene
Reduce object reference overhead - Store field numbers (int) in arrays instead of field name strings. This reduces the number of objects that the GC needs to traverse, thereby significantly reducing Gen 2 GC time.
https://issues.hibernatingrhinos.com/issue/RavenDB-24815/Large-reference-graph-for-arrays-of-strings