Часто бывает, что программа загружает какие-то данные в память и оставляет их там на долгое время (а то и до конца работы) неизмененными.
При этом используются структуры данных, оптимизированные как для чтения, так и для записи.
Например, вы читаете из базы данных Ансамбль список идентификаторов всех генов человека (включая всевозможные микроРНК и т. д. — всего чуть больше 50 000).
Если считать их в стандартный ArrayList, то на 32-битном HotSpot вы потратите чуть больше 4 мегабайт. Можно ли сэкономить память, зная, что коллекция больше не изменится? Мы не будем касаться возможности чтения данных из базы данных по частям: это тема для отдельного разговора.
Кроме того, чтение по частям можно комбинировать с описанными здесь подходами, добиваясь дополнительной экономии памяти.
Мы также не будем использовать, например, опцию виртуальной машины -XX:+UseCompressedStrings: она экономит память не только в нашем случае, но и может комбинироваться с нашими методами.
Прежде чем что-то оптимизировать, нужно это измерить.
В нашем случае речь идет об объеме памяти, занимаемой списком.
При этом, конечно, нас интересует не мелкий размер (размер самого объекта ArrayList), а сохраняемый размер (размер ArrayList и всех объектов, на которые он ссылается прямо или косвенно).
Мы произведем замеры с помощью Анализатор памяти Eclipse .
Найдем в куче нужный объект (например, по имени класса) и воспользуемся опцией «Показать сохраненный набор».
Получаем такую картину:
Как и ожидалось, сам ArrayList весит совсем немного — 24 байта.
Массив ссылок Object[] на строки, определенные внутри ArrayList, весит довольно много, но все же более 90% пространства занимают сами строковые объекты и связанные с ними массивы char[].
Небольшого улучшения можно добиться, урезав массив Object[] до фактического количества элементов.
Как вы знаете, ArrayList выделяет пространство с запасом, чтобы не перераспределять его каждый раз при добавлении элемента.
Поскольку мы знаем, что больше никаких элементов добавляться не будет, этот запас нам не нужен.
Вы можете сделать это просто и эффективно следующим образом:
Здесь результат уже не ArrayList (точнее ArrayList, но другой).List<String> genes = readGenes(); genes = Arrays.asList(genes.toArray(new String[genes.size()]));
Сначала создаем копию массива необходимой длины, а затем делаем над копией обертку.
Теперь сохраненный набор выглядит так:
Массив стал немного меньше, и в итоге мы сэкономили 0,44% места.
Все в порядке, мы просто разогреваемся.
Давайте посмотрим на эти массивы char[].
Они содержат идентификаторы генов типа «ENSG00000121410» (15 символов — 30 байт), еще 8 байт системной информации об объекте и 4 байта длины.
И эти 42 байта выравниваются по 8, то есть добавляется 6 неиспользуемых байт. Таким образом, из 48 байт только 30 несут полезную информацию (с помощью +UseCompressedStrings их можно сократить до 15, тогда доля «лишней» информации увеличится еще больше).
Что можно с этим сделать? Давайте вспомним, как работает String.substring. Результирующая строка использует общий буфер char[] с родительской строкой: значения смещения и счетчика дочерней строки устанавливаются соответствующим образом.
При этом с точки зрения пользователя дочерняя строка ничем не отличается от обычной, но если родительскую удалили, то полный буфер все равно сохраняется, даже если отрезать один символ из мегабайтной строки.
Иногда это раздражает, приходится искать такие места в коде и подписывать новую String, но сейчас нам наоборот поможет. Мы поместим все строки массива в один буфер char[].
Для этого напишем вспомогательный метод в каком-нибудь служебном классе: public static List<String> compactify(Collection<String> list) {
StringBuilder sb = new StringBuilder();
for( String item : list ) sb.append(item);
String data = sb.toString();
List<String> result = new ArrayList<String>(list.size());
int index = 0;
for( String item : list ) {
result.add(data.substring(index, index + item.length()));
index += item.length();
}
return result;
}
Метод сначала объединяет все строки исходной коллекции в одну длинную строку, а затем разрезает ее на подстроки с помощью substring. Давайте прогоним наш список генов через эту, казалось бы, бесполезную операцию и посмотрим на сохранившийся набор:
Да, это уже существенно.
Мы сохранили почти все упомянутые выше дополнительные байты, получив в общей сложности 24% от исходного размера.
Если строки в вашем массиве короче 15 символов, то экономия будет еще больше.
Кстати, обратите внимание, что при создании ArrayList мы явно указали его длину, поэтому Object[] не содержит лишних элементов, то есть первую оптимизацию мы не потеряли.
Полученный массив также можно безопасно использовать для записи; надо только иметь в виду, что любая строка из него тянет за собой все остальные, поэтому если мы решим удалить строки, то не освободим всю память.
Но поскольку мы договорились, что массив доступен только для чтения, то всё в порядке.
Будьте осторожны, если во входном массиве были повторяющиеся строки: функция не проверяет это, и вы можете потерять больше, чем получите.
Тем не менее, 24% недостаточно.
Глаза режут объекты String, которые по сути не несут никакой пользы (сами данные находятся в char[]).
Может, их можно не хранить, а перерезать веревку по требованию? Для этого нам придется написать собственную реализацию List и хранить смещения начала строк: public class CompactStringList extends AbstractList<String> implements RandomAccess {
private String data;
private int[] offsets;
public CompactStringList(Collection<String> list) {
StringBuilder sb = new StringBuilder();
offsets = new int[list.size()];
int offset = 0, i = 0;
for( String item : list ) {
sb.append(item);
offsets[i++] = offset;
offset+=item.length();
}
data = sb.toString();
}
public String get(int index) {
return index == offsets.length - 1 ? data.substring(offsets[index]) :
data.substring(offsets[index], offsets[index + 1]);
}
public int size() {
return offsets.length;
}
}
В конструкторе мы снова склеили все входные строки в одну, сохранив в массив смещения начал строк.
При получении элемента (я для простоты не проверяю правильность параметра) отрезаем от строки нужный кусок.
В этом случае создается только новый объект String, а сами данные не копируются.
Давайте посмотрим на сохранившийся набор такого объекта:
Теперь мы сэкономили 55,5% по сравнению с исходной версией, теперь можно начинать танцевать.
Конечно, последняя оптимизация самая спорная и зависит от дальнейших вариантов использования.
Например, если вы часто получаете один и тот же элемент и сохраняете полученные строки в других коллекциях, вы получите объекты String. Хотя они указывают на общий буфер, вы потеряете 40 байт на строку (в 32-битном HotSpot).
Не увлекайтесь оптимизацией ради оптимизации: может стать только хуже.
Однако во многих приложениях этот метод может оказаться полезным.
Ну или хотя бы расширит понимание возможностей Java для некоторых читателей.
Теги: #оптимизация памяти #только для чтения #коллекции #списки #java #строки #java
-
Короче Ребята, С Наступающим
19 Oct, 24 -
11-Й Риф Пройдет В Новом Формате
19 Oct, 24 -
Удобство Использования Списков Элементов
19 Oct, 24