之前做的一个项目,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内存拷贝的资料请关注代码网其它相关文章!
发表评论