【问题标题】:Copy-on-Write and varargin in MATLABMATLAB 中的 Copy-on-Write 和 varargin
【发布时间】:2021-04-17 19:14:41
【问题描述】:

MATLAB 文档的 Avoid Unnecessary Copies of Data 部分可以找到以下语句:

写时复制

如果函数不修改输入参数,MATLAB 不会复制输入变量中包含的值。

在这种情况下,没有关于 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 而不显式创建元胞数组,还是 vararginab 的显式串联和c(因此内存将在元胞数组中存储三个变量的副本)?

【问题讨论】:

标签: matlab memory memory-management copy-on-write


【解决方案1】:

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

一个 MATLAB 变量:

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

变量共享:

MATLAB 以下列方式共享变量:

深拷贝:有问题的变量不与任何其他变量共享任何内容。

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

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

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

句柄复制:如果 OOP classdef 变量是从句柄派生的,那么多个变量本质上是共享的。在 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.

原问题:

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

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

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

【讨论】:

  • 感谢您详细解释 C mex 行为。但是我很想知道新的 C++ API 是否是 C mxArray 的包装器,以及 jit 编译器是否使用 C mex 结构。他们是否可以使用比 C mex 更低级别的数据结构和 API?
  • 可能无法确定这一点,因为它没有记录在案。我们确实知道 MATLAB 级别的所有变量都是 mxArray,这就是在调用者级别来回传递的内容。 C++ 包装器是否在其专有类代码中在后台传递 mxArrays,或者他们是否只是从接口处的 mxArrays 中提取相关信息和指针并在 C++ 类代码中使用这些信息和指针,这是任何人的猜测。由于 C++ 代码被记录为使用写时复制,我们也知道它可以访问上面讨论的共享信息。
  • 但是如果 C++ 类访问共享信息,并且可以取消共享一个数组,它必须有一个指向底层 mxArray 结构的指针。一个共享数组不能有两个计数器。
【解决方案2】:

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 文档中找到该信息。他们绝对应该包括它。
  • 换句话说,在基本实现中,我们可以将 MATLAB 变量视为指针,将元胞数组视为指针数组。
  • 上述示例的实际行为将取决于 MATLAB 版本。在大多数 MATLAB 版本中,varargin{1} 最终将成为 a 的共享数据副本,而不是 a 的引用副本。只有在更高版本的 MATLAB 中,您才能获得参考复制行为。但是,在任何一种情况下,如果您更改其中任何一种的元素,写入时复制规则都将适用。
【解决方案3】:

作为@rahnema1 stated,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 C API(实际上是关于 C 的一般知识)的了解接近于零,所以我没有给予适当的关注,也没有意识到真正的这种方法的潜力。现在我发现它是任何对内存管理和 MATLAB 感兴趣的人的圣杯。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-10
  • 2014-03-25
  • 1970-01-01
  • 2011-05-28
相关资源
最近更新 更多