Skip to content

Latest commit

 

History

History
629 lines (526 loc) · 21 KB

ArrayPassingBenchmarks.md

File metadata and controls

629 lines (526 loc) · 21 KB

JNI Array Passing Benchmarks

The code contrasts several different approaches to passing an array of complex objects from C++ into Java. This can be done as either 2 independent arrays (one for each complex object property), or as an array of tuple objects holding the values. Allocation can be done in either C++ or Java. Such a scenario is common when writing a Java API wrapper for an existing C++ project. NOTE: The C++ JNI code includes appropriate error checking, as the code has to be correct as well as performant!

The complex object in Java looks like:

public class FooObject {
  final String name;
  final long value;

  public FooObject(final String name, final long value) {
    this.name = name;
    this.value = value;
  }
}

The complex object in C++ looks like:

class FooObject {
  public:
    FooObject(const std::string& n, int64_t v) : name(n), value(v){}

    const std::string& GetName() const { return name; }
    int64_t GetValue() const { return value; }

  private:
    const std::string name;
    const int64_t value;
};

The goal is to benchmark different approaches for returning Arrays/Lists of the C++ FooObject to Java.

Scenario 1 - Allocate Complex Object array in Java, Fill in C++

We allocate a Java array in Java, and then in C++ we create Java complex objects and add them to the array. We then return to Java and wrap the array in an ArrayList.

public class AllocateInJavaGetArray implements JniListSupplier<FooObject> {
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
    final int len = (int) getArraySize(nativeObjectArray.get_nativeHandle());
    final FooObject objectList[] = new FooObject[len];
    getArray(nativeObjectArray.get_nativeHandle(), objectList);
    return Arrays.asList(objectList);
  }

  private static native long getArraySize(final long handle);
  private static native void getArray(final long handle, final FooObject[] objectList);
}
jlong Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetArray_getArraySize(
    JNIEnv *, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  return static_cast<jlong>(cpp_array.size());
}

void Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetArray_getArray(
    JNIEnv *env, jclass, jlong handle, jobjectArray jobject_array) {
  const jclass jfoo_obj_clazz = FooObjectJni::getJClass(env);
  if (jfoo_obj_clazz == nullptr) {
    // exception occurred accessing class
    return;
  }

  auto* cpp_array = reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  for (jsize i = 0; i < env->GetArrayLength(jobject_array); i++) {
    jnibench::FooObject foo_obj = (*cpp_array)[static_cast<size_t>(i)];

    jobject jfoo_obj = FooObjectJni::construct(env, jfoo_obj_clazz, foo_obj);
    if (jfoo_obj == nullptr) {
        // exception occurred
        return;
    }

    env->SetObjectArrayElement(jobject_array, i, jfoo_obj);
    if(env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      // or ArrayStoreException
      env->DeleteLocalRef(jfoo_obj);
      return;
    }

    env->DeleteLocalRef(jfoo_obj);
  }
}

Scenario 2 - Allocate Complex Object array and fill with mutable objects in Java, mutate the objects in C++

We allocate a Java array in Java, and fill it with mutable complex java objects. In C++ we then update the mutable objects, then returning to Java, where we wrap the array in an ArrayList.

public class AllocateInJavaGetMutableArray implements JniListSupplier<FooObject> {
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
    final int len = (int) getArraySize(nativeObjectArray.get_nativeHandle());
    final FooObject objectList[] = new FooObject[len];
    for (int i = 0; i < len; i++) {
      objectList[i] = new FooObject();
    }

    getArray(nativeObjectArray.get_nativeHandle(), objectList);

    return Arrays.asList(objectList);
  }

  private static native long getArraySize(final long handle);
  private static native void getArray(final long handle, final FooObject[] objectList);
}
jlong Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetMutableArray_getArraySize(
    JNIEnv *, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  return static_cast<jlong>(cpp_array.size());
}

void Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetMutableArray_getArray(
    JNIEnv *env, jclass, jlong handle, jobjectArray jobject_array) {
  const jclass jfoo_obj_clazz = FooObjectJni::getJClass(env);
  if (jfoo_obj_clazz == nullptr) {
    // exception occurred accessing class
    return;
  }

  const jfieldID fid_name = FooObjectJni::getNameField(env, jfoo_obj_clazz);
  if (fid_name == nullptr) {
    // exception occurred accessing field
    return;
  }

  const jfieldID fid_value = FooObjectJni::getValueField(env, jfoo_obj_clazz);
  if (fid_value == nullptr) {
    // exception occurred accessing field
    return;
  }

  auto* cpp_array = reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  for (jsize i = 0; i < env->GetArrayLength(jobject_array); i++) {
    jnibench::FooObject foo_obj = (*cpp_array)[static_cast<size_t>(i)];

    jobject jfoo_obj = env->GetObjectArrayElement(jobject_array, i);
    if(env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      // or ArrayStoreException
      if (jfoo_obj != nullptr) {
        env->DeleteLocalRef(jfoo_obj);
      }
      return;
    }

    // set name field
    jstring jname = env->NewStringUTF(foo_obj.GetName().c_str());
    if (env->ExceptionCheck()) {
      if (jname != nullptr) {
        env->DeleteLocalRef(jname);
      }
      env->DeleteLocalRef(jfoo_obj);
      return;
    }
    env->SetObjectField(jfoo_obj, fid_name, jname);
    if (env->ExceptionCheck()) {
          env->DeleteLocalRef(jname);
          env->DeleteLocalRef(jfoo_obj);
          return;
    }
    env->DeleteLocalRef(jname);

    // set value field
    env->SetLongField(jfoo_obj, fid_value, static_cast<jlong>(foo_obj.GetValue()));
    if (env->ExceptionCheck()) {
      env->DeleteLocalRef(jfoo_obj);
      return;
    }

    env->DeleteLocalRef(jfoo_obj);
  }
}

Scenario 3 - Allocate 2 arrays in Java, Fill in C++, copy to Complex Object Array in Java

In Java we allocate 2 arrays, one for each property of the complex object of which we ultimately want to return an array of. We then pass those 2 arrays to C++ via JNI. In C++ we populate those two arrays, and return them to Java. Back in Java we create an array of complex objects based on the values of those two arrays.

public class AllocateInJavaGet2DArray implements JniListSupplier<FooObject> {
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
    final int len = (int) getArraySize(nativeObjectArray.get_nativeHandle());
    final String names[] = new String[len];
    final long values[] = new long[len];

    getArrays(nativeObjectArray.get_nativeHandle(), names, values);

    final List<FooObject> objectList = new ArrayList<>();
    for (int i = 0; i < len; i++) {
      objectList.add(new FooObject(names[i], values[i]));
    }
    return objectList;
  }

  private static native long getArraySize(final long handle);
  private static native void getArrays(final long handle,
      final String[] paths, final long[] targetSizes);
}
jlong Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGet2DArray_getArraySize
  (JNIEnv *, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  return static_cast<jlong>(cpp_array.size());
}

void Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGet2DArray_getArrays(
    JNIEnv *env, jclass, jlong handle, jobjectArray name_array, jlongArray value_array) {
  jlong* value_array_ptr = env->GetLongArrayElements(value_array, nullptr);
  if (value_array_ptr == nullptr) {
    // exception thrown: OutOfMemoryError
    return;
  }

  auto* cpp_array = reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  for (jsize i = 0; i < env->GetArrayLength(name_array); i++) {
    jnibench::FooObject foo_obj = (*cpp_array)[i];

    jstring jname = env->NewStringUTF(foo_obj.GetName().c_str());
    if (jname == nullptr) {
      // exception thrown: OutOfMemoryError
      env->ReleaseLongArrayElements(value_array, value_array_ptr, JNI_ABORT);
      return;
    }
    env->SetObjectArrayElement(name_array, i, jname);
    if (env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      env->DeleteLocalRef(jname);
      env->ReleaseLongArrayElements(value_array, value_array_ptr, JNI_ABORT);
      return;
    }

    value_array_ptr[i] = static_cast<jlong>(foo_obj.GetValue());
  }

  env->ReleaseLongArrayElements(value_array, value_array_ptr, 0);
}

