CodeGym /Kurslar /JAVA 25 SELF /Böyük faylları hissələrə bölmək

Böyük faylları hissələrə bölmək

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Niyə böyük faylları tək axında oxumaq — kərpici bir-bir daşımaq kimidir

Böyük fayllarla işləyəndə — onlarla və ya yüzlərlə meqabayt, hətta giqabayt olduqda — tək axınla oxuma və ya yazma tez bir zamanda dar boğaza çevrilir. Tək axın yükə tab gətirmir: disk məlumatı proqramın emal etmə sürətindən daha tez verə bilər.

Hətta sürətli SSD-niz olsa belə, axın diskdə deyil, əlavə xərclərdə ilişir — kontekst dəyişmələri, buferlərlə iş, yaddaşda verilənlərin çevrilməsi. Nəticədə performans düşür, prosessor isə “darixir”, çünki digər nüvələr boş dayanır.

Tutaq ki, siz nəhəng bir jurnal faylında sözlərin sayını hesablamaq qərarına gəldiniz. Bunu ardıcıl etsəniz, tək axın faylı yavaş-yavaş “didəcək”, sizsə sadəcə gözləyəcəksiniz. Amma faylı hissələrə bölüb bir neçə axına tapşırsanız, proses xeyli sürətlənəcək: hər biri öz payını emal edəcək və nəticədə diskin potensialından demək olar ki, tam istifadə olunacaq.

Praktikada bu belə görünür: keçid qabiliyyəti 2 GB/s olan SSD-də tək axınla oxuma cəmi təxminən 300–500 MB/s verir. Paralel oxuduqda isə daşıyıcıdan onun bacardığı maksimumu “çıxarmaq” mümkündür.

2. Chunking — faylı sizin xeyrinizə işlətməyin yolu

Fayl bütöv şəkildə emal etmək üçün çox böyük olduqda ən məntiqli addım — onu hissələrə bölməkdir. Bu üsula chunking (ing. chunk — “hissə”) deyilir. Fikir sadədir: böyük faylı bir neçə məntiqi seqmentə bölürsünüz və hər axına öz sahəsini tapşırırsınız.

Hər axın hansı yerdən (offset) başlamalı və harada dayanmalı olduğunu bilir. O, yalnız öz hissəsini oxuyur, verilənləri emal edir, sonra isə nəticələr bir ümumi yekunda birləşdirilir.

Bu yanaşma prosessorun bütün nüvələrini eyni vaxtda işə salmağa imkan verir və emalı əhəmiyyətli dərəcədə sürətləndirir, xüsusilə də müasir SSD və ya NVMe-diskiniz varsa. Sətirlərin sayılması, mətndə axtarış və ya statistikanın toplanması kimi tapşırıqlarda chunking “turbo” effekti verir — xüsusi zəhmət olmadan sürəti artırır.

Çank ölçüsünü necə seçmək

Çankın ölçüsü demək olar ki, porsiyanın ölçüsü kimidir: çox kiçik olsa — doğramaqdan yorulacaqsınız, çox böyük olsa — “həzmi” çətin olacaq. Hər şey tapşırıqdan və maşının imkanlarından asılıdır.

Orta hesabla, hər axın üçün 8–64 MB aralığı yaxşı nəticələr verir. Əksər tapşırıqlar üçün təxminən 10–20 MB götürmək kifayətdir, amma ideal rəqəm yoxdur — hamısı təcrübə ilə seçilir. Əsas odur ki, hissə əlavə axın keçidlərinə vaxt itirməmək üçün kifayət qədər böyük olsun, eyni zamanda prosessor keşini və ya bütün yaddaşı “dolduracaq” qədər də böyük olmasın.

Mətrlə işləyirsinizsə — məsələn, söz sayırsınız və ya uyğunluq axtarırsınız — çankların sətirləri və ya sözləri ortadan “parçalamaması” vacibdir. Adətən bunu sadə həll edirlər: hissələr arasında kiçik üst-üstə düşmə (overlap) edirlər və ya sərhədləri ən yaxın sətirsonu simvoluna qədər sürüşdürürlər. Beləliklə, emal dəqiq, nəticə isə təmiz və proqnozlaşdırılan qalır.

