JavaRush /Java Blog /Random-KO /파일에 대한 바이트별 작업
Joysi
레벨 41

파일에 대한 바이트별 작업

Random-KO 그룹에 게시되었습니다
에 특별한

시작하자

레벨 18에서는 바이트 단위 파일 읽기의 첫 번째 작업이 시작되었습니다. 즉, 파일을 읽은 다음 최소/최대 바이트를 찾거나 순서대로 출력하는 등의 작업이 시작되었습니다.
파일에 대한 바이트별 작업 - 1
여기 사람들은 매우 똑똑합니다. 그들은 컬렉션에 대해 알고 있으며 정렬하고 삽입할 수 있다는 것을 알고 있습니다. 컬렉션은 강력한 메커니즘입니다. 그리고 JavaRush 이전에는 많은 사람들이 이를 전혀 사용하지 않았습니다. 물론 그것들을 연구하고 엉뚱한 곳을 치는 것은 칭찬할 만하다. 그래서. 작업에 없는 문제를 살펴보겠습니다(해결 시 스포일러가 없도록). 하지만 매우 유사한 문제도 있습니다.
  • 콘솔에서 파일 이름을 입력하세요.
  • 파일에서 모든 바이트를 읽습니다.
  • 반복을 무시하고 바이트 코드별로 내림차순으로 정렬합니다.
  • 표시하다
  • I/O 스트림 닫기
입력 파일의 바이트 예 44 83 44 출력 예 83 44 추가로 변수를 도입 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.");
    }
}
모든 것을 훌륭하게 해결합니다! 테스트(만약 있었다면 쾅하고 통과했을 것입니다). 그러나 인생에서 "엄마가 액자를 씻었습니다"라는 줄만 포함하는 파일은 거의 없습니다. 우리 프로그램에 46MB 파일을 공급해 보겠습니다. (현재 기준으로는 그리 많지 않은 것 같습니다.) 뭐죠, 프로그램이 220초 동안 실행되는데요. 저녁에 1Gb 파일을 공급하려는 시도(MPEG4 영화의 크기는 최고 품질이 아님)가 실패했습니다. 나는 아침에도 여전히 프로그램을 읽고 있었고, 벌써 출근해야 했습니다. 문제는 무엇입니까? ArrayList<Integer>아마도 내부에 10억 개의 요소가 사용 중일 것입니다 . 각 요소는 최소 16바이트를 차지합니다(헤더: 8바이트 + 필드 int: 4바이트 + 다중도 8에 대한 정렬: 4바이트). 전체적으로 우리는 RAM 크기가 8인 메모리에 자발적으로 16Gb의 데이터를 넣습니다. 우리는 더 잘할 것입니다. 컬렉션에 대해 더 자세히 살펴보겠습니다. 만세, 우리는 필요한 것을 찾았습니다.

트리셋을 만나보세요

이것은 많은 것입니다 :
  • 두 개의 동일한 요소를 저장하는 것을 허용하지 않습니다(즉, 10억 개가 아닌 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.");
    }
}
출력은 46MB 파일, 176초입니다. 1GB 파일 - 3시간 5분. 진전은 분명합니다. 결과를 “기다릴” 수 있었고 46MB 파일이 눈에 띄게 빠르게 처리되었습니다. 계속하세요. 컬렉션을 포기해 봅시다 (이것은 일부에게는 극도로 고통스러울 것입니다). 우리는 간단한 배열을 사용할 것입니다(매우 원시적입니다). 한 가지 중요한 점 에 주목해 봅시다 . 발견된 바이트 수는 길이 256의 배열에 포함될 수 있습니다. 따라서 읽은 바이트에 해당하는 배열 요소를 1씩 늘리면 됩니다.

배열 - 바이트 단위

