【问题标题】:refactoring Java arrays and primitives (double[][]) to Collections and Generics (List<List<Double>>)将 Java 数组和原语 (double[][]) 重构为集合和泛型 (List<List<Double>>)
【发布时间】:2010-11-27 10:08:31
【问题描述】:

我一直在重构我几年前以类似 FORTRAN 的风格编写的一次性代码。现在大部分代码都更有条理和可读性。然而,算法的核心(对性能至关重要)使用 1 维和 2 维 Java 数组,其典型代表是:

    for (int j = 1; j < len[1]+1; j++) {
        int jj = (cont == BY_TYPE) ? seq[1][j-1] : j-1;
        for (int i = 1; i < len[0]+1; i++) {
            matrix[i][j] = matrix[i-1][j] + gap;
            double m = matrix[i][j-1] + gap;
            if (m > matrix[i][j]) {
                matrix[i][j] = m;
                pointers[i][j] = UP;
            }
            //...
        }
    }

为了清晰、可维护性和与其余代码的交互,我想对其进行重构。但是在阅读Java Generics Syntax for arraysJava Generics and numbers 时,我有以下担忧:

  • 性能。该代码计划使用大约 10^8 - 10^9 秒/年,这几乎是可管理的。我的阅读表明,将 double 更改为 Double 有时可以将性能提高 3 倍。我想要这方面的其他经验。我还希望从 foo[] 移动到 List 也会很受欢迎。我没有第一手的知识,再次经验会很有用。

  • 数组绑定检查。这在 double[] 和 List 中的处理方式不同吗?这有关系吗?我预计一些问题会超出界限,因为该算法相当简单并且只应用于少数数据集。

  • 如果我不重构,那么代码就会有两种方法的丑陋且可能很脆弱的混合。我已经在尝试编写如下内容:

    列表 和 列表[]

并了解擦除不会使这变得漂亮,并且充其量会引起编译器警告。如果没有非常复杂的结构,似乎很难做到这一点。

  • 已过时。一位发帖人建议应该废弃 Java 数组。我认为这不会发生 RSN,但我想摆脱过时的方法。

总结到目前为止的共识:

  • 集合对原始数组的性能有显着影响,尤其是对于矩阵等构造。这是在自动(取消)装箱数字和访问列表项时发生的

  • 对于严格的数值(科学)算法,数组表示法 [][] 实际上更易于阅读,但变量的命名应尽可能有用

  • 泛型和数组不能很好地混合。将数组包装在类中以将它们传入/传出紧密算法可能很有用。

几乎没有客观的理由做出改变

问题 @SeanOwen 建议从循环中取出常量值会很有用。假设我没有搞砸这看起来像:

 int len1 = len[1];
 int len0 = len[0];
 int seq1 = seq[1];
 int[] pointersi;
 double[] matrixi;
 for (int i = 1; i < len0+1; i++) {
     matrixi = matrix[i];
     pointersi = pointers[i];
 }
 for (int j = 1; j < len1+1; j++) {
    int jj = (cont == BY_TYPE) ? seq1[j-1] : j-1;
    for (int i = 1; i < len0+1; i++) {
        matrixi[j] = matrixi[j] + gap;
        double m = matrixi[j-1] + gap;
        if (m > matrixi[j]) {
            matrixi[j] = m;
            pointersi[j] = UP;
        }
        //...
    }
}

我认为编译器在做这类事情时应该很聪明。我们还需要这样做吗?

【问题讨论】:

  • +1 表示引起极大兴趣的问题

标签: java arrays generics collections


【解决方案1】:

我认为编译器应该是 聪明地做这种事情。做 我们还需要这样做吗?

JIT 负责处理它可能是对的,但如果这部分对性能如此关键,那么尝试和基准测试不会有任何坏处。

【讨论】:

  • 为什么需要 JIT 参与?这看起来像是一种优化,在字节码编译阶段同样可用——而且更明智。当然,如果从中获取循环限制值的数组可能可以从其他线程访问,那么即使是 JIT 也无法进行这种优化,因为程序逻辑可能会被改变。
【解决方案2】:

除了坚持使用数组之外,我认为您可以通过一些有意义的方式收紧这段代码。例如:

  • 确实,不要每次都计算循环边界,省去它们
  • 您反复引用矩阵[i]。只需保存对该子数组的引用,而不是每次都取消引用 2D 数组
  • 如果您可以在外循环而不是内循环中循环 i,则该技巧会更加有用
  • 它变得越来越极端,但将 j-1 的值保存在本地甚至可能被证明是值得的,而不是重新计算
  • 最后,如果您真的很关心性能,请在生成的字节码上运行 ProGuard 优化器,让它执行一些编译器优化,例如展开或窥孔优化

