1. IO‑da «darboğaz» (bottleneck) nədir
Bir kassalı və uzun növbəli supermarket təsəvvür edin. Hər bir alıcı — sizin proqramınızdır, kasssa isə oxuma və ya yazma üçün müraciət etdiyiniz disk və ya şəbəkədir. Alıcı nə qədər “sürətli” olsa da, əgər kasssa yavaş işləyirsə, növbə böyüyəcək və performans — azalacaq.
Proqramlaşdırmada «darboğaz» (ingiliscə — «bottleneck») — tətbiqin ümumi iş sürətini məhdudlaşdıran sistem hissəsidir. Giriş‑çıxış (IO, Input/Output) əməliyyatları üçün belə darboğaz demək olar ki, həmişə diskə və ya şəbəkəyə oxu/yazma sürəti olur. Niyə? Çünki müasir prosessor saniyədə milyardlarla əməliyyat yerinə yetirə bilir, lakin disk (xüsusilə HDD) məlumatları minlərlə, hətta on minlərlə dəfə yavaş oxuyur və yazır.
IO‑da «darboğaz» nümunələri
- Böyük faylların yavaş açılması və ya oxunması. Nəhəng faylı dövrədə “parça‑parça” oxumağa çalışırsınızsa, amma çox kiçik bufer istifadə edirsinizsə və ya bayt‑bayt oxuyursunuzsa — sürət məyusedici, istifadəçi isə narazı olacaq.
- Loqların yazılmasında gecikmələr. Loqlaşdırma sinxron aparılır və hər mesaj dərhal diskə yazılırsa, tətbiq gözlə görüləcək dərəcədə “donub” qala bilər.
- IO üzərində axınların bloklanması. Proqramın bir neçə axını eyni anda oxu və ya yazma əməliyyatlarının bitməsini gözləyirsə, bütün sistem yavaşlamağa başlayır.
Niyə IO yavaşdır?
Operativ yaddaşla işləyərkən hər şey demək olar ki, ani baş verir və giriş‑çıxışın tamamilə başqa cür təşkil olunduğunu unutmaq asandır. Disk nə qədər müasir olsa da, RAM‑dan qat‑qat yavaşdır: sərt disk təxminən min dəfə geridə qalır, hətta sürətli müasir SSD belə yüzlərlə dəfə uduzur. Şəbəkə ilə vəziyyət daha da pisdir. Məlumatlar sizdə yox, serverdə və ya buluddadırsa, burda ötürmə qabiliyyəti və gecikmələr təsir göstərir, buna görə də çıxış nəzərəçarpacaq dərəcədə yavaşdır.
Buna bir qat da əlavə olunur — əməliyyat sistemi. Hər oxu və ya yazma sorğusu drayverlərdən, keşləmə mexanizmlərindən, təhlükəsizlik və giriş hüquqlarının yoxlanılmasından keçir. Bütün bu mexanizmlər vacibdir, amma gecikmə də əlavə edirlər. Nəticədə istənilən giriş‑çıxış əməliyyatı yaddaşla işdən xeyli yavaşdır və məhz buna görə proqramçılar keşləri, buferləşdirməni və asinxron yanaşmaları yüksək qiymətləndirirlər.
2. Aşağı performansın tipik səbəbləri
İndi isə gəlin baxaq, hansı səhvlər və uğursuz qərarlar IO‑nu ən əsl “butulka boğazına” çevirir.
Kiçik porsiyalarla tez‑tez müraciətlər
Ən çox rastlanan yeni başlayan səhvi — faylı bayt‑bayt və ya simvol‑simvol oxumaq və ya yazmaqdır. Bu, 3 kq alma almaq üçün mağazaya gedib hər dəfə bir alma almaq, onu evə aparmaq, sonra mağazaya qayıdıb növbəti almanı götürmək və beləliklə 3 kq tamamlanana qədər davam etdirməyə bənzəyir. Guya tapşırığı yerinə yetirirsiniz, amma, yumşaq desək, səmərəsizdir. Fayllarla da eyni hekayədir: məlumatlarla böyük porsiyalarla işləmək əvəzinə, proqram çox vaxtını xidməti çağırışlara sərf edir.
«Anti‑pattern» nümunəsi:
// Çox yavaşdır: bayt-bayt oxuma
try (InputStream in = new FileInputStream("bigfile.txt")) {
int b;
while ((b = in.read()) != -1) {
// Bir baytı emal edirik
}
}
Hər bir in.read() çağırışı — diskə ayrıca müraciətdir. Fayl böyükdürsə — belə çağırışlar milyonlarla olacaq!
Buferləşdirmənin olmaması
Buferləşdirmə — məlumatların bayt‑bayt oxunub/yazılması deyil, bloklar halında (məsələn, 4 KB və ya 8 KB) qruplaşdırılmasıdır. Buferləşdirmədən istifadə etməsəniz, diskə düşən yük dəfələrlə artır, performans isə azalır. Java‑da bunun üçün hazır siniflər var: BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter.
Böyük həcimli verilənlərin sinxron emalı
Əgər böyük faylları bir axında oxuyur və ya yazırsınızsa, proqram işi davam etdirməzdən əvvəl IO əməliyyatının bitməsini gözləyəcək. Bu, xüsusilə istifadəçi interfeyslərində (GUI) və ya server tətbiqlərində hiss olunur — burada “donma” yolverilməzdir.
Paralellik mümkünkən tək axınlı emal
Bəzən bir neçə faylı eyni anda oxumaq və ya yazmaqla (məsələn, loq paketlərini emal etmək) emalı sürətləndirmək mümkündür. Amma hər şey bir axında edilirsə — prosessorun və diskin imkanlarından tam istifadə etmirsiniz.
3. Problemləri necə aşkar etmək olar
IO performansı problemi kod yazma mərhələsində tez‑tez gözə çarpmır. Hər şey işləyir... ta ki daha böyük faylı emal etməyə çalışmayana və ya proqramı real yüklərlə serverdə işə salmayana qədər. Buna görə darboğazları tapmağı və təhlil etməyi bacarmaq vacibdir.
Profilerlərdən istifadə
Profilerlər — tətbiqinizin ən çox vaxtı harada keçirdiyini “gizlicə” görməyə kömək edən xüsusi proqramlardır. Java üçün pulsuz və pullu alətlər var:
- VisualVM — JDK ilə gəlir, qrafiklər qurur, “isti nöqtələri” (hot spots) göstərir.
- JProfiler — dərin təhlil üçün güclü kommersiya aləti.
Profiler vasitəsilə, məsələn, proqramın zamanın 80%‑ni read() və ya write() metodunda keçirdiyini görmək və nəticə çıxarmaq olar.
Əməliyyatların icra vaxtının loqlaşdırılması
Bəzən ayrı əməliyyatların icra vaxtını sadəcə “ölçmək” kifayətdir:
long start = System.currentTimeMillis();
processFile("bigfile.txt");
long end = System.currentTimeMillis();
System.out.println("Emal vaxtı: " + (end - start) + " ms");
Əgər emal şübhəli dərəcədə çox vaxt aparırsa — IO baş verən yeri axtarın. Ölçməni utilitə çıxarmaq rahatdır, məsələn, çağırışları taymer‑metodla bürümək.
Kodu səmərəsiz patternlərə görə təhlil etmək
Aşağıdakı “qırmızı bayraqlara” diqqət yetirin:
- Fayl oxu və ya yazmasının baş verdiyi daxil edilmiş dövrlər.
- read() və ya write() metodlarından buffersiz istifadə.
- Faylın hər iterasiyada açılıb bağlanması.
- Loqların “isti” kod hissəsində sinxron rejimdə yazılması.
Maraqlı fakt
Böyük layihələrdə bəzən ayrıca “loqlar üçün loq‑fayllar” yaradılır — hansı kod hissəsinin daha çox loq yazdığını və sistemi ləngitdiyini anlamaq üçün.
4. Aparat amillərinin təsiri
Hətta ideal kod yazsanız belə, “dəmir” sizi yarı yolda qoya bilər. Gəlin müxtəlif qurğu tiplərinin IO sürətinə necə təsir etdiyini araşdıraq.
SSD vs HDD
- HDD (sərt disk): yavaşdır, xüsusən verilənlərə təsadüfi çıxışda. Böyük faylların ardıcıl oxunması ilə yaxşı işləyir, lakin tez‑tez kiçik əməliyyatlarda “fikrə dalır”.
- SSD (solid‑state disk): HDD‑dən onlarla dəfə sürətlidir, xüsusən təsadüfi çıxış və paralel əməliyyatlarda. Amma SSD belə “operativ yaddaş”dan geri qalır.
Şəbəkə sürəti
Fayllar şəbəkə diskində və ya buludda saxlanılırsa, ötürmə sürəti şəbəkənin buraxılış qabiliyyətindən, gecikmələrdən və bəzən internetdəki “tıxaclardan” asılıdır. Serveriniz qonşu otaqda olsa belə, şəbəkə diski darboğaza çevrilə bilər.
Fayl sistemi
Müxtəlif fayl sistemləri (NTFS, ext4, FAT32, exFAT) böyük fayllarla, çoxlu sayda kiçik fayllarla, paralel çıxışla fərqli cür öhdəsindən gəlir. Bəzən fayl sistemini dəyişmək kodu dəyişmədən performansı artırır.
Keş və bufer ölçüsü
Əməliyyat sistemi və disklər işi sürətləndirmək üçün tez‑tez öz keşlərindən istifadə edirlər. Keş kiçikdirsə, məlumat çoxdursa — əməliyyatların bir hissəsi keşdən “kənara düşəcək” və sürət azalacaq.
5. Təcrübə: faylı buferlə və buffersiz oxuma sürətinin müqayisəsi
Sözümüz havada qalmasın deyə kiçik bir eksperiment aparaq. Faylı oxumağın iki yolunu müqayisə edək: bayt‑bayt və buferlə.
Bayt-bayt oxuma (yavaşdır)
import java.io.FileInputStream;
import java.io.IOException;
public class SlowReadExample {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
try (FileInputStream in = new FileInputStream("bigfile.txt")) {
int b;
while ((b = in.read()) != -1) {
// Sadəcə oxuyuruq, heç nə etmirik
}
}
long end = System.currentTimeMillis();
System.out.println("Bayt-bayt oxuma: " + (end - start) + " ms");
}
}
Bufferlə oxuma (sürətlidir)
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FastReadExample {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("bigfile.txt"))) {
int b;
while ((b = in.read()) != -1) {
// Sadəcə oxuyuruq, heç nə etmirik
}
}
long end = System.currentTimeMillis();
System.out.println("Bufferlə oxuma: " + (end - start) + " ms");
}
}
Nəticə: Hətta kiçik fayllarda fərq bir neçə dəfə, böyük fayllarda isə onlarla və yüzlərlə dəfə ola bilər! Özünüz yoxlayın (amma çay hazırlayın — birinci variant çox vaxt apara bilər).
6. Cədvəl: sürətlərin müqayisəsi
| Oxuma üsulu | Fayl ölçüsü | Vaxt (təxmini) |
|---|---|---|
| Bayt-bayt | 100 MB | 30–60 saniyə |
| Bufferlə (8 KB) | 100 MB | 1–2 saniyə |
| Bufferlə (64 KB) | 100 MB | 0,7–1,5 saniyə |
Göstəricilər təxmindir, amma fərqlərin miqyası təsirlidir!
7. Vizual sxem: buferləşdirmə niyə IO‑nu sürətləndirir
flowchart LR
A[Sizin kodunuz] --> B[Yaddaşda buffer]
B --> C[Əməliyyat sistemi]
C --> D[Fayl sistemi]
D --> E[Disk/Şəbəkə]
- Buffersiz: diskə hər müraciət — ayrı əməliyyatdır.
- Bufferlə: yaddaşda çox əməliyyat, diskə bir əməliyyat.
8. IO və performansla işləyərkən tipik səhvlər
Səhv №1: Bayt və ya simvol üzrə oxuma/yazma.
Bu klassikadır. Tapşırıq sadə görünsə belə, həmişə buferləşdirmədən istifadə edin (BufferedInputStream, BufferedReader və s.).
Səhv №2: İcra vaxtını nəzərə almamaq.
Kodu işləmə vaxtını ölçməsəniz, ləngimənin harada olduğunu bilməyəcəksiniz. Nöqtəvi ölçmələr üçün System.currentTimeMillis() və ya daha dəqiq profilerlər kömək edəcək.
Səhv №3: Faylların dövrədə açılıb bağlanması.
Hər bir açma/bağlama — bahalı əməliyyatdır. Faylı bir dəfə açın, onunla işləyin, sonra bağlayın.
Səhv №4: Aparat məhdudiyyətlərini görməzdən gəlmək.
HDD‑dən SSD sürəti “çıxarmağa” çalışmayın. Bir faylla işləmək üçün yüzlərlə axın işə salmayın: disk öhdəsindən gəlməyəcək.
Səhv №5: “İsti” kod hissəsində loqların sinxron yazılması.
Loqlaşdırma — IO‑dur. Əgər bu kritik yerlərdə icra olunursa, proqram yavaşıyacaq. Asinxron loqlaşdırmanı və buferləşdirməni nəzərdən keçirin.
GO TO FULL VERSION