【问题标题】:Array access optimization数组访问优化
【发布时间】:2010-11-28 13:45:41
【问题描述】:

我在 Java 中有一个 10x10 数组,数组中的一些项没有使用,我需要遍历所有元素作为方法的一部分。什么会更好:

  1. 遍历所有带有 2 个 for 循环的元素并检查 nulltype 以避免错误,例如

    for(int y=0;y<10;y++){
        for(int x=0;x<10;x++){
           if(array[x][y]!=null)
                //perform task here
        }
    }
    
  2. 还是保留所有已使用地址的列表会更好...比如说点数组列表?

  3. 我没有提到的不同。

我期待任何答案:)

【问题讨论】:

  • 视情况而定。但最好把你的 for 循环反过来。
  • 最好让循环反过来?不确定它们是否相同大小有什么区别。另外,忘了说,我是在安卓平台上开发的。
  • developer.android.com/guide/practices/design/… 从性能方面看,最好将所有内容提取到局部变量中,避免查找。
  • @Dawson:根据编译器和 CPU,您可能会通过切换循环顺序获得巨大的性能提升。见lbrandy.com/blog/2009/03/more-cache-craziness
  • 我认为 ArrayList 中的查找比空指针检查的成本更高,因为分支预测器与 if 语句配合得更好。如果数组大小是固定的,那么 JIT 可能会展开循环。

标签: java android arrays optimization


【解决方案1】:

对于一百个元素,可能不值得使用任何经典的稀疏数组 实施。但是,您并没有说您的数组有多稀疏,因此请分析它并查看与您正在执行的任何处理相比,您花费了多少时间跳过空项。

(正如 Tom Hawtin - tackline 提到的)在使用数组数组时,您应该尝试循环遍历每个数组的成员,而不是循环遍历不同数组的相同索引。不过,并非所有算法都允许您这样做。

for ( int x = 0; x < 10; ++x ) {
    for ( int y = 0; y < 10; ++y ) {
       if ( array[x][y] != null )
            //perform task here
    }
}

for ( Foo[] row : array ) {
    for ( Foo item : row ) {
       if ( item != null )
            //perform task here
    }
}

您可能还会发现使用空对象比测试空对象更好,这取决于您正在执行的操作的复杂性。不要使用模式的多态版本——多态分派的成本至少与测试和分支一样多——但如果你对具有零对象的属性求和,在许多 CPU 上可能会更快。

double sum = 0;

for ( Foo[] row : array ) {
    for ( Foo item : row ) {
       sum += item.value();
    }
}

至于什么适用于 android,我不确定;再次,您需要测试和分析任何优化。