Scenario 4 - Allocate Complex Object Array in C++, Fill in C++

In C++ we allocate a Java array, and then we create Java complex objects and add them to the array. We then return to Java and wrap the array in an ArrayList.

public class AllocateInCppGetArray implements JniListSupplier<FooObject> {
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
    return Arrays.asList(getArray(nativeObjectArray.get_nativeHandle()));
  }

  private static native FooObject[] getArray(final long handle);
}
jobjectArray Java_com_evolvedbinary_jnibench_common_array_AllocateInCppGetArray_getArray(
    JNIEnv *env, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  jsize length = static_cast<jsize>(cpp_array.size());

  jclass jfoo_obj_clazz = FooObjectJni::getJClass(env);
  if (jfoo_obj_clazz == nullptr) {
    // exception occurred accessing class
    return nullptr;
  }

  jobjectArray java_array = env->NewObjectArray(length, jfoo_obj_clazz, nullptr);
  if (java_array == nullptr) {
      // exception thrown: OutOfMemoryError
      return nullptr;
  }

  for (size_t i = 0; i < cpp_array.size(); ++i) {
    const jnibench::FooObject& foo_obj = cpp_array[i];
    jobject jfoo_obj = FooObjectJni::construct(env, jfoo_obj_clazz, foo_obj);
    if (jfoo_obj == nullptr) {
        // exception occurred
        env->DeleteLocalRef(java_array);
        return nullptr;
    }
    env->SetObjectArrayElement(java_array, static_cast<jsize>(i), jfoo_obj);
    if (env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      // or ArrayStoreException
      env->DeleteLocalRef(jfoo_obj);
      env->DeleteLocalRef(java_array);
      return nullptr;
    }

    env->DeleteLocalRef(jfoo_obj);
  }
  return java_array;
}

Scenario 5 - Allocate 2 arrays in C++, Fill in C++, copy to Complex Object Array in Java

In C++ we allocate 2 Java arrays, one for each property of the complex object of which we ultimately want to return an array of. We then populate those 2 arrays, and return them to Java. Back in Java we create an array of complex objects based on the values of those two arrays.