3. Mövqeli giriş üçün alətlər: FileChannel və MappedByteBuffer

FileChannel: mövqeli IO

FileChanneljava.nio.channels paketindən olan bir sinifdir, fayllarla aşağı səviyyədə işləməyə, o cümlədən faylın istənilən mövqeyindən oxumağa və yazmağa imkan verir.

Əsas metodlar:

  • position(long newPosition) — oxuma/yazma üçün mövqeni (offset) təyin edir.
  • read(ByteBuffer dst, long position) — fayldan buferə göstərilən mövqedən etibarən məlumat oxuyur (kanalın cari mövqeyini dəyişmir!).
  • write(ByteBuffer src, long position) — göstərilən mövqedən fayla məlumat yazır.

Nümunə: faylın hissəsini oxumaq

try (FileChannel channel = FileChannel.open(Path.of("bigfile.txt"), StandardOpenOption.READ)) {
    long chunkSize = 16 * 1024 * 1024; // 16 MB
    long offset = 0;
    ByteBuffer buffer = ByteBuffer.allocate((int) chunkSize);
    int bytesRead = channel.read(buffer, offset);
    // buffer faylın ilk 16 MB-ını ehtiva edir
}

Üstünlüklər:

  • İstənilən mövqedən oxumaq/yazmaq mümkündür.
  • Paralel emal üçün əlverişlidir: hər axın öz hissəsi ilə işləyir.

MappedByteBuffer: yaddaşa xəritələnmiş fayllar

MappedByteBuffer — faylın bir hissəsini “yaddaşa xəritələməyə” (map) imkan verən xüsusi buferdir. Əməliyyat sistemi məlumatların diskdən yaddaşa yüklənməsini və geri yazılmasını özü idarə edir.

Bu necə işləyir?

  • Faylın bir hissəsini yaddaşa xəritələyirsiniz.
  • Buferdən oxuyur/yazırsınız — ƏS lazım olan səhifələri özü yükləyir.
  • read/write üçün açıq çağırışlar yoxdur — hər şey yaddaş vasitəsilə baş verir.

Nümunə:

try (FileChannel channel = FileChannel.open(Path.of("bigfile.txt"), StandardOpenOption.READ)) {
    long chunkSize = 16 * 1024 * 1024; // 16 MB
    long offset = 0;
    MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, offset, chunkSize);
    // İndi buffer bayt massivi kimi davranır, amma məlumatlar müraciət olunduqca diskdən oxunur
}

Üstünlüklər:

  • Çox yüksək sürət (xüsusilə SSD-lərdə).
  • Sadelik: massiv kimi oxu/yaz.

Mənfi cəhətlər:

  • Virtual yaddaşdan istifadə edir — fayl çox böyükdürsə yaddaşı “doldura” bilər.
  • Yaddaşdan çıxarılmanı idarə etmək çətindir (bufer lazım olduğundan daha uzun müddət yaddaşda qala bilər).
  • Çox böyük fayllar üçün həmişə rahat deyil (32-bit sistemlərdə 2–4 GB-dan böyük).

4. Nümunə: paralel oxuma və sözlərin sayılması

Tapşırıq: böyük mətni olan faylda (məsələn, 10 GB-lıq jurnal) sözlərin sayını paralel emal vasitəsilə hesablamaq.

Addım 1. Faylı çanklara bölürük

  • Faylın ölçüsünü götürürük: long fileSize = Files.size(path);
  • Çank ölçüsünü seçirik, məsələn, 16 MB.
  • Hər çank üçün offset hesablayırıq: offset = chunkIndex * chunkSize;
  • Son çank ölçücə daha kiçik ola bilər.

Addım 2. Axınlar üçün tapşırıqlar yaradırıq

  • Hər çank üçün Callable<Integer> (və ya Runnable) yaradırıq, o:
    • Öz fayl hissəsini FileChannel.read(ByteBuffer, offset) və ya MappedByteBuffer ilə açır.
    • Öz hissəsindəki sözlərin sayını hesablayır.
    • Nəticəni qaytarır (sözlərin sayı).

