当前位置: 代码网 > it编程>编程语言>Asp.net > C#使用Marshal.AllocHGlobal与ArrayPool实现内存拷贝的几种方法

C#使用Marshal.AllocHGlobal与ArrayPool实现内存拷贝的几种方法

2026年07月16日 Asp.net 我要评论
之前做的一个项目,500万像素的相机,30fps。回调里拿到的数据是intptr,想转成byte[]做后续处理。我用的marshal.copy,每帧拷5mb,看着挺正常。后来换了一台100fps的相机

之前做的一个项目,500万像素的相机,30fps。回调里拿到的数据是intptr,想转成byte[]做后续处理。

我用的marshal.copy,每帧拷5mb,看着挺正常。后来换了一台100fps的相机,cpu直接飙到80%,gc一秒触发好几次,程序开始丢帧。

问题不在marshal.copy本身慢,在频率太高了。每秒100次,每次从非托管内存拷5mb到托管数组,500mb/s的内存搬运,加上gc压力,cpu扛不住了。

前面聊过arraypool、span这几套内存拷贝方案,但是没有系统地进行和opencvsharp的mat和halcon的himage进行对比,它们的底层都是非托管内存。用好它们提供的零拷贝机制,能省掉不少不必要的数据搬运。

一、marshal.copy:最直接,但不是最快的

标准用法:

intptr pdata = getimagefromcamera();
int datasize = width * height;
byte[] buffer = new byte[datasize];
marshal.copy(pdata, buffer, 0, datasize);
// 处理buffer
marshal.copy干的是“从非托管内存复制到托管数组”的事,这是跨内存边界的数据拷贝,开销比托管数组之间的复制大。

另一层开销来自每次new byte[datasize]——每帧分配5mb的托管数组,100fps就是每秒500mb的分配量。gc频繁触发gen0和gen1回收,导致stw(stop-the-world)暂停。

实测数据(500万像素mono8,100fps) :

方式

cpu占用

gc触发频率

丢帧率

marshal.copy + new byte[]

65%

每秒多次

marshal.copy + arraypool

35%

几乎为零

marshal.copy + 预分配数组

40%

几乎为零

arraypool解决了gc问题,但marshal.copy的拷贝开销本身还在。有没有办法减少拷贝次数?

二、arraypool:借了再还,gc不慌

arraypool的核心思路:从池里借一块内存,用完还回去,下次继续用

byte[] buffer = arraypool<byte>.shared.rent(datasize);
marshal.copy(pdata, buffer, 0, datasize);
// 处理buffer
arraypool<byte>.shared.return(buffer, cleararray: false);

rent可能返回比datasize更大的数组,处理时用精确长度。return时的cleararray: false避免清空数据开销。

每个图像保存的完整模式

void imagecallback(intptr pdata, ref mv_frame_out_info_ex info, intptr user)
{
    int datasize = (int)(info.nwidth * info.nheight);
    byte[] buffer = arraypool<byte>.shared.rent(datasize);
    try
    {
        marshal.copy(pdata, buffer, 0, datasize);
        _queue.enqueue(buffer);
    }
    catch
    {
        arraypool<byte>.shared.return(buffer);
    }
}

三、memcpy + 预分配:绕开gc和池

如果不想依赖arraypool、想完全控制内存生命周期,可以预分配一个大数组,每次拷贝到固定偏移位置。

private byte[] _preallocatedbuffer;
private long _writeoffset;
public void init(int buffersize)
{
    _preallocatedbuffer = new byte[buffersize];
    _writeoffset = 0;
}
public void writeimage(intptr pdata, int datasize)
{
    // 检查空间
    if (_writeoffset + datasize > _preallocatedbuffer.length) return;
    // 拷贝到预分配数组的指定偏移位置
    marshal.copy(pdata, _preallocatedbuffer, (int)_writeoffset, datasize);
    _writeoffset += datasize;
}

这样全程只有启动时的一次大内存分配,运行时零gc分配。适合环形缓冲或连续录制的场景。

四、终极方案:直接操作intptr,跳过托管内存

