1. Giriş
Fərz edin ki, siz qələm ilə məktub yazırsınız, amma yalnız çox kiçik bir silgi qalıb və o bir söz üçün yetərlidir. Silməsəniz davam edə bilməzsiniz. Sizin bir dəfə üçün daha çox silmək imkanınız olsaydı, yaxşı olardı, düzdür? Bufferlənmə təxminən belə "silgi-topu" kimidir: o, kiçik-kiçik parçalarda yox, böyük hissələrlə işləməyə imkan verir.
Proqramlaşdırmada bufferlənmə — diskə oxuma və yazma əməliyyatı baş verməzdən əvvəl məlumatların müvəqqəti olaraq RAM-da ("buffer"da) saxlanılmasıdır. Bu, paltar səbəti kimidir: corabları həftə ərzində ora yığır və bir dəfə yuyursunuz, tək-tək yox. Nəticədə vaxt (və resurslar!) daha az sərf olunur.
I/O əməliyyatları
Sərt diskə, SSD-yə və ya flash-a müraciətlər — prosessor üçün ən yavaş əməliyyatlardan biridir. Operativ yaddaş (RAM) təxminən min dəfə daha sürətlidir! Buna görə hər Write və ya Read çağırışında məlumatlar dərhal diskə gəlsə, proqramınız köhnə 512 MB RAM-li Windows XP ocaq kimi yavaşlayardı.
Bufferlənmə fiziki diskə real müraciətlərin sayını azaltmaq və performansı artırmaq üçün yaradılıb.
2. I/O zamanı bufferlənmə necə işləyir
Buffer — müvəqqəti məlumatların yerləşdiyi RAM parçasıdır. İş prinsipi belədir:
Fayla yazarkən:
- Sizin kod bir neçə dəfə Write() çağırır.
- Bütün məlumatlar əvvəlcə bufferə yığılır.
- Buffer dolduqda və ya əməliyyat bitməli olduqda, bufferin məzmunu bir böyük kəsiklə diskə yazılır.
Fayldan oxuyarkən:
- Siz bir qədər məlumat oxumağı xahiş edirsiniz.
- Sistem fayldan dərhal böyük bir parçanı oxuyub bufferə qoyur.
- Növbəti çağırışda məlumatlar artıq bufferdə olur və diskə müraciət etmək lazım deyil.
Nəticədə:
- Diskə müraciətlərin sayı azalır.
- Oxuma və yazma daha sürətli gedir.
3. .NET-də bufferlənmə: harada tətbiq olunur
.NET-də əksər I/O stream-lər default üzrə bufferlənmədən istifadə edir:
- StreamWriter / StreamReader
- FileStream
- BufferedStream
- Hətta Console.Out!
Amma buffer ölçüsünü və istifadəsini konfiqurasiya etmək olar (və tez-tez lazımdır).
Niyə bu vacibdir?
Böyük həcmdə məlumat yazıb-oxuyanda (log-lar, DB, multimedia emalı) düzgün konfiqurasiya edilmiş buffer proqramınızı dəfələrlə sürətləndirə bilər. Bufferlənmə olmasa, hətta güclü CPU da məlumat gözləməkdən "esnəyə" bilər.
4. Bufferlənmə olmadan sadə nümunə
İlk öncə faylı hər baytı ayrıca yazsaq necə olardı (belə etməyin!):
string path = "slowfile.txt";
using (FileStream fs = new FileStream(path, FileMode.Create))
{
for (int i = 0; i < 100000; i++)
{
fs.WriteByte((byte)'A'); // Hər dəfə 1 bayt yazırıq!
}
}
Console.WriteLine("Hazır! (amma çox yavaş)");
Bu nümunədə 100 000 real disk müraciəti olur! Hətta SSD də deyər: "nə üçün mənə belə edirsən?.."
Hansı buffer ölçüsünü seçmək?
Bu işdən asılıdır:
- .NET-də default olaraq daxili buffer üçün tez-tez 4 KB və ya 8 KB istifadə olunur.
- Böyük fayllar (100 MB və daha çox) üçün 16 KB, 64 KB və ya hətta 1 MB buffer istifadə etmək olar.
- Çox böyük buffer də pisdir: RAM itkisidir və bəzən faydası yoxdur.
Qızıl qayda: təxmin etməyin, ölçün (profiling)! Bəzən bufferin artırılması işi 10 dəfə sürətləndirir, bəzən demək olar ki, heç təsir etmir.
5. Bufferlənmə: I/O-nu sürətləndiririk
"Bufferlənmə" sözü fayllar kontekstində topdan alışa çox bənzəyir. Bananları tək-tək daşımırıq, qutunu götürürük.
.NET-də demək olar ki, bütün I/O stream-lər default-da bufferlənmədən istifadə edir, amma istisnalar var: məsələn, siz özünüz FileStream-u və onun parametrlərini idarə etdikdə, və ya çox kiçik buffer və ya buffer olmadan işləyərkən.
Bufferlənmə I/O-nu necə sürətləndirir?
Böyük bloklarla oxuyub-yazanda əməliyyat sistemi işi optimallaşdıra bilər: bir neçə əməliyyatı birləşdirmək, diskə müraciətlərin sayını azaltmaq, növbəti fayl hissəsini öncədən yükləmək (prefetching).
İllüstrasiya: Fayl oxuma — bufferlə və buffersiz
| Variant | Müraciət sayı | Vaxt, şərti |
|---|---|---|
| 1 baytla oxuma | 10 000 000 | 10 dəqiqə |
| 4096 baytla oxuma | 2 500 | 5 saniyə |
Qiymətlər təxmini olsa da, fərq göz qabağındadır!
6. FileStream və .NET-də bufferlənmə
FileStream sinfi — fayllarla işin ən aşağı səviyyəli vasitəsidir, maksimal nəzarət verir, amma diqqət tələb edir. Onun constructor-u buffer ölçüsünü təyin etməyə imkan verir:
// FileMode.Open: mövcud faylı açırıq
// FileAccess.Read: oxuyuruq
// FileShare.Read: başqalarına oxumağa icazə veririk
// bufferSize: buffer ölçüsü baytla
var fs = new FileStream("bigfile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 8192)
// Faylla daha sürətli işləyirik
Default olaraq FileStream 4096 bayt ölçüsündə buffer istifadə edə bilər, amma fayl böyükdürsə siz daha böyük dəyər təyin edə bilərsiniz (məsələn, 16KB, 64KB və ya 1MB).
Məsləhət: çox böyük buffer qoymayın
Çox böyük buffer çox RAM sərf edir və sürət artımı verməyə bilər — müasir OS-lər də blokları cache-ləməkdə yaxşıdır. Çoxluq üçün optimal buffer adətən 4KB-dan 128KB-a qədərdir.
Performans problemi nə vaxt daha aydın görünür?
- Çox sayda kiçik faylları surətli kopyalayanda (məsələn, foto faylları).
- Böyük faylları kiçik hissələrlə oxuyanda (1 bayt, və ya bufferlənmə olmadan 1 sətri oxumaq).
- Bir anda çox fayl açanda (məsələn, skript bütün log fayllarında axtarış edir).
- Şəbəkə paylaşımları ilə işləyərkən (latensiya + şəbəkə yüklənməsi).
- Kütləvi əməliyyatlarda: arxivləşdirmə, backup, import/export.
7. Faylı "köhnə üsulla" və "sürətlə" kopyalayırıq
Gəlin real həyatda proqramın sürətinə təsir edən yanaşmaları müqayisə edək.
Çox yavaş:
// ❌ Pis — 1 baytla oxuyub yazırıq
using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);
int b;
while ((b = source.ReadByte()) != -1)
{
dest.WriteByte((byte)b);
}
Əhəmiyyətli dərəcədə sürətli:
// ✅ Yaxşı — böyük bloklarla oxuyub yazırıq
byte[] buffer = new byte[16 * 1024]; // 16 KB
int bytesRead;
using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
{
dest.Write(buffer, 0, bytesRead);
}
Super-sürətli (və sadə):
// 🚀 File.Copy — içində optimallaşdırılmış bufferlənmədən istifadə edir
File.Copy("source.bin", "dest.bin");
Niyə bloklarla işləməyi bilməliyik? Çünki bəzən sadəcə kopyalamaq yox, fayl məzmununu eyni vaxtda emal etmək lazım gəlir (məsələn, sətirləri filtr etmək, şifrələmək, cəmləri hesablamaq).
İş vaxtının müqayisəsi
Təcrübəni göstərmək üçün cədvəl (təxmini qiymətlər, amma fərqi göstərir):
| Metod | Fayl ölçüsü 1GB | Vaxt (təxmini) |
|---|---|---|
| 1 baytla | 1GB | ~30 dəqiqə |
| 4KB bloklarla | 1GB | ~20 saniyə |
| Daxili File.Copy | 1GB | ~5 saniyə |
Bu testi vacib fayllarda və sistem SSD-də keçirməyin — yoxsa disk və sizin əsəbləriniz arasında "sülh müqaviləsi" pozula bilər.
8. Faydalı nüanslar
"Yavaşıqlıqlar" başqa haradan gəlir?
Diskin fizikasından və yanlış blok ölçüsündən başqa, proqramın yavaş işləməsinə səbəb olan digər amillər də var:
- Faylları "uçuşda" açıb bağlamaq (daha yaxşısı bir dəfə açıb işləmək, sonra bağlamaq).
- I/O əməliyyatlarını əsas thread-də icra etmək (UI-ni yavaşladır, əgər Windows Forms/WPF/MAUI istifadə edirsinizsə).
- Yaddaş çatışmazlığı: əməliyyat sistemi səhifələri RAM və disk arasında swap etməyə başlayır — ikiqat yavaşlama.
- Antiviruslar, Windows axtarış indeksləri, fon proseslər — bəzən onlar fayla "maraq göstərərək" görünmədən işi yavaşladır.
Praktik tətbiq
Real layihədə: əgər siz fayl emalı (log, media, sənədlər), cloud storage xidməti, hesabat toplayıcı, backup hazırlayırsınızsa — mütləq "sürətli I/O" sualı ilə qarşılaşacaqsınız. Bufferlənmə, böyük bloklar və hazır alətlərdən (məsələn, File.Copy) istifadə fayllarla işləmənin əsaslarıdır.
Müsahibədə: sizdən soruşa bilərlər — "Niyə faylı 1 baytla oxumaq antipattern-dir?" Və ya mass kopyalama necə sürətləndirilir. Bufferlənmə haqqında biliklər sizə inam verər, nümunələr və həllər gətirə biləcəksiniz.
İşdə: bəzən hər şey yaxşı işləyir, amma sonra SSD-dən şəbəkə diskə keçəndə və ya OS yenilənəndə iş yavaşlayır. I/O-nun iş prinsipini bilsəniz, səbəbi tez tapıb optimallaşdırma təklif edə bilərsiz.
I/O sürətləndirmək üçün faydalı məsləhətlər
- Həmişə bufferli I/O-dan istifadə edin (BufferedStream, FileStream içində bufferin konfiqurasiyası).
- 4KB-dan böyük bloklarla oxuyun və yazın.
- Faylları açıb bağlama sayını minimallaşdırın — bir dəfə açın, işləyin, sonra bağlayın.
- Mümkünsə asinxron metodlardan istifadə edin (ReadAsync, WriteAsync) — onlar I/O-ni sürətləndirmir, amma tətbiqinizin "gözləməməsinə" imkan verir.
- Əgər çox böyük fayllarla işləyirsinizsə — Memory<T>, Span<T> tiplərini öyrənin.
- Daxili funksiyalara etibar edin: File.Copy, File.Move və s. — bunlar sistem çağırışlarından maksimum istifadə edirlər.
.NET siniflərində bufferlənmə
Gəlin kiçik cədvələ baxaq — kim necə bufferləyir:
| Sinif | Default bufferləmə | Konfiqurasiya oluna bilən buffer |
|---|---|---|
|
Bəli | Bəli (constructor ilə) |
|
Bəli | Bəli (constructor vasitəsilə) |
|
Bəli | Bəli |
|
Xeyr (sadəcə wrapper) | Bəli |
|
Bəli | Xeyr |
.NET-də demək olar ki, heç nə buffer olmadan işləməz — çünki bu səmərəli deyil.
Bufferi əllə "flush" etmək lazım olanda
Bəzən məlumat bufferdə qalır və siz istəyirsiniz ki, o, dərhal diskə yazılsın. Məsələn, log yazırsınız və proqram qəfil bağlanır. Nə etmək olar?
Belə hallarda .Flush() metodu çağırılır:
using var fs = new FileStream("log.txt", FileMode.Append);
using var writer = new StreamWriter(fs);
writer.WriteLine("Nə isə vacib");
writer.Flush(); // Bufferi dərhal diskə atmaq
Flush — belə demək olar, "Hə, rəflərə yığılıb, indi hamısını saxla!". Bütün qeyri-saxlanmış məlumatlar həqiqətən yazılacaq.
9. Praktika sualları: tipik səhvlər və nüanslar
Yeni başlayanların ən yayğın məyusluqlarından biri: "Mən fayla yazdım, amma orda boşdur?!" Səbəb — məlumatlar hələ bufferdən "flush" olunmayıb. Proqram güclü bufferləşdirmə edir və hər zaman dərhal yazmır. Belə vəziyyətdən Flush() çağırmaqla və ya stream-i bağlamaq (Dispose()) ilə qaçınmaq olar.
Başqa problem: siz böyük faylı yazmaq üçün nəhəng buffer ayırdınız, amma sistemdə az yaddaş var — proqram yavaşlamağa başlayır. Çox böyük buffer həmişə yaxşı deyil, həddindən artıq istifadə etməyin.
GO TO FULL VERSION