我一直在重构一次性代码,这是我几年前以类似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 arrays和Java Generics and number时,我有以下问题:
性能。该代码计划使用大约10 ^ 8 - 10 ^ 9秒/年,这几乎是可管理的。我的阅读建议将double变为Double有时可以在性能上增加3倍。我想要其他经验。我也希望从foo []移动到List也会受到影响。我没有第一手的知识,经验也很有用。
数组绑定检查。这在double []和List中有不同的处理方式吗?我希望有些问题违反了界限,因为算法非常简单,只适用于少数数据集。
如果我不重构那么代码就有两种方法的丑陋且可能很脆弱的混合。我已经在尝试编写如下内容:
列表与LT;加倍[] GT;和 列表与LT;双&GT; []
并且理解擦除不会使这个漂亮,并且最多会引起编译器警告。如果没有非常复杂的结构,似乎很难做到这一点。
摘要目前的共识:
集合对原始数组的性能有很大影响,特别是对于矩阵等构造。这是在自动(非)拳击数字和访问列表项中引起的
对于紧密的数值(科学)算法,数组符号[] []实际上更容易阅读,但变量应尽可能有用地命名
泛型和数组混合不好。将数组包装在类中以将它们输入/输出紧密算法可能很有用。
进行更改没有客观理由
问题 @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;
}
//...
}
}
我认为编译器在做这类事情时应该很聪明。我们还需要这样做吗?
答案 0 :(得分:7)
我读了肯特贝克关于编写最佳实践的优秀书籍(http://www.amazon.com/Implementation-Patterns/dp/B000XPRRVM)。还有一些有趣的表现数字。 具体来说,数组和各种集合之间存在比较,并且数组实际上要快得多(与ArrayList相比可能是x3)。
另外,如果你使用Double而不是double,你需要坚持使用它,并且不使用double,因为自动(联合)拳击将会扼杀你的表现。
考虑到您的性能需求,我会坚持原始类型数组。
更重要的是,我只会计算循环中条件的上限。 这通常在循环之前完成。
但是,如果你不喜欢只在循环中使用的上限变量可以在循环外部访问,你可以利用for循环的初始化阶段,如下所示:
for (int i=0, max=list.size(); i<max; i++) {
// do something
}
我不相信java中数组的过时。对于性能关键的循环,我看不到任何语言设计者拿走最快的选项(特别是如果差异是x3)。
我理解您对可维护性的关注,以及与应用程序其余部分的一致性。但我相信关键循环有权采用某些特殊做法。
我会尝试在不改变代码的情况下使代码尽可能清晰:
private final
,那么在运行时它们会被内联的可能性很大(因为它们很短),因此在运行时不会对性能产生影响。答案 1 :(得分:3)
一般原则是在Java中使用通用集合而不是数组,但它只是一个指导原则。我的第一个想法是不要改变这个工作代码。如果你真的想做这个改变,那么就对这两种方法进行基准测试。
正如您所说,性能至关重要,在这种情况下,满足所需性能的代码优于不具备所需性能的代码。
在装箱/打开双打时,您可能还会遇到自动装箱问题 - 这可能是一个更微妙的问题。
Java语言人员一直非常严格地保持JVM在不同版本之间兼容,所以我看不到数组在任何地方 - 我不会把它们称为过时的,只是比其他选项更原始。
答案 2 :(得分:3)
我完全同意KLE的回答。因为代码对性能至关重要,所以我也会保留基于数组的数据结构。我相信,只是引入集合,原始类型和泛型的包装器不会提高可维护性和清晰度。
此外,如果此算法是应用程序的核心,并且已经使用了好几年,那么机会相当低,需要维护,如修复错误或改进。
为了清晰,可维护性和 与其余代码连接 我想重构一下。
我没有改变数据结构,而是专注于重命名,并可能将部分代码移动到私有方法。通过查看代码,我不知道发生了什么,而且正如我所看到的那样,问题或多或少是技术变量和字段名称。
只是一个例子:一个二维数组只是命名为'矩阵'。但很明显,这是一个矩阵,所以将它命名为“矩阵”是非常多余的。重命名它以使其变得清晰,这个矩阵真正用于什么,内部有什么类型的数据会更有帮助。
另一位候选人是你的第二行。通过两次重构,我将'jj'重命名为更有意义的东西,并将表达式移动到一个带有'speak'名称的私有方法。
答案 3 :(得分:2)
我认为数组是在算法中存储过程数据的最佳方式。由于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预存了一些数字实例的包装器Object(例如前100个整数),这样你就可以更快地访问它们,但我认为这对那么多数据没什么帮助。
答案 4 :(得分:1)
我认为编译器应该是 聪明地做这种事情。做 我们还需要这样做吗?
JIT可能正确处理它,但如果这部分性能如此重要,那么尝试和基准测试不会受到影响。
答案 5 :(得分:0)
当您知道列表的确切尺寸时,您应该坚持使用数组。数组本身并不坏,它们不会去任何地方。如果要执行大量(非顺序)读写操作,则应使用数组而不是列表,因为列表的访问方法会带来很大的开销。
答案 6 :(得分:0)
除了坚持使用数组之外,我认为你可以用一些有意义的方式收紧这些代码。例如: