博客 / 詳情

返回

一次弄懂 C# 內聯數組(Inline Array):高性能數組的新選擇

簡介

內聯數組是 C# 12.NET 8 中引入的一個高級特性,它允許開發者創建固定大小的、在棧上分配或內聯在結構體中的數組。這個特性主要用於高性能場景,可以避免堆分配和垃圾回收的開銷。

性能優勢

內聯數組的主要優勢在於性能:

  • 棧上分配:避免堆分配和垃圾回收
  • 內存局部性:元素在內存中連續存儲,提高緩存命中率
  • 減少指針間接尋址:直接訪問元素,不需要通過數組對象引用

內聯數組 vs 傳統數組

特性 內聯數組 傳統數組
內存位置 棧/包含結構體內存 託管堆
分配開銷 無額外分配 需要堆分配
最大長度 受棧空間限制(通常≤1MB) 受GC限制(通常≤2GB)
適用場景 高性能計算/嵌入式開發 通用場景
靈活性 固定長度 動態長度
C#版本要求 ≥ C# 12 (.NET 8+) 所有版本

基本語法

內聯數組使用 InlineArray 特性和特定的模式來定義:

using System.Runtime.CompilerServices;

[InlineArray(Size)]
struct InlineArrayStruct
{
    private T _element; // 單一字段,表示數組的起點
}
  • Size:一個編譯時常量,表示數組的固定長度。
  • T:數組元素的類型(可以是值類型或引用類型)。
  • 單一字段:結構體中只能有一個字段,表示數組的起點,編譯器會將其擴展為固定大小的連續內存。
[System.Runtime.CompilerServices.InlineArray(10)]
public struct MyInlineArray
{
    private int _element0; // 只需要定義一個字段,實際大小由特性指定
}

創建和使用內聯數組

基本定義和使用

using System;
using System.Runtime.CompilerServices;

// 定義包含10個整數的內聯數組
[InlineArray(10)]
public struct IntArray10
{
    private int _element0;
    
    // 可以添加方法或屬性來增強功能
    public int Length => 10;
    
    public int Sum()
    {
        int sum = 0;
        for (int i = 0; i < Length; i++)
        {
            sum += this[i];
        }
        return sum;
    }
}

class Program
{
    static void Main()
    {
        IntArray10 array = new IntArray10();
        
        // 初始化數組
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = i * 2;
        }
        
        // 訪問元素
        for (int i = 0; i < array.Length; i++)
        {
            Console.WriteLine($"array[{i}] = {array[i]}");
        }
        
        Console.WriteLine($"總和: {array.Sum()}");
    }
}

Span<T> 結合

[InlineArray(5)]
struct FloatBuffer
{
    private float _element;
}

var buffer = new FloatBuffer();
buffer[0] = 1.5f;
buffer[1] = 2.5f;

Span<float> span = buffer;
Console.WriteLine(span[0]); // 輸出: 1.5
Console.WriteLine(span.Length); // 輸出: 5

泛型內聯數組

[InlineArray(5)]
public struct GenericArray<T>
{
    private T _element0;
    
    public int Length => 5;
    
    public void Initialize(T initialValue)
    {
        for (int i = 0; i < Length; i++)
        {
            this[i] = initialValue;
        }
    }
}

// 使用示例
GenericArray<string> stringArray = new GenericArray<string>();
stringArray.Initialize("default");

在性能敏感場景中使用

內聯數組適合需要極致性能的場景,例如處理向量或緩衝區。

[InlineArray(3)]
struct Vector3D
{
    private float _element;
}

Vector3D AddVectors(Vector3D a, Vector3D b)
{
    var result = new Vector3D();
    for (int i = 0; i < 3; i++)
    {
        result[i] = a[i] + b[i];
    }
    return result;
}

var v1 = new Vector3D { [0] = 1.0f, [1] = 2.0f, [2] = 3.0f };
var v2 = new Vector3D { [0] = 4.0f, [1] = 5.0f, [2] = 6.0f };
var sum = AddVectors(v1, v2);

Console.WriteLine($"({sum[0]}, {sum[1]}, {sum[2]}"); // 輸出: (5, 7, 9)
  • Vector3D 模擬一個三維向量,內聯數組確保內存連續。
  • 適合遊戲開發或科學計算。

與本機代碼交互

內聯數組的連續內存佈局使其非常適合與本機代碼(如 C/C++ )交互。

using System.Runtime.InteropServices;

[InlineArray(8)]
struct CharBuffer
{
    private char _element;
}

[DllImport("someNativeLib.dll")]
extern static void ProcessBuffer(ref CharBuffer buffer);

var buffer = new CharBuffer();
buffer[0] = 'H';
buffer[1] = 'e';
buffer[2] = 'l';
buffer[3] = 'l';
buffer[4] = 'o';
ProcessBuffer(ref buffer);
  • CharBuffer 的內存佈局與 C 語言中的 char[8] 兼容。
  • 通過 ref 傳遞,確保本機代碼可以直接操作內存。

