Статьи

Ускорение благодаря быстрой Java и сериализации файлов

Начиная с первой версии Java, многие разработчики изо дня в день пытались достичь как минимум такой же высокой производительности, как в C / C ++. Поставщики JVM делают все возможное, внедряя некоторые новые алгоритмы JIT, но многое еще предстоит сделать, особенно в том, как мы используем Java.

Например, в сериализации объектов <-> можно многое выиграть, особенно в записи / чтении объектов, которые легко помещаются в памяти. Я постараюсь пролить свет на эту тему.

Все тесты были выполнены на простом объекте, показанном ниже: 

public class TestObject implements Serializable {

  private long longVariable;
  private long[] longArray;
  private String stringObject;
  private String secondStringObject; //just for testing nulls

  /* getters and setters */
}

Чтобы быть более кратким, я покажу только методы записи (хотя другой способ также очень похож). Полный исходный код доступен на моем GitHub ( http://github.com/jkubrynski/serialization-tests ).


Самая стандартная сериализация Java (с которой мы все начинаем) выглядит так:

    public void testWriteBuffered(TestObject test, String fileName) throws IOException {
      ObjectOutputStream objectOutputStream = null;
      try {
        FileOutputStream fos = new FileOutputStream(fileName);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        objectOutputStream = new ObjectOutputStream(bos);
        objectOutputStream.writeObject(test);
      } finally {
        if (objectOutputStream != null) {
          objectOutputStream.close();
        }
      }
    } 

Самый простой способ ускорить стандартную сериализацию — использовать объект RandomAccessFile:

    public void testWriteBuffered(TestObject test, String fileName) throws IOException {
      ObjectOutputStream objectOutputStream = null;
      try {
        RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
        FileOutputStream fos = new FileOutputStream(raf.getFD());
        objectOutputStream = new ObjectOutputStream(fos);
        objectOutputStream.writeObject(test);
      } finally {
        if (objectOutputStream != null) {
          objectOutputStream.close();
        }      
    } 

Более сложной техникой является использование каркаса Kryo. Разница между старой и новой версией огромна. Я проверил оба. Поскольку сравнение производительности не показывает каких-либо существенных различий, я остановлюсь на второй версии, так как она намного удобнее для пользователя и даже несколько быстрее.

    private static Kryo kryo = new Kryo(); // version 2.x

    public void testWriteBuffered(TestObject test, String fileName) throws IOException {
      Output output = null;
      try {
        RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
        output = new Output(new FileOutputStream(raf.getFD()), MAX_BUFFER_SIZE);
        kryo.writeObject(output, test);
      } finally {
        if (output != null) {
          output.close();
        }
      }
    } 

Последний вариант — это решение, вдохновленное статьей Мартина Томпсона ( http://mechanical-sympathy.blogspot.gr/2012/07/native-cc-like-performance-for-java.html ). Он показывает, как играть с памятью на C ++ и на Java ?

    public void testWriteBuffered(TestObject test, String fileName) throws IOException {
      RandomAccessFile raf = null;
      try {
        MemoryBuffer memoryBuffer = new MemoryBuffer(MAX_BUFFER_SIZE);
        raf = new RandomAccessFile(fileName, "rw");
        test.write(memoryBuffer);
        raf.write(memoryBuffer.getBuffer());
      } catch (IOException e) {
        if (raf != null) {
          raf.close();
        }
      }
    } 

Метод записи TestObject показан ниже:

  public void write(MemoryBuffer unsafeBuffer) {
    unsafeBuffer.putLong(longVariable);
    unsafeBuffer.putLongArray(longArray);
    // we support nulls
    boolean objectExists = stringObject != null;
    unsafeBuffer.putBoolean(objectExists);
    if (objectExists) {
      unsafeBuffer.putCharArray(stringObject.toCharArray());
    }
    objectExists = secondStringObject != null;
    unsafeBuffer.putBoolean(objectExists);
    if (objectExists) {
      unsafeBuffer.putCharArray(secondStringObject.toCharArray());
    }
  }   

Прямой буферный класс памяти (сокращен, чтобы показать идею):

public class MemoryBuffer {
  // getting Unsafe by reflection
  public static final Unsafe unsafe = UnsafeUtil.getUnsafe();

  private final byte[] buffer;

  private static final long byteArrayOffset = unsafe.arrayBaseOffset(byte[].class);
  private static final long longArrayOffset = unsafe.arrayBaseOffset(long[].class);
  /* other offsets */

  private static final int SIZE_OF_LONG = 8;
  /* other sizes */

  private long pos = 0;

  public MemoryBuffer(int bufferSize) {
    this.buffer = new byte[bufferSize];
  }

  public final byte[] getBuffer() {
    return buffer;
  }

  public final void putLong(long value) {
    unsafe.putLong(buffer, byteArrayOffset + pos, value);
    pos += SIZE_OF_LONG;
  }

  public final long getLong() {
    long result = unsafe.getLong(buffer, byteArrayOffset + pos);
    pos += SIZE_OF_LONG;
    return result;
  }

  public final void putLongArray(final long[] values) {
    putInt(values.length);
    long bytesToCopy = values.length << 3;
    unsafe.copyMemory(values, longArrayOffset, buffer, byteArrayOffset + pos, bytesToCopy);
    pos += bytesToCopy;
  }


  public final long[] getLongArray() {
    int arraySize = getInt();
    long[] values = new long[arraySize];
    long bytesToCopy = values.length << 3;
    unsafe.copyMemory(buffer, byteArrayOffset + pos, values, longArrayOffset, bytesToCopy);
    pos += bytesToCopy;
    return values;
  }

  /* other methods */
} 

Результаты многих часов пробежек Калипера показаны ниже:

  Полная поездка [нс]  Стандартное отклонение [нс] 
стандарт   207307  2362
Стандарт на РАФ  42661  733
KRYO 1.x   12027  112
KRYO 2.x  11479  259
небезопасный  8554  91

В итоге мы можем сделать несколько выводов:

  • Небезопасная сериализация более чем в 23 раза быстрее, чем стандартное использование java.io.Serializable
  • Использование RandomAccessFile может ускорить стандартную буферизованную сериализацию почти в 4 раза
  • Криодинамическая сериализация примерно на 35% медленнее, чем реализованный вручную прямой буфер.


Наконец, как мы видим, золотого молотка все еще нет. Для многих из нас получение 3000 нс (0,003 мс) не стоит написания пользовательских реализаций для каждого объекта, который мы хотим сериализовать с файлами. А для стандартных решений мы в основном выберем Kryo. Тем не менее, в системах с малой задержкой, где 100 нс кажутся вечностью, выбор будет совершенно другим.