Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions csharp/Platform.Unsafe.Benchmarks/MemoryBlockBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ public void MemoryBlockZero()
MemoryBlock.Zero(pointer, _array.Length);
}
}

[Benchmark]
public void GetMemoryChannelCount() => _ = MemoryBlock.MemoryChannelCount;
}
}
79 changes: 79 additions & 0 deletions csharp/Platform.Unsafe.Tests/MemoryChannelTests.cs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
}
83 changes: 78 additions & 5 deletions csharp/Platform.Unsafe/MemoryBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +17,76 @@ namespace Platform.Unsafe
/// </summary>
public static unsafe class MemoryBlock
{
private static readonly Lazy<int> _memoryChannelCount = new(() => DetectMemoryChannelCount());

/// <summary>
/// <para>Gets the number of memory channels available on the current system.</para>
/// <para>Получает количество каналов памяти, доступных в текущей системе.</para>
/// </summary>
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);
}

/// <summary>
/// <para>Zeroes the number of bytes specified in <paramref name="capacity"/> starting from <paramref name="pointer"/>.</para>
/// <para>Обнуляет количество байтов, указанное в <paramref name="capacity"/>, начиная с <paramref name="pointer"/>.</para>
Expand All @@ -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));
}
}
Expand Down
1 change: 1 addition & 0 deletions csharp/Platform.Unsafe/Platform.Unsafe.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="Platform.Numbers" Version="0.9.0" />
<PackageReference Include="System.Management" Version="8.0.0" Condition="'$(TargetFramework)' == 'net8'" />
</ItemGroup>

</Project>
Loading