"Effectively" create subclass of cell array to eliminate overhead when constructing large object arrays?

I'm trying to create a quaternion class, both as an attempt to better understand MATLAB (and programming in general, which I'm relatively limited in), and ultimately to use in a project for an engineering course—something like this, maybe, but at an undergrad engineering student's level of programming sophistication ;)
The issue I've run into is that constructing large arrays of these quaternion objects (100,000+ elements) takes quite a while. I'm not sure how large the arrays I'll need to use will be in practice, so it might be a non-issue, but I'd like it to be efficient in a general case if possible.
Currently, my class has one property ("Q"), which is a 4 element array containing the quaternion's coefficients. To assign this, my constructor takes in a numeric array, chops it up into a cell array ("coefs") where the element at each index is the quaternion coefficients for the corresponding index of the quaternion object array ("qtr"), then I do:
[qtr(:).Q] = coefs{:};
This is really slow (around 2 seconds for 1,000,000 elements on my computer). From browsing FAQs and using the profiler, it seems like basically all the time is going to overhead of accessing the .Q property at each array element. Chopping up the cell array (mat2cell) isn't crazy fast, but it only happens when constructing quaternions. The property accessing overhead is a concern because, when doing operations on quaternion arrays, I need to access all the properties of each input and construct a whole new quaternion for output, accessing properties all along the way. For example, the substantial part of my addition function is:
% Perform addition via cell array
q1Coefs = {q1(:).Q};
q2Coefs = {q2(:).Q};
qNewCoefs = cellfun(@plus,q1Coefs,q2Coefs,'Uni',0);
% Create & populate output quaternion
qNew(r1,c1) = quaternion;
[qNew(:).Q] = qNewCoefs{:};
I like using the cell functions because it's visually cleaner and I've been led to believe MATLAB might handle this method better than alternatives with for loops (I did try using parfor, but couldn't get it to improve performance). I compared the time it takes to create and add similarly-sized quaternions using my class with that class from the file exchange (which I assume is reasonably optimized) and they're just about the same. All this has made me wonder:
Am I able to "cut out the middleman" of converting to cell arrays and accessing the .Q property by somehow defining my quaternion arrays to be cell arrays of the coefficients? I know I can't create a subclass of cell arrays, but does that completely preclude me from the benefits of classes (operator overloading in particular, MATLAB "knowing" what a quaternion is) if I decide to define the quaternions as cell arrays? I could make my .Q property the cell array I want, but then all of my quaternion arrays will be considered "1x1 quaternions", and concatenating them becomes less useful. I'd like to avoid simply having a suite of functions that require the input to be of the form I want.
Any advice (or explanations on why what I want isn't possible) is appreciated! My classdef file is attached, which currently just does addition, subtraction, and a neat display method for individual quaternions.

2 Comments

Cell arrays are very inefficient expecially when they get very large. Here's an example for an array 1000 times smaller than yours:
N = 1000
A = ones(N,N);
ca = cell(N, N);
for k = 1 : numel(ca)
ca{k} = A(k);
end
whos
Name Size Bytes Class Attributes
A 1000x1000 8000000 double
N 1x1 8 double
ca 1000x1000 112000000 cell
Look at the Bytes used for each. The cell array takes up 14 times as much memory.
Try to avoid cell arrays if at all possible. Use tables or regular numerical arrays instead.
Hm, alright. In that case, would I want to do something like this to assign each .Q in qtr the 4 coefficients they need from coefs?
for n = 1:cOut % (number of columns in qtr = 1/4 the columns in coefs)
nCoefs = 4*n + (-3 : 0);
[qtr(:,n).Q] = deal(coefs(:,nCoefs));
end
This is about 20% quicker for a 1000x1000 coefs / 1000x250 qtr, which is nice, but still leaves pretty undesirable delays when working with arrays of that scale. Am I up against some limitation of MATLAB with respect to assigning properties? Can I vectorize the assignment of these properties in a straightforward way?

Sign in to comment.

 Accepted Answer

If I were you, I would not use arrays of objects. I would hold all of your quaternion data as an MxNx4 member of a scalar object. Below is just a sketch, and assumes your arrays will always be 2D or less.
classdef quaternionArray
properties
Q (:,:,4) = 0
end
methods
function obj=quaternionArray(Q)
obj.Q=Q;
end
function [m,n]=size(obj,varargin)
[m,n,~]=size(obj.Q)
if nargin==1, m=[m,n]; end
end
function out=subsref(obj,S)
switch S.type
case '()'
Q=cell(1,1,4);
for i=1:4
q=obj.Q(:,:,i);
Q{i}=q(S.subs{:});
end
out=quaternionArray(cell2mat(Q));
case '.'
out=obj.(S.subs);
otherwise
error '{} indexing not supported'
end
end
end
end

5 Comments

Ah! This looks exactly like what I'm looking for. I'd forgotten all about higher-dimensional arrays (which maybe doesn't bode well for trying to work with 4D numbers, haha). Do I understand correctly that:
  • This should potentially be far faster because I'll have all my numeric data after accessing only one property, instead of constantly dipping in and out of the properties of many different objects?
  • the overloaded size() function is what allows the arrays to display as anything other than "1x1 quaternionArray"?
  • The overloaded subsref() function allows me to grab all 4 of the coefficients I would expect to with only 2 indices?
The documentation for subsref() says that the officially recommended way of modifying indexing behavior is now to make your class a subclass of the matlab.mixin.indexing classes. Would this be a significant performance improvement over subsref(), or would I (as a novice programmer) potentially find it to be more trouble than it's worth?
(A somewhat tangentially-related question: could I/should I just be doing my custom display by overloading disp() rather than using matlab.mixin.CustomDisplay?)
Thank you, I'm looking forward to implementing this!
This should potentially be far faster because I'll have all my numeric data after accessing only one property, instead of constantly dipping in and out of the properties of many different objects?
Yes.
the overloaded size() function is what allows the arrays to display as anything other than "1x1 quaternionArray"?
Yes.
The overloaded subsref() function allows me to grab all 4 of the coefficients I would expect to with only 2 indices?
Yes, or even just one index, if you use linear indexing.
The documentation for subsref() says that the officially recommended way of modifying indexing behavior is now to make your class a subclass of the matlab.mixin.indexing classes.
Yeah, I have reservations about it, though. It has unexpected side effects as discussed here,
For your use case, it might work well. Your application fulfills the model of "class is a scalar masquerading as an array" mentioned in the discussion by @James Lebak, but since you say you are a beginner, I don't know if I would recommend it.
A somewhat tangentially-related question: could I/should I just be doing my custom display by overloading disp() rather than using matlab.mixin.CustomDisplay?
If mixin.CustomDisplay does what you want, I say use it.
Sweet, thanks for the extra info, and thanks again for the original suggestion!
For what it's worth, I think the general approach that @Matt J is recommending is good. You could use either modular indexing or subsref/subsasgn as he says.
Modular indexing will almost certainly perform better than subsref/subsasgn. There are more methods to implement (for example cat and empty are required by the interface).
My untested modular indexing sketch that corresponds to Matt's class is given below.
classdef quaternionArray < matlab.mixin.indexing.RedefinesParen
properties
Q (:,:,4) = 0
end
methods
function obj=quaternionArray(Q)
obj.Q=Q;
end
function [m,n]=size(obj,varargin)
% Exactly Matt's implementation
[m,n,~]=size(obj.Q);
if nargin == 1
m = [m n];
end
end
function out = cat(dim, varargin)
numCatArrays = nargin-1;
newArgs = cell(numCatArrays,1);
for ix = 1:numCatArrays
if isa(varargin{ix},'quaternionArray')
newArgs{ix} = varargin{ix}.Q;
else
error('Unable to concatenate');
end
out = quaternionArray(cat(dim,newArgs{:}));
end
end
end
methods (Access=protected)
function out=parenReference(obj,indexOp)
indices = transformIndices(indexOp(1).Indices);
obj.Q = obj.Q(indices{:});
if isscalar(indexOp)
out = obj;
return;
end
out = obj.(indexOp(2:end));
end
function obj = parenAssign(obj, indexOp, rhs)
if isscalar(indexOp)
ti = transformIndices(indexOp(1).Indices);
obj.Q(ti{:}) = rhs.Q;
return;
end
obj.(indexOp(2:end)) = rhs;
end
function out = parenListLength(~,~,~)
out = 1;
end
function out = parenDelete(obj, indexOp)
ti = transformIndices(indexOp(1).Indices);
obj.Q(ti{:}) = [];
end
end
methods (Static, Access=public)
function obj = empty()
obj = quaternionArray([]);
end
end
end
function indices = transformIndices(arraySize, indices)
if numel(indices) == 1
[idx, idy] = ind2sub(arraySize, indices{1});
indices = {idx, idy, 1:4};
else
assert(numel(indices == 2));
indices{3} = 1:4;
end
end

Sign in to comment.

More Answers (0)

Categories

Products

Release

R2020b

Asked:

on 6 Mar 2022

Edited:

on 8 Mar 2022

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!