You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#与C++间实现只读共享内存避免数据拷贝?

问题描述

我有一段C#代码用于生成3D几何数据,需传递给C++库进行渲染。当前通过互操作传递数据的方式如下(已简化传递的数组数量):

void LoadMeshData(Mesh meshData)
{
    var verticesArraySize = meshData.Vertices.Length;
    unsafe
    {                
        fixed (float* vertices = meshData.Vertices)
        {
            Interop.Data.MeshDataObject meshDataObject;
            meshDataObject.vertexArraySize = verticesArraySize;
            meshDataObject.vertexData = new IntPtr(vertices);
            SceneInterop.AddMeshData(meshDataObject);
        }
    }
}

Interop.Data.MeshDataObject是与C端匹配的POD结构体。由于fixed语句在调用后会超出作用域,目前只能将数组复制到C结构体中,这会造成大量内存浪费。我希望能将C#内存标记为永久“固定”,是否可行?C++库仅需读取该数据,且C#端仍需保留该数据。请问还有其他可避免数据拷贝的方案吗?


解决方案

1. 能否永久固定C#托管内存?

不行。CLR的垃圾回收机制依赖移动托管对象来优化内存布局,没有官方支持的“永久固定”托管内存的方式。强行通过非官方手段(比如无限期持有fixed块、调用未公开API)固定内存会导致严重的内存碎片,甚至引发GC崩溃,完全不可取。

2. 避免数据拷贝的可行方案

方案一:用GCHandle长期固定托管数组

这是官方支持的长期固定方式,通过GCHandle.Alloc固定数组,直到C++不再需要访问时再释放:

// 类级别集合,用于保存所有固定的句柄,方便后续释放
private List<GCHandle> _pinnedHandles = new List<GCHandle>();

void LoadMeshData(Mesh meshData)
{
    var vertices = meshData.Vertices;
    var verticesArraySize = vertices.Length;
    
    // 固定数组,阻止GC移动其内存
    var handle = GCHandle.Alloc(vertices, GCHandleType.Pinned);
    _pinnedHandles.Add(handle);
    
    // 获取固定后的内存地址
    IntPtr vertexPtr = handle.AddrOfPinnedObject();
    
    Interop.Data.MeshDataObject meshDataObject;
    meshDataObject.vertexArraySize = verticesArraySize;
    meshDataObject.vertexData = vertexPtr;
    
    SceneInterop.AddMeshData(meshDataObject);
}

// 在场景销毁、资源释放等时机,释放所有固定的内存
void ReleasePinnedResources()
{
    foreach (var handle in _pinnedHandles)
    {
        if (handle.IsAllocated)
            handle.Free();
    }
    _pinnedHandles.Clear();
}

注意:必须在合适时机释放GCHandle,否则会造成内存泄漏;长期固定的内存会增加GC的负担,需合理控制固定的对象数量。

方案二:使用非托管内存分配

直接在C#中分配不受GC管理的非托管内存,写入数据后传递给C++,C#端可通过指针或映射托管数组继续访问:

// 分配非托管内存,大小为数组元素数×单元素字节数
IntPtr vertexBuffer = Marshal.AllocHGlobal(meshData.Vertices.Length * sizeof(float));
// 将托管数组数据写入非托管内存(仅一次拷贝)
Marshal.Copy(meshData.Vertices, 0, vertexBuffer, meshData.Vertices.Length);

// 传递内存地址给C++
Interop.Data.MeshDataObject meshDataObject;
meshDataObject.vertexArraySize = meshData.Vertices.Length;
meshDataObject.vertexData = vertexBuffer;
SceneInterop.AddMeshData(meshDataObject);

// C#端如需继续访问,可直接通过指针操作(无拷贝)
unsafe
{
    float* vertexPtr = (float*)vertexBuffer.ToPointer();
    // 示例:修改第一个顶点数据
    vertexPtr[0] = 1.0f;
}

// 当不再需要该内存时,手动释放
// Marshal.FreeHGlobal(vertexBuffer);

这种方式的内存不受GC控制,C++可长期持有指针,但需严格手动管理内存,避免泄漏。

方案三:让C++分配内存,C#直接写入

如果C库支持,可由C提前分配内存,C#直接将数据写入该内存区域,完全避免拷贝:

// 调用C++接口分配对应大小的内存
IntPtr vertexBuffer = SceneInterop.AllocateVertexBuffer(meshData.Vertices.Length);
// 将C#数组直接写入C++分配的内存
Marshal.Copy(meshData.Vertices, 0, vertexBuffer, meshData.Vertices.Length);

// 通知C++内存已填充完成
SceneInterop.AddMeshData(new Interop.Data.MeshDataObject
{
    vertexArraySize = meshData.Vertices.Length,
    vertexData = vertexBuffer
});

这种方式的内存生命周期由C负责,适合C主导资源管理的场景。

方案四:使用Span<T>(.NET Core/.NET 5+)

对于.NET Core及更高版本,可结合Span<T>和GCHandle实现更简洁的固定逻辑,本质与方案一一致:

void LoadMeshData(Mesh meshData)
{
    Span<float> verticesSpan = meshData.Vertices.AsSpan();
    var handle = GCHandle.Alloc(verticesSpan, GCHandleType.Pinned);
    _pinnedHandles.Add(handle);
    
    IntPtr vertexPtr = handle.AddrOfPinnedObject();
    
    // 传递地址给C++...
}

Span<T>提供了更安全的内存访问方式,避免直接操作指针的风险。


内容的提问来源于stack exchange,提问作者mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 14:45:27