Статьи

Ява: Хроника Байтов, Пинающий Шины

Чтение и запись двоичных данных с помощью Java иногда может быть проблемой. Прочтите эту статью и узнайте, как использовать байты хроники, тем самым делая эти задачи быстрее и проще.

Недавно я участвовал в проекте с открытым исходным кодом «Chronicle Decentred», который представляет собой высокопроизводительную децентрализованную бухгалтерскую книгу, основанную на технологии блокчейна. Для нашего двоичного доступа мы использовали библиотеку «Хроники байтов», которая привлекла мое внимание. В этой статье я поделюсь некоторыми знаниями, которые я получил, используя библиотеку Bytes.

Что такое байты?

Bytes — это библиотека, которая обеспечивает функциональность, аналогичную встроенной в Java
ByteBuffer но, очевидно, с некоторыми расширениями. Оба предоставляют базовую абстракцию буфера для хранения байтов с дополнительными функциями по сравнению с необработанными массивами байтов. Они также являются ПРОСМОТРАМИ базовых байтов и могут быть подкреплены необработанным массивом байтов, а также собственной памятью (вне кучи) или, возможно, даже файлом.

Вот краткий пример использования байтов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// Allocate off-heap memory that can be expanded on demand.
Bytes bytes = Bytes.allocateElasticDirect();
 
// Write data
bytes.writeBoolean(true)
    .writeByte((byte) 1)
    .writeInt(2)
    .writeLong(3L)
    .writeDouble(3.14)
    .writeUtf8("Foo")
    .writeUnsignedByte(255);
 
System.out.println("Wrote " + bytes.writePosition() + " bytes");
System.out.println(bytes.toHexString());

Запуск приведенного выше кода приведет к следующему выводу:

1
2
3
Wrote 27 bytes
00000000 59 01 02 00 00 00 03 00  00 00 00 00 00 00 1f 85 Y······· ········
00000010 eb 51 b8 1e 09 40 03 46  6f 6f ff                ·Q···@·F oo·

Мы также можем прочитать данные, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
// Read data
boolean flag = bytes.readBoolean();
byte b = bytes.readByte();
int i = bytes.readInt();
long l = bytes.readLong();
double d = bytes.readDouble();
String s = bytes.readUtf8();
int ub = bytes.readUnsignedByte();
 
System.out.println("d = " + d);
 
bytes.release();

Это даст следующий результат:

1
d = 3.14

HexDumpBytes

Байты также предоставляют HexDumpBytes который облегчает документирование вашего протокола.

01
02
03
04
05
06
07
08
09
10
11
12
13
// Allocate off-heap memory that can be expanded on demand.
Bytes bytes = new HexDumpBytes();
 
// Write data
bytes.comment("flag").writeBoolean(true)
        .comment("u8").writeByte((byte) 1)
        .comment("s32").writeInt(2)
        .comment("s64").writeLong(3L)
        .comment("f64").writeDouble(3.14)
        .comment("text").writeUtf8("Foo")
        .comment("u8").writeUnsignedByte(255);
 
System.out.println(bytes.toHexString());

Это даст следующий результат:

1
2
3
4
5
6
7
59                                              # flag
01                                              # u8
02 00 00 00                                     # s32
03 00 00 00 00 00 00 00                         # s64
1f 85 eb 51 b8 1e 09 40                         # f64
03 46 6f 6f                                     # text
ff                                              # u8

Резюме

Как можно видеть, легко записывать и читать различные форматы данных, а байты поддерживают отдельные позиции записи и чтения, что делает его еще более простым в использовании (не нужно «переворачивать»
Buffer ). Приведенные выше примеры иллюстрируют «потоковые операции», в которых выполняются последовательные операции записи / чтения. Есть также «абсолютные операции», которые предоставляют нам произвольный доступ в пределах области памяти байтов.

Еще одна полезная особенность байтов заключается в том, что он может быть «эластичным» в том смысле, что его резервная память расширяется динамически и автоматически, если мы записываем больше данных, чем было выделено изначально. Это похоже на
ArrayList с начальным размером, который расширяется по мере добавления дополнительных элементов.

сравнение

Вот краткая таблица некоторых свойств, которые отличают
Bytes из ByteBuffer :

ByteBuffer Б
Максимальный размер [байты] 2 ^ 31 2 ^ 63
Отдельная позиция для чтения и записи нет да
Эластичные буферы нет да
Атомные операции (CAS) нет да
Детерминированный выпуск ресурсов Внутренний API (Очиститель) да
Возможность обойти начальный обнуление нет да
Чтение / запись строк нет да
Порядок байтов Большой и Маленький Только родной
Стоп Бит сжатия нет да
Сериализация объектов нет да
Поддержка RPC сериализации нет да

Как мне установить его?

Когда мы хотим использовать байты в нашем проекте, мы просто добавляем следующую зависимость Maven в наш файл pom.xml, и у нас есть доступ к библиотеке.

1
2
3
4
5
<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-bytes</artifactId>
    <version>2.17.27</version>
