我想更正和补充以前的答案。
- Object.clone 对数组使用未经检查的 System.arraycopy 实现;
- Object.clone 的主要性能改进,它是直接初始化 RAW 内存。在 System.arraycopy 的情况下,它还尝试将数组初始化与复制操作结合起来,正如我们在源代码中看到的那样,但它也对此进行了不同的额外检查,这与 Object.clone 不同。如果您只是禁用此功能(见下文),那么性能会非常接近(尤其是在我的硬件上)。
- 一个更有趣的事情是关于 Young 与 Old Gen。如果源数组对齐并在 Old Gen 内,这两种方法的性能都很接近。
- 当我们复制原始数组时 System.arraycopy 总是使用 generate_unchecked_arraycopy。
- 这取决于硬件/操作系统相关的实现,所以不要相信基准和假设,请自行检查。
说明
首先 clone 方法和 System.arraycopy 是内在函数。
Object.clone 和 System.arraycopy 使用 generate_unchecked_arraycopy。
如果我们再深入一点,我们可以看到 HotSpot 选择具体的实现,依赖于操作系统等。
很长。
让我们看看来自Hotspot 的代码。
首先,我们将看到 Object.clone (LibraryCallKit::inline_native_clone) 使用 generate_arraycopy,它在 -XX:-ReduceInitialCardMarks 的情况下用于 System.arraycopy。否则它会执行 LibraryCallKit::copy_to_clone,它会在 RAW 内存中初始化新数组(如果 -XX:+ReduceBulkZeroing,默认启用)。
相比之下,System.arraycopy 直接使用 generate_arraycopy,尝试检查 ReduceBulkZeroing(以及许多其他情况)并消除数组归零,并使用提到的附加检查,并且它还会进行附加检查以确保所有元素都已初始化,这与 Object.clone 不同。最后,在最好的情况下,它们都使用 generate_unchecked_arraycopy。
下面我展示了一些基准来查看这种对实践的影响:
- 第一个只是简单的基准测试,与上一个答案的唯一区别是数组未排序;我们看到 arraycopy 速度较慢(但不是两倍),结果 - https://pastebin.com/ny56Ag1z;
- 其次,我添加选项 -XX:-ReduceBulkZeroing 现在我发现这两种方法的性能非常接近。结果 - https://pastebin.com/ZDAeQWwx;
- 我还假设我们会有Old/Young之间的区别,因为数组对齐(这是Java GC的一个特性,当我们调用GC时,数组的对齐方式发生了变化,使用JOL很容易观察到)。令我惊讶的是,这两种方法的性能通常相同,并且降级。结果 - https://pastebin.com/bTt5SJ8r。对于相信具体数字的人来说,System.arraycopy 的吞吐量比 Object.clone 更好。
第一个基准测试:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopy {
@Param({"10", "1000", "100000"})
int size;
int[] source;
@Setup(Level.Invocation)
public void setup() {
source = create(size);
}
@Benchmark
public int[] clone(CloneVsArraycopy cloneVsArraycopy) {
return cloneVsArraycopy.source.clone();
}
@Benchmark
public int[] arraycopy(CloneVsArraycopy cloneVsArraycopy) {
int[] dest = new int[cloneVsArraycopy.size];
System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
return dest;
}
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(CloneVsArraycopy.class.getSimpleName())
.warmupIterations(20)
.measurementIterations(20)
.forks(20)
.build()).run();
}
private static int[] create(int size) {
int[] a = new int[size];
for (int i = 0; i < a.length; i++) {
a[i] = ThreadLocalRandom.current().nextInt();
}
return a;
}
}
在我的电脑上运行这个测试,我得到了这个 - https://pastebin.com/ny56Ag1z。
差别不是很大,但还是存在的。
第二个基准我只添加了一个设置-XX:-ReduceBulkZeroing并得到了这个结果https://pastebin.com/ZDAeQWwx。不,我们看到对于年轻一代来说,差异也小得多。
在第三个基准测试中,我只更改了设置方法并启用了 ReduceBulkZeroing 选项:
@Setup(Level.Invocation)
public void setup() {
source = create(size);
// try to move to old gen/align array
for (int i = 0; i < 10; ++i) {
System.gc();
}
}
差异要小得多(可能是错误间隔)-https://pastebin.com/bTt5SJ8r。
免责声明
这也可能是错误的。您应该自己检查。
另外
我认为,看看基准测试过程很有趣:
# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.arraycopy
# Parameters: (size = 50000)
# Run progress: 0,00% complete, ETA 00:07:30
# Fork: 1 of 5
# Warmup Iteration 1: 8,870 ops/ms
# Warmup Iteration 2: 10,912 ops/ms
# Warmup Iteration 3: 16,417 ops/ms <- Hooray!
# Warmup Iteration 4: 17,924 ops/ms <- Hooray!
# Warmup Iteration 5: 17,321 ops/ms <- Hooray!
# Warmup Iteration 6: 16,628 ops/ms <- What!
# Warmup Iteration 7: 14,286 ops/ms <- No, stop, why!
# Warmup Iteration 8: 13,928 ops/ms <- Are you kidding me?
# Warmup Iteration 9: 13,337 ops/ms <- pff
# Warmup Iteration 10: 13,499 ops/ms
Iteration 1: 13,873 ops/ms
Iteration 2: 16,177 ops/ms
Iteration 3: 14,265 ops/ms
Iteration 4: 13,338 ops/ms
Iteration 5: 15,496 ops/ms
对于 Object.clone
# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.clone
# Parameters: (size = 50000)
# Run progress: 0,00% complete, ETA 00:03:45
# Fork: 1 of 5
# Warmup Iteration 1: 8,761 ops/ms
# Warmup Iteration 2: 12,673 ops/ms
# Warmup Iteration 3: 20,008 ops/ms
# Warmup Iteration 4: 20,340 ops/ms
# Warmup Iteration 5: 20,112 ops/ms
# Warmup Iteration 6: 20,061 ops/ms
# Warmup Iteration 7: 19,492 ops/ms
# Warmup Iteration 8: 18,862 ops/ms
# Warmup Iteration 9: 19,562 ops/ms
# Warmup Iteration 10: 18,786 ops/ms
我们可以在此处观察 System.arraycopy 的性能降级。我在 Streams 中看到了类似的图片,并且编译器中存在错误。
我想这也可能是编译器中的错误。不管怎样,3次热身后性能下降很奇怪。
更新
什么是类型检查
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopyObject {
@Param({"100"})
int size;
AtomicLong[] source;
@Setup(Level.Invocation)
public void setup() {
source = create(size);
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public AtomicLong[] clone(CloneVsArraycopyObject cloneVsArraycopy) {
return cloneVsArraycopy.source.clone();
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public AtomicLong[] arraycopy(CloneVsArraycopyObject cloneVsArraycopy) {
AtomicLong[] dest = new AtomicLong[cloneVsArraycopy.size];
System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
return dest;
}
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(CloneVsArraycopyObject.class.getSimpleName())
.jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining", "-XX:-ReduceBulkZeroing")
.warmupIterations(10)
.measurementIterations(5)
.forks(5)
.build())
.run();
}
private static AtomicLong[] create(int size) {
AtomicLong[] a = new AtomicLong[size];
for (int i = 0; i < a.length; i++) {
a[i] = new AtomicLong(ThreadLocalRandom.current().nextLong());
}
return a;
}
}
未观察到差异 - https://pastebin.com/ufxCZVaC。
我想解释很简单,因为 System.arraycopy 在这种情况下是热内在的,真正的实现将只是内联而没有任何类型检查等。
注意
我同意 Radiodef 的观点,您会发现阅读 blog post 会很有趣,此博客的作者是 JMH 的创建者(或创建者之一)。