public class AllocateInCppGet2DArray implements JniListSupplier<FooObject> {
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
    final Object[][] objArr = get2DArray(nativeObjectArray.get_nativeHandle());
    final String[] names = (String[]) objArr[0];
    final Long[] values = (Long[]) objArr[1];
    final List<FooObject> objList = new ArrayList<>();
    for (int i = 0; i < names.length; ++i) {
      objList.add(new FooObject(names[i], values[i]));
    }
    return objList;
  }

  protected static native Object[][] get2DArray(final long handle);
}
jobjectArray Java_com_evolvedbinary_jnibench_common_array_AllocateInCppGet2DArray_get2DArray
  (JNIEnv *env, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  jsize len = static_cast<jsize>(cpp_array.size());

  jclass jstring_clazz = StringJni::getJClass(env);
  if (jstring_clazz == nullptr) {
    // exception occurred accessing class
    return nullptr;
  }

  jclass jlong_clazz = LongJni::getJClass(env);
  if (jlong_clazz == nullptr) {
      // exception occurred accessing class
      return nullptr;
  }

  jobjectArray jname_array = env->NewObjectArray(len, jstring_clazz, nullptr);
  if (jname_array == nullptr) {
    // exception thrown: OutOfMemoryError
    return nullptr;
  }
  jobjectArray jvalue_array = env->NewObjectArray(len, jlong_clazz, nullptr);
  if (jvalue_array == nullptr) {
    // exception thrown: OutOfMemoryError
    env->DeleteLocalRef(jname_array);
    return nullptr;
  }

  for (size_t i = 0; i < cpp_array.size(); ++i) {
    const jnibench::FooObject& foo_obj = cpp_array[i];
    jstring jname = env->NewStringUTF(foo_obj.GetName().c_str());
    if (env->ExceptionCheck()) {
      if (jname != nullptr) {
        env->DeleteLocalRef(jname_array);
        env->DeleteLocalRef(jvalue_array);
        env->DeleteLocalRef(jname);
      }
      return nullptr;
    }

    jobject jvalue = LongJni::construct(env, jlong_clazz, foo_obj.GetValue());
    if (jvalue == nullptr) {
      env->DeleteLocalRef(jname_array);
      env->DeleteLocalRef(jvalue_array);
      env->DeleteLocalRef(jname);
      return nullptr;
    }

    env->SetObjectArrayElement(jname_array, static_cast<jsize>(i), jname);
    if (env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      // or ArrayStoreException
      env->DeleteLocalRef(jname_array);
      env->DeleteLocalRef(jvalue_array);
      env->DeleteLocalRef(jname);
      env->DeleteLocalRef(jvalue);
      return nullptr;
    }
    env->SetObjectArrayElement(jvalue_array, static_cast<jsize>(i), jvalue);
    if (env->ExceptionCheck()) {
      // exception thrown: ArrayIndexOutOfBoundsException
      // or ArrayStoreException
      env->DeleteLocalRef(jname_array);
      env->DeleteLocalRef(jvalue_array);
      env->DeleteLocalRef(jname);
      env->DeleteLocalRef(jvalue);
      return nullptr;
    }

    env->DeleteLocalRef(jname);
    env->DeleteLocalRef(jvalue);
  }

  jobjectArray jobj_array = env->NewObjectArray(2, env->FindClass("java/lang/Object"), nullptr);
  if (jobj_array == nullptr) {
    // exception thrown: OutOfMemoryError
    env->DeleteLocalRef(jname_array);
    env->DeleteLocalRef(jvalue_array);
    return nullptr;
  }

  env->SetObjectArrayElement(jobj_array, 0, jname_array);
  if (env->ExceptionCheck()) {
    // exception thrown: ArrayIndexOutOfBoundsException
    // or ArrayStoreException
    env->DeleteLocalRef(jname_array);
    env->DeleteLocalRef(jvalue_array);
    env->DeleteLocalRef(jobj_array);
    return nullptr;
  }
  env->SetObjectArrayElement(jobj_array, 1, jvalue_array);
  if (env->ExceptionCheck()) {
    // exception thrown: ArrayIndexOutOfBoundsException
    // or ArrayStoreException
    env->DeleteLocalRef(jname_array);
    env->DeleteLocalRef(jvalue_array);
    env->DeleteLocalRef(jobj_array);
    return nullptr;
  }

  return jobj_array;
}

Scenario 6 - Allocate 2 arrays in C++, Fill in C++, copy to custom List (backed by 2 arrays) in Java

This is an extended version of Scenario 5, where the resultant 2 arrays are wrapped in a custom list. This scenario is concerned with reducing the number of data copies that are needed in Scenario 3. The C++ code is the same as that in Scenario 3, for the Java code see: AllocateInJavaGetArrayList.java.

Scenario 7 - Allocate ArrayList in Java, and fill with Complex Object in C++

This is similar to Scenario 1, but operates directly with a java.util.ArrayList instead of an array.

public class AllocateInJavaGetArrayList implements JniListSupplier<FooObject> {
  @Override
  public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
      final List<FooObject> objectList = new ArrayList<>(len);
      getList(nativeObjectArray.get_nativeHandle(), objectList);
      return objectList;
  }

  private static native long getListSize(final long handle);
  private static native void getList(final long handle, final List<FooObject> list);
}
jlong Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetArrayList_getListSize(
    JNIEnv *, jclass, jlong handle) {
  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  return static_cast<jlong>(cpp_array.size());
}