</dependency>

Если вы используете другой инструмент сборки, например Gradle, вы можете увидеть, как зависеть от байтов, нажав на эту ссылку .

Получение байтовых объектов

Объект Bytes может быть получен многими способами, включая обертку существующего ByteBuffer. Вот некоторые примеры:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Allocate Bytes using off-heap direct memory
// whereby the capacity is fixed (not elastic)
Bytes bytes = Bytes.allocateDirect(8);
 
// Allocate a ByteBuffer somehow, e.g. by calling
// ByteBuffer's static methods or by mapping a file
ByteBuffer bb = ByteBuffer.allocate(16);
//
// Create Bytes using the provided ByteBuffer
// as backing memory with a fixed capacity.
Bytes bytes = Bytes.wrapForWrite(bb);
 
// Create a byte array
byte[] ba = new byte[16];
//
// Create Bytes using the provided byte array
// as backing memory with fixed capacity.
Bytes bytes = Bytes.wrapForWrite(ba);
 
// Allocate Bytes which wraps an on-heap ByteBuffer
Bytes bytes = Bytes.elasticHeapByteBuffer(8);
// Acquire the current underlying ByteBuffer
ByteBuffer bb = bytes.underlyingObject();
 
// Allocate Bytes which wraps an off-heap direct ByteBuffer
Bytes bytes = Bytes.elasticByteBuffer(8);
// Acquire the current underlying ByteBuffer
ByteBuffer bb = bytes.underlyingObject();
 
// Allocate Bytes using off-heap direct memory
Bytes bytes = Bytes.allocateElasticDirect(8);
// Acquire the address of the first byte in underlying memory
// (expert use only)
long address = bytes.addressForRead(0);
 
// Allocate Bytes using off-heap direct memory
// but only allocate underlying memory on demand.
Bytes bytes = Bytes.allocateElasticDirect();

Освобождение байтов

С ByteBuffer мы обычно не имеем никакого контроля над тем, когда основная память фактически возвращается обратно в операционную систему или кучу. Это может быть проблематично, когда мы выделяем большие объемы памяти и когда фактические объекты ByteBuffer как таковые не являются сборщиком мусора.

Вот как проблема может проявиться: даже если
ByteBuffer объекты ByteBuffer малы, они могут содержать ByteBuffer ресурсы в основной памяти. Только когда ByteBuffers собирает мусор, возвращается основная память. Таким образом, мы можем оказаться в ситуации, когда у нас в куче небольшое количество объектов (скажем, у нас есть 10 байтовых буферов по 1 ГБ каждый). JVM не находит причин для запуска сборщика мусора с несколькими объектами в куче. Таким образом, у нас достаточно кучи памяти, но в любом случае может не хватить памяти процесса.

Байты предоставляют детерминистские средства быстрого высвобождения базовых ресурсов, как показано в этом примере ниже:

1
2
3
4
5
6
Bytes bytes = Bytes.allocateElasticDirect(8);
try {
    doStuff(bytes);
} finally {
    bytes.release();
}

Это обеспечит высвобождение основных ресурсов памяти сразу после использования.

Если вы забудете вызвать release() , Байты по-прежнему будут освобождать базовые ресурсы, когда сборка мусора происходит точно так же, как ByteBuffer , но вы можете ByteBuffer память, ожидая, что это произойдет.

Запись данных

Запись данных может быть выполнена двумя основными способами с использованием любого из них:

  • Потоковые операции
  • Абсолютные операции

Потоковые операции

Потоковые операции выполняются как последовательность операций, каждая из которых последовательно размещает свое содержимое в основной памяти. Это очень похоже на обычный последовательный файл, который увеличивается с нулевой длины и выше, когда содержимое записывается в файл.

1
2
3
4
// Write in sequential order
bytes.writeBoolean(true)
    .writeByte((byte) 1)
    .writeInt(2)

Абсолютные Операции

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

1
2
3
4
// Write in any order
bytes.writeInt(2, 2)
    .writeBoolean(0, true)
    .writeByte(1, (byte) 1);

Вызов операций абсолютной записи не влияет на позицию записи, используемую для потоковых операций.

Чтение данных

Чтение данных также может выполняться с использованием потоковых или абсолютных операций.

Потоковые операции

Аналогично записи, вот так выглядит потоковое чтение:

1
2
3
boolean flag = bytes.readBoolean();
byte b = bytes.readByte();
int i = bytes.readInt();

Абсолютные Операции

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

1
2
3
int i = bytes.readInt(2);
boolean flag = bytes.readBoolean(0);
byte b = bytes.readByte(1);

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

Разнообразный

Байты поддерживают написание строк, чего не делает ByteBuffer:

1
bytes.writeUtf8("The Rain in Spain stays mainly in the plain");

Есть также методы для атомарных операций:

1
bytes.compareAndSwapInt(16, 0, 1);

Это будет атомарно устанавливать значение int в позиции 16 в 1, если и только если оно равно 0. Это обеспечивает поточно-ориентированные конструкции, которые будут сделаны с использованием байтов. ByteBuffer не может предоставить такие инструменты.

