Данная статья продолжает цикл моих переводов статей Jakob Jenkov об оптимизации Java приложений.
То, как ваше Java-приложение считывает данные, может оказать большое влияние на его производительность чтения. В этой статье я опишу несколько различных шаблонов чтения и объясню их характеристики производительности.
Чтение в новый объект
Первый шаблон чтения Java - это шаблон чтения в новый объект. Это то, чему вы обычно учитесь в университете как "правильному" способу чтения данных.
Шаблон чтения в новый - это шаблон использования метода чтения, который считывает какие-то данные и возвращает новую структуру данных с прочитанными данными. Сначала приведем простой пример структуры данных:
public class MyData {
public int val1 = 0;
public int val2 = 0;
}
Вот пример метода чтения, который считывает данные в объект MyData:
public MyData readMyData(byte[] source) {
MyData myData = new MyData();
myData.val1 = source[0];
myData.val2 = source[1];
return myData;
}
Как вы можете видеть, метод readMyData() возвращает объект MyData. Сначала создается объект MyData. Во-вторых, метод readMyData() считывает данные в объект Mydata. В-третьих, объект MyData возвращается вызывающему коду.
Что стоит отметить в этом шаблоне, так это то, что каждый раз, когда вы вызываете метод readMyData(), возвращается новый объект MyData. Вот почему шаблон называется read-into-new, что означает, что данные считываются в новый объект.
Если метод readMyData() вызывается часто, это приведет к созданию большого количества объектов MyData. Это оказывает давление на систему распределения объектов и сборщик мусора. Это приводит к снижению производительности и, возможно, к более длительным перерывам в сборке мусора время от времени.
Другим недостатком шаблона чтения в новый объект является то, что каждый из них может располагаться в очень разных областях памяти компьютера. Это означает, что вероятность того, что объект является кэшем процессора, невелика.
Чтение в существующий объект
Шаблон чтения считывает данные в существующий объект вместо создания нового для каждого вызова метода чтения. Это означает, что один и тот же объект может быть сброшен и повторно использован для нескольких вызовов метода read. Вот как выглядел бы более ранний метод readMyData() с использованием шаблона чтения в существующий объект:
public MyData readMyData(byte[] source, MyData myData) {
myData.val1 = source[0];
myData.val2 = source[1];
return myData;
}
Основное отличие от предыдущей версии заключается в том, что эта принимает объект MyData для считывания данных в качестве параметра. Теперь вызывающий метод readMyData() должен решить, следует ли повторно использовать существующий экземпляр Mydata или следует создать новый.
Повторное использование экземпляра MyData вместо создания нового сэкономит время и память по сравнению с постоянным созданием нового экземпляра. Это также снизит нагрузку на сборщик мусора Java, поэтому риск длительных пауз при сборке мусора снижается.
Повторное использование объекта также означает, что вероятность того, что объект находится в кэше процессора, намного выше, чем при создании нового объекта для каждого вызова метода readMyData().
Считывание из источника
Шаблон считывания из источника не считывает данные в объекты. Вместо этого он считывает необходимые значения непосредственно из базового источника данных.
Считывание значений непосредственно из источника данных может сэкономить некоторое время, поскольку данные не нужно сначала копировать в объект, прежде чем их можно будет использовать. При необходимости значения копируются непосредственно из базового источника данных.
Считывание значений непосредственно из источника данных также имеет то преимущество, что из базового источника данных будут скопированы только те данные, которые фактически используются. Таким образом, если коду чтения требуется только часть данных, копируются только эти части.
Чтобы изменить код предыдущего примера для чтения данных непосредственно из базового источника, нам необходимо изменить реализацию класса MyData:
public class MyData() {
private byte[] source = null;
public MyData() {
}
public void setSource(byte[] source) {
this.source = source;
}
public int getVal1() {
return this.source[0];
}
public int getVal2() {
return this.source[1];
}
}
Чтобы использовать класс MyData в его новой версии, вы будете использовать следующий код:
byte[] source = ... //get bytes from somewhere
MyData myData = new MyData();
myData.setSource(source);
int val1 = myData.getVal1();
int val2 = myData.getVal2();
Обратите внимание, что вы можете повторно использовать экземпляр MyData. Просто вызовите setSource(), когда вам нужно считывать данные из нового массива байтов.
Также, данные копируются только один раз - из массива байтов в код, использующий значение. Сначала он не копируется из массива байтов в объект MyData, а затем оттуда в любое вычисление, требующее значения.
И наконец, только в том случае, если вы действительно вызовете как getVal1(), так и getVal2(), соответствующие данные будут считаны из базового массива байтов. Если для вычисления требуется только одно из значений, только это значение должно быть считано из массива байтов. Это экономит время, когда используется только часть данных.
Метод чтения, который считывает данные в объект, чаще всего не знает, какой объем данных необходим. Таким образом, нормально копировать все данные в объект. Если только вы не создадите несколько методов чтения, адаптированных для каждого вычисления, но это добавит вам больше работы.
Навигатор
Если базовый источник данных содержит несколько "записей" или "объектов", вы можете изменить шаблон считывания источника данных на шаблон навигатора. Шаблоны навигатора работают аналогично шаблону считывания, но добавляют методы для навигации между записями или объектами в базовом источнике.
Предполагая, что каждый объект MyData состоит из 2 байт из базового источника, вот как будет выглядеть класс MyData с добавленным методом навигации:
public class MyData() {
private byte[] source = null;
private int offset = 0;
public MyData() {
}
public void setSource(byte[] source, int offset) {
this.source = source;
this.offset = offset;
}
public int getVal1() {
return this.source[this.offset];
}
public int getVal2() {
return this.source[this.offset + 1];
}
public void next() {
this.offset += 2; //2 bytes per record
}
public boolean hasNext() {
this.offset < this.source.length;
}
}
Первое изменение заключается в том, что метод setSource() теперь принимает дополнительный параметр, называемый offset. Это не является строго необходимым, но это позволяет навигатору MyData начинать со смещения в массив исходных байтов вместо первого байта.
Второе заключается в том, что методы getVal1() и getVal2() теперь используют значение внутренней переменной смещения в качестве индекса в исходном массиве при считывании значений.
Третье изменение - это добавление метода next(). Метод next() увеличивает внутреннюю переменную смещения на 2, так что переменная смещения указывает на следующую запись в массиве.
Четвертое изменение является добавлением метода hasNext(), который возвращает значение true, если в исходном массиве байтов содержится больше записей (байтов).
Вы используете навигационную версию MyData следующим образом:
byte[] source = ... // get byte array from somewhere
MyData myData = new MyData();
myData.setSource(source, 0);
while(myData.hasNext()) {
int val1 = myData.getVal1();
int val2 = myData.getVal2();
myData.next();
}
Как вы можете видеть, использование класса MyData в реализации шаблона navigator довольно просто. Очень похоже на использование стандартного итератора Java.
#code #java #performance