高級用法

與不安全代碼結合

[InlineArray(8)]
public unsafe struct DoubleArray
{
    private double _element0;
    
    public fixed int Length => 8;
    
    public double* GetPointer()
    {
        fixed (double* ptr = &this[0])
        {
            return ptr;
        }
    }
}

模擬多維數組

// 使用一維內聯數組模擬二維數組
[InlineArray(16)] // 4x4 矩陣
public struct Matrix4x4
{
    private float _element0;
    
    public int Rows => 4;
    public int Columns => 4;
    
    public float this[int row, int col]
    {
        get => this[row * Columns + col];
        set => this[row * Columns + col] = value;
    }
    
    public static Matrix4x4 Identity()
    {
        var matrix = new Matrix4x4();
        for (int i = 0; i < 4; i++)
        {
            matrix[i, i] = 1.0f;
        }
        return matrix;
    }
}

與 Span 和 Memory 互操作

[InlineArray(100)]
public struct Buffer100
{
    private byte _element0;
    
    public int Length => 100;
    
    public Span<byte> AsSpan()
    {
        return MemoryMarshal.CreateSpan(ref this[0], Length);
    }
    
    public ReadOnlySpan<byte> AsReadOnlySpan()
    {
        return MemoryMarshal.CreateReadOnlySpan(ref this[0], Length);
    }
}

底層原理

編譯後代碼結構

// 原始代碼
[InlineArray(5)]
public struct Buffer5 { private int _element; }

// 近似編譯結果
public struct Buffer5
{
    private int _element0;
    private int _element1;
    private int _element2;
    private int _element3;
    private int _element4;

    public ref int this[int index]
    {
        get
        {
            if ((uint)index >= 5)
                throw new IndexOutOfRangeException();
            return ref Unsafe.Add(ref _element0, index);
        }
    }
}

適用場景

  • 高性能計算:遊戲引擎、圖形處理或科學計算中需要連續內存的場景。
  • 緩衝區管理:處理固定大小的緩衝區,如網絡數據包或文件讀寫。
  • 與本機代碼交互:與 C/C++ 或其他本機庫交互,傳遞連續內存塊。
  • 替代小型數組:在結構體中替代 T[] 字段,減少堆分配。
  • 向量/矩陣操作:表示數學向量或矩陣,優化內存訪問。

注意事項

僅限結構體:

  • 只能定義在 struct 中,不能用於 class
  • 這是因為結構體是值類型,內存分配更可控。

固定大小:

  • 內聯數組的大小在編譯時必須是常量,無法動態調整。
  • 不適合需要變長數組的場景。

單一字段限制:

  • 帶有 [InlineArray] 的結構體只能包含一個字段。
  • 如果需要其他字段,必須使用嵌套結構體。
[InlineArray(10)]
struct InvalidBuffer
{
    private int _element;
    private int _otherField; // 錯誤:只能有一個字段
}

性能優勢:

  • 內聯數組分配在棧上(對於局部變量)或嵌入結構體中,減少堆分配。
  • 連續內存佈局減少緩存未命中(cache miss),提高性能。

版本要求:

  • 內聯數組是 C# 12(.NET 8)的新特性,需確保項目目標框架為 .NET 8.0 或更高。
  • 需要 System.Runtime.CompilerServices.InlineArray 特性。

索引越界:

  • 內聯數組支持索引訪問,但不會自動檢查越界。
  • 使用 Span<T> 操作時,Span<T> 會提供邊界檢查。
[InlineArray(2)]
struct SmallBuffer
{
    private int _element;
}

var buffer = new SmallBuffer();
buffer[2] = 1; // 運行時異常:索引越界

與普通數組的對比:

  • 普通數組(T[]):分配在託管堆上,動態大小,支持垃圾回收。
  • 內聯數組:固定大小,嵌入結構體,連續內存,適合高性能場景。

與其他特性的對比

與普通數組(T[])的對比:

  • 普通數組是引用類型,分配在堆上,大小可動態調整。
  • 內聯數組是值類型的一部分,固定大小,內存連續,性能更高。

與固定大小緩衝區(fixed)的對比:

  • C#fixed 關鍵字用於在 unsafe 上下文中創建固定大小緩衝區。
  • 內聯數組是類型安全的,無需 unsafe,更易用。

Span<T>/Memory<T> 的對比:

  • 內聯數組是數據的存儲結構,而 Span<T>和Memory<T>是訪問視圖。
  • 內聯數組可直接轉換為 Span<T>,結合使用效率更高。

與棧分配(stackalloc)的對比:

  • stackalloc 在棧上分配臨時內存,生命週期短。
  • 內聯數組嵌入結構體,支持更靈活的生命週期和傳遞。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.