【讨论】:

    【解决方案3】:

    一般准则是在 Java 中优先使用泛型集合而不是数组,但这只是一个准则。我的第一个想法是不要更改此工作代码。如果您真的想进行此更改,请对这两种方法进行基准测试。

    正如您所说,性能至关重要,在这种情况下,满足所需性能的代码比不满足所需性能的代码更好。

    在对双打进行装箱/拆箱时,您可能还会遇到自动装箱问题——这可能是一个更微妙的问题。

    Java 语言人员一直非常严格地保持 JVM 在不同版本之间的兼容性,因此我看不到数组会在任何地方出现 - 我不会称它们为过时的,只是比其他选项更原始。

    【讨论】:

    • 对于性能关键代码,如果速度足够快,请坚持使用现有代码。
    • 首选集合而不是引用数组。但是 Java 库中没有原始数组的等价物。
    【解决方案4】:

    我阅读了 Kent Beck 撰写的一本关于编码最佳实践的优秀书籍 (http://www.amazon.com/Implementation-Patterns/dp/B000XPRRVM)。还有一些有趣的性能数据。 具体来说,有数组和各种集合之间的比较。,数组确实快得多(可能x3比ArrayList)。

    此外,如果您使用 Double 而不是 double,则需要坚持使用,并且不要使用 double,因为自动(取消)装箱会影响您的表现。

    考虑到您的性能需求,我会坚持原始类型数组


    更重要的是,对于循环中的条件,我会只计算一次上限。 这通常在循环之前的那一行完成。

    但是,如果您不喜欢仅在循环中使用的上限变量在循环外可访问,您可以像这样利用 for 循环的初始化阶段:

        for (int i=0, max=list.size(); i<max; i++) {
          // do something
        }
    

    我不相信 java 中的数组会过时。对于性能关键的循环,我看不到任何语言设计者会取消最快的选项(尤其是如果差异是 x3 时)。


    我理解您对可维护性以及与应用程序其余部分的一致性的担忧。但我相信关键循环有权进行一些特殊做法。

    我会尽量在不更改代码的情况下使代码尽可能清晰:

    • 仔细询问每个变量名称,最好是与我的同事进行 10 分钟的头脑风暴会议
    • 通过编写coding cmets(我一般反对使用它们,因为不清楚的代码应该明确,而不是注释;但关键循环证明了它的合理性)。
    • 根据需要使用 私有方法(正如 Andreas_D 在他的回答中指出的那样)。如果设为private final,它们很有可能(因为它们会很短)在运行时被内联,因此在运行时不会影响性能。

    【讨论】:

    • 整洁。我认为这会停止对 size() 的重复调用?
    • 是的,你是对的 ;-) 第一个 ';' 之前的循环部分是初始化,在循环开始前只做一次。
    • 我的项目涉及许多复杂的 DSP 计算,其中一些是在本机代码中运行的。我们正在处理从 4GB 到 2TB 的文件,并且采样率在 1GHZ 范围内。原始数组在内存和速度方面通常要好得多。如果您甚至需要接触本机代码 (JNI),原始数组也会更好。
    • @basszero 感谢您在这种极端情况下提供有用的意见,我们中很少有人能弄脏我们的手;-)
    【解决方案5】:

    我完全同意 KLE 的回答。因为代码对性能至关重要,所以我也会保留基于数组的数据结构。而且我相信,仅仅为原始类型和泛型引入集合、包装器不会提高可维护性和清晰度。

    此外,如果此算法是应用程序的核心并且已经使用了几年,那么它需要维护(例如错误修复或改进)的机会相当低。

    为了清晰、可维护性和 与其余代码交互 我想重构它。

    我不会更改数据结构,而是专注于重命名并且可能将部分代码移动到私有方法。通过查看代码,我不知道发生了什么,而且在我看来,问题在于或多或少是简短的技术变量和字段名称。

    只是一个例子:一个二维数组被命名为“矩阵”。但很明显,这是一个矩阵,因此将其命名为“矩阵”是非常多余的。重命名它会更有帮助,这样它就会变得清晰,这个矩阵真正用于什么,里面有什么样的数据。

    另一个候选人是你的第二行。通过两次重构,我会将“jj”重命名为更有意义的名称,并将表达式移至具有“说话”名称的私有方法。

    【讨论】:

    • +1 的好答案,我也同意你的看法。也许也可以将私有方法设为最终方法,以提高编译器内联它的机会。
    • 谢谢。我实际上已经在最新版本中做到了这一点 - 它有很大帮助。但是我想我会发布原件,因为它很好地显示了问题。
    【解决方案6】:

    我认为数组是在算法中存储过程数据的最佳方式。由于 Java 不支持运算符重载(我认为数组不会很快过时的原因之一)切换到集合会使代码很难阅读:

    double[][] matrix = new double[10][10];
    double t = matrix[0][0];
    
    List<List<Double>> matrix = new ArrayList<List<Double>>(10);
    Collections.fill(matrix, new ArrayList<Double>(10));
    double t = matrix.get(0).get(0); // autoboxing => performance
    

    据我所知,Java 为 Number 实例(例如前 100 个整数)预先存储了一些包装对象,以便您可以更快地访问它们,但我认为这对那么多数据没有多大帮助。

    【讨论】:

    • 同意。所编写的代码完全可读,并且使用数组表示矩阵比使用带有所有丑陋嵌套gets() 的列表列表更容易理解。
    【解决方案7】:

    当您知道列表的确切尺寸时,您应该坚持使用数组。数组本质上并不坏,而且它们不会去任何地方。如果您正在执行大量(非顺序)读写操作,您应该使用数组而不是列表,因为列表的访问方法会带来很大的开销。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-21
      • 1970-01-01
      • 2019-02-05
      • 1970-01-01
      • 2018-07-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多