GC最佳化:棧記憶體、span、NativeMemory、指標、池化記憶體 筆記

darklx發表於2024-11-16

stackalloc


使用棧記憶體,減少GC壓力

var wordMatchCounts = stackalloc float[wordCount];

Span


Span 支援 reinterpret_cast 的理念,即可以將 Span 強制轉換為 Span

Span 支援 reinterpret_cast 的理念,即可以將 Span 強制轉換為 Span(其中,Span 中的索引 0 對映到 Span 的前四個位元組)。這樣一來,如果讀取位元組緩衝區,可以安全高效地將它傳遞到對分組位元組(視作整數)執行操作的方法。

Span 也能裝在集合之ValueListBuilder & .AsSpan()

.NET 內部提升效能物件:ValueListBuilder & .AsSpan()

ValueListBuilder & .AsSpan()

.NET Core 原始碼中的內部提升效能物件:ValueListBuilder & .AsSpan()

它在 String.Replace 中被使用

public unsafe string Replace(string oldValue, string? newValue) {
	  ArgumentException.ThrowIfNullOrEmpty(oldValue, nameof (oldValue));
	  if (newValue == null)	newValue = string.Empty;
	  // ISSUE: untyped stack allocation
	  ValueListBuilder<int> valueListBuilder = new ValueListBuilder<int>(new Span<int>((void*) __untypedstackalloc(new IntPtr(512)), 128));

	  if (oldValue.Length == 1)
	  {
		if (newValue.Length == 1)
		  return this.Replace(oldValue[0], newValue[0]);
		char ch = oldValue[0];
		int elementOffset = 0;

		while (true)
		{
		  int num = SpanHelpers.IndexOf(ref Unsafe.Add<char>(ref this._firstChar, elementOffset), ch, this.Length - elementOffset);
		  if (num >= 0){
			valueListBuilder.Append(elementOffset + num);
			elementOffset += num + 1;
		  }
		  else break;
		}
	  }
	  else{
		int elementOffset = 0;
		while (true){
		  int num = SpanHelpers.IndexOf(ref Unsafe.Add<char>(ref this._firstChar, elementOffset), this.Length - elementOffset, ref oldValue._firstChar, oldValue.Length);
		  if (num >= 0){
			valueListBuilder.Append(elementOffset + num);
			elementOffset += num + oldValue.Length;
		  }
		  else break;
		}
	  }
	  if (valueListBuilder.Length == 0) eturn this;
	  string str = this.ReplaceHelper(oldValue.Length, newValue, **valueListBuilder.AsSpan()**);
	  valueListBuilder.Dispose();
	  return str;
	}

.NET 內部類直接將集合轉回為 Span<T>:CollectionsMarshal.AsSpan<string>(List<string>)

	private static unsafe string JoinCore<T>(ReadOnlySpan<char> separator, IEnumerable<T> values){

	  if (typeof (T) == typeof (string)){
		if (values is List<string> list)
		  return string.JoinCore(separator, (ReadOnlySpan<string>) CollectionsMarshal.AsSpan<string>(list));
		if (values is string[] array)
		  return string.JoinCore(separator, new ReadOnlySpan<string>(array));
	  }

ref struct,使用ref讀取值型別,避免值型別複製


使用ref讀取值型別,避免值型別複製,但要注意對當前值型別的修改,會影響被ref的那個值型別,因為本質上你在操作一個指標

ref var hierarchy = ref ph[i];
ref var words = ref hierarchy.Words;

Unsafe.IsNullRef

可以使用 Unsafe.IsNullRef 來判斷一個 ref 是否為空。如果使用者沒有對 Foo.X 進行初始化,則預設是空引用:

ref  struct Foo {
  public  ref  int X;
  public  bool IsNull => Unsafe.IsNullRef(ref X);
  public  Foo(ref  int x) { X = ref x; }
}

1.4 NativeMemory

相比 Marshal.AllocHGlobal 和 Marshal.FreeHGlobal,其實現在更推薦 NativeMemory.*,有諸多好處:

  • 支援控制是否零初始化

  • 支援控制記憶體對齊

  • 引數是 nuint 型別,支援在 64 位程序上支援分配超過 int 上限的大小

1.5 struct 直接轉換記憶體資料

1.5.1 C#使用struct直接轉換下位機資料

資料結構
假定下位機(C語言編寫)給到我們的資料結構是這個,傳輸方式為小端方式

typedef struct {

		 unsigned long int time;          // 4個位元組

		 float tmpr[3];                   //  4*3 個位元組

		 float forces[6];                 //  4*6個位元組

		 float distance[6];               // 4*6個位元組

} dataItem_t;

方法1(略麻煩)

首先需要定義一個struct:

[StructLayout(LayoutKind.Sequential, Size = 64, Pack = 1)]
public struct HardwareData {
	//[FieldOffset(0)]
	public UInt32 Time; // 4個位元組

	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
	//[FieldOffset(4)]
	public float[] Tmpr; //  3* 4個位元組
	
	//[FieldOffset(16)]
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
	public float[] Forces; //  6* 4個位元組

	//[FieldOffset(40)]
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
	public float[] Distance; //  6*4個位元組

}

然後使用以下程式碼進行轉換

// converts byte[] to struct
public static T RawDeserialize(byte[] rawData, int position) {
	int rawsize = Marshal.SizeOf(typeof(T));
	if (rawsize > rawData.Length - position) throw new ArgumentException("Not enough data to fill struct. Array length from position: " + (rawData.Length - position) + ", Struct length: " + rawsize);
	IntPtr buffer = Marshal.AllocHGlobal(rawsize);
	Marshal.Copy(rawData, position, buffer, rawsize);
	T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T));
	Marshal.FreeHGlobal(buffer);
	return retobj;
}

