在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Matlab中变量拷贝的原理?-- copy-on-write和mex参数传递机制解析题记剖析:
C、C++语言里调用函数时有三种不同的传参方式,分别为:传值,传址(即指针),传引用。他们之间的 区别可以用下面的三句话高度概括:
传值不改初衷,
传址远程操控,
引用就是别名。
当采用传值的方式时,函数内的任何操作均不会对实参造成任何影响,而后面的两种参数传递方式则可以对原始实参数据造成影响。为减少对原始数据的修改,Matlab统一采用了传值的参数传递方式,函数内的任何读写操作均不会影响实参的原始数据。总而言之,在matlab里不要指望通过参数传递实现对原始数据的修改。如果需要修改,则需要借助matlab函数的返回结果进行间接操作。
本质概括:写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。字面意思:“”当write的时候才会copy"。 其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
Matlab真是让人又爱又恨,诸多强大的工具包以及矩阵(向量)运算的便捷与高效让人欲罢不能(那种一行代码抵C++等效的10+行循环代码的感觉真是爽到不行)。不过最近做实验用Matlab让我吃了不少苦头,主要就是因为对它的copy-on-write机制以及mex文件的参数传递机制不了解。另外感觉这方面内容虽然一般不怎么涉及,但是对于理解Matlab并且充分发挥出它对矩阵运算的高效性而又不陷入无谓且难以察觉的臭虫堆(切身体会啊!)十分重要,又加之中文的关于这方面的材料十分稀缺(我几乎就没见到……),所以在这里稍作总结,便于今后自己查阅也方便有需要的人。
1. 先解释下2个关键词: a. copy-on-write:我们晓得变量在内存里存储都有一个地址,而有的时候我们需要把一个变量A的值拷贝一份产生一个新的变量B。所谓copy-on-write说的就是制造这么一个假象,让你觉得好像已经拷贝过了,但是其实只是让那个新变量也指向原来的内存地址(因为这样对于仅仅读取变量值而言是等效的),只有当那个值必须发生改变的时候(或者说被写入的时候)才赶紧把原先的内容拷贝出来,存到一个新的地方(如果改写的是A,就存给B;如果改写的是B,就存给A)。 说白了就是偷懒+说谎——直到要被戳穿了不得已为止。不过这样做当然是有好处的,因为拷贝内存是有开销的(即使是线性开销),因此能省则省呗。特别对于Matlab这样的动不动开个庞大矩阵做运算的语言来讲,copy-on-write对于性能提升至关重要。 b. mex传参:mex是"Matlab Executable"的缩写,是个非常强大的机制。有了它以后,Matlab难以体现其优势的代码(比如多重循环)就可以转而用C(C++貌似也可以)或者FORTRAN来实现,通过预先编译产生动态链接库文件(就是dll)为Matlab进程调用,并且调用接口与直接调用Matlab函数几乎完全一样(好吧,只是"几乎"而已,不然我就不会碰到那些个bug了……)。 具体的mex介绍不是本文重点,就此略过。这里所谓的"mex传参"就是指在Matlab里调用mex函数的时候,不同的传参方式对于mex函数实际运行的影响。
2. Matlab的变量类型有蛮多种,为了后面方便讨论,我们这里采用以下划分标准: a. Scalar类型:就是单个的数值(实数或者虚数都可以,总之是个复数吧),也可以理解成1*1的矩阵; b. Array类型:包括最简单的向量(1*n或者n*1),矩阵(m*n)以及多维向量(n1*n2*...*nk),其中的元素是Scalar类型; c. Structure类型:由一些Field组成(A.f1, A.f2, ...),其中每个Field值可以是Scalar类型、Array类型或者Structure类型; d. 其他类型:所有不符合上述定义的其他合法类型(比如String类型,Cell类型等等)。 备注:我们这样分类仅仅为了后面讨论方便,因此Structure和Array都是狭义的,后文所得结论皆针对此狭义定义而言(倒也不是说对广义的就不成立,是我懒得做进一步验证了……)
3. 有两种不同的情形都可能导致copy-on-write,即赋值与函数传参,这里我们再做点小约束(假设A和B是两个Matlab变量): a. 赋值:就是单纯的A=B,先不考虑对A或者B做任何附加操作,比如下标索引:A(1:5)=B(1:5)、算术运算:A=B.^2之类的; b. 传参:就是单纯的f(A)(当然f(A, B, ...)对于我们讨论A也无妨啦) 备注:其实对于很多附加操作,结论很容易推出来,不过,呃,为了严谨记么,就先不考虑它们了。
OK了,上结论!!!
4. copy-on-write: a. 对于赋值情形:Scalar变量没有copy-on-write;Array变量有;Structure变量的所有Field都是copy-on-write(不管该Field是不是Scalar类型变量),但是各管各的,互不影响。 举例:A = 1; B = A; (B和A不共享数据,分别存在各自的内存地址里)
A = [1 2]; B = A; (B和A共享数据,没有发生实际拷贝) B(1) = 0; (copy-on-write!A的内存布局不变,B的数据存到新的地方)
A.f1 = 1; A.f2 = [1 1]; B = A; (B和A完全共享数据,包括A.f1和B.f1) B.f1 = 2; (copy-on-write!A的内存布局不变,B.f2也不变,B.f1的数据存到新的地方) 备注: 1. 对于多维向量,因为实际存储在内存中的时候和列向量并没有区别,都是连续存储的,所以发生copy-on-write的时候是整体拷贝,并不会因为只修改了其中某一维而继续共享其他维的数据。 2. 这里(点我)举报了Matlab的相关臭虫一枚,感兴趣的可以看下。大意是说,在函数里初始化一个向量,然后修改其中一个元素值,你说会不会copy-on-write咧?(答案是不一定!如果是这么初始化的:data=[0 0 0 0],那么修改data(1)=5之后data的数据整个copy了一份挪了窝;而如果是这样呢:data(1:4)=0或者data=zeros(1,4)?那么修改data(1)=5之后数据还在原地,没有拷贝!) b. 对于传参情形:比较清楚,统统都是copy-on-write!
备注: 1. 网上看到有的材料说Matlab可能在调用函数前进行pre-parse,先判断一下参数在函数中会不会被修改,如果是read-only的函数,就copy-on-write;如果有修改的可能,就直接拷贝一份传进去。但是我实验下来并不是这样!传参的时候全部都是copy-on-write,也就是说一旦参数在函数里被修改,那么马上拷贝一份新的出来,函数外的变量不会受到影响(具体的实验方法后面会提到,也许对于不同版本的Matlab结论不同,列位看官也可以自己试一下)。 2. 这种机制并不完全尽如人意,比如这里(点我)就给Matlab的开发人员提了一个很好的问题,比如我现在有这么一个函数: function data = transform(data) data = data + 1; end 其实我只需要这个函数帮我把data加1就完了,但是实际调用"A = transform(A)"的结果是:会发生2次A的拷贝!第1次是在data+1完了之后赋值更新data的时候(因为copy-on-write,传参的时候其实并没有拷贝),第2次是函数结束的时候把数据返回给A。copy-on-write把拷贝尽可能延迟当然很好,可是在这个情形下,我们其实更加希望——索性不要拷贝!呃,这个说起来其实也不是copy-on-write的错啦……
5. mex传参:这里先稍微介绍一下mex传参的机制。其实Matlab里的所有Scalar变量和Array变量在内存里的存储方式都差不多,就是从某一个首地址P开始连续存储(对于矩阵或是多维数组而言,就是以前面的维数优先展开成向量存储,比如一个2*2的矩阵A存起来就是A(1,1)、A(2,1)、A(1,2)、A(2,2)依次排开)。所以到了mex函数里,其实就只看到数组形式:想索引A(1,2)?就用a(2)吧(假设a是A对应的数组首地址)。再加上Matlab函数传参的copy-on-write机制(注意传参发生时,还在Matlab中),我们其实在mex函数里就可以直接取到参数的实际内存地址(还没有发生任何拷贝哦~),就像是在传引用。 因此针对上面的备注2里提到的问题,我们就可以利用mex巧妙地加以解决。怎么做呢?写一个mex文件,没有返回值(调用的时候直接写transform(A)),实现"data = data + 1"的等效功能,然后利用copy-on-write!它保证把参数数据的真实内存地址信息递交给mex函数(用mxGetPr可以取到首地址,用mxGetDimensions可以取到维数信息,其实也就是数据长度),之后就可以直接对数据进行操作啦,完事之后直接返回,一次拷贝都没有!不必担心copy-on-write在你改写数据的时候出来捣乱,因为你的改写发生在mex函数里,动态加载运行以后Matlab移交控制权,对其无能为力,想要拷贝也无法了! 很赞是不是?知道了是很赞,可是假设你不晓得这其中有这些小秘密咧(比如昨天的我)……就会莫名奇妙地发现调用函数以后——谁动了我的参数!以后可得注意了。 硬币的另一面是:有时候我们真的需要在函数里修改参数值,但是又不希望函数外的变量受到影响,怎么办?(如果不使用mex,这个问题根本不存在,因为Matlab的copy-on-write保证了这一点) 这里提供一个办法,或曰小伎俩,就是在传参的时候不要老老实实传,像这样:f(A),而是加一个全程下标索引,像这样:f(A(1:end))! 为什么这样可行呢? 因为当Matlab知道你要传的参数仅仅是真实数据的其中一部分的时候(我们只不过用"1:end"就能轻易地骗倒它),原先的"连续存储"就失效了(多维向量的连续下标索引出来的数据在一维展开的情形下一般都是散落在内存各处的),但是mex函数接受的参数必须是数组形式——换言之,在内存里是连续存储的,所以Matlab只好把这些数据拷贝出来重新在内存里连续排好,再传给mex函数——也就是说,我们强制Matlab在mex传参的时候发生一次参数拷贝。然后我们就能在mex函数里放心地对参数为所欲为了~ 不过这样还带来了一个副产品——有时候我们希望在mex传参的时候把实际是整型的变量用int32转换之后再传(Matlab默认的类型都是double)。在这个转换的过程中,因为double和int类型在内存中的表示是截然不同的,因此必然导致原来的参数数据不能直接传给mex,所以这种时候也会发生"拷贝"。 总结一下就是:Matlab里的mex传参在直接引用参数的情况下(比如f(A))就相当于传引用,mex函数直接对参数进行读写操作,不发生参数拷贝;而在原参数的内存存储形式不能直接以数组形式传递给mex函数的时候,就会发生参数拷贝,从而mex函数对参数所作的任何修改都对外透明。
6. 文中关于copy-on-write的结论部分来自于这里与这里,且都经过本人实验证实,实验方法为采用"format debug"命令(很遗憾,似乎该命令没有文档说明……)。基本用法如下: >> format debug >> a = magic(2) % 注意不要打分号";" a =
Structure address = 16849840 % a变量的结构体地址(元信息) m = 2 % 行数 n = 2 % 列数 pr = 1a321130 % 实部内存首地址 pi = 0 % 虚部内存首地址 1 3 % 数据内容 4 2 通过设断点和观察输出就可以看到各变量的内存地址了,进而可以甄别到底是发生instant-copy还是copy-on-write。 关于"Structure address"(以下简称"Sa")到底是个什么东西,我曾经以为我知道,但是做了几个实验以后就彻底迷惑了。实验结果是——对Scalar或者Array变量进行赋值,得到新的Sa;对Structure类型变量进行赋值,Sa不变;传参时不管是Scalar/Array/Structure变量,都得到新的Sa……哪位高人给指点指点! 文中关于mex传参的结论来源于我的切身的惨痛的经历,没有进行进一步查证,不过我觉得我的解释挺靠谱的,列为看官爱信不信~
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论