避免不必要的数据副本
将值传递给函数
当使用输入参量调用函数时,MATLAB® 会将值从调用函数的工作区复制到被调用函数的参数变量中。但 MATLAB 会应用各种技术,避免在不需要时复制这些值。
MATLAB 没有像 C++ 之类的语言那样提供定义值引用的方法。但 MATLAB 允许多个输出和多个输入参数,因此您知道有哪些值要输入到函数中,以及要从函数中输出哪些值。
写入时复制
如果函数未修改输入参量,则 MATLAB 不会复制输入变量中包含的值。
例如,假设您将一个大型数组传递给函数。
A = rand(1e7,1); B = f1(A);
函数 f1 将输入数组 X 中的每个元素乘以 1.1,并将结果赋给变量 Y。
function Y = f1(X) Y = X.*1.1; % X is a shared copy of A end
由于此函数并未修改输入值,因此局部变量 X 和调用方工作区中的变量 A 共享数据。执行 f1 后,赋给 A 的值不变。调用方工作区中的变量 B 包含按元素相乘的结果。输入按值进行传递。但是,调用 f1 时没有制作副本。
函数 f2 确实会修改输入变量的本地副本,从而导致本地副本与输入 A 不共享。现在,函数中的 X 值在调用方工作区中是输入变量 A 的独立副本。当 f2 将结果返回到调用方的工作区时,局部变量 X 将被销毁。
A = rand(1e7,1); B = f2(A);
function Y = f2(X) X = X.*1.1; % X is an independent copy of A Y = X; % Y is a shared copy of X end
以 MATLAB 表达式方式传递输入
您可以将一个函数的返回值用作另一个函数的输入参量。例如,使用 rand 函数直接为函数 f2 创建输入。
B = f2(rand(1e7,1));
保存 rand 返回值的唯一变量是函数 f2 的工作区中的临时变量 X。在调用方的工作区中,不存在这些值的共享副本或独立副本。直接传递函数输出可以节省在被调函数中创建输入值副本所需的时间和内存。当输入值不会再次使用时,可以使用此方法。
就地赋值
当您不需要保留原始输入值时,可以将函数的输出赋给与输入相同的变量。
A = f2(A);
就地赋值发生在前面介绍的写入时复制行为之后:修改输入变量值会生成这些值的临时副本。
MATLAB 在某些条件下可以应用内存优化。请参考以下示例。canBeOptimized 函数在变量 A 中生成一个很大的随机数数组。然后它调用局部函数 fLocal,传递 A 作为输入,并将局部函数的输出赋给相同的变量名称。
function canBeOptimized A = rand(1e7,1); A = fLocal(A); end function X = fLocal(X) X = X.*1.1; end
由于对局部函数的调用 A = fLocal(A) 将输出赋给变量 A,因此 MATLAB 在执行函数的过程中不需要保留 A 的原始值。对 fLocal 内的 X 所做的修改不会产生数据副本。赋值 X = X.*1.1 只是就地修改 X,不会为乘法结果分配新数组。消除局部函数中的副本可以节省内存并提高大型数组的执行速度。
但是,如果局部函数中的赋值需要数组索引,则 MATLAB 无法应用此优化。例如,修改在 updateCells 中创建的元胞数组需要对局部函数 gLocal 中的 X 进行索引。对于每个循环迭代 i,X{i} = X{i}*1.1 形式的循环赋值产生与 X{i} 大小相同的临时变量,用于计算和存储 X{i}*1.1 的值。MATLAB 在将临时变量的值赋给 X{i} 后销毁该临时变量。
function updateCells C = num2cell(rand(1e7,1)); C = gLocal(C); end function X = gLocal(X) for i = 1:length(X) X{i} = X{i}*1.1; end end
存在几项其他限制。在函数抛出错误时可能会用到变量的情况下,MATLAB 不能应用内存优化。因此,这种优化不适用于脚本、命令行、对 eval 的调用以及 try/catch 代码块内的代码。此外,MATLAB 在调用函数的执行过程中,当原始变量可直接访问时,不会应用内存优化。例如,如果 fLocal 是嵌套函数,则 MATLAB 无法应用优化,因为它会与父函数共享变量。最后,当指定的变量声明为全局变量或持久变量时,MATLAB 也不会应用内存优化。
调试使用就地赋值的代码
当 MATLAB 对赋值语句应用就地优化时,赋值左侧的变量会设置为一种临时状态,这种状态使得变量在 MATLAB 执行赋值语句的右侧之前都不可访问。如果 MATLAB 在调试器中停止,而此时还未将语句右侧的执行结果赋给变量,这时检查左侧的变量就会报错,指示该变量不可用。
例如,此函数的变量 A 和 B 的维度不匹配。
function A = inPlace A = rand(100); B = rand(99); dbstop if error A = A.*B; end
执行该函数会抛出错误并在调试器中停止运行。
inPlace
Arrays have incompatible sizes for this operation. Error in inPlace (line 5) A = A.*B;
在调试模式下尝试查看变量 A 的值会导致错误,因为该变量暂时不可用。
K>> A
Variable "A" is inaccessible. When a variable appears on both sides of an assignment statement, the variable may become temporarily unavailable during processing.
为了在调试时获得更大的灵活性,请重构您的代码以删除就地赋值。例如,将结果赋给另一个变量。
function A = inPlace A = rand(100); B = rand(99); dbstop if error % Assign result to C instead of A C = A.*B; A = C; end
则变量 A 在调试器中可见。
为什么要使用传值语义
MATLAB 在向函数传递参量以及从函数返回值时使用传值语义。在某些情况下,传值会在被调函数中生成原始值的副本。但是,传值语义也有一些好处。
当调用函数时,您知道输入变量不会在调用方工作区中被修改。因此,不需要只是为了防止这些值可能被修改而在函数内或在调用位置创建输入副本。只有赋给返回值的变量会被修改。
此外,如果按引用传递变量的函数中发生错误,可以避免出现损坏工作区变量的可能性。
句柄对象
有一些特殊的对象称为句柄。保存同一个句柄的副本的所有变量都可以访问和修改同一个底层对象。在特定的情况下,即当对象表示物理对象(例如窗口、绘图、设备或人)而不是数学对象(如数字或矩阵)时,句柄对象很有用。
句柄对象从 handle 类派生而来,该类提供事件和侦听程序、析构函数方法以及动态属性支持等函数。
有关值和句柄的详细信息,请参阅句柄类和值类的比较和Which Kind of Class to Use。