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
22 changes: 22 additions & 0 deletions csharp/Platform.Unsafe.Tests/ZeroMemoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Xunit;

namespace Platform.Unsafe.Tests
Expand All @@ -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}");
}
}
}
141 changes: 135 additions & 6 deletions csharp/Platform.Unsafe/MemoryBlock.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -14,6 +15,13 @@ namespace Platform.Unsafe
/// </summary>
public static unsafe class MemoryBlock
{
private static readonly Lazy<int> _physicalCoreCount = new(() => GetPhysicalCoreCount());

/// <summary>
/// <para>Gets the number of physical CPU cores.</para>
/// <para>Получает количество физических ядер ЦП.</para>
/// </summary>
public static int PhysicalCoreCount => _physicalCoreCount.Value;
/// <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 @@ -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));
}
}
Expand All @@ -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<string>();
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);
}
}
}
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.0' OR '$(TargetFramework)' == 'net8'" />
</ItemGroup>

</Project>
107 changes: 107 additions & 0 deletions experiments/PhysicalCoreTest.cs
Original file line number Diff line number Diff line change
@@ -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<string>();

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
}
}
}
61 changes: 61 additions & 0 deletions experiments/PhysicalCoresResearch.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading