设置数组所有值的最快方法?


73

我有一个char [],我想将每个索引的值设置为相同的char值。
有一种很明显的方法(迭代):

  char f = '+';
  char [] c = new char [50];
  for(int i = 0; i < c.length; i++){
      c[i] = f;
  }

但是我想知道是否存在一种可以利用的方法System.arraycopy或某种等效方法可以绕过迭代的需要。有没有办法做到这一点?

编辑:Arrays.java

public static void fill(char[] a, int fromIndex, int toIndex, char val) {
        rangeCheck(a.length, fromIndex, toIndex);
        for (int i = fromIndex; i < toIndex; i++)
            a[i] = val;
    }

这是完全相同的过程,表明可能没有更好的方法可以做到这一点。
+1那些建议fill的人-你们都是正确的,谢谢。


1
附录中的JDK代码版本显示了在某些JDK版本中完成的“混乱”:某个位置的外部标志指示应在方法中绕过数组边界检查,然后在外部添加显式边界检查。循环。由于边界检查不仅本身就很昂贵,而且使其他优化复杂化,因此可以显着提高性能。
Hot Licks 2012年

@Bombe用于自定义密码字段,因此我必须即时替换char文档中的每一个'•'-这意味着它必须尽可能地响应。可能会说为什么不随便为每个索引设置值?它用于和drawString,因此我可以对•的文本进行反锯齿。fill似乎运作良好。:)
rtheunissen 2012年

@ paranoid-android,因此您的用户确实能够每秒输入1000个以上的字符吗?我很佩服。
孟买

3
是的,我正在为超人编码。
rtheunissen

我认为最好使用“填充”,因为它是内置类的标准方法,并且可以在运行时由JVM更改为更有效的实现。我认为更多的事情一定会发生。Java在设计上不支持直接内存访问,但这并不意味着生成的代码也不应该支持它。
弱点

Answers:



52

作为另一种选择,也是后代,我最近对此进行了研究,找到了一种解决方案,该解决方案可以通过将一些工作交给System类来缩短循环时间(如果您使用的JVM足够聪明的话)进入记忆集操作:

/*
 * initialize a smaller piece of the array and use the System.arraycopy 
 * call to fill in the rest of the array in an expanding binary fashion
 */
public static void bytefill(byte[] array, byte value) {
  int len = array.length;

  if (len > 0){
    array[0] = value;
  }

  //Value of i will be [1, 2, 4, 8, 16, 32, ..., len]
  for (int i = 1; i < len; i += i) {
    System.arraycopy(array, 0, array, i, ((len - i) < i) ? (len - i) : i);
  }
}

该解决方案摘自R. Dimpsey,R。Arora,K。Kuiper的IBM研究论文“ Java服务器性能:构建高效,可扩展的Jvm的案例研究”

简化说明

就像注释所暗示的那样,这将目标数组的索引0设置为您的值,然后使用System类将一个对象(即索引0的对象复制到索引1,然后将这两个对象(索引0和1)复制到2和3),然后这四个对象(0、1、2和3)分别变成4,5,6和7,依此类推...

效率(在撰写本文时)

快速浏览一下,抓住System.nanoTime()之前和之后并计算出我想出的持续时间:-

  • 该方法:332,617-390,262(10次测试中“最高-最低”)
  • Float[] n = new Float[array.length]; //Fill with null :666,650
  • 通过循环设置:3743488 - 9767744 (“最高-最低” 10个测试)
  • Arrays.fill12,539,336

JVM和JIT编译

应该注意的是,随着JVM和JIT的发展,这种方法很可能已经过时,因为只需使用,库和运行时优化就可以达到甚至超过这些数量fill()。在撰写本文时,这是我找到的最快的选择。已经提到现在可能不是这种情况,但是我没有检查。这就是Java的美丽和诅咒。


2
这将是公认的答案,当您寻找速度时,Arrays.fill对于大型阵列来说太慢了。OP指出“最快的方式”。这个答案确实有帮助
ME

12

采用 Arrays.fill

  char f = '+';
  char [] c = new char [50];
  Arrays.fill(c, f)

7

Java程序员的FAQ部分B第6节建议:

public static void bytefill(byte[] array, byte value) {
    int len = array.length;
    if (len > 0)
    array[0] = value;
    for (int i = 1; i < len; i += i)
        System.arraycopy( array, 0, array, i,
            ((len - i) < i) ? (len - i) : i);
}

这本质上是对System.arraycopy的log2(array.length)调用,它有望利用优化的memcpy实现。

但是,现代Java JIT(例如Oracle / Android JIT)是否仍需要此技术?


我找到了本教程,但没有找到具体的链接-您知道它在哪里吗?
Karussell

