用Java对象模仿Matlab结构体

3
这是我的问题(大局观)。我有一个项目,使用了大而复杂的(也就是包含多个嵌套结构层次)Matlab结构。这是可预见的缓慢(尤其是在尝试加载/保存时)。我试图通过将其中一些结构转换为Java对象来提高运行时间。但问题在于,这些Matlab结构中的数据在很多地方都被访问,所以任何需要重写访问语法的事情都是不可行的。因此,我需要Java对象尽可能地模拟Matlab结构的行为,特别是在访问其中存储的值时(这些值只在一个地方设置,因此Java中没有运算符重载设置的因素需要考虑)。
我遇到的问题(小局观)在于从这些结构的数组中访问数据。例如:
person(1)
  .age = 20
  .name
    .first = 'John'
    .last = 'Smith

person(2)
  .age = 25
  .name
    .first = 'Jane'
    .last = 'Doe'

Matlab将允许您执行以下操作,
>>age = [person(1:2).age]
age = 
    20    25

尝试使用Java实现相同的功能,

>>jperson = javaArray('myMatlab.Person', 2);
>>jperson(1) = Person(20, Name('John', 'Smith'));
>>jperson(2) = Person(25, Name('Jane', 'Doe'));
>>age = [jperson(1:2).age]
??? No appropriate method or public field age for class myMatlab.Person[]

有没有办法让Java对象模仿这种行为? 我首先想到的是简单地扩展Person[] class,但似乎不可能,因为它是final。 我的第二种方法是创建一个包含Person数组列表的包装类,但我不认为这会起作用,因为调用

wrappedPerson(1:2)

这段代码中的"()"操作符要么被解释为对一个wrappedPerson类的构造函数的调用,要么是尝试访问一个不存在的WrappedPerson数组的元素(因为Java不允许我重载"()"操作符)。任何见解都将不胜感激。

我正在使用的Java类的代码如下:

public class Person {
  int _age;
  ArrayList<Name> _names;

  public Person(int age, Name name) {
    _age = age;
    _names.add(name);
  }

  public int age() {return _age;}
  public void age(int age) {_age = age;}
  public Name[] name() {return _names.toArray(new Name[0]);}
  public void name(Name name) { _names.add(name);}
}

public class Name {
  String _first;
  String _last;

  public Name(String first, String last) {
    _first = first;
    _last = last;
  }

  public int first() {return _first;}
  public void first(String firstName) {_first = firstName;}
  public int last() {return _last;}
  public void last(String lastName) {_last = lastName;}
}

你是如何期望传入1:2的,考虑到“:”不是Java运算符? - Garrett Hall
Matlab会自动处理这种转换。如果你有一个大小为4的Person数组,person = new Person[4],你可以在Matlab中调用person(1:2),它会返回数组的前两个元素作为大小为2的Person数组。 - PaxRomana99
等一下...这个[foo(1:3).bar.baz]的语法在普通的Matlab结构体中是否有效,还是会抛出一个()-indexing must appear last in an index expression的错误? - Andrew Janke
取决于“bar”是什么。如果它是一个结构体数组,那么你需要分两步来做,{a = [foo(1:3).bar]; a = [a(:).baz]}。 - PaxRomana99
1个回答

6
TL;DR: 使用一些高级的OOP M代码技巧,可以改变 (). 的行为,通过定义一个在Java包装器类之上定义 subsref 的Matlab包装器类来实现。但是由于固有的Matlab-to-Java开销,它可能不会比普通Matlab代码更快,只是更复杂和繁琐。除非您也将逻辑移入Java中,否则此方法可能不会加速您的代码。

在开始之前,请先对从您的Matlab代码调用的Java结构的性能进行基准测试。虽然Java字段访问和方法调用本身要比Matlab快得多,但从M代码调用它们存在相当大的开销,因此,除非您将大部分逻辑下推到Java中,否则您可能最终会损失速度。每次穿过M代码到Java层,都需要付出代价。请参考这个答案中的基准测试:Is MATLAB OOP slow or am I doing something wrong? 让您有一个大致的了解。(完全披露:那是我的答案。)它不包括Java字段访问,但由于自动装箱开销,它可能与方法调用的数量相当。如果您像示例中那样编写Java类,使用getter和setter方法而不是公共字段(也就是说,在“良好”的Java风格中),则每次访问都会产生Java方法调用的成本,与纯Matlab结构相比,它将是糟糕的。

尽管如此,如果您想要在 x = [foo(1:2).bar] 语法内部使其能够在M代码中正常工作,其中 foo 是一个Java数组,基本上是可以实现的。在Matlab OOP中定义一个自定义的JavaArrayWrapper类对应于您的Java数组包装器类,并将您的(可能已包装的)Java数组包装在其中。覆盖 subsrefsubsasgn 以处理 ().。对于 (),对数组进行正常的子集处理,并将其包装在JavaArrayWrapper中返回。对于 . 情况:

  • 如果包装的对象是标量,则像平常一样调用Java方法。
  • 如果包装的对象是数组,则循环遍历它,在每个元素上调用Java方法,并收集结果。如果结果是Java对象,则将它们包装在JavaArrayWrapper中返回。

但是。由于穿过Matlab / Java界限的开销,这将很慢,可能比纯Matlab代码慢一个数量级。