如果后续处理可以不依赖托管数组,为什么不直接在intptr上操作?

span<byte>可以包装非托管内存:

intptr pdata = getimagefromcamera();
int datasize = width * height;
// 直接把intptr包装成span,零拷贝
span<byte> imagespan = new span<byte>(pdata.topointer(), datasize);
// 在span上做处理(读取像素值、计算直方图等)
for (int i = 0; i < imagespan.length; i++)
{
    byte val = imagespan[i];
    // 计算...
}

但有一个限制:如果要把数据传给其他库(如opencv的mat、skiasharp的bitmap),有的要求数据在托管内存或固定的非托管内存中。这时候必须拷贝。

什么时候可以直接操作intptr? 自己做像素级运算、算直方图、均值滤波这类纯读取操作,完全不需要拷贝,直接在intptr上操作就行。

五、opencvsharp的mat包装(零拷贝)

opencvsharp的mat可以直接包装intptr指向的内存,不需要拷贝。​​​​​​​

intptr pdata = getimagefromcamera();
mat mat = new mat(height, width, mattype.cv_8uc1, pdata);
// 直接在mat上做处理
cv2.gaussianblur(mat, mat, new size(3, 3), 0);

mat直接使用pdata指向的内存,没有任何数据拷贝。后续mat引用的是同一个内存地址,pdata在回调返回后可能被释放,所以mat必须在回调内用完,或者用clone做深拷贝。

六、halcon的himage包装(零拷贝)

halcon也可以直接包装intptr。​​​​​​​

intptr pdata = getimagefromcamera();
himage image = new himage("byte", width, height, pdata);
// 直接处理
himage filtered = image.gaussfilter(3.0);

同样零拷贝,同样要注意生命周期。

七、几种方案怎么选

场景

推荐方案

原因

高频采集(>50fps)

arraypool + marshal.copy

避免gc,复用内存

超高频采集(>200fps)

直接操作intptr/span

零拷贝,开销最小

传给opencv处理

mat包装intptr

零拷贝,mat直接引用

传给halcon处理

himage包装intptr

零拷贝,himage直接引用

跨线程/异步传递

必须拷贝(arraypool)

原始内存生命周期不可控

长时间连续采集

预分配大数组 + 偏移写入

零分配,环形缓冲

核心原则:能零拷贝就不拷贝,需要拷贝就用arraypool复用内存,不到万不得已不用new byte[]

八、三个容易踩的坑

坑一:intptr指向的内存被释放了,还在用

相机回调返回后,pdata指向的内存可能被sdk回收。如果要在回调之外使用,必须拷贝到托管内存。零拷贝方案只适用于回调函数内部同步处理。

坑二:arraypool借出来的数组比请求的大

arraypool<byte>.shared.rent(datasize)返回的数组长度可能大于datasize。传给其他函数时,用buffer.asspan(0, datasize)截取精确长度。

坑三:return时没清空数据,下一帧读到了残留

如果return用了cleararray: false,下次rent拿到的数组可能还留着上一帧的数据。覆盖写没问题,但如果是部分填充,可能有残留。不确定的时候用cleararray: true

九、总结

内存拷贝这事,核心就四个字——减少拷贝

1. 能直接操作intptr就操作,不用转托管数组(自己写像素运算)

2. 必须转数组就用arraypool,别new byte[](高频采集场景)

3. 传给opencv/halcon直接用mat/himage包装,零拷贝(第三方库支持)

4. 真要拷贝就用marshal.copy + arraypool组合(低gc、高吞吐)

把回调里的new byte[]换成arraypool,gc压力能降一个量级。把不用跨线程传递的数据直接用span包装在intptr上,连拷贝都省了。

小提示:不确定自己的数据是否需要拷贝,先看生命周期——数据只在回调函数里用,不用拷贝,直接用span包装intptr;数据要传到后台线程或存盘,必须拷贝,用arraypool。先把这句话记住,再根据具体场景选方案。

以上就是c#使用marshal.allochglobal与arraypool实现内存拷贝的几种方法的详细内容,更多关于c# marshal.allochglobal与arraypool内存拷贝的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com