|
|
|
@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Memory; |
|
|
|
public abstract class MemoryAllocator |
|
|
|
{ |
|
|
|
private const int OneGigabyte = 1 << 30; |
|
|
|
private long accumulativeAllocatedBytes; |
|
|
|
private int trackingSuppressionCount; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
|
|
|
|
@ -23,9 +25,41 @@ public abstract class MemoryAllocator |
|
|
|
/// </summary>
|
|
|
|
public static MemoryAllocator Default { get; } = Create(); |
|
|
|
|
|
|
|
internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; |
|
|
|
/// <summary>
|
|
|
|
/// Gets the maximum number of bytes that can be allocated by a memory group.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
|
|
|
|
/// 1 GB for 32-bit processes.
|
|
|
|
/// </remarks>
|
|
|
|
internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the maximum allowed total allocation size, in bytes, for the current process.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// The allocation limit is determined based on the process architecture. For 64-bit processes,
|
|
|
|
/// the limit is higher than for 32-bit processes.
|
|
|
|
/// </remarks>
|
|
|
|
internal long AccumulativeAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 8L * OneGigabyte : 2L * OneGigabyte; |
|
|
|
|
|
|
|
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte; |
|
|
|
/// <summary>
|
|
|
|
/// Gets the maximum size, in bytes, that can be allocated for a single buffer.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// The single buffer allocation limit is set to 1 GB by default.
|
|
|
|
/// </remarks>
|
|
|
|
internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets a value indicating whether change tracking is currently suppressed for this instance.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// When change tracking is suppressed, modifications to the object will not be recorded or
|
|
|
|
/// trigger change notifications. This property is used internally to temporarily disable tracking during
|
|
|
|
/// batch updates or initialization.
|
|
|
|
/// </remarks>
|
|
|
|
private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
|
|
|
|
@ -53,6 +87,11 @@ public abstract class MemoryAllocator |
|
|
|
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes); |
|
|
|
} |
|
|
|
|
|
|
|
if (options.AccumulativeAllocationLimitMegabytes.HasValue) |
|
|
|
{ |
|
|
|
allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L; |
|
|
|
} |
|
|
|
|
|
|
|
return allocator; |
|
|
|
} |
|
|
|
|
|
|
|
@ -72,6 +111,10 @@ public abstract class MemoryAllocator |
|
|
|
/// Releases all retained resources not being in use.
|
|
|
|
/// Eg: by resetting array pools and letting GC to free the arrays.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// This does not dispose active allocations; callers are responsible for disposing all
|
|
|
|
/// <see cref="IMemoryOwner{T}"/> instances to release memory.
|
|
|
|
/// </remarks>
|
|
|
|
public virtual void ReleaseRetainedResources() |
|
|
|
{ |
|
|
|
} |
|
|
|
@ -102,11 +145,137 @@ public abstract class MemoryAllocator |
|
|
|
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes); |
|
|
|
} |
|
|
|
|
|
|
|
// Cast to long is safe because we already checked that the total length is within the limit.
|
|
|
|
return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options); |
|
|
|
long totalLengthInBytesLong = (long)totalLengthInBytes; |
|
|
|
this.ReserveAllocation(totalLengthInBytesLong); |
|
|
|
|
|
|
|
using (this.SuppressTracking()) |
|
|
|
{ |
|
|
|
try |
|
|
|
{ |
|
|
|
MemoryGroup<T> group = this.AllocateGroupCore<T>(totalLength, totalLengthInBytesLong, bufferAlignment, options); |
|
|
|
group.SetAllocationTracking(this, totalLengthInBytesLong); |
|
|
|
return group; |
|
|
|
} |
|
|
|
catch |
|
|
|
{ |
|
|
|
this.ReleaseAccumulatedBytes(totalLengthInBytesLong); |
|
|
|
throw; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options) |
|
|
|
where T : struct |
|
|
|
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Tracks the allocation of an <see cref="IMemoryOwner{T}" /> instance after reserving bytes.
|
|
|
|
/// </summary>
|
|
|
|
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
|
|
|
|
/// <param name="owner">The allocation to track.</param>
|
|
|
|
/// <param name="lengthInBytes">The allocation size in bytes.</param>
|
|
|
|
/// <returns>The tracked allocation.</returns>
|
|
|
|
protected IMemoryOwner<T> TrackAllocation<T>(IMemoryOwner<T> owner, ulong lengthInBytes) |
|
|
|
where T : struct |
|
|
|
{ |
|
|
|
if (this.IsTrackingSuppressed || lengthInBytes == 0) |
|
|
|
{ |
|
|
|
return owner; |
|
|
|
} |
|
|
|
|
|
|
|
return new TrackingMemoryOwner<T>(owner, this, (long)lengthInBytes); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reserves accumulative allocation bytes before creating the underlying buffer.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lengthInBytes">The number of bytes to reserve.</param>
|
|
|
|
protected void ReserveAllocation(long lengthInBytes) |
|
|
|
{ |
|
|
|
if (this.IsTrackingSuppressed || lengthInBytes <= 0) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes); |
|
|
|
if (total > this.AccumulativeAllocationLimitBytes) |
|
|
|
{ |
|
|
|
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); |
|
|
|
InvalidMemoryOperationException.ThrowAllocationOverLimitException((ulong)lengthInBytes, this.AccumulativeAllocationLimitBytes); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Releases accumulative allocation bytes previously tracked by this allocator.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="lengthInBytes">The number of bytes to release.</param>
|
|
|
|
internal void ReleaseAccumulatedBytes(long lengthInBytes) |
|
|
|
{ |
|
|
|
if (lengthInBytes <= 0) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>An <see cref="IDisposable"/> that restores tracking when disposed.</returns>
|
|
|
|
internal IDisposable SuppressTracking() => new TrackingSuppressionScope(this); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Temporarily suppresses accumulative allocation tracking within a scope.
|
|
|
|
/// </summary>
|
|
|
|
private sealed class TrackingSuppressionScope : IDisposable |
|
|
|
{ |
|
|
|
private MemoryAllocator? allocator; |
|
|
|
|
|
|
|
public TrackingSuppressionScope(MemoryAllocator allocator) |
|
|
|
{ |
|
|
|
this.allocator = allocator; |
|
|
|
_ = Interlocked.Increment(ref allocator.trackingSuppressionCount); |
|
|
|
} |
|
|
|
|
|
|
|
public void Dispose() |
|
|
|
{ |
|
|
|
if (this.allocator != null) |
|
|
|
{ |
|
|
|
_ = Interlocked.Decrement(ref this.allocator.trackingSuppressionCount); |
|
|
|
this.allocator = null; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Wraps an <see cref="IMemoryOwner{T}"/> to release accumulative tracking on dispose.
|
|
|
|
/// </summary>
|
|
|
|
private sealed class TrackingMemoryOwner<T> : IMemoryOwner<T> |
|
|
|
where T : struct |
|
|
|
{ |
|
|
|
private IMemoryOwner<T>? owner; |
|
|
|
private readonly MemoryAllocator allocator; |
|
|
|
private readonly long lengthInBytes; |
|
|
|
|
|
|
|
public TrackingMemoryOwner(IMemoryOwner<T> owner, MemoryAllocator allocator, long lengthInBytes) |
|
|
|
{ |
|
|
|
this.owner = owner; |
|
|
|
this.allocator = allocator; |
|
|
|
this.lengthInBytes = lengthInBytes; |
|
|
|
} |
|
|
|
|
|
|
|
public Memory<T> Memory => this.owner?.Memory ?? Memory<T>.Empty; |
|
|
|
|
|
|
|
public void Dispose() |
|
|
|
{ |
|
|
|
// Ensure only one caller disposes the inner owner and releases the reservation.
|
|
|
|
IMemoryOwner<T>? inner = Interlocked.Exchange(ref this.owner, null); |
|
|
|
if (inner != null) |
|
|
|
{ |
|
|
|
inner.Dispose(); |
|
|
|
this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|