我的答案中有链接,可以更深入地讨论此方法@Karussell
Ross Drew

6

System.arraycopy是我的答案。请让我知道还有什么更好的方法。谢谢

private static long[] r1 = new long[64];
private static long[][] r2 = new long[64][64];

/**Proved:
 * {@link Arrays#fill(long[], long[])} makes r2 has 64 references to r1 - not the answer;
 * {@link Arrays#fill(long[], long)} sometimes slower than deep 2 looping.<br/>
 */
private static void testFillPerformance() {
    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
    System.out.println(sdf.format(new Date()));
    Arrays.fill(r1, 0l);

    long stamp0 = System.nanoTime();
    //      Arrays.fill(r2, 0l); -- exception
    long stamp1 = System.nanoTime();
    //      System.out.println(String.format("Arrays.fill takes %s nano-seconds.", stamp1 - stamp0));

    stamp0 = System.nanoTime();
    for (int i = 0; i < 64; i++) {
        for (int j = 0; j < 64; j++)
            r2[i][j] = 0l;
    }
    stamp1 = System.nanoTime();
    System.out.println(String.format("Arrays' 2-looping takes %s nano-seconds.", stamp1 - stamp0));

    stamp0 = System.nanoTime();
    for (int i = 0; i < 64; i++) {
        System.arraycopy(r1, 0, r2[i], 0, 64);
    }
    stamp1 = System.nanoTime();
    System.out.println(String.format("System.arraycopy looping takes %s nano-seconds.", stamp1 - stamp0));

    stamp0 = System.nanoTime();
    Arrays.fill(r2, r1);
    stamp1 = System.nanoTime();
    System.out.println(String.format("One round Arrays.fill takes %s nano-seconds.", stamp1 - stamp0));

    stamp0 = System.nanoTime();
    for (int i = 0; i < 64; i++)
        Arrays.fill(r2[i], 0l);
    stamp1 = System.nanoTime();
    System.out.println(String.format("Two rounds Arrays.fill takes %s nano-seconds.", stamp1 - stamp0));
}

12:33:18
阵列的2循环需要133536纳秒。
System.arraycopy循环需要22070纳秒。
一轮Arrays.fill需要9777纳秒。
两轮Arrays.fill需要93028纳秒。

12:33:38
阵列的2循环需要133816纳秒。
System.arraycopy循环需要22070纳秒。
一轮Arrays.fill需要17042纳秒。
两轮Arrays.fill需要95263纳秒。

12:33:51
阵列的2循环时间为199187纳秒。
System.arraycopy循环需要44140纳秒。
一轮Arrays.fill需要19555纳秒。
两轮Arrays.fill需要449219纳秒。

12:34:16
阵列的2循环耗时199467纳秒。
System.arraycopy循环需要42464纳秒。
一轮Arrays.fill需要17600纳秒。
两轮Arrays.fill需要170971纳秒。

12:34:26
阵列的2循环需要198907纳秒。
System.arraycopy循环需要24584纳秒。
一轮Arrays.fill需要10616纳秒。
两轮Arrays.fill需要94426纳秒。


7
为什么当日志显示Arrays.fill(...)每次运行都更快时,为什么选择System.arraycopy(...)?通常,它要快得多!
ingyhere 2014年

1
对于阅读此答案的人,以防您在方法开始时错过odys的评论。一轮Arrays.fill实际上是使用Arrays.fill(Object [],Object),它将用对r1的引用填充外部数组(即,之后设置r2 [i] [j] = 42将设置r2 [x] [j]对于所有x)= 42,显然不是预期的行为。
ggf31416 '18 -10-29

5

从Java-8开始,setAll方法有四个变体,它使用提供的生成器函数来计算每个元素,从而设置指定数组的所有元素。

在这四个重载中,只有三个重载接受这样声明的原语数组:





如何使用上述方法的示例:

// given an index, set the element at the specified index with the provided value
double [] doubles = new double[50];
Arrays.setAll(doubles, index -> 30D);

// given an index, set the element at the specified index with the provided value
int [] ints = new int[50];
Arrays.setAll(ints, index -> 60);

 // given an index, set the element at the specified index with the provided value
long [] longs = new long[50];
Arrays.setAll(longs, index -> 90L);

提供给该setAll方法的函数接收元素索引并返回该索引的值。

您可能想知道字符数组如何?

这是该setAll方法的第四个重载起作用的地方。由于没有消耗字符基元数组的重载,因此我们唯一的选择是将字符数组的声明更改为type Character[]

如果Character不适合将数组类型更改为,则可以使用Arrays.fill方法。

结合使用该setAll方法的示例Character[]

