Special for
Начнем'c
В 18 уровне начались первые задачи побайтного чтения файлов: прочитать файл, далее найти минимальные/максимальные байты или вывести в упорядоченном виде и т.п.Народ тут весьма ушлый. Знают про коллекции и про то, что они могут сортировать, вставлять.
Коллекции — мощный механизм. И многие не применяли их вообще до JavaRush-а.
Оно, конечно, похвально изучать их и пытаться приткнуть куда не попадя.
И так. Возьмем задачу, которой нет в заданиях (чтобы не было спойлеров при решении), но есть сильно похожие:
- Ввести с консоли имя файла
- Считать все байты из файла.
- Не учитывая повторений - отсортировать их по байт-коду в убывающем порядке.
- Вывести на экран
- Закрыть поток ввода-вывода
startTime
и finishTime
— чтобы засечь время выполнения программы. Для вычисления использовал i3-3GHz/8Gb RAM/HDD WD Blue-1Tb/Win7-64/jdk-8u73-windows-x64
(примеры программ в вариантах 1-2 взяты из форума info.javarush , они чуть модифицированы только для сортировки в возрастающем порядке - то есть они РЕАЛЬНЫЕ!!)
Решаем в лоб:
// Вариант 1. Загоняем в коллекцию и сортируем используя ее метод Collections.sort
public class Solution {
public static void main(String[] args) throws Exception {
FileInputStream inputStream = new FileInputStream(new BufferedReader(new InputStreamReader(System.in)).readLine());
long startTime = System.currentTimeMillis();
ArrayList<Integer> listData = new ArrayList<Integer>();
while (inputStream.available() > 0) listData.add(inputStream.read());
inputStream.close();
ArrayList<Integer> result = new ArrayList<Integer>(new HashSet<Integer>(listData));
Collections.sort(result);
while (!result.isEmpty()) {
System.out.print(result.get(result.size()-1) + " ");
result.remove(result.get(result.size()-1));
}
long finishTime = System.currentTimeMillis();
System.out.println("\nвремя работы=" + (finishTime-startTime) + "ms.");
}
}
Решает все замечательно! Тест (если бы был — прошелся бы на ура). Но в жизни мало файлов содержащих только строчку "Мама мыла раму". Давайте скормим нашей программе файл в 46Мб (по нынешним меркам вроде и не особо много). Что такое, программа выполняется 220 секунд. Попытка скормить с вечера 1Gb файл (размер MPEG4 фильма не в самом лучшем качестве) не увенчалась успехом. Программа утром все еще читала - а мне идти на работу уже.
В чем проблема? Наверное в использовании ArrayList<Integer>
у которого внутри 1 миллиард элементов. Каждый элемент его занимает 16 байт минимум (Заголовок: 8 байт + Поле int: 4 байта + Выравнивание для кратности 8: 4 байта). Итого мы добровольно загоняем в память 16 Gb данных при размере оперативы в 8.
Будем делать лучше. Нырнем в коллекции глубже. И ура, нашлось то, что нам нужно.
Встречаем TreeSet
Это множество:- не допускает хранение двух одинаковых элементов (а значит мы будем хранить в памяти все 255 элементов, вместо миллиарда!)
- при манипуляциях со своими элементами автоматом упорядочивает (само сортирует - вот он, верх совершенства!)
// Вариант 2. Загоняем в ТreeSet который сам сортирует (лютый win!)
public class Solution {
public static void main(String[] args) throws Exception {
FileInputStream inputStream = new FileInputStream(new BufferedReader(new InputStreamReader(System.in)).readLine());
byte[] arrBytes = new byte[256];
long startTime = System.currentTimeMillis();
SortedSet<Integer> list = new TreeSet<Integer>();
while(inputStream.available()>0) list.add(inputStream.read());
inputStream.close();
while (!list.isEmpty()) {
System.out.print(list.last() + " ");
list.remove(list.last());
}
long finishTime = System.currentTimeMillis();
System.out.println("\nвремя работы=" + (finishTime-startTime) + "ms.");
}
}
Имеем на выходе:
46Мб файл 176 секунд.
1Gb файл - 3 часа 5 минут.
Прогресс на лицо. Мы смогли "дождаться" результатов, да и 46Мб файл заметно быстрее обрабатывается.
Идем дальше. Давайте попытаемся отказаться от коллекций (это будет для некоторых мучительно больно). Будем использовать простые массивы (это так примитивно).
Заметим одну важную вещь. Кол-во встречающихся байт можно загнать в массив длиной 256. Так просто будем увеличивать на единицу соответствующий считанному байту элемент массива.
Массив — побайтно
// Вариант 3. Считываем массив побайтно.
public class Solution {
public static void main(String[] args) throws Exception {
FileInputStream inputStream = new FileInputStream(new BufferedReader(new InputStreamReader(System.in)).readLine());
long[] arrBytes = new long[256];
long startTime = System.currentTimeMillis();
while (inputStream.available() > 0) arrBytes[inputStream.read()]++;
inputStream.close();
// Выводим отсортированный по байт-коду в обратном порядке
for (long i = 255; i >= 0 ; i--)
if (arrBytes[(int) i] > 0) System.out.print(i + " ");
long finishTime = System.currentTimeMillis();
System.out.println("\nвремя работы=" + (finishTime-startTime) + "ms.");
}
}
Имеем на выходе:
46Мб файл 158 секунд.
1Gb файл - 2 часа 55 минут.
Опять улучшение, но небольшое. И мы сделали все простыми инструментами. Не использовали микроскоп для забивания гвоздей.
Теперь лирическое отступление.
Вспомним устройство компьютера.
Память ОЗУ (DRAM) где обычно выполняется программа и хранятся переменные имеет высокую скорость доступа, но небольшой размер.
Память на жестком/flash диске (HDD или Flash-накопители) где обычно хранятся файлы, наоборот имеет низкую скорость доступа, но большой размер.
Так что когда мы побайтно читаем 1Gb файл (то есть миллиард раз обращаемся к HDD) - мы тратим много времени на работу с низкоскоростным устройством (по песчинке перекладываем песок с кузова КамАЗа в песочницу).
Попробуем еще улучшить.
Вывалим сразу ВЕСЬ КамАЗ с песком за один раз!
// Вариант 4. Считываем массив сразу целиком за раз в память.
public class Solution {
public static void main(String[] args) throws Exception {
FileInputStream inputStream = new FileInputStream(new BufferedReader(new InputStreamReader(System.in)).readLine());
long[] arrBytes = new long[256];
long startTime = System.currentTimeMillis();
byte fileImage[]=new byte[inputStream.available()];
long fileSize=fileImage.length;
inputStream.read(fileImage);
for (int i = 0; i = 0 ; i--)
if (arrBytes[(int) i] > 0) System.out.print(i + " ");
long finishTime = System.currentTimeMillis();
System.out.println("\nвремя работы=" + (finishTime-startTime) + "ms.");
}
}
небольшое, но опять таки важное отступление
Заметим:
- индекс у arrBytes определен в пределах 0..255,
- fileImage - массив байт, элементы которого имеют значение -128..127
arrBytes[fileImage[i] & 0b11111111]++;
которая банально сбросит бит знака и вернет нам значение в диапазоне 0..255
И так, результаты:
46Мб файл 0.13 секунд (меньше секунды).
1Gb файл - 9 секунд.
Мы сделали это! Мы невероятно круты! Ускорились с 3 часов до 9 секунд.
Все, можно откинуться в кресле и попить чайку. А теперь еще один эксперимент - попробуем файл в 32 Gb (например, HD фильм). Получим в результате треск работающего HDD с вываливанием программы в Windows. КамАЗ вывалив кузов с песком сломал песочницу!
Что будем делать? Вспомним еще один факт. Файлы в ОС хранятся обычно порциями (кластерами) по 2-64Кб (зависит от типа файловой системы, настроек и т.п.). Будем считывать порциями, для примера в 64000 байт. Попытаемся разгрузить КамАЗ экскаватором достаточно большими порциями:
Используем буфер
// Вариант 5. Считываем массив кусками.
public class Solution {
public static void main(String[] args) throws Exception {
FileInputStream inputStream = new FileInputStream(new BufferedReader(new InputStreamReader(System.in)).readLine());
long[] arrBytes = new long[256];
long startTime = System.currentTimeMillis();
int bufferSize = 64000;
byte buffer[] = new byte[64000];
while (inputStream.available() > 0) {
if (inputStream.available() < 64000) bufferSize = inputStream.available();
inputStream.read(buffer, 0, bufferSize );
for (int i = 0; i = 0 ; i--)
if (arrBytes[(int) i] > 0) System.out.print(i + " ");
long finishTime = System.currentTimeMillis();
System.out.println("\nвремя работы=" + (finishTime-startTime) + "ms.");
}
}
В итоге получили:
46Мб файл 0.08 секунд (меньше секунды).
1Gb файл - 0.9 секунд(меньше секунды).
32Gb файл - 31 секунда.
Заметим для 1 Gb файла мы улучшили производительность с нескольких часов до долей секунд!!!
На этом скромном факте закончим эксперимент и улучшение начального кода. Мы достигли прогресса во многом - нас радуют новые показатели расхода памяти и времени работы. Также мы не подтягиваем в данном случае бесполезные коллекции из стандартной библиотеки.
P.S. Кто-то скажет пример надуманный и т.п. Но полно похожих задач — проанализировать огромный объем элементов, имеющих конечное число состояний.
Например изображения (RGB - обычно хранятся в 24 байтах, в нашем случае long[] arrRGB = new long[256*256*256] занял бы в памяти всего 64Мб),
музыка (амплитуда обычно оцифровывается в 16 или 24 бита) или дискретные показатели датчиков и т.п.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
вместо int если использовать long, тоже не получается нужный мне результат.
.available() имеет корни из InputStream (возможно и более древние), отсюда и «наследие» в виде int.
Кликните, зажав ctrl на read();
Важный момент — чтобы узнать размер файла не обязательно его считывать, так как эта информация отдельно хранится в файловой системе.
P.S. >>> в данном случае аналогично делению (так как делитель — степень двойки)
Для информации можно глянуть developer.alexanderklimov.ru/android/java/bitwise.php и т.п. ресурсы
Почему тогда такого не происходит в Варианте 4 и необходимо использовать побитовое И?
при этом, собственно, inputStream.read() возвращает int (то есть внутри себя уже преобразовал байт в целое, учитывая возможный отрицательный байт).
А в варианте4, мы используем промежуточный байтовый буфер, который byte fileImage[] и который надо преобразовать
Если мы будем уберем побитовое (arrBytes[fileImage[i]]++), то получим отрицательный индекс (при считывание байта в битовом диапазоне 1000 0000 — 1111 1111 ), что приведет нас к java.lang.ArrayIndexOutOfBoundsException. Можете сами проэкспериментировать.
Файл 45 Mб. Пример5 130 мс. Мой вариант 3113 мс.
Действительно, элегантное решение Joysi в 24 раза производительнее. Но все таки, справедливости ради надо заметить, что получилось не 176 сек, как в Пример2 с TreeSet, а 3 сек, т.е. быстрее в 58 раз! Из чего вывод, что к такому гигантскому замедлению привело не столько даже использование коллекций и их встроенных механизмов сортировки, а отсутствие буферизации, всю крутизну которой можно оценить наглядно. Перетаскивать песок из нашего камаза чайной ложкой — то еще удовольствие)
Помимо прочего, при реализации даже очевидных задач всегда следует проводить возможную оптимизацию в нескольких плоскостях( в данном случае 2х: какую структуру использовать для хранения результат и как считывать файл с диска). Если взять задачу вывода в порядке убывания по цвет-коду из огромныx BMP/PNG файлов то сортировка 256*256*256 возможных элементов при вставке в TreeSet будет работать по времени по другому (он же будет внутри себя перестраивать дерево сортировки своих элементов). Надо экспериментировать.
Плюс пример с массивом привел, чтобы сломать некую шаблонность у некоторых при работе с массивами, когда его индекс последовательно перебирают либо в цикле, либо явно указывают константно:
А конструкция использования в качестве индекса вычисляемого/считываемого выражения, например вида:
вызывают сначала недоумение.
ответ тут:
info.javarush.ru/Joysi/2016/02/20/World-of-Bytes-1-%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0-%D1%81-%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8-.html
Как БЫСТРО отсортировать массив байт a[i] без операций сравнений?
вот допустим такой
и просто вывести его в консоль? То есть:
100 30 20 30 50 60 30 6 2 — в -> 2 6 20 30 30 30 50 60 100
А секрет — именно в конструкции arrBytes[a[i]].
По шагам:
1. Объявили дополнительный массив arrBytes[] в котором ровно 256 элементов — то есть все ВОЗМОЖНЫЕ значения, которые могут быть в байте. Значение элемента массива — это сколько раз он у нас встречается в основном массиве a[]. Поэтому мы объявили его как long — потому что каждый элемент массива a[] может встретиться много раз (для файла, например).
2. У этого массива сразу все 256 элементов равны нулю. То что нужно, пока мы не начали обрабатывать исходный массив a[] — мы выставим, что пока ни один элемент-байт не встретили.
3. Дальше поэлементно смотрим массив a[i], допустим в нем a[i]=30. Значит 30 встречается еще один раз.
— мы и увеличиваем на единицу сколько раз встретили значение 30, то есть . Таким образом за один просмотр в цикле мы «прощупали» все содержимое массива.
4. Чтобы вывести его по возрастанию достаточно подряд с 0 до 255 повторить сколько раз мы нашли очередной по порядку элемент.
Аналогично можно ОЧЕНЬ быстро отсортировать массивы огромной длины, но имеющие конечное мно
Например тут
А для TreeSet, также capacity, размер(количество корзин) и loadFactor?
И еще не пробывали ли Вы использовать LinkedList, дело в том, что при вставке в ArrayList, когда в нем не остается места он расширяется, т.е. выделяется дополнительная память, создается новый массив вдвое большего размера, и все элементы со старого массива копируются в новый массив, что с LinkedList не проиходит, просто присваивается предпоследнему элементу массива(до вставки последнему) ссылка на последний вставленный элемент.
на 46Мб файле программа умирает на строчке
через ~десять минут, так скушала всю доступную память
Заметим, что ArrayList отрабатывает все ~ за 3.5 минуты.
По поводу Set-ов в данное случае, по моему неактуально, так как конечное число элементов = 256.
В этом случае множество (причем в данном случае неважно, будет TreeSet или HashSet) строится буквально на первых значениях считанных байт из файла. Далее просто отбрасывает очередные значения.
И вообще глобально, с байтами нужно так работать? Их во что-то преобразовать можно? Или это просто пример, а нужны как вы писали для анализа изображений, музыки?
И спасибо за разъяснения ещё раз.
255 (в привычном десятичном беззнаковом, или — 128 в форме со знаком),
как 0xFF в шестнадцатеричном.
В двоичном небольшие числа иногда удобно писать, так как видно какие биты (в данном случае все 8) участвуют в логической операции «И». Можно записать и в другом виде, но так, по моему нагляднее и удобнее (это субъективно все).
Откуда тут вообще возникли биты-байты. Основная причина в том, что в Java отсутствуют привычные беззнаковые целые типы данных. А считанный байт — он и есть беззнаковый и его удобно представлять как число от 0 до 255 (а не как от -128 до 127). Отрицательные числа причем образуются не просто как смещение на константную величину — en.wikipedia.org/wiki/Two%27s_complement (есть и на русском, но URL выглядит кошмарно).
С байтами можно работать по разному — например, рассматривать перевести и рассматривать далее как int. Но тогда они будут занимать в 4 раза больше места в памяти.
В примере inputStream.read() требует массив байт как параметр. И мне было жалко процессорного времени на дополнительное преобразование и место в памяти.
Музыка вообще понятие обширное. Оцифрованная музыка в MIDI формате например использует и знаковое и беззнаковое байтовое представление.
P.S. Но по любому, битовую арифметику надо знать уверенно. Например, IP-адресация (сети, подсети, маски и т.п.) построена на ней.
long[] arrBytes = new long[256];
// Выводим отсортированный по байт-коду в обратном порядке
for (long i = 255; i >= 0; i--)
if (arrBytes[(int) i] > 0) System.out.print(i + " ");
}
Вот этот кусок. Мы создаем массив размером с байт. А потом делаем? И почему в размере байта?
почему выводим в консоль просто i? Не понятен пока смысл всего этого. Как отсортировать в обычном порядке?