// Вариант 3. Считываем массив поbyteно.
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();
        // Выводим отсортированный по byte-codeу в обратном порядке
        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.");
    }
}
출력은 46MB 파일, 158초입니다. 1GB 파일 - 2시간 55분. 다시 한 번 개선되었지만 작습니다. 그리고 우리는 간단한 도구를 사용하여 모든 작업을 수행했습니다. 손톱 박는 데 현미경을 사용하지 않았습니다 . 이제 서정적 여담. 컴퓨터의 구조를 기억해 봅시다. 주로 프로그램이 실행되고 변수가 저장되는 램 메모리(DRAM)는 접근 속도는 빠르지만 크기가 작다. 반면, 일반적으로 파일이 저장되는 하드/플래시 드라이브 (HDD 또는 플래시 드라이브)의 메모리는 접근 속도는 느리지만 크기가 큽니다. 따라서 1GB 파일을 바이트 단위로 읽을 때(즉, HDD에 수십억 번 액세스) 저속 장치를 사용하여 작업하는 데 많은 시간을 소비합니다(KamAZ 트럭 본체에서 모래 알갱이를 한 알씩 전송합니다) 샌드박스에 넣습니다). 더욱 개선해 보도록 하겠습니다.

KAMAZ 트럭 전체를 모래와 함께 한 번에 버리자!

// Вариант 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.");
    }
}
작지만 다시 한 번 중요한 여담입니다 .
  1. arrBytes 인덱스는 0..255 내에서 정의됩니다.
  2. fileImage는 요소의 값이 -128..127인 바이트 배열입니다.
arrBytes[fileImage[i] & 0b11111111]++; 따라서 바이트를 계산하기 위해 단순히 부호 비트를 재설정하고 0..255 범위의 값을 반환하는 구성을 사용합니다. 따라서 결과는 다음과 같습니다. 46MB 파일 0.13초(1초 미만). 1GB 파일 - 9초. 우리는 해냈다! 우리는 엄청나게 멋지다! 3시간에서 9초로 빨라졌습니다. 그게 다입니다. 의자에 편안히 앉아 차를 마실 수 있습니다. 이제 또 다른 실험입니다. 32GB 파일(예: HD 영화)을 사용해 보겠습니다. 결과적으로 Windows에서 프로그램이 충돌하면서 작동 중인 HDD에서 딱딱거리는 소리가 들립니다. KamAZ가 시체를 모래로 버리고 모래상자를 깨뜨렸습니다! 우리는 무엇을해야합니까? 한 가지 사실을 더 기억합시다. OS의 파일은 일반적으로 2-64Kb의 부분(클러스터)에 저장됩니다(파일 시스템 유형, 설정 등에 따라 다름). 예를 들어 64,000바이트 등의 부분을 읽어보겠습니다. 굴삭기로 KamAZ를 상당히 큰 부분으로 언로드해 보겠습니다.

버퍼 사용

// Вариант 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.");
    }
}
결과적으로 다음과 같은 결과를 얻었습니다: 46MB 파일 0.08초(1초 미만). 1Gb 파일 - 0.9초(1초 미만) 32Gb 파일 - 31초. 1Gb 파일의 경우 성능이 몇 시간 에서 몇 초로 향상되었습니다 !!! 이 정도 사실을 바탕으로 실험을 마무리하고 초기 코드를 개선하겠습니다. 우리는 여러 면에서 진전을 이루었습니다. 메모리 소비 및 작동 시간에 대한 새로운 지표에 만족합니다. 또한 이 경우 표준 라이브러리에서 쓸모없는 컬렉션을 가져오지 않습니다. PS 누군가는 이 예가 터무니없다고 말할 것입니다. 그러나 유한한 수의 상태를 가진 엄청난 양의 요소를 분석하는 것과 같은 유사한 작업이 많이 있습니다. 예를 들어, 이미지(RGB - 일반적으로 24바이트에 저장됨, 우리의 경우 long[] arrRGB = new long[256*256*256]는 메모리에서 64MB만 차지함), 음악(진폭은 일반적으로 16비트 또는 24비트로 디지털화됨) ) 또는 개별 표시기 센서 등
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION