MATLAB 中的写时复制和 varargin

Copy-on-Write and varargin in MATLAB

MATLAB 文档有 Avoid Unnecessary Copies of Data 部分,其中可以找到以下语句:

Copy-on-Write

If a function does not modify an input argument, MATLAB does not make a copy of the values contained in the input variable.

在那个上下文中没有关于 varargin 的字眼。我试图制定一个能够监控内存使用情况的功能,但没有成功。所以我想问一下:写时复制功能是否适用于 varargin?

假设函数 function Y = f(x,y,z) 与函数 function Y = f(varargin)。在第一种情况下,函数调用 f(a,b,c) 不会复制 abc(无论变量类型如何)。在第二种情况下,函数调用 f(a,b,c) 的行为不明确。 MATLAB 会指出 varargin{1}avarargin{2}bvarargin{3}c 而没有显式创建元胞数组,还是 varargin abc 的显式串联(因此内存将在元胞数组中存储三个变量的副本)?

varargin 是一个元胞数组。当您将一个对象放入元胞数组时,该对象并没有真正被复制,但它的引用计数增加了:

a = [1 2 3];
b = 5;
c = {4, 6};
varargin = {a,b,c};

这里只是增加了abc指向的对象的引用计数。当您这样做时:

varargin{1}(2) = 7;

因为它要写入 a 指向的对象,所以它会复制该数组对象并将新数组的第二个元素设置为 7。新数组放置在 varargin 的第一个单元格中,a 指向的对象的引用计数减少。然而,MATLAB jit 编译器可能会做更多的优化,它可能会就地创建变量,因此根本不会创建元胞数组。另一种可能的优化可能与标量等小对象有关。它们是廉价对象,可以廉价复制,并且它们可能没有引用计数。

一样,MATLAB 的写时复制机制(也称为惰性复制)适用于每个副本,而不仅仅是函数参数。

证明这一点的一种方法是使用从 Yair's Undocumented MATLAB Blog 修改的以下 MEX 文件:

#include "mex.h"
#include <cstdint>
void mexFunction( int /*nlhs*/, mxArray* plhs[], int nrhs, mxArray const* prhs[]) {
   if (nrhs < 1) mexErrMsgTxt("One input required.");
   plhs[0] = mxCreateNumericMatrix(1, 1, mxUINT64_CLASS, mxREAL);
   std::uint64_t* out = static_cast<std::uint64_t*>(mxGetData(plhs[0]));
   out[0] = reinterpret_cast<std::uint64_t>(mxGetData(prhs[0]));
}

您可以将其另存为 getaddr.cpp 并使用 mex getaddr.cpp 进行编译。您现在有一个函数可以显示存储数组数据的地址。

例如,如果我们复制一个数组,副本将具有相同的数据地址。当我们写入副本时,它的数据地址会改变:

>> a=zeros(5);
>> getaddr(a)
ans =
  uint64
   105553130112928
>> a(1)=1;
>> getaddr(a)
ans =
  uint64
   105553130112928
>> b=a;
>> getaddr(b)
ans =
  uint64
   105553130112928
>> b(1)=4;
>> getaddr(b)
ans =
  uint64
   105553130078944

元胞数组也是如此,这与问题直接相关,因为输入参数收集在元胞数组中 varargin:

>> a=zeros(5);
>> b=zeros(8);
>> v={a,b};
>> getaddr(a)
ans =
  uint64
   105553130246144
>> getaddr(v{1})
ans =
  uint64
   105553130246144

请注意,元胞数组只不过是数据类型为“数组”的数组,因此可以包含任何类型的数组作为其元素。基本上它是一个指向其他数组的指针数组。

这是一个乍看起来更复杂的主题。部分原因是它没有完整记录在 MATLAB 文档中,部分原因是多年来幕后的共享机制发生了变化。首先,我将简要描述什么是 MATLAB 变量。然后我将描述 MATLAB 使用的各种共享机制。最后,我将描述这些共享机制在幕后如何在 MATLAB 中使用。

一个 MATLAB 变量:

MATLAB 变量基本上是一个称为 mxArray 的 C 结构,具有用于保存大小、class、存储 class 和数据指针等信息的各种字段。此 C 结构的地址通常称为变量的“结构地址”,数据指针通常称为“Pr”、“Pi”、“Ir”、“Jc”等。对于更高版本的 MATLAB complex数据是交错的,没有 Pi 指针。对于固有数字、逻辑和字符 classes,数据直接位于 Pr 和 Pi 数据指针(以及用于稀疏变量索引的 Ir 和 Jc 指针)之后。对于 OOP classdef class 变量,在实际数据所在的数据指针后面有一个专有结构,用户无法直接访问它(IMO 的一个基本缺陷限制了 OOP 的实用性classmex 例程中的 def 变量。

变量共享:

MATLAB 通过以下方式共享变量:

深层复制:有问题的变量不与任何其他变量共享任何内容。

共享数据拷贝:多个变量可以有不同的结构地址但有相同的数据指针。例如,这通常是直接对整个变量赋值或对整个变量进行重塑的结果。 mxArray (CrossRef) 中曾经有一个字段,它是所有这些变量的链表的一部分。更高版本的 MATLAB 只有一个计数器来告诉您有多少变量是列表的一部分,但是列表本身不再可供用户访问。

参考副本:多个变量可以具有完全相同的结构地址。 mxArray (refcount) 中的一个字段指示有多少变量共享相同的结构地址。这是通常用于单元格或结构变量元素的内容。

父副本:不像上面那样真正是副本本身,但在嵌套结构和元胞数组中,由于上游共享,变量最终可以与变量的其他部分或其他变量中的变量共享。 mxArray 本身对此没有任何指示。即,CrossRef 和 refcount 看似未共享,但实际上正在发生共享。

Handle Copy:如果OOP classdef变量是从handle派生的,那么多个变量本质上是共享的。 mxArray 本身不会对此进行指示,并且这些变量不遵循正常的“写时复制”或“惰性复制”规则。

什么时候使用共享?

这是它变粘的地方。规则没有公布,并且多年来一直在变化。我能做的就是举个例子:

-- 共享数据复制示例 --

A = B; % direct whole variable assignment (earlier versions of MATLAB)

A{1} = B; % assigning from workspace into cell or struct (earlier versions of MATLAB)

A = reshape(B,whatever); % reshape of full variable

B{1} % cell or struct element in expression or assignment

fun(B); % function arguments are passed as shared data copies of original

A = typecast(B,'whatever'); % later versions of MATLAB only. Early versions did deep copy.

-- 参考复制例子--

A = B; % direct whole variable assignment (later versions of MATLAB)

A{1} = B{1}; % assignment among cell or struct elements

A = 1:5; % literal assignment of small variable can result in background reference copy

--父副本示例--

A.x = 5; B = A; % A.x is sharing with B.x through the parent A and B sharing.

原题:

非 Mex 函数参数通过某种类型的复制机制传递到函数中。无论是文字变量还是 varargin,通常都会使用共享数据副本(用于显式参数或作为构建 varargin 元胞数组的结果)。我看到的唯一例外是有时嵌套函数可以传递标量变量的深层副本而不是共享数据副本。因此,“写时复制”或“惰性复制”机制适用于函数内部的文字参数和可变参数,因为在这两种情况下,您实际上都在使用共享数据副本(或者可能是以后版本中的引用副本) MATLAB) 函数内部的原件。需要注意的是,如果您在函数调用中使用特殊语法,您可以让 MATLAB 解析器识别您正在尝试“就地”修改变量并避免否则会发生的深层复制。

Mex 函数参数有些不同。旧版本的 MATLAB 总是习惯于传入原始变量结构地址,但更高版本的 MATLAB 使用与非 Mex 函数相同的规则并传入共享数据副本(尽管标量可能作为深层副本传入)。

所以函数中的“写时复制”或“惰性复制”机制真的没什么特别的。传入了原始变量的共享数据副本或引用副本。因此,如果不对其进行任何更改,则不会在函数内部进行深拷贝。如果您确实更改了参数变量的元素,那么将首先进行深拷贝(即取消共享)。但这是在 MATLAB 的任何级别都会发生的行为......如果您更改共享变量的元素,则必须首先进行深度复制。无论您是否在函数内部,都适用相同的规则...如果变量是共享的并且您更改了一个元素,那么将首先进行深度复制。