简介
内联数组是 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 结合
[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
{
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);
高级用法
与不安全代码结合
内联数组天然支持非托管内存操作,使其成为与底层系统交互的理想选择。通过 unsafe 关键字,开发者可以直接获取内联数组的内存指针。这种机制允许在保持类型安全的同时,进行高效的内存拷贝或指针算术运算。例如,在需要直接操作硬件寄存器或调用 C 语言库函数时,内联数组可以无缝转换为指针类型,避免了传统数组在托管堆与非托管内存之间频繁转换带来的性能损耗。这种结合不仅提升了代码的执行效率,还简化了复杂内存管理的逻辑。
模拟多维数组
虽然 C# 原生支持多维数组,但内联数组提供了一种更轻量级的替代方案。通过定义包含内联数组字段的结构体,可以手动构建多维数据结构。这种方式允许开发者精确控制内存布局,确保数据在内存中连续存储。对于某些对缓存命中率要求极高的场景,手动管理多维数据的线性布局可能比使用锯齿数组或交错数组更具优势。开发者可以根据具体需求,灵活设计嵌套结构,以实现类似多维数组的访问语义,同时保持内存的高效利用。
与 Span 和 Memory 互操作
内联数组与 Span
底层原理
编译后代码结构
理解内联数组的性能优势,关键在于剖析其编译后的代码结构。在编译阶段,编译器会将内联数组字段直接嵌入到包含它的结构体中,而不是像普通数组那样在堆上分配一个独立的对象。这意味着内联数组的大小在编译时即已确定,且其内存布局是紧凑的。当结构体被实例化时,内联数组的数据直接存储在栈帧或结构体的内存空间中,无需额外的堆分配和垃圾回收开销。这种设计使得内联数组在访问速度和内存占用上具有显著优势,特别适合高频调用的场景。
适用场景
-
高性能计算:在游戏引擎、图形处理或科学计算中,需要连续内存以优化缓存利用率的场景。
-
缓冲区管理:处理固定大小的缓冲区,如网络数据包解析或文件读写操作。
-
与本机代码交互:与 C/C++ 或其他本机库交互时,传递连续内存块以提高效率。
-
替代小型数组:在结构体中替代普通数组字段,减少堆分配和碎片化。
-
向量/矩阵操作:表示数学向量或矩阵,优化内存访问模式以提升计算速度。
注意事项
仅限结构体:
-
内联数组只能定义在 struct 中,不能用于 class。
-
这是因为结构体是值类型,内存分配更可控,符合内联数组的设计初衷。
固定大小:
-
内联数组的大小在编译时必须是常量,无法在运行时动态调整。
-
不适合需要变长数组的场景,使用前需明确数据规模。
单一字段限制:
-
带有 InlineArray 特性的结构体只能包含一个内联数组字段。
-
如果需要其他字段,必须使用嵌套结构体来组织数据。
性能优势:
-
内联数组分配在栈上(对于局部变量)或嵌入结构体中,显著减少堆分配。
-
连续内存布局减少缓存未命中(Cache Miss),提高 CPU 缓存命中率与性能。
版本要求:
-
内联数组是 .NET 8 的新特性,需确保项目目标框架为 .NET 8 或更高。
-
需要启用 Experimental 特性以使用相关 API。
索引越界:
内联数组的特性与边界检查
内联数组支持通过索引进行访问,但在默认情况下不会自动执行越界检查,这为高性能场景提供了基础。当使用 Span 操作时,Span 机制会介入并提供边界检查,确保数据访问的安全性。这种设计允许开发者在性能与安全之间做出权衡。
与普通数组的对比
普通数组(T[])通常分配在托管堆上,具有动态大小的特点,并支持垃圾回收机制。相比之下,内联数组的大小是固定的,直接嵌入在结构体内部,占用连续的内存空间。这种特性使其非常适合对性能要求极高的场景,避免了堆分配和垃圾回收带来的开销。
与其他特性的对比
与普通数组(T[])的对比
- 普通数组属于引用类型,分配在堆上,其大小可以在运行时动态调整。
- 内联数组作为值类型的一部分,具有固定大小和连续的内存布局,从而提供更高的访问性能。
与固定大小缓冲区(fixed)的对比
C#中的fixed关键字用于在unsafe上下文中创建固定大小缓冲区。- 内联数组具备类型安全性,无需使用
unsafe,因此在易用性上更具优势。
与 Span/Memory 的对比
- 内联数组主要用于数据的存储,而
Span则作为访问视图存在。和Memory - 内联数组可以直接转换为
Span,两者结合使用可以显著提升效率。
与栈分配(stackalloc)的对比
stackalloc用于在栈上分配临时内存,其生命周期较短。- 内联数组嵌入在结构体中,支持更灵活的生命周期管理以及数据传递方式。
[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;
}
}
[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);
}
}
}
[InlineArray(10)]
struct InvalidBuffer
{
private int _element;
private int _otherField; // 错误:只能有一个字段
}
[InlineArray(2)]
struct SmallBuffer
{
private int _element;
}
var buffer = new SmallBuffer();
buffer[2] = 1; // 运行时异常:索引越界
CharBuffer
C
char[8]
ref
C/C++
T[]
struct
class
[InlineArray]
cache miss
C# 12(.NET 8)
.NET 8.0
System.Runtime.CompilerServices.InlineArray









