diff --git a/csharp/Platform.Unsafe.Benchmarks/MemoryBlockBenchmarks.cs b/csharp/Platform.Unsafe.Benchmarks/MemoryBlockBenchmarks.cs index f86bcd2..afdc4e6 100644 --- a/csharp/Platform.Unsafe.Benchmarks/MemoryBlockBenchmarks.cs +++ b/csharp/Platform.Unsafe.Benchmarks/MemoryBlockBenchmarks.cs @@ -44,5 +44,8 @@ public void MemoryBlockZero() MemoryBlock.Zero(pointer, _array.Length); } } + + [Benchmark] + public void GetMemoryChannelCount() => _ = MemoryBlock.MemoryChannelCount; } } diff --git a/csharp/Platform.Unsafe.Tests/MemoryChannelTests.cs b/csharp/Platform.Unsafe.Tests/MemoryChannelTests.cs new file mode 100644 index 0000000..58bc4bc --- /dev/null +++ b/csharp/Platform.Unsafe.Tests/MemoryChannelTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; +using Xunit; + +namespace Platform.Unsafe.Tests +{ + public static class MemoryChannelTests + { + [Fact] + public static void MemoryChannelCountIsValid() + { + // The detected memory channel count should be at least 1 and reasonable + var channelCount = MemoryBlock.MemoryChannelCount; + + Assert.True(channelCount >= 1, $"Memory channel count should be at least 1, got {channelCount}"); + Assert.True(channelCount <= 16, $"Memory channel count seems unreasonable, got {channelCount}"); + } + + [Fact] + public static void MemoryChannelCountIsConsistent() + { + // The detected memory channel count should be consistent across multiple calls + var channelCount1 = MemoryBlock.MemoryChannelCount; + var channelCount2 = MemoryBlock.MemoryChannelCount; + + Assert.Equal(channelCount1, channelCount2); + } + + [Fact] + public static void MemoryChannelCountRespectsProcessorLimit() + { + // The memory channel count should not exceed max(1, Environment.ProcessorCount / 2) + var channelCount = MemoryBlock.MemoryChannelCount; + var maxExpectedChannels = Math.Max(1, Environment.ProcessorCount / 2); + + Assert.True(channelCount <= maxExpectedChannels, + $"Memory channel count ({channelCount}) should not exceed max(1, ProcessorCount/2) ({maxExpectedChannels})"); + } + + [Fact] + public static void ZeroMemoryWithDetectedChannels() + { + // Test that memory zeroing works correctly with the detected channel count + var bytes = new byte[4096]; // Larger buffer to benefit from parallelization + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i % 256)); + } + + unsafe + { + fixed (byte* pointer = bytes) + { + MemoryBlock.Zero(pointer, bytes.Length); + } + } + + for (int i = 0; i < bytes.Length; i++) + { + Assert.Equal(0, bytes[i]); + } + } + + [Fact] + public static void DefaultChannelCountForNonWindows() + { + // On non-Windows platforms, we should get the default behavior + // This is more of a behavioral test since we can't easily mock the platform + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var channelCount = MemoryBlock.MemoryChannelCount; + var maxExpectedChannels = Math.Max(1, Environment.ProcessorCount / 2); + // Should fall back to default behavior, but respect processor limits + Assert.True(channelCount >= 1 && channelCount <= maxExpectedChannels, + $"Non-Windows channel count ({channelCount}) should be between 1 and {maxExpectedChannels}"); + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Unsafe/MemoryBlock.cs b/csharp/Platform.Unsafe/MemoryBlock.cs index 05862e6..86d1317 100644 --- a/csharp/Platform.Unsafe/MemoryBlock.cs +++ b/csharp/Platform.Unsafe/MemoryBlock.cs @@ -4,6 +4,9 @@ using System.Threading.Tasks; using static System.Runtime.CompilerServices.Unsafe; +using System.Management; +using System.Runtime.InteropServices; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace Platform.Unsafe @@ -14,6 +17,76 @@ namespace Platform.Unsafe /// public static unsafe class MemoryBlock { + private static readonly Lazy _memoryChannelCount = new(() => DetectMemoryChannelCount()); + + /// + /// Gets the number of memory channels available on the current system. + /// Получает количество каналов памяти, доступных в текущей системе. + /// + public static int MemoryChannelCount => _memoryChannelCount.Value; + + private static int DetectMemoryChannelCount() + { + try + { + // Try to detect memory channels on Windows using WMI + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return DetectMemoryChannelCountWindows(); + } + } + catch + { + // Fall through to default behavior if detection fails + } + + // Default to 2 channels (dual-channel memory is most common) + // But respect the processor count limitation to avoid wasting resources + var defaultChannels = 2; + var maxThreads = Math.Max(1, Environment.ProcessorCount / 2); + return Math.Min(defaultChannels, maxThreads); + } + + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private static int DetectMemoryChannelCountWindows() + { + using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMemory"); + using var results = searcher.Get(); + + var interleaveDataDepth = 0; + var memoryDeviceCount = 0; + + foreach (ManagementObject obj in results) + { + memoryDeviceCount++; + + // Try to get InterleaveDataDepth property + var depth = obj["InterleaveDataDepth"]; + if (depth != null && depth is uint depthValue && depthValue > 0) + { + interleaveDataDepth = Math.Max(interleaveDataDepth, (int)depthValue); + } + } + + var maxThreads = Math.Max(1, Environment.ProcessorCount / 2); + + // If we found InterleaveDataDepth, use it + if (interleaveDataDepth > 0) + { + return Math.Min(interleaveDataDepth, maxThreads); + } + + // Fallback: estimate based on memory device count + // Common configurations: 2 DIMMs = dual channel, 4 DIMMs = quad channel + if (memoryDeviceCount >= 4) + { + return Math.Min(4, maxThreads); + } + + // Default to dual channel, but respect processor limits + return Math.Min(2, maxThreads); + } + /// /// Zeroes the number of bytes specified in starting from . /// Обнуляет количество байтов, указанное в , начиная с . @@ -27,16 +100,16 @@ public static unsafe class MemoryBlock public static void Zero(void* pointer, long capacity) { // A way to prevent wasting resources due to Hyper-Threading. - var threads = Environment.ProcessorCount / 2; - if (threads <= 1) + var maxThreads = Environment.ProcessorCount / 2; + if (maxThreads <= 1) { ZeroBlock(pointer, 0, capacity); } else { - // Using 2 threads because two-channel memory architecture is the most available type. - // CPUs mostly just wait for memory here. - threads = 2; + // Use detected memory channel count, but cap it by processor count / 2 + // CPUs mostly just wait for memory here, so we optimize for memory bandwidth. + var threads = Math.Min(MemoryChannelCount, maxThreads); Parallel.ForEach(Partitioner.Create(0L, capacity), new ParallelOptions { MaxDegreeOfParallelism = threads }, range => ZeroBlock(pointer, range.Item1, range.Item2)); } } diff --git a/csharp/Platform.Unsafe/Platform.Unsafe.csproj b/csharp/Platform.Unsafe/Platform.Unsafe.csproj index 9828835..7b7c7a5 100644 --- a/csharp/Platform.Unsafe/Platform.Unsafe.csproj +++ b/csharp/Platform.Unsafe/Platform.Unsafe.csproj @@ -39,6 +39,7 @@ +