Почему System.arraycopy работает быстро

Java задняя часть JVM Tomcat
Почему System.arraycopy работает быстро

предисловие

Операции копирования массивов часто встречаются в программировании на Java.Как правило, существуют следующие четыре способа копирования массивов.

  • forTraverse перебирает исходный массив и присваивает каждый элемент целевому массиву.
  • cloneметод, исходный вызов массиваcloneНовый клонированный объект назначается целевому массиву, более подробные сведения о клонах см. в предыдущей статье "Клонирование объектов с точки зрения JDK".
  • System.arraycopy, реализация копирования массива, предоставляемая JVM.
  • Arrays.copyof, который на самом деле вызываетSystem.arraycopy.

для обхода

В этом случае в слое Java записывается цикл for для обхода каждого элемента массива и его копирования.Если он не оптимизирован компилятором, он соответствует байт-коду операции обхода массива, а механизм выполнения зацикливается на получить массив в соответствии с этими байт-кодами, затем выполнить операцию копирования для каждого элемента .

Использование массива

Он очень прост в использовании, например, копирование массива следующим образом.

int size = 10000;
int[] src = new int[size];
int[] des = new int[size];
System.arraycopy(src, 0, des, 0, size);

метод копирования массива

Этот метод используется для выполнения операции копирования из указанного исходного массива.Вы можете указать начальную позицию и скопировать элементы указанной длины в указанный целевой массив. Метод является собственным методом, объявленным следующим образом:

@HotSpotIntrinsicCandidate
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

О @HotSpotIntrinsicCandidate

Эта аннотация является стандартной аннотацией HotSpot VM. Метод, отмеченный ею, указывает на то, что это встроенный метод HotSpot VM. HotSpot VM внесет в него некоторые улучшения для повышения производительности выполнения, например, возможно, ручную сборку или ручную сборку. написан компилятор промежуточного языка для замены реализации этого метода. Хотя он объявлен как нативный метод, он отличается от других нативных методов в JDK: нативный метод будет реализован внутри JVM, а остальные будут реализованы в библиотеке JDK. С точки зрения вызова накладные расходы также сохраняются, поскольку внутренняя реализация JVM вызывается напрямую вместо обычного поиска JNI.

собственный метод копирования массива

Класс Java System имеет статический блок, который выполняется при загрузке класса, который выполняет соответствующиеregisterNativesРодной метод.

public final class System {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

в соответствующемSystem.cсерединаJava_java_lang_System_registerNativesМетод выглядит следующим образом, вы можете видеть, что есть три нативных метода, привязанных к встроенным методам JVM, один из которыхarraycopy, его соответствующая функция(void *)&JVM_ArrayCopy.

JNIEXPORT void JNICALL
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}

#define OBJ "Ljava/lang/Object;"
static JNINativeMethod methods[] = {
    {"currentTimeMillis", "()J",              (void *)&JVM_CurrentTimeMillis},
    {"nanoTime",          "()J",              (void *)&JVM_NanoTime},
    {"arraycopy",     "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy},
};

Затем через вышеизложенноеarraycopyметод связан со следующимJVM_ArrayCopyпредыдущая логика в основном используется для проверки того, пусты ли исходный массив и целевой массив, и если они пусты, генерируется нулевой указатель; затем преобразуйте объект исходного массива и объект целевого массива вarrayOop, то есть описание объекта массива,assertиспользуется для определения того, являются ли они объектами; последнийs->klass()->copy_arrayЭто настоящая операция копирования массива.

JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
                               jobject dst, jint dst_pos, jint length))
  JVMWrapper("JVM_ArrayCopy");
  // Check if we have null pointers
  if (src == NULL || dst == NULL) {
    THROW(vmSymbols::java_lang_NullPointerException());
  }
  arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
  arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
  assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
  assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
  // Do copy
  s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END

Основные и распространенные типы

По упомянутому вышеs->klass()->copy_arrayЧтобы завершить операцию копирования, процесс обработки фактически имеет различную обработку в соответствии с различными типами Java.Массивы можно разделить на базовые типы и общие типы в соответствии с типами элементов в них.Соответствующие JVM:TypeArrayKlassиObjArrayKlass.

TypeArrayKlass

Здесь некоторый исходный код проверки удаляется, оставляя основной код.Поскольку он включает перемещение указателей в памяти, чтобы повысить эффективность операции присваивания, начальная и конечная позиции преобразуются вchar*,log2_element_sizeзаключается в вычислении длины типа элемента массиваlogЗначение, позиция может быть быстро рассчитана по операции смещения позже. иarray_header_in_bytesВычисление смещения первого элемента.

void TypeArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d, int dst_pos, int length, TRAPS) {
  ....
  int l2es = log2_element_size();
  int ihs = array_header_in_bytes() / wordSize;
  char* src = (char*) ((oop*)s + ihs) + ((size_t)src_pos << l2es);
  char* dst = (char*) ((oop*)d + ihs) + ((size_t)dst_pos << l2es);
  Copy::conjoint_memory_atomic(src, dst, (size_t)length << l2es);
}

затем кCopy::conjoint_memory_atomicфункция, основная логика этой функции заключается в том, чтобы определить, к какому базовому типу принадлежит элемент, а затем вызвать соответствующую функцию. Поскольку уже есть начальный и конечный указатели, операции с быстрой памятью могут выполняться в зависимости от типа. Вот пример интегрального типа, который вызоветCopy::conjoint_jints_atomicфункция.

