Skip to content

Conversation

@grisha-kotler
Copy link
Member

@grisha-kotler grisha-kotler commented Aug 4, 2025

Consists of 2 changes:

if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length));

if (length * sizeof(T) > LOH_THRESHOLD)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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);

@grisha-kotler grisha-kotler force-pushed the RavenDB-24810 branch 3 times, most recently from 3e602e2 to c90f8ff Compare August 5, 2025 14:24

if (length * sizeof(T) > LOH_THRESHOLD)
{
_ptr = UnmanagedStringArray.Segment.AllocateMemory(_elementSize * _length, type);
Copy link
Member

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

Copy link
Member Author

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.

if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length));

if (length * sizeof(T) > LOH_THRESHOLD)
Copy link
Member

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.

using (_indexPointers)
using (_termInfos)
{
OnArrayHolderDisposed?.Invoke(_managedAllocations);
Copy link
Member

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)

Copy link
Member Author

@grisha-kotler grisha-kotler Oct 5, 2025

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.

Copy link
Member

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(() =>
Copy link
Member

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.

Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@arekpalinski arekpalinski merged commit f37cf97 into ravendb:ravendb/v5.4 Oct 21, 2025
2 checks passed
@grisha-kotler grisha-kotler changed the title RavenDB-24810 & RavenDB-24815 [5.4] - RavenDB-24810 & RavenDB-24815 Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants