34.3.2 Indexed Assignment Optimization

Octave’s ubiquitous lazily-copied pass-by-value semantics implies a problem for performance of user-defined subsasgn methods. Imagine the following call to subsasgn

ss = substruct ("()", {1});
x = subsasgn (x, ss, 1);

where the corresponding method looking like this:

function x = subsasgn (x, ss, val)
  x.myfield (ss.subs{1}) = val;

The problem is that on entry to the subsasgn method, x is still referenced from the caller’s scope, which means that the method will first need to unshare (copy) x and x.myfield before performing the assignment. Upon completing the call, unless an error occurs, the result is immediately assigned to x in the caller’s scope, so that the previous value of x.myfield is forgotten. Hence, the Octave language implies a copy of N elements (N being the size of x.myfield), where modifying just a single element would actually suffice. In other words, a constant-time operation is degraded to linear-time one. This may be a real problem for user classes that intrinsically store large arrays.

To partially solve the problem Octave uses a special optimization for user-defined subsasgn methods coded as m-files. When the method gets called as a result of the built-in assignment syntax (not a direct subsasgn call as shown above), i.e., x(1) = 1, AND if the subsasgn method is declared with identical input and output arguments, as in the example above, then Octave will ignore the copy of x inside the caller’s scope; therefore, any changes made to x during the method execution will directly affect the caller’s copy as well. This allows, for instance, defining a polynomial class where modifying a single element takes constant time.

It is important to understand the implications that this optimization brings. Since no extra copy of x will exist in the caller’s scope, it is solely the callee’s responsibility to not leave x in an invalid state if an error occurs during the execution. Also, if the method partially changes x and then errors out, the changes will affect x in the caller’s scope. Deleting or completely replacing x inside subsasgn will not do anything, however, only indexed assignments matter.

Since this optimization may change the way code works (especially if badly written), a function optimize_subsasgn_calls is provided to control it. This feature is enabled by default. Another way to avoid the optimization is to declare subsasgn methods with different output and input arguments like this:

function y = subsasgn (x, ss, val)