为了让它快速运行,您可以提供一个相应的自定义Java类,包装Java数组并使用Java Reflection API从每个选择的数组成员对象中提取属性并将其收集到数组中。关键是,在Matlab中进行“链式”引用时,如x = foo(1:3).a.b.c,并且foo是一个对象时,它不会逐步评估其中的每一步,即先评估foo(1:3) ,然后在结果上调用.a,等等。实际上,它会解析整个(1:3).a.b.c引用,将其转换为结构化参数,并将整个引用传递给foosubsref方法,该方法负责解释整个链。隐式调用看起来像这样。
x = subsref(foo, [ struct('type','()','subs',{{[1 2 3]}}), ...
                   struct('type','.', 'subs','a'), ...
                   struct('type','.', 'subs','b'), ... 
                   struct('type','.', 'subs','c') ] )

因此,假设您可以事先访问整个引用“链”,如果foo是定义subsasgn的M代码包装类,您可以将整个引用转换为Java参数,并通过单个方法调用传递给Java包装类,然后使用Java反射在Java层动态浏览包装的数组、选择引用元素并执行链接的引用。例如,它会在类似于以下的Java类中调用getNestedFields().

public class DynamicFieldAccessArrayWrapper {
    private ArrayList _wrappedArray;

    public Object getNestedFields(int[] selectedIndexes, String[] fieldPath) {
        // Pseudo-code:
        ArrayList result = new ArrayList();
        if (selectedIndexes == null) {
            selectedIndexes = 1:_wrappedArray.length();
        }
        for (ix in selectedIndexes) {
            Object obj = _wrappedArray.get(ix-1);
            Object val = obj;
            for (fieldName in fieldPath) {
                java.lang.reflect.Field field = val.getClass().getField(fieldName);
                val = field.getValue(val);
            }
            result.add(val);
        }
        return result.toArray(); // Return as array so Matlab can auto-unbox it; will need more type detection to get array type right
    }
}

然后,您的 M 代码包装器类将检查结果并决定它是否应作为 Matlab 数组或逗号分隔列表(即多个 argouts,使用 [...] 收集)返回为基本类型,或者应包装在另一个 JavaArrayWrapper M 代码对象中。

这个 M 代码包装器类看起来会像这样。

classdef MyMJavaArrayWrapper < handle
    % Inherit from handle because Java objects are reference-y
    properties
        jWrappedArray  % holds a DynamicFieldAccessArrayWrapper
    end
    methods
        function varargout = subsref(obj, s)
            if isequal(s(1).type, '()')
                indices = s(1).subs;
                s(1) = [];
            else
                indices = [];
            end
            % TODO: check for unsupported indexing types in remaining s
            fieldNameChain = parseFieldNamesFromArgs(s);
            out = getNestedFields( jWrappedArray, indices, fieldNameChain );
            varargout = unpackResultsAndConvertIfNeeded(out);
        end
    end
end

执行subsasgn调用时涉及到的结构体编组和解组成本可能会压倒Java代码的速度优势。您可以通过用C中的MEX实现替换M代码实现subsasgn来消除这种开销,使用JNI生成Java对象,调用getNestedFields并将结果转换为Matlab结构。这远远超出了我能给出示例的范围。如果您觉得这有点可怕,我完全同意。您在这里碰到了语言的边缘,并试图从用户空间扩展语言(尤其是提供新的句法行为)真的很难。我不认真地在生产代码中执行此类操作;只是试图概述您正在查找的问题领域。您是否正在处理这些深度嵌套结构的同质数组?也许可以将它们转换为“平面组织”的结构,其中一次结构标量,而不是具有标量字段的结构数组。然后,您可以在纯M代码中对它们进行矢量化操作。这将使事情变得更快,特别是使用save和load时,其中开销按照mxarray每个缩放。

首先,非常感谢您的回复。那里有很多很棒的信息需要我去处理。回答您的一些问题, 1)在我的情况下,跨越Matlab / Java边界几乎肯定是值得的,因为我可以在Java端进行大量并行处理,而我没有工具箱在Matlab端进行处理。最初的基准测试表明有实质性的节省,这就是为什么我继续到这一点的原因。您提出了一个有趣的问题,关于字段访问与方法访问。我会去检查一下。 - PaxRomana99
很不幸,我被困在现有的结构组织中。 一个快速的后续问题,实现M代码层作为MCOS还是UDD会更好? - PaxRomana99
是的,如果你在Java方面进行的工作不是仅仅将其用作“愚蠢”的数据容器,而是进行了非平凡的工作,那么你可以看到很大的改进。只需尝试定义Java接口以便它接受大型参数,最大化每个方法调用传递的数据比率,以摊销越过边界的成本。 - Andrew Janke
  1. 很难说。如果你追求速度,UDD在几个版本前是更快的,但我最近没有进行基准测试。请在您的版本中尝试(请参见我的链接答案)。但MCOS支持handle语义,在某些情况下更符合Java对象行为。如果您需要支持obj.method(arg1, arg2)语法,则必须使用MCOS。但是,method(obj, arg1, arg2)语法更快,并且允许您在MCOS和UDD实现之间切换,因此如果有选择,应优先选择该语法。此外,现在Mathworks支持MCOS,这可能是最重要的。
- Andrew Janke
嗯...看起来字段访问可能比方法访问更快。我用Java编写了一个测试类,其中包含一个公共双精度浮点数和一个返回该双精度浮点数的方法。然后我使用两种访问方法从Matlab中调用了它10,000次。结果是字段访问需要0.3297秒才能运行,而方法访问需要0.6328秒才能运行。 - PaxRomana99

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接