C#中的fixed 语句的工作原理
fixed 语句是 C# 中指针功能的核心组成部分,它的工作原理涉及到 CLR(公共语言运行时)的垃圾回收机制。让我详细解释它的工作原理:
1. 垃圾回收与内存移动
在 .NET 的托管环境中,垃圾回收器(GC)会定期执行以下操作:
- 回收不再使用的内存
- 压缩堆 - 移动存活对象来消除内存碎片
// 如果没有 fixed,可能会发生这种情况
object obj1 = new MyClass(); // 地址: 0x1000
object obj2 = new MyClass(); // 地址: 0x2000
// GC 执行后,对象可能被移动
// obj1 可能从 0x1000 移动到 0x1500
// 如果此时有指针指向 0x1000,就会指向错误的位置!
2. fixed 语句的作用机制
基本工作原理
unsafe void Example()
{
byte[] buffer = new byte[100];
fixed (byte* ptr = buffer)
{
// 在这个代码块内:
// 1. buffer 数组被"固定"在内存中
// 2. GC 不会移动这个对象
// 3. ptr 安全地指向数组的起始位置
for (int i = 0; i < 100; i++)
{
ptr[i] = (byte)i;
}
} // fixed 块结束,buffer 不再被固定
}
3. CLR 内部的实现细节
GC 句柄(GC Handle)
fixed 语句在内部创建了一个 GC 句柄:
// 伪代码展示内部工作原理
unsafe void FixedInternalWorkflow()
{
byte[] buffer = new byte[100];
// fixed 开始时的内部操作:
GCHandle gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
byte* ptr = (byte*)gcHandle.AddrOfPinnedObject();
// 使用指针进行操作...
for (int i = 0; i < 100; i++)
{
ptr[i] = (byte)i;
}
}
finally
{
// fixed 结束时的清理
gcHandle.Free();
}
}
4. 多对象固定
unsafe void MultipleFixed()
{
byte[] buffer1 = new byte[50];
byte[] buffer2 = new byte[50];
// 同时固定多个对象
fixed (byte* ptr1 = buffer1, ptr2 = buffer2)
{
// 两个数组都被固定,GC 不会移动它们
for (int i = 0; i < 50; i++)
{
ptr1[i] = (byte)(i * 2);
ptr2[i] = (byte)(i * 2 + 1);
}
}
}
5. 嵌套和复杂场景
嵌套 fixed 语句
unsafe void NestedFixed()
{
byte[][] jaggedArray = new byte[10][];
for (int i = 0; i < 10; i++)
{
jaggedArray[i] = new byte[20];
}
fixed (byte** outerPtr = jaggedArray)
{
for (int i = 0; i < 10; i++)
{
// 嵌套 fixed - 固定内部数组
fixed (byte* innerPtr = jaggedArray[i])
{
innerPtr[0] = (byte)i;
}
}
}
}
6. 性能影响和最佳实践
性能考虑
unsafe class PerformanceConsiderations
{
// 不好的做法 - 长时间固定
void BadPractice()
{
byte[] buffer = new byte[1000];
fixed (byte* ptr = buffer)
{
// 执行耗时操作 - 长时间阻止 GC 压缩堆
LongRunningOperation(ptr);
}
}
// 好的做法 - 最小化固定时间
void GoodPractice()
{
byte[] buffer = new byte[1000];
// 只固定必要的时间
fixed (byte* ptr = buffer)
{
// 快速操作
InitializeBuffer(ptr, 1000);
}
// 后续操作不使用指针
ProcessBuffer(buffer);
}
private void LongRunningOperation(byte* ptr) { /* 耗时操作 */ }
private void InitializeBuffer(byte* ptr, int length) { /* 快速初始化 */ }
private void ProcessBuffer(byte[] buffer) { /* 处理缓冲区 */ }
}
7. 替代方案和现代方法
使用 Span 避免固定
// 现代方式 - 不需要 fixed
void ModernApproach()
{
byte[] buffer = new byte[100];
// 使用 Span<T>,不需要 unsafe 上下文
Span<byte> span = buffer;
for (int i = 0; i < span.Length; i++)
{
span[i] = (byte)i;
}
// 或者使用 MemoryMarshal 进行更高级的操作
Span<int> intView = MemoryMarshal.Cast<byte, int>(span);
}
8. 内部工作原理详解
GC 的固定表(Pinning Table)
当 fixed 语句执行时:
- 对象标记:CLR 在内部标记对象为"已固定"
- 更新固定表:GC 维护一个固定表来跟踪所有固定对象
- GC 行为调整:
- 标记阶段:正常标记存活对象
- 压缩阶段:跳过固定对象,不移动它们
- 内存分配:在固定对象周围分配新对象
内存布局示例
堆内存布局(GC 前):
[对象A][对象B(固定)][对象C][空闲空间]
GC 压缩后:
[对象A][对象B(固定)][对象C][空闲空间]
↑
保持原位,其他对象可能移动
9. 使用注意事项
重要限制
unsafe class Limitations
{
void ImportantNotes()
{
// 1. 只能固定引用类型(数组、字符串)
int[] array = new int[10];
string str = "hello";
fixed (int* p1 = array) // 正确
fixed (char* p2 = str) // 正确
// 2. 不能固定值类型变量(除非它们是装箱的)
int value = 42;
// fixed (int* p3 = &value) // 错误!
// 3. 正确的方式:使用 stackalloc 或取地址操作符
int* p3 = &value; // 正确 - 栈上的变量不需要固定
}
}
10. 实际应用场景
图像处理示例
unsafe class ImageProcessor
{
public void ProcessImage(byte[] imageData)
{
fixed (byte* dataPtr = imageData)
{
// 高性能图像处理
int width = 1024;
int height = 768;
for (int y = 0; y < height; y++)
{
byte* rowPtr = dataPtr + (y * width * 3); // 3 bytes per pixel (RGB)
for (int x = 0; x < width; x++)
{
byte* pixelPtr = rowPtr + (x * 3);
// 处理像素
pixelPtr[0] = (byte)(pixelPtr[0] * 1.2); // R
pixelPtr[1] = (byte)(pixelPtr[1] * 1.1); // G
pixelPtr[2] = (byte)(pixelPtr[2] * 0.9); // B
}
}
}
}
}
总结
fixed 语句的工作原理可以概括为:
- 通知 GC:标记特定对象为不可移动
- 创建安全上下文:确保指针在整个代码块期间有效
- 自动清理:在代码块结束时自动释放固定
- 性能权衡:在内存压缩效率和指针访问性能之间取得平衡
在现代 C# 开发中,除非有明确的性能需求,否则建议优先使用 Span<T> 和 Memory<T> 等安全替代方案。
参考:
(2)https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/unsafe-code#fixed-size-buffers

浙公网安备 33010602011771号