Бенчмаркинг

Как быстро это байты? Ну, как всегда, ваш пробег может варьироваться в зависимости от множества факторов. Давайте сравним ByteBuffer и Bytes которых мы выделяем область памяти, выполняем некоторые общие операции с ней и измеряем производительность с помощью JMH (код инициализации не показан для краткости):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Benchmark
public void serializeByteBuffer() {
    byteBuffer.position(0);
    byteBuffer.putInt(POINT.x()).putInt(POINT.y());
}
 
 
@Benchmark
public void serializeBytes() {
    bytes.writePosition(0);
    bytes.writeInt(POINT.x()).writeInt(POINT.y());
}
 
@Benchmark
public boolean equalsByteBuffer() {
    return byteBuffer1.equals(byteBuffer2);
}
 
@Benchmark
public boolean equalsBytes() {
   return bytes1.equals(bytes2);
}

Это дало следующий результат:

1
2
3
4
5
Benchmark                          Mode  Cnt         Score          Error  Units
Benchmarking.equalsByteBuffer     thrpt    3   3838611.249 ± 11052050.262  ops/s
Benchmarking.equalsBytes          thrpt    3  13815958.787 ±   579940.844  ops/s
Benchmarking.serializeByteBuffer  thrpt    3  29278828.739 ± 11117877.437  ops/s
Benchmarking.serializeBytes       thrpt    3  42309429.465 ±  9784674.787  ops/s

Вот диаграмма различных тестов, показывающих относительную производительность (чем выше, тем лучше):

Bytes производительности лучше, чем ByteBuffer для запуска тестов.

Вообще говоря, имеет смысл повторно использовать прямые буферы вне кучи, поскольку их размещение относительно дорого. Повторное использование может быть сделано разными способами, включая переменные ThreadLocal и пул. Это верно для обоих
Bytes и ByteBuffer .

Тесты проводились на Mac Book Pro (середина 2015 года, 2,2 ГГц Intel Core i7, 16 ГБ) и на Java 8 с использованием всех доступных потоков. Следует отметить, что вы должны запустить свои собственные тесты, если вы хотите соответствующее сравнение, относящееся к конкретной проблеме.

API и потоковые вызовы RPC

Легко настроить всю структуру с удаленными вызовами процедур (RPC) и API, используя байты, которые поддерживают запись и воспроизведение событий. Вот короткий пример, где MyPerson является POJO, который реализует интерфейс BytesMarshable . Нам не нужно реализовывать какие-либо методы в BytesMarshallable поскольку он поставляется с реализациями по умолчанию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final class MyPerson implements BytesMarshallable {
 
    private String name;
    private byte type;
    private double balance;
 
    public MyPerson(){}
 
  // Getters and setters not shown for brevity
 
}
 
interface MyApi {
    @MethodId(0x81L)
    void myPerson(MyPerson byteable);
}
 
static void serialize() {
    MyPerson myPerson = new MyPerson();
    myPerson.setName("John");
    yPerson.setType((byte) 7);
    myPerson.setBalance(123.5);
 
    HexDumpBytes bytes = new HexDumpBytes();
    MyApi myApi = bytes.bytesMethodWriter(MyApi.class);
 
    myApi.myPerson(myPerson);
 
    System.out.println(bytes.toHexString());
 
}

Вызов serialize() выдаст следующий вывод:

1
2
3
4
81 01                                           # myPerson
   04 4a 6f 68 6e                                  # name
   07                                              # type
   00 00 00 00 00 e0 5e 40                         # balance

Как видно, очень легко увидеть, как составляются сообщения.

Байт с файловой поддержкой

Очень просто создать байты с отображением файлов, которые растут по мере добавления большего количества данных, как показано ниже:

1
2
3
4
5
6
7
try {
    MappedBytes mb = MappedBytes.mappedBytes(new File("mapped_file"), 1024);
    mb.appendUtf8("John")
    .append(4.3f);
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

Это создаст файл отображения памяти с именем «mapped_file».

1
2
3
4
5
$ hexdump mapped_file
0000000 4a 6f 68 6e 34 2e 33 00 00 00 00 00 00 00 00 00
0000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0001400

Лицензирование и зависимости

Bytes имеет открытый исходный код и лицензируется под дружественную для бизнеса лицензию Apache 2, что позволяет легко включать его в ваши собственные проекты, будь то коммерческие или нет.

Байты имеют три зависимости времени выполнения: chronicle-core , slf4j-api и
com.intellij:annotations которые, в свою очередь, лицензируются в соответствии с Apache 2, MIT и Apache 2.

Ресурсы

Хроника байтов: https://github.com/OpenHFT/Chronicle-Bytes

Библиотека Bytes предоставляет много интересных функций и обеспечивает хорошую производительность.

Смотреть оригинальную статью здесь: Java: Хроники байтов, пинать шины

Мнения, высказанные участниками Java Code Geeks, являются их собственными.