【讨论】:

    【解决方案2】:

    您尝试的任何解决方案都需要在尽可能类似于生产条件的受控条件下进行测试。由于 Java 的特性,您需要稍微练习一下代码才能获得可靠的性能统计信息,但我相信您已经知道了。

    这就是说,您可以尝试几种方法,我已使用这些方法成功地优化了我的 Java 代码(但不适用于 Android JVM

    for(int y=0;y<10;y++){
        for(int x=0;x<10;x++){
           if(array[x][y]!=null)
                //perform task here
        }
    }
    

    在任何情况下都应该重做

    for(int x=0;x<10;x++){
        for(int y=0;y<10;y++){
           if(array[x][y]!=null)
                //perform task here
        }
    }
    

    您通常会通过缓存行引用来提高性能。假设数组的类型为Foo[][]

    for(int x=0;x<10;x++){
        final Foo[] row = array[x];
        for(int y=0;y<10;y++){
           if(row[y]!=null)
                //perform task here
        }
    }
    

    final 与变量一起使用应该有助于 JVM 优化代码,但我认为现代 JIT Java 编译器在许多情况下可以自行判断变量是否在代码中更改。另一方面,有时这可能会更有效,尽管我们肯定会进入微优化领域:

    Foo[] row;
    for(int x=0;x<10;x++){
        row = array[x];
        for(int y=0;y<10;y++){
           if(row[y]!=null)
                //perform task here
        }
    }
    

    如果你不需要知道元素的索引来执行任务,你可以这样写

    for(final Foo[] row: array){
        for(final Foo elem: row
           if(elem!=null)
                //perform task here
        }
    }
    

    您可以尝试的另一件事是将数组展平并将元素存储在Foo[] 数组中,以确保最大的引用局部性。您无需担心内部循环,但在引用特定数组元素时需要进行一些索引运算(而不是遍历整个数组)。取决于您这样做的频率,它可能有益,也可能无益。

    由于大多数元素将不为空,因此将它们保留为稀疏数组对您不利,因为您会失去引用的局部性。

    另一个问题是空测试。空测试本身并不会花费太多,但它后面的条件语句会花费很多,因为您在代码中获得了一个分支并在错误的分支预测上浪费了时间。您可以做的是使用“空对象”,任务可以在其上执行,但相当于非操作或同样良性的东西。根据您要执行的任务,它可能适合您,也可能不适合您。

    希望这会有所帮助。

    【讨论】:

    • 干杯,总是很高兴看到对这件事的另一种看法,只是为了确定。也感谢其他所有人!
    • 我认为在没有任何分析的情况下给出这个建议有点危险。对 JVM 的优化行为做出广泛的断言是错误的秘诀。我的理解是“final”不再是一个重要的优化,即使没有关键字,JVM 也能够进行相同的优化。我也怀疑道森是否会通过删除 if-null 检查来实现收益。它确实必须是基本代码才能克服 BNE 的成本,特别是因为该元素通常不会为空。底线,配置文件第一,断言第二:)
    • 附言。关于最后一段的同样问题......我写道,空检查本身应该很便宜。
    【解决方案3】:

    我同意带有 null 测试的数组是最好的方法,除非您期望填充稀疏的数组。

    原因:

    1- 密集数组的内存效率更高(列表需要存储索引)

    2- 密集数组的计算效率更高(您只需将刚刚检索到的值与 NULL 进行比较,而不必从内存中获取索引)。

    还有一个小建议,但在 Java 中,您通常最好在可能的情况下使用一维数组(二维中的方形/矩形数组)来伪造一个多维数组。边界检查每次迭代只发生一次,而不是两次。不确定这是否仍然适用于 android VM,但它一直是一个问题。无论如何,如果循环不是瓶颈,您可以忽略它。

    【讨论】:

      【解决方案4】:

      取决于您的矩阵的稀疏/密集程度。

      如果它是稀疏的,你最好存储一个点列表,如果它是密集的,则使用 2D 数组。如果介于两者之间,您可以使用存储子矩阵列表的混合解决方案。

      无论如何,这个实现细节都应该隐藏在一个类中,因此您的代码也可以随时在这些表示中的任何一个之间进行转换。

      我不鼓励您在未分析您的实际应用程序的情况下选择这些解决方案。

      【讨论】:

      • 很可能是密集的,它用于 Android 平台的 Puzzle Bobble 克隆。
      【解决方案5】:

      保存点的 ArrayList 将是“过度工程化”的问题。你有一个多维数组;迭代它的最佳方法是使用两个嵌套的 for 循环。除非您可以更改数据的表示,否则它的效率大致相同。

      只要确保按行顺序,而不是按列顺序。

      【讨论】:

      • 我的想法完全一样,尤其是从 ArrayList 中添加或删除内容复制到新的 Array...
      【解决方案6】:

      您最好使用列表而不是数组,尤其是因为您可能不会使用整个数据集。这有几个优点。

      1. 您没有检查空值,可能不会意外尝试使用空对象。
      2. 内存效率更高,因为您不会分配可能未使用的内存。

      【讨论】:

      • 我选择了多维数组,因为所有位置都最有可能被使用。我正在做的工作是针对 Android 平台的,对象创建非常昂贵,因此最好预先预留空间以实现实时性能。
      • 在现代 JVM 中,空值检查非常快,所以这不是问题。他只需要记住检查它们。
      • 好的,我明白了,在这种情况下,您正在处理经典的空间与性能。如果您需要表演,请吞下热门歌曲。
      • 另一方面,代码分支可能会降低性能,因此最好使用避免条件执行代码的解决方案。
      • 我建议您编辑问题以说明您正在优化性能,这样人们就不必深入研究这些 cmets 来查看您正在寻找的优化类型。
      猜你喜欢
      • 2010-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-07
      • 1970-01-01
      • 1970-01-01
      • 2021-12-10
      • 2021-04-21
      相关资源
      最近更新 更多