Addım 3. Tapşırıqları ExecutorService vasitəsilə işə salırıq

  • Axın hovuzu yaradırıq: ExecutorService pool = Executors.newFixedThreadPool(N);
  • Tapşırıqları hovuza göndəririk: List<Future<Integer>> results = pool.invokeAll(tasks);
  • Nəticələri toplayırıq: bütün future-lardan dəyərləri cəmləyirik.

Kod nümunəsi (sadələşdirilmiş):

import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;

public class ParallelWordCount {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("bigfile.txt");
        long fileSize = Files.size(path);
        int chunkSize = 16 * 1024 * 1024; // 16 MB
        int chunks = (int) ((fileSize + chunkSize - 1) / chunkSize);

        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        List<Future<Integer>> results = new ArrayList<>();

        for (int i = 0; i < chunks; i++) {
            long offset = (long) i * chunkSize;
            long size = Math.min(chunkSize, fileSize - offset);

            results.add(pool.submit(() -> {
                try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
                    MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, offset, size);
                    byte[] bytes = new byte[(int) size];
                    buffer.get(bytes);
                    String text = new String(bytes);
                    // Vacibdir: sözü parçalamamaq üçün çank sərhədlərini düzgün işləmək!
                    return countWords(text);
                }
            }));
        }

        int totalWords = 0;
        for (Future<Integer> f : results) {
            totalWords += f.get();
        }
        pool.shutdown();
        System.out.println("Total words: " + totalWords);
    }

    private static int countWords(String text) {
        // Ən sadə üsul: boşluqlara görə bölmək və boş sətirləri süzmək
        String[] words = text.split("\\s+");
        int count = 0;
        for (String w : words) {
            if (!w.isBlank()) count++;
        }
        return count;
    }
}

Diqqət: real tapşırıqlarda çank sərhədlərini diqqətlə işləmək lazımdır ki, söz və ya sətir iki axın arasında parçalanmasın. Adətən kiçik overlap edirlər (məsələn, +100 bayt) və çankın başlanğıc/sonunu düzəldirlər.

5. Nəticələr və ən yaxşı təcrübələr

  • Böyük fayllar üçün çanklara bölmə və paralel emaldan istifadə edin.
  • Mövqeli giriş üçün FileChannel, yaddaşa xəritələnmiş fayllar üçün MappedByteBuffer tətbiq edin.
  • Çank ölçüsünü təcrübə ilə seçin; meyar — prosessor keşinin ölçüsü və diskin keçid qabiliyyəti.
  • Çank sərhədlərini diqqətlə işləyin (xüsusilə mətnlər üçün).
  • Paralel emal üçün ExecutorService və axın hovuzundan istifadə edin.
  • Axın sayını şişirtməyin: adətən SSD üçün 2–4 axın kifayətdir.
  • Yaddaş sərfiyyatına nəzarət edin: MappedByteBuffer çoxlu virtual yaddaş tuta bilər.

6. Böyük fayllarla və chunking ilə işləyərkən tipik səhvlər

Səhv №1: Bütün faylı yaddaşa oxumaq. Böyük faylların emalında bu, OutOfMemoryError-a səbəb ola bilər. Bunun əvəzinə məlumatları hissə-hissə (çanklarla) oxuyun.

Səhv №2: Çank sərhədlərinin yanlış işlənməsi. Faylı sətir və ya söz sərhədlərini nəzərə almadan bölmək məlumatın “parçalanmasına” və nəticənin səhv olmasına gətirib çıxara bilər.

Səhv №3: Qeyri‑optimal çank ölçüsü. Çox kiçik çanklar axınların idarə olunması üçün əlavə xərclər yaradır, çox böyük çanklar isə yaddaşdan səmərəsiz istifadə edir.

Səhv №4: FileChannel-ın bağlanmaması. Bu, resurs sızmalarına gətirib çıxarır. Kanalın bağlanmasına zəmanət vermək üçün try-with-resources istifadə edin.

Səhv №5: Həddən artıq çox axın. Axınların sayı çox olduqda, disk sorğuları yetişdirmir və performans artmaq əvəzinə azalır.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION