Loading That C# Knowledge Back Into Cache

I was brushing up on my C# knowledge after submitting a resume for a C# programming job because I haven’t done any serious work in it in ten years. I used it to build a tool that would tell me what me L1 and L2 cache sizes are because on a whim I was reading about cache lines and multithreading and learning why C++ developers are constantly blathering on about cache misses when they’re peacocking.

Of course I would choose an obnoxiously difficult API function to marshal. (Marshalling is a big blind spot in my C# skill set.) Thankfully, StackOverflow had my back. Here it is in most of its glory.

Marshall.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;

// https://stackoverflow.com/questions/6972437/pinvoke-for-getlogicalprocessorinformation-function

namespace LogicalProcessorInformation
{
	// [StructLayout(LayoutKind.Sequential)] isn't needed here
	// C#, Visual Basic, and C++ compilers apply the Sequential layout value to structures by default.
	public struct PROCESSORCORE
	{
		public byte Flags;
	};

	public struct NUMANODE
	{
		public uint NodeNumber;
	}

	public enum PROCESSOR_CACHE_TYPE
	{
		CacheUnified,
		CacheInstruction,
		CacheData,
		CacheTrace
	}

	public struct CACHE_DESCRIPTOR
	{
		public byte Level;
		public byte Associativity;
		public ushort LineSize;
		public uint Size;
		public PROCESSOR_CACHE_TYPE Type;
	}

	// A union is a structure with an Explicit layout and FieldOffset
	[StructLayout(LayoutKind.Explicit)]
	public struct SYSTEM_LOGICAL_PROCESSOR_INFORMATION_UNION
	{
		[FieldOffset(0)] // The [FieldOffset(int offset)] attribute in C# is used to specify the exact physical position, in bytes, of a field within the unmanaged representation of a struct
		public PROCESSORCORE ProcessorCore;
		[FieldOffset(0)]
		public NUMANODE NumaNode;
		[FieldOffset(0)]
		public CACHE_DESCRIPTOR Cache;
		[FieldOffset(0)]
		private readonly UInt64 Reserved1;
		[FieldOffset(8)]
		private readonly UInt64 Reserved2;
	}

	public enum LOGICAL_PROCESSOR_RELATIONSHIP
	{
		RelationProcessorCore,
		RelationNumaNode,
		RelationCache,
		RelationProcessorPackage,
		RelationGroup,
		RelationAll = 0xffff
	}

	public struct SYSTEM_LOGICAL_PROCESSOR_INFORMATION
	{
		public UIntPtr ProcessorMask;
		public LOGICAL_PROCESSOR_RELATIONSHIP Relationship;
		public SYSTEM_LOGICAL_PROCESSOR_INFORMATION_UNION ProcessorInformation;
	}

	internal partial class Marshall // internal = accessible only within this assembly
	{
		// by making a nested class for the native types and functions, I can protect
		// access to them despite them needed to be public because of the Marshall object
		private class Native
		{
			[DllImport(@"kernel32.dll",SetLastError=true)]
            public static extern bool GetLogicalProcessorInformation(
				IntPtr Buffer,
				ref uint ReturnLength
			);

			private const int ERROR_INSUFFICIENT_BUFFER=122;
			public required SYSTEM_LOGICAL_PROCESSOR_INFORMATION[] buffer; // by nested this in a private class, I've essentially made this private

			[SetsRequiredMembers] // without this, new() in the Marshall() constructor will fail to compile because buffer is marked required and the compiler doesn't know this constructore fulfills that requirement.
			public Native()
			{
				uint size=0;
				// GetLastPInvokeError() supersedes GetLastSystemError(): https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getlastpinvokeerror?view=net-10.0#remarks
				// this call is intended to fail, triggering it to return the required size
				if (GetLogicalProcessorInformation(IntPtr.Zero,ref size)) // IntPtr = pointer to a handle
				{
					throw new InvalidOperationException("Failed to obtain required size of memory to allocate");
				}
				else
				{
					var errorCode=Marshal.GetLastPInvokeError();
					if (errorCode != ERROR_INSUFFICIENT_BUFFER) throw new Win32Exception(errorCode);
				}

				unsafe
				{
					IntPtr memory=(IntPtr)NativeMemory.Alloc(size);
					try
					{
						if (GetLogicalProcessorInformation(memory,ref size))
						{
							int structSize=Marshal.SizeOf<SYSTEM_LOGICAL_PROCESSOR_INFORMATION>();
							int count=(int)size/structSize;
							buffer=new SYSTEM_LOGICAL_PROCESSOR_INFORMATION[count];
							IntPtr offset=memory;
							for (int index=0; index < count; index++)
							{
								var candidate=Marshal.PtrToStructure<SYSTEM_LOGICAL_PROCESSOR_INFORMATION>(offset);
								buffer[index]=(SYSTEM_LOGICAL_PROCESSOR_INFORMATION)candidate;
								offset+=structSize;
							}
						}
						else
						{
							throw new Win32Exception();
						}
					}
					finally
					{
						NativeMemory.Free((void*)memory);
					}
				}
			}
		}

		private readonly Native native;

		public Marshall()
		{
			native=new(); // see note on SetsRequiredMembers attribute on Native
		}

		public ref readonly SYSTEM_LOGICAL_PROCESSOR_INFORMATION[] Processors()
		{
			return ref native.buffer;
		}
	}
}

Code language: C# (cs)
CacheEntry.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace LogicalProcessorInformation
{
    public partial class CacheEntry: UserControl
    {
        public CacheEntry(int level,int size)
        {
            InitializeComponent();
            textLevel.Text=level.ToString();
            textSize.Text=size.ToString();
        }

        private void CacheEntry_Load(object sender,EventArgs e)
        {
			ClientSize=new Size(ClientSize.Width,textLevel.Height+textSize.Height+Padding.Vertical+textSize.Margin.Vertical+textLevel.Margin.Vertical);
        }
    }
}

Code language: C# (cs)
CacheInformation.cs
using System;
using System.CodeDom;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;

namespace LogicalProcessorInformation
{
    public partial class CacheInformation: Form
    {
        public CacheInformation()
        {
            InitializeComponent();
		}

		private void LogicalProcessorInformation_Load(object sender,EventArgs e)
        {
			try
			{
				var marshall=new Marshall();
				foreach (var entry in marshall.Processors())
				{
					// we're only interested in entries with cache information
					if (entry.ProcessorInformation.Cache.Level == 0 || entry.ProcessorInformation.Cache.Size == 0) continue;

					// mask has a bit set for each logical processor this entry represents
					var mask=entry.ProcessorMask;
					List<int> CPUs=[];
					for (int position=0; position < UIntPtr.Size; position++)
					{
						if (((int)mask & (1 << position)) != 0) CPUs.Add(position);
					}

					foreach (var CPU in CPUs) // for each bit that was set above
					{
						var CPUName=CPU.ToString(); // make the position of the bit our tab text
						var page=Listing.TabPages.Cast<TabPage>().FirstOrDefault(tab => tab.Name == CPUName);
						FlowLayoutPanel scrollablePanel;

						// set up a new page if there isn't one, otherwise use the one we got from LINQ
						if (page is null)
						{
							scrollablePanel=new FlowLayoutPanel
							{
								Dock=DockStyle.Fill,
								FlowDirection=FlowDirection.TopDown,
								WrapContents=false,
								AutoScroll=true
							};
							SizeChanged+=new EventHandler(ScrollablePanel_SizeChanged!);
							page=new TabPage
							{
								Name=CPUName,
								Dock=DockStyle.Fill,
								Text=CPUName
							};
							page.Controls.Add(scrollablePanel);
							Listing.TabPages.Add(page);
						}
						else
						{
							scrollablePanel=page.Controls.OfType<FlowLayoutPanel>().First();
						}

						CacheEntry entryForm=new (entry.ProcessorInformation.Cache.Level,(int)entry.ProcessorInformation.Cache.Size);
						scrollablePanel.Controls.Add(entryForm);
						scrollablePanel.Controls.Add(entryForm);
					}
				}
			}

			catch (Win32Exception exception)
			{
				CriticalError(exception.Message);
			}

			catch (InvalidOperationException exception)
			{
				CriticalError(exception.Message);
			}

			catch (Exception exception)
			{
				MessageBox.Show(exception.ToString(),"Unknown Error",MessageBoxButtons.OK,MessageBoxIcon.Error);
			}
		}

		private static void CriticalError(string message)
		{
			MessageBox.Show(message,"Critical Error",MessageBoxButtons.OK,MessageBoxIcon.Error);
		}

		private void ScrollablePanel_SizeChanged(object sender,EventArgs e)
		{
			if (sender is not FlowLayoutPanel) return;
			var panel=sender as FlowLayoutPanel;
			foreach (var entry in panel!.Controls.OfType<CacheEntry>())
			{
				entry.Width=panel.Width-entry.Margin.Horizontal;
			}
		}
    }
}

Code language: C# (cs)

Leave a Reply

Your email address will not be published. Required fields are marked *