void Java_com_evolvedbinary_jnibench_common_array_AllocateInJavaGetArrayList_getList(
    JNIEnv *env, jclass, jlong handle, jobject jlist) {

  const jclass jfoo_obj_clazz = FooObjectJni::getJClass(env);
  if (jfoo_obj_clazz == nullptr) {
    // exception occurred accessing class
    return;
  }

  const jmethodID add_mid = ListJni::getListAddMethodId(env);
  if (add_mid == nullptr) {
    // exception occurred accessing method
    return;
  }

  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  for (auto foo_obj : cpp_array) {
    // create java FooObject
    const jobject jfoo_obj = FooObjectJni::construct(env, jfoo_obj_clazz, foo_obj);
    if (jfoo_obj == nullptr) {
      // exception occurred constructing object
      return;
    }

    // add to list
    const jboolean rs = env->CallBooleanMethod(jlist, add_mid, jfoo_obj);
    if (env->ExceptionCheck() || rs == JNI_FALSE) {
      // exception occurred calling method, or could not add
      env->DeleteLocalRef(jfoo_obj);
      return;
    }
  }
}

Scenario 8 - Allocate ArrayList in C++, and fill with Complex Object in C++

This is similar to Scenario 4, but operates directly with a java.util.ArrayList instead of an array.

public class AllocateInCppGetArrayList implements JniListSupplier<FooObject> {
    public List<FooObject> getObjectList(final NativeObjectArray<FooObject> nativeObjectArray) {
        return getArrayList(nativeObjectArray.get_nativeHandle());
    }

    private static native List<FooObject> getArrayList(final long handle);
}
jobject Java_com_evolvedbinary_jnibench_common_array_AllocateInCppGetArrayList_getArrayList(
    JNIEnv *env, jclass, jlong handle) {

  const jclass jfoo_obj_clazz = FooObjectJni::getJClass(env);
  if (jfoo_obj_clazz == nullptr) {
    // exception occurred accessing class
    return nullptr;
  }

  const jclass clazz_array_list = ListJni::getArrayListClass(env);
  const jmethodID ctor_array_list = ListJni::getArrayListConstructorMethodId(env);
  if (ctor_array_list == nullptr) {
    // exception occurred accessing method
    return nullptr;
  }

  const jmethodID add_mid = ListJni::getListAddMethodId(env);
  if (add_mid == nullptr) {
    // exception occurred accessing method
    return nullptr;
  }

  const auto& cpp_array = *reinterpret_cast<std::vector<jnibench::FooObject>*>(handle);
  const jsize len = static_cast<jsize>(cpp_array.size());

  // create new java.util.ArrayList
  const jobject jlist = env->NewObject(clazz_array_list, ctor_array_list,
              static_cast<jint>(len));
  if (env->ExceptionCheck()) {
    // exception occurred constructing object
    if (jlist != nullptr) {
      env->DeleteLocalRef(jlist);
    }
    return nullptr;
  }

  for (auto foo_obj : cpp_array) {
    // create java FooObject
    const jobject jfoo_obj = FooObjectJni::construct(env, jfoo_obj_clazz, foo_obj);
    if (jfoo_obj == nullptr) {
      // exception occurred constructing object
      return nullptr;
    }

    // add to list
    const jboolean rs = env->CallBooleanMethod(jlist, add_mid, jfoo_obj);
    if (env->ExceptionCheck() || rs == JNI_FALSE) {
      // exception occurred calling method, or could not add
      env->DeleteLocalRef(jlist);
      env->DeleteLocalRef(jfoo_obj);
      return nullptr;
    }
  }

  return jlist;
}

Array Passing Results

Test machine: MacBook Pro 15-inch 2019: 2.4 GHz 8-Core Intel Core i9 / 32 GB 2400 MHz DDR4. OS X 10.15.2 / Liberica OpenJDK 8.

$ java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)
$ clang --version
Apple clang version 11.0.3 (clang-1103.0.32.62)
Target: x86_64-apple-darwin19.6.0
Thread model: posix

The com.evolvedbinary.jnibench.consbench.Benchmark class already calls each scenario 1,000,000 times, so for the benchmark we repeated this 100 times and plotted the results.

Image of JNI Array Passing Benchmark Results when size is 2 Image of JNI Array Passing Benchmark Results when size is 20

Array Passing Conclusions

The fastest approach appears to be by performing most of the allocations in Java, and then passing arrays of simple types between C++ and Java. For the array/list of complex objects to be returned from C++ to Java, allocating one array in Java for each of the complex objects property's, and then populating those arrays in C++ seems to be the most performant approach (see AllocatedInJavaGet2DArray.java).