diff --git a/csharp/Platform.Unsafe.Tests/ZeroMemoryTests.cs b/csharp/Platform.Unsafe.Tests/ZeroMemoryTests.cs index 7976eb4..fb2dfa2 100644 --- a/csharp/Platform.Unsafe.Tests/ZeroMemoryTests.cs +++ b/csharp/Platform.Unsafe.Tests/ZeroMemoryTests.cs @@ -1,3 +1,4 @@ +using System; using Xunit; namespace Platform.Unsafe.Tests @@ -21,5 +22,26 @@ public static void ZeroMemoryTest() Assert.Equal(0, bytes[i]); } } + + [Fact] + public static void PhysicalCoreCountTest() + { + // Test that physical core count is reasonable + var physicalCores = MemoryBlock.PhysicalCoreCount; + var logicalProcessors = Environment.ProcessorCount; + + // Physical cores should be at least 1 + Assert.True(physicalCores >= 1, $"Physical cores should be at least 1, got {physicalCores}"); + + // Physical cores should not exceed logical processors + Assert.True(physicalCores <= logicalProcessors, + $"Physical cores ({physicalCores}) should not exceed logical processors ({logicalProcessors})"); + + // On most systems, physical cores should be at least half of logical processors + // (allowing for hyper-threading) + var expectedMinimum = Math.Max(1, logicalProcessors / 2); + Assert.True(physicalCores >= expectedMinimum, + $"Physical cores ({physicalCores}) should be at least {expectedMinimum}"); + } } } diff --git a/csharp/Platform.Unsafe/MemoryBlock.cs b/csharp/Platform.Unsafe/MemoryBlock.cs index 05862e6..ec1c5d8 100644 --- a/csharp/Platform.Unsafe/MemoryBlock.cs +++ b/csharp/Platform.Unsafe/MemoryBlock.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using static System.Runtime.CompilerServices.Unsafe; @@ -14,6 +15,13 @@ namespace Platform.Unsafe /// public static unsafe class MemoryBlock { + private static readonly Lazy _physicalCoreCount = new(() => GetPhysicalCoreCount()); + + /// + /// Gets the number of physical CPU cores. + /// Получает количество физических ядер ЦП. + /// + public static int PhysicalCoreCount => _physicalCoreCount.Value; /// /// Zeroes the number of bytes specified in starting from . /// Обнуляет количество байтов, указанное в , начиная с . @@ -26,17 +34,17 @@ public static unsafe class MemoryBlock [MethodImpl(MethodImplOptions.AggressiveInlining)] 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 physicalCores = PhysicalCoreCount; + if (physicalCores <= 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; + // For memory operations, optimal thread count is typically min(physical_cores, memory_channels). + // Most systems have dual-channel memory, so we limit to 2 threads for optimal memory bandwidth utilization. + // More threads would compete for memory bandwidth without providing benefits. + var threads = Math.Min(physicalCores, 2); Parallel.ForEach(Partitioner.Create(0L, capacity), new ParallelOptions { MaxDegreeOfParallelism = threads }, range => ZeroBlock(pointer, range.Item1, range.Item2)); } } @@ -55,5 +63,126 @@ private static void ZeroBlock(void* pointer, long from, long to) } InitBlock(offset, 0, unchecked((uint)length)); } + + private static int GetPhysicalCoreCount() + { + try + { + // Try platform-specific detection first + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return GetPhysicalCoreCountWindows(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return GetPhysicalCoreCountLinux(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return GetPhysicalCoreCountMacOS(); + } + } + catch + { + // If platform-specific detection fails, fall back to the original approach + } + + // Fallback: assume hyper-threading and divide by 2 + // This maintains backward compatibility with the original behavior + return Math.Max(1, Environment.ProcessorCount / 2); + } + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Method is only called on Windows platform as checked by caller")] + private static int GetPhysicalCoreCountWindows() + { + // Use WMI to get actual physical core count on Windows + try + { + using var searcher = new System.Management.ManagementObjectSearcher("SELECT NumberOfCores FROM Win32_Processor"); + int totalCores = 0; + foreach (System.Management.ManagementObject obj in searcher.Get()) + { + totalCores += (int)(uint)obj["NumberOfCores"]; + } + return totalCores > 0 ? totalCores : Math.Max(1, Environment.ProcessorCount / 2); + } + catch + { + return Math.Max(1, Environment.ProcessorCount / 2); + } + } + + private static int GetPhysicalCoreCountLinux() + { + // Parse /proc/cpuinfo to get physical core count on Linux + try + { + var cpuInfo = System.IO.File.ReadAllText("/proc/cpuinfo"); + var lines = cpuInfo.Split('\n'); + var physicalIds = new System.Collections.Generic.HashSet(); + int coresPerPhysicalCpu = 1; + + foreach (var line in lines) + { + if (line.StartsWith("physical id")) + { + var parts = line.Split(':'); + if (parts.Length > 1) + { + physicalIds.Add(parts[1].Trim()); + } + } + else if (line.StartsWith("cpu cores")) + { + var parts = line.Split(':'); + if (parts.Length > 1 && int.TryParse(parts[1].Trim(), out int cores)) + { + coresPerPhysicalCpu = cores; + } + } + } + + var physicalCoreCount = physicalIds.Count * coresPerPhysicalCpu; + return physicalCoreCount > 0 ? physicalCoreCount : Math.Max(1, Environment.ProcessorCount / 2); + } + catch + { + return Math.Max(1, Environment.ProcessorCount / 2); + } + } + + private static int GetPhysicalCoreCountMacOS() + { + // On macOS, use sysctl to get physical core count + try + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "sysctl", + Arguments = "-n hw.physicalcpu", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (int.TryParse(output, out int physicalCores) && physicalCores > 0) + { + return physicalCores; + } + } + catch + { + // Fall through to fallback + } + + return Math.Max(1, Environment.ProcessorCount / 2); + } } } diff --git a/csharp/Platform.Unsafe/Platform.Unsafe.csproj b/csharp/Platform.Unsafe/Platform.Unsafe.csproj index 9828835..ba853ed 100644 --- a/csharp/Platform.Unsafe/Platform.Unsafe.csproj +++ b/csharp/Platform.Unsafe/Platform.Unsafe.csproj @@ -39,6 +39,7 @@ + diff --git a/experiments/PhysicalCoreTest.cs b/experiments/PhysicalCoreTest.cs new file mode 100644 index 0000000..7de6380 --- /dev/null +++ b/experiments/PhysicalCoreTest.cs @@ -0,0 +1,107 @@ +using System; +using System.Management; +using System.Runtime.InteropServices; + +namespace PhysicalCoreExperiments +{ + class Program + { + static void Main() + { + Console.WriteLine($"Environment.ProcessorCount: {Environment.ProcessorCount}"); + + // Test different approaches to get physical core count + Console.WriteLine("\n=== Different approaches to get physical cores ==="); + + try + { + // Approach 1: WMI (Windows only) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + int physicalCores = GetPhysicalCoresWmi(); + Console.WriteLine($"Physical cores (WMI): {physicalCores}"); + } + + // Approach 2: /proc/cpuinfo parsing (Linux) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + int physicalCores = GetPhysicalCoresLinux(); + Console.WriteLine($"Physical cores (Linux): {physicalCores}"); + } + + // Current approach + int currentApproach = Environment.ProcessorCount / 2; + Console.WriteLine($"Current approach (ProcessorCount/2): {currentApproach}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + static int GetPhysicalCoresWmi() + { + int physicalCores = 0; + using (var searcher = new ManagementObjectSearcher("SELECT NumberOfCores FROM Win32_Processor")) + { + foreach (var item in searcher.Get()) + { + physicalCores += int.Parse(item["NumberOfCores"].ToString()); + } + } + return physicalCores; + } + + static int GetPhysicalCoresLinux() + { + try + { + var cpuInfo = System.IO.File.ReadAllText("/proc/cpuinfo"); + var lines = cpuInfo.Split('\n'); + var physicalIds = new System.Collections.Generic.HashSet(); + + foreach (var line in lines) + { + if (line.StartsWith("physical id")) + { + var parts = line.Split(':'); + if (parts.Length > 1) + { + physicalIds.Add(parts[1].Trim()); + } + } + } + + return physicalIds.Count * GetCoresPerPhysicalCpu(); + } + catch + { + return Environment.ProcessorCount / 2; // fallback + } + } + + static int GetCoresPerPhysicalCpu() + { + try + { + var cpuInfo = System.IO.File.ReadAllText("/proc/cpuinfo"); + var lines = cpuInfo.Split('\n'); + + foreach (var line in lines) + { + if (line.StartsWith("cpu cores")) + { + var parts = line.Split(':'); + if (parts.Length > 1 && int.TryParse(parts[1].Trim(), out int cores)) + { + return cores; + } + } + } + } + catch { } + + return 1; // fallback + } + } +} \ No newline at end of file diff --git a/experiments/PhysicalCoresResearch.md b/experiments/PhysicalCoresResearch.md new file mode 100644 index 0000000..6e5788d --- /dev/null +++ b/experiments/PhysicalCoresResearch.md @@ -0,0 +1,61 @@ +# Research: Physical CPU Core Detection in .NET 8 + +## Current Approach +- `Environment.ProcessorCount / 2` - assumes hyper-threading and divides by 2 +- Comment explains: "A way to prevent wasting resources due to Hyper-Threading" + +## Problem +- Environment.ProcessorCount returns logical processors, not physical cores +- Dividing by 2 is an assumption that may not always be correct: + - Some CPUs don't support hyper-threading + - Some systems have hyper-threading disabled + - Modern CPUs may have different hyper-threading ratios + +## Possible Solutions + +### Option 1: Keep current approach (simplest) +Pros: +- Cross-platform compatible +- Simple and fast +- Works reasonably well for most cases +- No additional dependencies + +Cons: +- Not accurate on all systems +- Makes assumptions about hyper-threading + +### Option 2: Platform-specific detection +Pros: +- More accurate +- Can handle different CPU architectures + +Cons: +- Complex implementation +- Platform-specific code +- Requires additional dependencies (WMI on Windows) +- May not work in all environments (containers, etc.) + +### Option 3: Hybrid approach with fallback +Pros: +- More accurate when possible +- Falls back to current approach +- Gradual improvement + +Cons: +- More complex +- Still requires platform-specific code + +## Analysis of Current Usage +The code is used for memory zeroing operations where: +- Memory bandwidth is the bottleneck (not CPU) +- Too many threads can actually hurt performance +- The goal is to avoid over-subscribing memory channels + +## Recommendation +For this specific use case (memory operations), the current approach might actually be better than true physical core detection because: + +1. Memory operations are bandwidth-limited, not compute-limited +2. The comment mentions "two-channel memory architecture is the most available type" +3. The code hardcodes to 2 threads anyway: `threads = 2;` + +The real question is whether we need physical core detection or if the current approach is actually optimal for memory operations. \ No newline at end of file diff --git a/experiments/Program.cs b/experiments/Program.cs new file mode 100644 index 0000000..e85f274 --- /dev/null +++ b/experiments/Program.cs @@ -0,0 +1,55 @@ +using System; +using Platform.Unsafe; + +namespace PhysicalCoreTest +{ + class Program + { + static void Main() + { + Console.WriteLine("=== Physical Core Detection Test ==="); + Console.WriteLine($"Environment.ProcessorCount (logical processors): {Environment.ProcessorCount}"); + Console.WriteLine($"MemoryBlock.PhysicalCoreCount (physical cores): {MemoryBlock.PhysicalCoreCount}"); + Console.WriteLine($"Previous approach (ProcessorCount / 2): {Environment.ProcessorCount / 2}"); + + Console.WriteLine("\n=== Memory Block Zero Test ==="); + + unsafe + { + const int testSize = 1024 * 1024; // 1MB + var buffer = new byte[testSize]; + + fixed (byte* ptr = buffer) + { + // Fill with non-zero values first + for (int i = 0; i < testSize; i++) + { + buffer[i] = (byte)(i % 256); + } + + Console.WriteLine($"Before Zero: buffer[100] = {buffer[100]}, buffer[500] = {buffer[500]}"); + + // Test our Zero method + MemoryBlock.Zero(ptr, testSize); + + Console.WriteLine($"After Zero: buffer[100] = {buffer[100]}, buffer[500] = {buffer[500]}"); + + // Verify all bytes are zero + bool allZero = true; + for (int i = 0; i < testSize; i++) + { + if (buffer[i] != 0) + { + allZero = false; + break; + } + } + + Console.WriteLine($"All bytes are zero: {allZero}"); + } + } + + Console.WriteLine("\nPhysical core detection test completed successfully!"); + } + } +} \ No newline at end of file diff --git a/experiments/TestPhysicalCores.cs b/experiments/TestPhysicalCores.cs new file mode 100644 index 0000000..e85f274 --- /dev/null +++ b/experiments/TestPhysicalCores.cs @@ -0,0 +1,55 @@ +using System; +using Platform.Unsafe; + +namespace PhysicalCoreTest +{ + class Program + { + static void Main() + { + Console.WriteLine("=== Physical Core Detection Test ==="); + Console.WriteLine($"Environment.ProcessorCount (logical processors): {Environment.ProcessorCount}"); + Console.WriteLine($"MemoryBlock.PhysicalCoreCount (physical cores): {MemoryBlock.PhysicalCoreCount}"); + Console.WriteLine($"Previous approach (ProcessorCount / 2): {Environment.ProcessorCount / 2}"); + + Console.WriteLine("\n=== Memory Block Zero Test ==="); + + unsafe + { + const int testSize = 1024 * 1024; // 1MB + var buffer = new byte[testSize]; + + fixed (byte* ptr = buffer) + { + // Fill with non-zero values first + for (int i = 0; i < testSize; i++) + { + buffer[i] = (byte)(i % 256); + } + + Console.WriteLine($"Before Zero: buffer[100] = {buffer[100]}, buffer[500] = {buffer[500]}"); + + // Test our Zero method + MemoryBlock.Zero(ptr, testSize); + + Console.WriteLine($"After Zero: buffer[100] = {buffer[100]}, buffer[500] = {buffer[500]}"); + + // Verify all bytes are zero + bool allZero = true; + for (int i = 0; i < testSize; i++) + { + if (buffer[i] != 0) + { + allZero = false; + break; + } + } + + Console.WriteLine($"All bytes are zero: {allZero}"); + } + } + + Console.WriteLine("\nPhysical core detection test completed successfully!"); + } + } +} \ No newline at end of file diff --git a/experiments/TestProject.csproj b/experiments/TestProject.csproj new file mode 100644 index 0000000..f0d9c0d --- /dev/null +++ b/experiments/TestProject.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8 + true + latest + enable + + + + + + + \ No newline at end of file