Смесь: Неочевидное + Оптимизация
Вложений: 3
Всем привет!
Я сейчас занимаюсь оптимизацией приложения: j2me-клиент для мобильной соц. сети Galaxy. (можете загуглить, не буду рекламировать) Решил создать эту тему, чтобы делиться наблюдениями, слушать советы и всё такое прочее. Зачем вообще всё это: В обычных мобильниках (не смартфонах) для джава-проги доступно около 2 мб оперативной памяти. Как раз-таки памяти мне и не хватает для юзерского комфорта. При старте приложения - главный экран - съедается порядка 1 мб. Остаётся ещё 1 до ошибки "Out of memory". Что есть в приложении: * непрерывный коннект к чат-серверу по сокету * самодельный браузер, псевдо-хтмл, много картинок - грузятся по хттп. * планета с персонажами (костюмы, одежда), объектами, фоном/интерьером * чатик Вложение 21254 Вложение 21255 Вложение 21256 Масштабы проекта: * 25 packages * 177 классов и интерфейсов суммарно Например, в пакете browser 50 классов/интерфейсов. Что заметно жрёт оперативку: * создание новых объектов, в частности строк (парсинг страниц, получение команд из сокета) * картинки (браузер и планета) Я юзаю ручной вызов GC (сборщик мусора), и по возможности стараюсь перехватить переполнение памяти через catch(OutOfMemoryError out) {} но не всегда срабатывает, прога крэшится и закрывается. Т.к. j2me - это младшая, но родная, сестра "обычной" java, то тема может быть полезна широкому кругу читателей. :) Далее отдельными постами рассказ. |
Ответ: Смесь: Неочевидное + Оптимизация
0. Для справки
Внутренние объекты java при дефолтном создании: * Vector - элементы хранятся в массиве. v = new Vector() - выделяется память под 10 элементов, при выходе за границы внутренний массив вектора пересоздаётся, расширяясь вдвое (могу наврать тут, может и не в 2) * Hashtable h = new Hashtable() - размерность таблицы равна 11, хз почему так * StringBuffer - данные хранятся во внутреннем массиве chars[] sb = new StringBuffer() - размерность равна 16 1. Коннект и реконнект к чат-серверу Раньше при каждом подключении или переподключении создавался новый поток, в котором новый сокет. Теперь есть всего 1 бесконечный сетевой поток, внутри которого могут много раз пересоздаваться объекты типа сокет. вынес всю логику переподключения в класс сетевого потока, так что в логике приложения просто делаем коннект и забываем о нём, и если произойдёт разрыв сокета, то он автоматически будет пытаться восстановить коннект 2. Загрузка ресурсов по http Раньше а) на каждый запрос картинки создавался отдельный поток в лоадере б) для каждой загрузки страницы создавался новый лоадер + поток в нём Теперь есть всего 1 бесконечный поток лоадера, внутрь которого вкидываются задачи на загрузку ресурсов. Пока остаётся под вопросом реализация ajax-запросов, наверное для аякса придётся мастерить отдельный поток, чтобы отклик не зависел от текущих запросов на загрузку других ресурсов. 3. LinkedList vs Vector Стараюсь по возможности заменить использование Vector на LinkedList, чтобы не тратились ресурсы при расширении размерности вектора. * LinkedList - мой самодельный двунаправленный список, ибо в комплекте j2me списков нет. Vector хорош, когда нужен произвольный доступ к элементам по индексу в массиве. 4. Шрифты Используются рисованные шрифты. При загрузке красятся в несколько цветов. Для кнопочной версии приложения красим, подменяя цвета в палитре, для сенсорной версии переводим картинку в rgba[], красим через hsv преобразование, и далее создаём из полученных new_rgba[] новую картинку. Раньше для каждого шрифта грузились все данные, картинки + описательные структуры данных, массивы смещений символов и массивы ширин букв и прочее Теперь для одинаковых шрифтов, различающихся только цветом, грузим описательные структуры только 1 раз, остальным шрифтам присваиваем указатели на эти структуры. В частности, в шрифте есть массивы * private int arrWidth[]; * private int arrBlock[]; * private int arrOffset[]; Количество символов = 337. На 1 шрифт приходится 337*3*4 = 4044 байта. Умножим это на 5 крашенных вариантов и получим 20,2 кб. гуд! 5. Смайлы, ч1 Раньше мы использовали анимированные смайлы. Пришлось для хранения смайла использовать класс View, который умеет проигрывать анимацию. View содержит набор Pic Pic содержит ImageObject ImageObject содержит Image (это джавовская картинка) * ImageObject - обёртка над стандартной картинкой, хранить ключ-url и саму картинку, и немного служебной инфы Раньше каждый смайл имел тип View Теперь каждый смайл имеет тип ImageObject Это позволило съэконосить 70 кб памяти! В любом случае, буду делать рефактор классов View и Pic. 6. Смайлы, ч2 В данный момент пытаюсь улучшить функцию разбивки текста на текст и смайлы. RegExp'ов в j2me нет, да они и оперативы жрут скорее всего больше доступного. Всего есть 76 текстовых вариантов смайлов. Сейчас 1. проходим по всем смайлам, ищём их в строке, запоминаем самый ближний к началу строки 2. делим строку на текст до смайла (если есть), сам смайл (если есть) и остаток строки снова подаём на вход пункта 1. Пока что сделал предварительную проверку по 4 символам, которые уникальны для смайлов - : ; % ) и др. Для строк без смайлов это даёт прирост скорости раз в 7. Со смайлами - замедляет, но не сильно, т.к. джавовский string.IndexOf() работает очень шустро. Сложение строк На днях я обнаружил опасность. При сложении строк компилятор подставляет туда StringBuffer. s = s+"1024"; превратится в s = new StringBuffer().append(s).append(1024).toString(); Bроде всё круто, но при этом StringBuffer будет пересоздавать/расширять внутренний массив, и мы получаем в оперативке массив, почти вдвое больший чем сумма наших исходных строк! В джаве строки - это структуры со ссылкой на массив chars[] и переменными start и len. Если текст жирный, например 10+ кб, как браузерная страница, то прибавлением к строке любого символа расширит строку до двукратного размера. Можно обрезать лишние символы в оперативке через s = new String(s) если на них больше никто не ссылается, но они всё равно будут живы до очередного вызова сборщика мусора. Продолжение следует. :) |
Ответ: Смесь: Неочевидное + Оптимизация
Улучшение поиска смайлов в тексте. Завершил.
Фишки новой функции: * предварительный поиск смайловых символов (даёт ускорение только если в тексте нет смайлов) * поиск сразу всех смайлов, сортировка по позиции смайла в тексте делается сразу Т.к. текстовые варианты некоторых смайлов являются подстрокой других, например "0 : )" или "{ : )" включают в себя ": )", то пришлось делать проверку - не попадает ли очередной найденный смайл в позицию уже найденных. Проверка - это цикл по найденным смайлам. Раз уж никуда не деться от этого цикла, то в нём делаю проверку соотнесения позиции текущего найденного смайла с найденными ранее, тем самым определяю позицию для вставки, т.е. не нужно потом сортировать все смайлы по позиции. Зачем нужна сортировка? Чтобы можно было указывать лимит смайлов, свыше которого всё считается обычным текстом. Победа: * время поиска сократилось в 2 раза * объём потребляемой памяти сократился в 3 раза Конечно, в реальности цифры могут быть чуть больше и чуть меньше, но в целом ожидаю именно такой прогресс. Тестирование: Для теста взял некий текст (под спойлером) длиной 639 символов, содержащий в середине 4 смайла. Поиск производился в цикле, 500 итераций. Результаты теста: * новая функция, тест №1: использовано памяти перед тестом: 943 кб время выполнения: 1836 мс память после теста: 1254 кб * старая функция, тест №2: использовано памяти перед тестом: 943 кб время выполнения: 4361 мс память после теста: 1896 кб Я результатом доволен, опасался что новый вариант будет жрать памяти больше старого. Спасибо за внимание! |
Ответ: Смесь: Неочевидное + Оптимизация
Загрузка текста из файла
Джава хорошо дружит с кодировкой UTF-8. В ней на один символ приходится от 1 до 2 байтов. Когда-то давно я пробовал загрузить текст из файла, и возникли проблемы. В итоге я извратился, сделав свой формат строки, в котором символ представлен по типу джавовского: size|byte1[byte2] Так и жил с этим. А недавно, делая проект на андроид, и пытаясь там грузить УТФ-текст из файла, я обнаружил, что в начале строки вылазит какой-то непонятный символ, но остальная часть строки нормальная. Сделал самое простое: t = t.substring(1) Но так как этот символ есть не всегда, то пришлось в начало добавлять спец. символ, типа #, и далее i = t.indexOf('#'); t = t.substring(i); Как-то это стрёмно, вставлять символ, выцеплять подстроку. А потом обнаружил в Notepad++ кодировку "UTF-8 without BOM", решил сохранить в неё, и вуаля - всё чётко, только мой текст! Ещё: * в Qt строки в utf-8 сохраняются без BOM. * в php чтение из файла - тоже лучше использовать без BOM, иначе левый символ в начале * ответ на стековерфлоу: What's different between utf-8 and utf-8 without BOM? Даёшь хранение текста в формате "UTF-8 without BOM"! Моя функция загрузки текста: |
Ответ: Смесь: Неочевидное + Оптимизация
Цитата:
|
Ответ: Смесь: Неочевидное + Оптимизация
imper, да я попутал, спасибо за уточнение!
|
Ответ: Смесь: Неочевидное + Оптимизация
Hashtable
Собираюсь выпилить Hashtable по максимуму, т.к. у меня нет многотысячных наборов ключ-значение, для которых хэш-таблица могла бы дать ускорение поиска по ключу, сделаю поиск перебором. Зато это избавит меня от пересоздания "внутренностей" хэш-таблицы при достижении maxCapacity. Вот статья для углубленного изучения: java.util.Hashtable from inside (на русском). |
Ответ: Смесь: Неочевидное + Оптимизация
Может, не перебором, а хотя бы бинарный? Отсортировать по ключам, если, конечно, нет частых вставок и удалений.
И ещё - для поиска смайликов можно всё-таки сделать конечный автомат. Не знаю как по памяти, но по скорости выполнения должен быть сильно лучше. |
Ответ: Смесь: Неочевидное + Оптимизация
Там значений в списке в среднем 5 штук. Иногда больше. Сортировка больше займет. :)
Но я потестирую скорость хэш-таблицы и своего варианта, если не устроит, то подумаю над бинарным. Igor, можешь дать ссылок или описать суть того, как конечный автомат применить к смайлам? |
Ответ: Смесь: Неочевидное + Оптимизация
Во всяких парсерах для компиляторов активно используется.
моё видение того как оно работает: |
Ответ: Смесь: Неочевидное + Оптимизация
Понял суть, спасибо.
Хардкодить не наш метод, ибо 76 комбинаций, начиная от : ) и заканчивая :troll: Всё же эти посимвольные проверки окажутся медленнее чем поиск подстроки всей текстовой комбинации смайла (инфа почти 100%). Волшебный String.indexOf () Давным-давно жил-был битмапный шрифт. И было у него несколько проверок на диапазоны символов, которые позволяли ему рисоваться правильно, в соответствие с задумкой Вызывателя функций. Что за проверки? Зачем? Символы в картинках поделены на блоки - русские маленькие, рус большие, английские маленькие, большие, числа, символы-закорючки. Для них и были проверки некие в цикле по тексту. PHP код:
Однако! Это оказалось медленнее, чем PHP код:
Соответственно, при загрузке шрифта требуется дополнительный массив, в котором будут соответсвия индексов и блоков-картинок, что совсем не накладно. Кстати, буквы ёЁ стоят особняком от остальных русских букв в таблице символов, что совсем нектати. |
Ответ: Смесь: Неочевидное + Оптимизация
|
Ответ: Смесь: Неочевидное + Оптимизация
Копирование массивов, System.arrayCopy()
Чудесный и шустрый метод для копирования данных из одного массива в другой, как целиком, так и части. У себя в коде использую * при расширении массивов, создаём массив большего размера, копируем в него текущий массив (надо бы избавиться от этих expand'ов) * при чтении байтов из сокета Чтение из сокета: Читаем из сокета байты в фиксированный буфер, выцепляем команды. Всё бы хорошо, но данные идут сплошным потоком, т.е. буфер может быть заполнен полностью, и при этом его окончание - это лишь часть от команды, чтобы вытащить команду, нужно догрузить оставшуюся часть. С давних пор у меня делался сдвиг содержимого этого буфера от последней неполной команды в начало буфера: Код:
for(int k = lastPos; k < size; ++k) { Оговорка: я копирую часть массива в тот же самый массив. В доке сказано, что если копируемые области совпадут, то будет создан промежуточный массив. Количество наложений областей копирования я не замерял, в теории должно быть редко, т.к. средняя длина команд заметно меньше размера буфера. Есть мысль вообще не сдвигать байты, замутить циклическое заполнение массива. В прошлый раз не сообразил как это удобно сделать. А ещё - при выцеплении команды нужно будет копировать два куска данных - начало команды с конца буфера и её продолжение с начала буфера. Официальное описание функции arrayCopy() (english). |
Ответ: Смесь: Неочевидное + Оптимизация
Цитата:
|
Ответ: Смесь: Неочевидное + Оптимизация
Цитата:
|
Ответ: Смесь: Неочевидное + Оптимизация
Загрузка советов - "лишние" 400 кб RAM
В проге есть советы, которые показываются в окне "ожидайте...", пока грузится страница браузера. В данный момент советов много, суммарное количество символов текста около 5900. т.е. примерно 5900*4 = 23,6 кб. Есть версионность, т.е. советы присылаются из сокета только если в клиенте устаревшая версия. Иначе достаём из кэша. Выяснил вот что. 1. До загрузки советов занято 1130 кб оперативки 2. Каждый совет в среднем жрёт 1-2 кб 3. На момент загрузки всех советов из сокета занято 1205 кб 4. После завершения функции tipsPrepare() занято 1548 кб 5. После очередного вызова сборщика мусора занято 1110 кб Принцип добавления советов 1. Каждый совет присылается в отдельной команде 2. При получении команды добавляем совет в список (LinkedList) 3. Добавляем все советы 4. Создаём строковый массив tips, из которого будем рандомно выцеплять совет для показа 5. В цикле проходим по списку с советами, заполняя массив и формируя длинную строку, содержащую все советы, чтобы далее сохранить эту строку в кэш (внутр. память) 6. чистим список советов (хотя это особо ничего не даёт, т.к. ссылки на строки есть в массиве) Самое прожорливое место - формирование строки для сохранения в кэш. Опять это "дурацкое" пересоздание массива chars внутри StringBuffer'a при сложении строк. PHP код:
Далее идёт сохранение в кэш, которое и доводит до финального уровня 1548 кб. Решение, оптимизация Сделал переменную int tipsStrLen, в которую плюсую длину каждого совета при его добавлении в список, и юзаю StringBuffer заранее указанной длины: PHP код:
* tips.prepared: 1218 кб * tips. saved: 1269 кб (тут без изменений, главное что суммарно занято теперь намного меньше) Ура, товарищи! :) Замечания: 1. Советы меняются не слишком часто, т.е. обычно грузятся из кэша, при этом такой перерасход памяти не наблюдается. В основном грузятся при первом входе в чат, но ничего не мешает получить список команд при любом очередном перелёте на планету. 2. Храню в кэше всё одной строкой через \r\n, при загрузке разбиваю строку на отдельные советы. Возможно, лучше хранить отдельно каждый совет. 3. При каждом перелёте на планету советы заново создавались из строки, взятой из кэша, это память почти не расходует, но это лишняя ненужная операция*. Тоже поправил. * время разбивки строки на компе неинформативно, а на мобилке замерять лень. |
Ответ: Смесь: Неочевидное + Оптимизация
Замутил удобоваримые для мозга сортировки образов внутри объектов.
Образы сортируются сначала по глубине depth - это слой рисования: -1 задний, 0 средний, 1 передний; затем по addOrder - порядку добавления образа в объект. Т.к. я отказался от массивов в пользу списков, то пришлось переделывать сортировку. Вот что получилось. LinkedList.sortBubble() Было: здесь в массив mas (писал ещё в те времена, когда массивы называл mas, а не arr) накапливаем количество одинаковых чисел, идущих подряд, чтобы потом отсортировать среди них по addOrder. рекламный слоган: "Тройной FOR - это по-нашему!" :) Стало: А вот наш чудесный "комплексный" компаратор, который включает в себя сравнение по двум параметрам - depth и addOrder. Добавление условий PHP код:
PHP код:
|
Ответ: Смесь: Неочевидное + Оптимизация
|
Ответ: Смесь: Неочевидное + Оптимизация
В стандартном Vector каждый метод synchronized - теоретически, из-за этого может быть медленнее.
А вообще, можно посмотреть исходники и на их основе написать свою версию, заточенную под требования конкретного случая. |
Ответ: Смесь: Неочевидное + Оптимизация
Цитата:
2. вставка в середину. в джаве есть arrayCopy, сдвигается с его помощью, наверное поэтому шустро. я обычно вставляю в список или удаляю через ссылку на узел (ListNode), т.е. по какому-то условию в цикле, когда у меня есть этот самый узел, а это быстро, т.к. нужно просто переприсвоить ссылки next, prev PHP код:
Минус списка - создаётся мелочёвка в виде узлов ListNode для каждого элемента, сборщику мусора нужно будет чистить эти узлы после удаления элементов. Igor, спасибо что напомнил про synchronized. |
Ответ: Смесь: Неочевидное + Оптимизация
Решил переделать циклы перебора списков.
Было так: PHP код:
такой код потенциально опасен - если в цикле обхода списка вызывается функция, которая должна пробегать по этому же списку, то бегунок current становится неправильным. для решения я сделал (залипуху/костыль/______) в виде дополнительной переменной storedCurrent и два метода store/restore, полагая, что более глубокой вложенности не будет. глубже вложенности пока нет, но нафиг надо. лишнее поле, работающее не универсально (вот если бы был стек сохраняемых бегунков, то другое дело), и нужное лишь для одного места в коде. Стало так: PHP код:
проход в обратном порядке делается аналогично, node = list.last(); node.next, node.item - да, они public Примечание: иногда нужно пропустить очередной next, в этом случае можно либо писать так: PHP код:
PHP код:
Теперь хочу отказаться от этого, чтобы не было путаницы. пример: вызываешь в цикле метод list.skipStep(), надеешься что он не подведёт, а в условии цикла про это забываешь, делая простой node = node.next; UPD: большинство списков требуют только однопаправленный проход. Есть мысль сделать односвязный список и юзать его по максимуму. Неудобство - функция удаления узла, т.к. из текущего узла никак не выцепить предыдущий, чтобы перекинуть дальше его ссылку next. Скорее всего сделаю этот список, в функцию удаления буду передавать два узла - подлежащий_удалению и предыдущий_для_него. Удалений элементов не слишком много, все они в цикле, т.е. можно легко хранить внешний указатель на prev. |
Ответ: Смесь: Неочевидное + Оптимизация
Переписал HTML- и JSON- парсеры
У нас в проекте используется псевдо-html. Часть тегов соответствуют обычному хтмл, часть - наши самодельные теги. В инете я нашёл парсер json для j2me. Но он мне показался слишком громоздким, дофигища классов. В итоге я написал свой, всего 100 строк кода (со второй попытки, первая попытка была страшновата для неподготовленного мозга). Скорость работы и потребляемую память не сравнивал с "интернетовским". Минус - юзаю рекурсию. Но мы большую вложенность не предполагаем юзать, а каких-нибудь 10 уровней внутрь мобилки тянут. А вот html парсер я сравнивал со своим же, который юзаю в текущей версии. Пока что сравнение сделал для страницы регистрации (другие популярные страницы - на очереди). текущий время: 29 мс память: 20 кб новый время: 12-15 мс память: 13 кб Сравнивал на ПК-эмуляторе, это конечно не качественно. Потом проверю на мобильнике. Ещё из фишек нового парсера - можно делать неограниченную вложенность тегов. Текущий не умеет делать вложенные однотипные теги, например таблицу в таблицу нельзя или span в span. Теперь можно. Ну и новый тег style добавили, теперь можно стили юзать, это должно сократить код, облегчить вёрстку. |
Ответ: Смесь: Неочевидное + Оптимизация
Уже давненько открыл для себя в нетбинсе фичу автоподстановки, которая предлагает варианты сразу при написании первой буквы.
По дефолту эта фича выключена. Очень ускоряет кодинг. Скрин. |
Ответ: Смесь: Неочевидное + Оптимизация
Мы зарелизили новую версию - Galaxy 9.0!
Не сегодня, пораньше. Вчера я уже допилил багфиксы для 9.0.4. К делу. На некоторых телефонах прога закрывалась сразу при запуске, выдавая NullPointerException. Включая 2 наших тестовых телефона, и это хорошо - было на чём разбираться. Сроки горели, пришлось с багом релизить. (Но мы не афишировали обнову сразу, лишь спустя несколько дней), так что не было поголовных апдейтов. Тем более что это j2me, нет автообновлений. Жалобы про нулпоинтер, конечно, посыпались в службу поддержки. Начал разбираться. Встал вопрос - как это дебажить. В андроиде халява - по шнуру тебе в дебаг сливается стек с ошибкой, видишь откуда корни, а на j2me хрен, - даже текстом на экран просто так не выведешь, т.к. при появлении ошибки вылазит системный алерт и ничего не успеешь понять. В итоге я сделал функцию waitForKey, которая крутит бесконечный цикл, пока не нажмём клавишу. Благо клавиши обрабатываются в отдельном системном потоке, и мы их можем ловить при наличии бесконечного цикла в главном потоке. Комбинируя waitForKey с выводом на экран каждого шага загрузки ресурсов, я обнаружил, что проблема в функции загрузки текста из файла: Изучил её внутренности, вроде всё в порядке, но я выдвинул гипотезу-причину, переделал, но не помогло. :) [гипотеза: некоторые телефоны выдают некорректную длину данных в потоке: int size = dis.available();] Дальнейший пошаговый дебаг выявил окончательную причину. Так в чём же тут дело? Ответ: Попутно узнал, что в конструкции try - catch - finally секция finally выполнится, даже если в try есть досрочный return. |
Ответ: Смесь: Неочевидное + Оптимизация
Узнал интересную штуку для дебага - определение вложенности вызовов функций:
Код:
try { Блин, это гениально! Помогло мне разобраться в длинной череде вызовов: |
Ответ: Смесь: Неочевидное + Оптимизация
Прокрутка браузерных страниц.
Раньше у меня при прокрутке страницы делался сдвиг всех элементов формы. В итоге для жирных страниц совершалось много теледвижений, проходов по вложенным тегам. Теперь просто сдвигаю канвас при отрисовке страницы. Дополнительно пришлось * поправить функцию setClip, т.к. при сдвиге канваса область отсечения также уезжает. * поправить пункцию isElementInView, т.к. раньше в ней завязка была на то что меняем координаты самих элементов. |
Ответ: Смесь: Неочевидное + Оптимизация
С функциями graphics.setClip() и graphics.translate() оказалось больше проблем, чем хотелось бы.
Прокачал класс Viewport - хэлпер для работы с клиппингом. Н-р, он умеет делать lock/unlock, чтобы последующие установки клипа не выходили за эту залоченную область. Что узнал: 1. Важен порядок вызова функций setClip => translate != translate => setClip при установке области клипа учитывается текущий сдвиг координат, но все последующие никак не влияют, запоминает именно сдвиг в момент установки клипа. 2. Реальные координаты клипа g.getClipX() и g.getClipY() - возвращаются значения с учётом сдвига координат. 3. g.translate(int x, int y) - это не передвинуть канвас в указанную точку, а сдвинуть на указанное количество относительно текущей позиции. могли бы написать в доке dx, dy для понятности. 4. Внутренности графикса по части клипа и транслэйта сложнее, чем думал (см. скрин) |
Ответ: Смесь: Неочевидное + Оптимизация
Если кто-то использует NetBeans IDE и любит тёмное оформление, то можете установить Darcula плагин.
https://github.com/Revivius/nb-darcula Он делает тёмным весь интерфейс, а не только область кода, как некоторые. Портал с плагинами недоступен, пришлось компилить из исходников. скачать из дропбокса nb-darcula-1.5.nbm |
Часовой пояс GMT +4, время: 10:46. |
vBulletin® Version 3.6.5.
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Перевод: zCarot