diff --git a/src/benchmarks/micro/MicroBenchmarks.csproj b/src/benchmarks/micro/MicroBenchmarks.csproj index 56be2af97cc..86dc94605ba 100644 --- a/src/benchmarks/micro/MicroBenchmarks.csproj +++ b/src/benchmarks/micro/MicroBenchmarks.csproj @@ -111,7 +111,9 @@ + + diff --git a/src/benchmarks/micro/libraries/Common/AlignedMemory.cs b/src/benchmarks/micro/libraries/Common/AlignedMemory.cs new file mode 100644 index 00000000000..a7188984785 --- /dev/null +++ b/src/benchmarks/micro/libraries/Common/AlignedMemory.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Memory; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System +{ + internal sealed class AlignedMemory : MemoryManager + { + private bool _disposed; + private int _refCount; + private IntPtr _memory; + private int _length; + + private unsafe AlignedMemory(void* memory, int length) + { + _memory = (IntPtr)memory; + _length = length; + } + + public static unsafe AlignedMemory Allocate(nuint length, nuint alignment) + => new AlignedMemory(NativeMemory.AlignedAlloc(length, alignment), (int)length); + + public bool IsDisposed => _disposed; + + public unsafe override System.Span GetSpan() => new System.Span((void*)_memory, _length); + + public override MemoryHandle Pin(int elementIndex = 0) + { + unsafe + { + Retain(); + if ((uint)elementIndex > _length) throw new ArgumentOutOfRangeException(nameof(elementIndex)); + void* pointer = Unsafe.Add((void*)_memory, elementIndex); + return new MemoryHandle(pointer, default, this); + } + } + + private bool Release() + { + int newRefCount = Interlocked.Decrement(ref _refCount); + + if (newRefCount < 0) + { + throw new InvalidOperationException("Unmatched Release/Retain"); + } + + return newRefCount != 0; + } + + private void Retain() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(AlignedMemory)); + } + + Interlocked.Increment(ref _refCount); + } + + protected override unsafe void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + NativeMemory.AlignedFree(_memory.ToPointer()); + _disposed = true; + } + + protected override bool TryGetArray(out ArraySegment arraySegment) + { + // cannot expose managed array + arraySegment = default; + return false; + } + + public override void Unpin() => Release(); + } +} \ No newline at end of file diff --git a/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.RandomAccess.NoBuffering.cs b/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.RandomAccess.NoBuffering.cs new file mode 100644 index 00000000000..f3d9a2c0961 --- /dev/null +++ b/src/benchmarks/micro/libraries/System.IO.FileSystem/Perf.RandomAccess.NoBuffering.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Extensions; +using MicroBenchmarks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO.Tests +{ + [BenchmarkCategory(Categories.Libraries, Categories.NoWASM)] + [OperatingSystemsFilter(allowed: true, platforms: OS.Windows)] // NO_BUFFERING is supported only on Windows: https://github.com/dotnet/runtime/issues/27408 + public class Perf_RandomAccess_NoBuffering + { + private const int OneKibibyte = 1 << 10; // 1024 + private const int OneMibibyte = OneKibibyte << 10; + private const int HundredMibibytes = OneMibibyte * 100; + private const FileOptions NoBuffering = (FileOptions)0x20000000; + private const FileOptions AsyncNoBuffering = FileOptions.Asynchronous | NoBuffering; + + static int PageSize = Environment.SystemPageSize; + + private Dictionary _sourceFilePaths, _destinationFilePaths; + private Dictionary _countToAlignedMemory; + private Dictionary[]> _countToMemory; + private Dictionary[]> _countToReadOnlyMemory; + + private void Setup(params long[] fileSizes) + { + _countToAlignedMemory = new Dictionary() + { + { 4, Enumerable.Range(0, 4).Select(_ => AlignedMemory.Allocate((nuint)PageSize, (nuint)PageSize)).ToArray() }, + { 16, Enumerable.Range(0, 16).Select(_ => AlignedMemory.Allocate((nuint)PageSize, (nuint)PageSize)).ToArray() }, + }; + _countToMemory = _countToAlignedMemory.ToDictionary(pair => pair.Key, pair => pair.Value.Select(aligned => aligned.Memory).ToArray()); + _countToReadOnlyMemory = _countToAlignedMemory.ToDictionary(pair => pair.Key, pair => pair.Value.Select(aligned => (System.ReadOnlyMemory)aligned.Memory).ToArray()); + + _sourceFilePaths = fileSizes.ToDictionary(size => size, size => CreateFileWithRandomContent(size)); + _destinationFilePaths = fileSizes.ToDictionary(size => size, size => CreateFileWithRandomContent(size)); + + static string CreateFileWithRandomContent(long fileSize) + { + string filePath = FileUtils.GetTestFilePath(); + File.WriteAllBytes(filePath, ValuesGenerator.Array((int)fileSize)); + return filePath; + } + } + + [GlobalCleanup] + public void Cleanup() + { + foreach (string filePath in _sourceFilePaths.Values.Concat(_destinationFilePaths.Values)) + { + File.Delete(filePath); + } + foreach (IDisposable alignedMemory in _countToAlignedMemory.Values.SelectMany(buffers => buffers)) + { + alignedMemory.Dispose(); + } + } + + [GlobalSetup(Targets = new[] { nameof(ReadScatterAsync), nameof(WriteGatherAsync) })] + public void SetupBigFileBenchmarks() => Setup(OneMibibyte, HundredMibibytes); + + public IEnumerable ReadWrite_MultipleBuffers_Arguments() + { + yield return new object[] { OneMibibyte, 4 }; // medium size file, 4xpage size user buffers + yield return new object[] { OneMibibyte, 16 }; // medium size file, 16xpage size user buffers + yield return new object[] { HundredMibibytes, 4 }; // big file, 4xpage size user buffers + yield return new object[] { HundredMibibytes, 16 }; // big file, 16xpage size user buffers + } + + [Benchmark] + [ArgumentsSource(nameof(ReadWrite_MultipleBuffers_Arguments))] + public async Task ReadScatterAsync(long fileSize, int count) + { + CancellationToken cancellationToken = CancellationToken.None; + System.Memory[] buffers = _countToMemory[count]; + long bytesRead = 0; + using (SafeFileHandle fileHandle = File.OpenHandle(_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, AsyncNoBuffering)) + { + while (bytesRead < fileSize) + { + bytesRead += await RandomAccess.ReadAsync(fileHandle, buffers, bytesRead, cancellationToken); + } + } + + return bytesRead; + } + + [Benchmark] + [ArgumentsSource(nameof(ReadWrite_MultipleBuffers_Arguments))] + public async Task WriteGatherAsync(long fileSize, int count) + { + CancellationToken cancellationToken = CancellationToken.None; + System.ReadOnlyMemory[] buffers = _countToReadOnlyMemory[count]; + long buffersSize = count * PageSize; + using (SafeFileHandle fileHandle = File.OpenHandle(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, AsyncNoBuffering)) + { + long bytesWritten = 0; + for (int i = 0; i < fileSize / buffersSize; i++) + { + await RandomAccess.WriteAsync(fileHandle, buffers, bytesWritten, cancellationToken); + bytesWritten += buffersSize; + } + } + } + } +} \ No newline at end of file