// converts a struct to byte[]
public static byte[] RawSerialize(object anything) {
	int rawSize = Marshal.SizeOf(anything);
	IntPtr buffer = Marshal.AllocHGlobal(rawSize);
	Marshal.StructureToPtr(anything, buffer, false);
	byte[] rawDatas = new byte[rawSize];
	Marshal.Copy(buffer, rawDatas, 0, rawSize);
	Marshal.FreeHGlobal(buffer);
	return rawDatas;
}

注意這裡我使用的方式為LayoutKind.Sequential,如果直接使用LayoutKind.Explicit並設定FieldOffset會彈出一個詭異的錯誤System.TypeLoadException:"Could not load type 'ConsoleApp3.DataItem' from assembly 'ConsoleApp3, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field."。

方法2

既然用上了 unsafe,就乾脆直接一點。

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct DataItem {
	public UInt32 time; // 4個位元組
	public fixed float tmpr[3]; //  3* 4個位元組
	public fixed float forces[6]; //  6* 4個位元組
	public fixed float distance[6]; //  6*4個位元組
}

這樣,獲得陣列可以直接正常訪問了。


C#使用struct直接轉換下位機資料

https://blog.51cto.com/u_15127641/2754559

1.6 string.Join 內部實現解析

CollectionsMarshal.AsSpan(valuesList)

if (values is List<string?> valuesList) {
    return JoinCore(separator.AsSpan(), CollectionsMarshal.AsSpan(valuesList));
}
if (values is string?[] valuesArray)
{
    return JoinCore(separator.AsSpan(), new ReadOnlySpan<string?>(valuesArray));
}

Join

public static string Join(string? separator, IEnumerable<string?> values)
{
	if (values is List<string?> valuesList)
	{
		return JoinCore(separator.AsSpan(), CollectionsMarshal.AsSpan(valuesList));
	}
	if (values is string?[] valuesArray)
	{
		return JoinCore(separator.AsSpan(), new ReadOnlySpan<string?>(valuesArray));
	}
	if (values == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values);
	}
	using (IEnumerator<string?> en = values.GetEnumerator())
	{
		if (!en.MoveNext())
		{
			return Empty;
		}
		string? firstValue = en.Current;
		if (!en.MoveNext())
		{
			// Only one value available
			return firstValue ?? Empty;
		}
		// Null separator and values are handled by the StringBuilder
		var result = new ValueStringBuilder(stackalloc char[256]);
		result.Append(firstValue);
		do
		{
			result.Append(separator);
			result.Append(en.Current);
		}
		while (en.MoveNext());
		return result.ToString();
	}
}

JoinCore

private static string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<string?> values)
{
	if (values.Length <= 1)
	{
		return values.IsEmpty ?
			Empty :
			values[0] ?? Empty;
	}

	long totalSeparatorsLength = (long)(values.Length - 1) * separator.Length;
	if (totalSeparatorsLength > int.MaxValue)
	{
		ThrowHelper.ThrowOutOfMemoryException();
	}
	int totalLength = (int)totalSeparatorsLength;

	// Calculate the length of the resultant string so we know how much space to allocate.
	foreach (string? value in values)
	{
		if (value != null)
		{
			totalLength += value.Length;
			if (totalLength < 0) // Check for overflow
			{
				ThrowHelper.ThrowOutOfMemoryException();
			}
		}
	}

	// Copy each of the strings into the result buffer, interleaving with the separator.
	string result = FastAllocateString(totalLength);
	int copiedLength = 0;

	for (int i = 0; i < values.Length; i++)
	{
		// It's possible that another thread may have mutated the input array
		// such that our second read of an index will not be the same string
		// we got during the first read.

		// We range check again to avoid buffer overflows if this happens.

		if (values[i] is string value)
		{
			int valueLen = value.Length;
			if (valueLen > totalLength - copiedLength)
			{
				copiedLength = -1;
				break;
			}

			// Fill in the value.
			FillStringChecked(result, copiedLength, value);
			copiedLength += valueLen;
		}

		if (i < values.Length - 1)
		{
			// Fill in the separator.
			// Special-case length 1 to avoid additional overheads of CopyTo.
			// This is common due to the char separator overload.

			ref char dest = ref Unsafe.Add(ref result._firstChar, copiedLength);

			if (separator.Length == 1)
			{
				dest = separator[0];
			}
			else
			{
				separator.CopyTo(new Span<char>(ref dest, separator.Length));
			}

			copiedLength += separator.Length;
		}
	}

	// If we copied exactly the right amount, return the new string.  Otherwise,
	// something changed concurrently to mutate the input array: fall back to
	// doing the concatenation again, but this time with a defensive copy. This
	// fall back should be extremely rare.
	return copiedLength == totalLength ?
		result :
		JoinCore(separator, values.ToArray().AsSpan());
}

相關文章