OpenJDK实现的System.arraycopy

12

在与JVM创建基于char[]的字符串相关的问题中,我提到当char[]被复制到新字符串的内部时,没有进行任何迭代,因为最终会调用System.arraycopy,该函数使用类似于memcpy的本地实现依赖级别来复制所需的内存(原问题)。

我想自己验证一下,于是我下载了Openjdk 7源代码并开始浏览。我在OpenJDK C++源代码中找到了System.arraycopy的实现,在openjdx/hotspot/src/share/vm/oops/objArrayKlass.cpp中:

if (stype == bound || Klass::cast(stype)->is_subtype_of(bound)) {
  // elements are guaranteed to be subtypes, so no check necessary
  bs->write_ref_array_pre(dst, length);
  Copy::conjoint_oops_atomic(src, dst, length);
} else {
  // slow case: need individual subtype checks

如果元素不需要类型检查(例如原始数据类型数组),则会调用Copy::conjoin_oops_atomic。

Copy::conjoint_oops_atomic函数位于'copy.hpp'中:

// overloaded for UseCompressedOops
static void conjoint_oops_atomic(narrowOop* from, narrowOop* to, size_t count) {
  assert(sizeof(narrowOop) == sizeof(jint), "this cast is wrong");
  assert_params_ok(from, to, LogBytesPerInt);
  pd_conjoint_jints_atomic((jint*)from, (jint*)to, count);
}

现在我们是平台相关的,因为复制操作基于操作系统 / 架构有不同的实现。我将以 Windows 为例。 openjdk\hotspot\src\os_cpu\windows_x86\vm\copy_windows_x86.inline.hpp

static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
// Do better than this: inline memmove body  NEEDS CLEANUP
if (from > to) {
  while (count-- > 0) {
    // Copy forwards
    *to++ = *from++;
  }
} else {
  from += count - 1;
  to   += count - 1;
  while (count-- > 0) {
    // Copy backwards
    *to-- = *from--;
  }
 }
}

但是让我惊讶的是,它遍历了元素(oop值),一个接一个地复制它们(貌似如此)。有人可以解释一下为什么即使在本机级别,也要通过迭代数组中的元素来进行复制吗?

1个回答

6
因为 jint 最接近于 int,而 int 最接近于旧硬件架构 WORD,它的大小基本上与数据总线的宽度相同。
今天的内存架构和 CPU 处理都设计成在缓存未命中的情况下尝试处理,并且内存位置倾向于预取块。你看到的代码的性能并不像你想象的那样“糟糕”。硬件更加智能化,如果你没有实际分析,你的“智能”获取例程可能实际上什么也没做(甚至会减慢处理速度)。
当你介绍硬件架构时,必须介绍简单的架构。现代架构做了很多事情,因此你不能假设看起来效率低下的代码实际上是效率低下的。例如,当进行内存查找以评估 if 语句的条件时,通常在查找发生时两个 if 语句的分支都会执行,而在数据可用于评估条件之后,“false”处理分支将被丢弃。如果你想要高效,你必须分析并根据分析的结果采取行动。
看一下 JVM 操作码部分的分支。你会发现这是一个 ifdef 宏的怪异之处,用于支持(曾经)三种不同的跳转方式到处理操作码的代码。这是因为这三种不同的方式在不同的 Windows、Linux 和 Solaris 架构上实际上会产生有意义的性能差异。
也许他们可以包括 MMX 例程,但他们没有这样做,这告诉我 SUN 认为在现代硬件上它并没有足够的性能提升值得担心。

哇,谢谢!第一次查看OpenJDK实现时有点困惑,所以我预计可能会出错。 :P 那么你认为这种优化是如何进行的呢?我做了一些测试,System.arraycopy在复制10000个int时比常规Java方式快两倍。在C++中,类似的任务仍然明显更快,尽管结果可能受到各种编译器优化的影响。 - Andrei Bârsan
C++的复制没有垃圾收集器在单独的线程上运行。即使您不生成垃圾,收集器也必须窃取一些周期来验证它没有工作要做。我不确定编译器是否展开了arraycopy循环,或者硬件是否将整个块的数组预取到缓存中。实际上,通过微码优化,这超出了我的知识深度。这就是为什么性能分析如此重要,它是证明优化是值得的测试。 - Edwin Buck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接