// given an index, set the element at the specified index with the provided value
Character[] character = new Character[50];
Arrays.setAll(characters, index -> '+'); 

虽然,这是更简单的使用Arrays.fill方法,而不是setAll方法来设置一个特定的值。

setAll方法的优点是您可以将数组的所有元素设置为相同的值,或者生成偶数,奇数或任何其他公式的数组:

例如

int[] evenNumbers = new int[10]; 
Arrays.setAll(evenNumbers, i -> i * 2);

尽管必须注意传递给parallelSetAll方法的函数必须没有副作用,但是并行执行的parallelSetAll方法也有一些重载。

结论

如果您的目标只是数组的每个元素设置一个特定的值,那么使用Arrays.fill重载将是最合适的选择。但是,如果您想更加灵活或按需生成元素,则可以使用Arrays.setAllArrays.parallelSetAll(在适当时)。


1
我发现将数组(大小500)设置为默认值时,setall的性能要比填充大约3倍
Nrj

3

如果您有另一个char数组,char[] b并且想要替换cb,则可以使用c=b.clone();


或者,如果阵列的长度可变,则创建一个“超长”原型并使用System.arraycopy。两种方法都可以有效地进行memcpy隐藏。
2012年



1

Arrays.fill(myArray, 'c');

Arrays.fill

尽管这样做很可能在后台执行循环,因此效率没有任何提高(除了节省代码的行之外)。如果您真的很在乎效率,请尝试以下比较:

int size = 50;
char[] array = new char[size];
for (int i=0; i<size; i++){
  array[i] = 'c';
}

请注意,上面没有为每次迭代调用array.size()。


对array.length的引用很便宜(而不是调用),并且很容易在循环外进行优化。
Hot Licks 2012年

@HotLicks您确定编译器会这样做吗?数组引用是否可能在循环内更改(并因此更改大小)?因此,编译器对此进行优化是否安全?我想如果它足够聪明,可以确保在循环内不修改数组引用,可以这样做。同样,您是否知道此优化已完成?
约翰·B

@HotLicks我可以假设您的语句包含数组尝试但不包含集合吗?
约翰·B

array不会在循环中更改-编译器很容易确定。而arraylength字节码直接引用的数组对象数组大小字段,所以它几乎一样便宜,如果留在循环仿佛搬了出来。甚至Sun自己的代码(在我的帖子中)也不必担心将引用移出循环。在Paranoid原始文档的附录中显示的Sun代码的更优化版本中,获得了最大的性能提升-该方法上有一个特殊/秘密标志可以关闭边界检查,而显式边界检查则在循环之外进行。
2012年

1
   /**
     * Assigns the specified char value to each element of the specified array
     * of chars.
     *
     * @param a the array to be filled
     * @param val the value to be stored in all elements of the array
     */
    public static void fill(char[] a, char val) {
        for (int i = 0, len = a.length; i < len; i++)
            a[i] = val;
    }

那就是Arrays.fill做到的方式。

(我想您可以使用JNI并使用memset。)


1

我对Ross Drew的回答略有改进。

对于小型阵列,由于与设置System.arraycopy相关的开销,一个简单的循环比System.arraycopy方法要快。因此,最好使用简单的循环填充数组的前几个字节,并且仅在填充的数组具有特定大小时才移至System.arraycopy。

当然,初始循环的最佳大小将取决于JVM和特定于系统。

private static final int SMALL = 16;

public static void arrayFill(byte[] array, byte value) {
  int len = array.length;
  int lenB = len < SMALL ? len : SMALL;

  for (int i = 0; i < lenB; i++) {
    array[i] = value;
  }

  for (int i = SMALL; i < len; i += i) {
    System.arraycopy(array, 0, array, i, len < i + i ? len - i : i);
  }
}

0

你可以用 arraycopy但要取决于您是否可以预定义源数组,-您是否每次需要填充不同的字符,还是要用相同的字符重复填充数组?

显然,填充的长度很重要-您需要一个比所有可能的目标都大的源,或者需要一个循环来重复数组复制大量数据直到目标满为止。

    char f = '+';
    char[] c = new char[50];
    for (int i = 0; i < c.length; i++)
    {
        c[i] = f;
    }

    char[] d = new char[50];
    System.arraycopy(c, 0, d, 0, d.length);

0

Arrays.fill是通用的最佳选择。如果从最新的idk 1.8 u102开始需要填充大型数组,则可以使用System.arraycopy更快的方法。您可以看一下这个替代的Arrays.fill实现:

根据JMH基准,大型阵列(1000 +)的性能几乎提高了2

无论如何,这些实现应仅在需要的地方使用。JDK Arrays.fill应该是首选。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.