void Copy::conjoint_memory_atomic(void* from, void* to, size_t size) {
  address src = (address) from;
  address dst = (address) to;
  uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;

  if (bits % sizeof(jlong) == 0) {
    Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong));
  } else if (bits % sizeof(jint) == 0) {
    Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint));
  } else if (bits % sizeof(jshort) == 0) {
    Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort));
  } else {
    // Not aligned, so no need to be atomic.
    Copy::conjoint_jbytes((void*) src, (void*) dst, size);
  }
}

conjoint_jints_atomicФункция в основном называетсяpd_conjoint_jints_atomicфункция, имеющая свою реализацию в разных операционных системах, см. здесьwindows_x86Реализация,

static void conjoint_jints_atomic(jint* from, jint* to, size_t count) {
    assert_params_ok(from, to, LogBytesPerInt);
    pd_conjoint_jints_atomic(from, to, count);
  }

Основная логика состоит в том, чтобы разделить репликацию на два случая: прямая репликация и обратная репликация. А присваивание осуществляется обходом массива по указателю, здесь выполняется копирование значения, которое некоторые называют так называемой «глубокой копией».

static void pd_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
  if (from > to) {
    while (count-- > 0) {
      // Copy forwards
      *to++ = *from++;
    }
  } else {
    from += count - 1;
    to   += count - 1;
    while (count-- > 0) {
      // Copy backwards
      *to-- = *from--;
    }
  }
}

заlong,short,byteи другие типы также обрабатываются аналогично, но в некоторых операционных системахcpuАрхитектура будет реализована на ассемблере.

ObjArrayKlass

Рассмотрим операцию копирования, когда в качестве элементов массива используются объекты обычного типа, здесь некоторые исходные коды проверки удаляются, оставляя основной код.UseCompressedOopsИдентификатор указывает на сжатие указателя объекта Java в JVM, в основном указывая, используется ли 32-разрядный или 64-разрядный указатель объекта. Игнорируйте его здесь, смотрите прямо на несжатый случай, то есть он вызоветdo_copy<oop>функция.

void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
                               int dst_pos, int length, TRAPS) {
  ...
  if (UseCompressedOops) {
    narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
    narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
    do_copy<narrowOop>(s, src, d, dst, length, CHECK);
  } else {
    oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
    oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
    do_copy<oop> (s, src, d, dst, length, CHECK);
  }
}

Этот код длиннее, такой же, я убираю часть кода, оставляю небольшой кусок кода, который может объяснить проблему. Здесь будут проводиться здесьs==dРешение заключается в том, что исходный массив и целевой массив могут быть равны, и если они не равны, необходимо оценить, совпадает ли тип элемента исходного массива с типом элемента целевого массива. суждение о подклассе. Алгоритм основного назначения в двух приведенных выше случаях таков:Copy::conjoint_oops_atomic.

template <class T> void ObjArrayKlass::do_copy(arrayOop s, T* src,
                               arrayOop d, T* dst, int length, TRAPS) {

  BarrierSet* bs = Universe::heap()->barrier_set();
  if (s == d) {
    bs->write_ref_array_pre(dst, length);
    Copy::conjoint_oops_atomic(src, dst, length);
  } else {
    Klass* bound = ObjArrayKlass::cast(d->klass())->element_klass();
    Klass* stype = ObjArrayKlass::cast(s->klass())->element_klass();
    if (stype == bound || stype->is_subtype_of(bound)) {
      bs->write_ref_array_pre(dst, length);
      Copy::conjoint_oops_atomic(src, dst, length);
    } else {
      ...
    }
  }
  bs->write_ref_array((HeapWord*)dst, length);
}

Эта функция также связана с операционной системой и архитектурой процессора, см. здесьwindows_x86Реализация очень проста, и она также напрямую назначается путем обхода указателя.oopэто объектный класс уровня JVM, и этот класс не был переписанoperator=Оператор по умолчанию копирует адрес, поэтому они по-прежнему указывают на одну и ту же память, которая также отражена в слое Java. Это так называемая «неглубокая копия».

static void conjoint_oops_atomic(oop* from, oop* to, size_t count) {
    pd_conjoint_oops_atomic(from, to, count);
}
static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
  if (from > to) {
    while (count-- > 0) {
      *to++ = *from++;
    }
  } else {
    from += count - 1;
    to   += count - 1;
    while (count-- > 0) {
      // Copy backwards
      *to-- = *from--;
    }
  }
}

Суммировать

System.arraycopyЭто внутренний метод внутри JVM, который создает копию массива Java с помощью ручной сборки или других методов оптимизации, что более эффективно, чем выполнение цикла for или клонирование непосредственно в Java. Чем больше массив, тем он очевиднее.

------------- Рекомендуем прочитать ------------

Резюме моей статьи за 2017 год — машинное обучение

Краткое изложение моих статей за 2017 год — Java и промежуточное ПО

Резюме моих статей 2017 года — глубокое обучение

Краткое изложение моих статей за 2017 год — исходный код JDK

Резюме моей статьи за 2017 год — обработка естественного языка

Резюме моих статей 2017 года — Java Concurrency

------------------рекламное время----------------

Меню официальной учетной записи было разделено на «распределенное», «машинное обучение», «глубокое обучение», «НЛП», «глубина Java», «ядро параллелизма Java», «исходный код JDK», «ядро Tomcat», и т.д. Там может быть один стиль, чтобы удовлетворить ваш аппетит.

Моя новая книга «Анализ дизайна ядра Tomcat» продана на Jingdong, и нуждающиеся друзья могут ее купить. Спасибо друзья.

Зачем писать «Анализ проектирования ядра Tomcat»

Добро пожаловать, чтобы следовать:

这里写图片描述