1. Dlaczego wejście-wyjście jest takie wolne?
„Wolność” wejścia-wyjścia (I/O) — jedno z wiecznych pytań. Nasz kod często „czeka” na dane dłużej, niż „myśli”. Rozgryźmy, dlaczego wizyty w „spiżarni” są powolne w porównaniu do pracy na „kuchni”.
Fizyczne ograniczenia sprzętu (Hardware Limitations)
Dyski talerzowe (HDD). Mechanika: talerze się kręcą, głowica się przesuwa. Trzeba czasu na przemieszczenie (seek time) i obrót — daje to dużą latencję.
Nośniki półprzewodnikowe (SSD). Szybsze niż HDD, brak mechaniki, ale zapisy i zarządzanie zużyciem komórek powodują, że operacje nie są natychmiastowe.
Sieć. Zależy od przepustowości i opóźnień, routerów itp. Nawet przy łączu gigabitowym odpowiedź z serwera zdalnego to milisekundy, a nie nanosekundy jak u CPU.
Narzuty systemu operacyjnego (OS Overhead)
- Sprawdzanie uprawnień. Czy proces może czytać/pisać plik?
- Znajdowanie danych pliku. System plików składa fragmenty.
- Buforowanie i cache. OS zarządza buforami dla efektywności.
- Przełączanie kontekstu. Gdy proces czeka na I/O, CPU się przełącza — to też kosztuje czas.
Wielkie rozdzielenie: prędkość CPU vs. prędkość I/O
- Operacja CPU: 0.2 – 0.5 nanosekund
- Odczyt z RAM: 10 – 100 nanosekund
- Odczyt z SSD: 50 – 100 mikrosekund
- Odczyt z HDD: 5 – 10 milisekund
- Zapytanie sieciowe: 10 – 100 milisekund i więcej
Różnica — olbrzymia. Jeśli „wołasz kuriera” dla każdej literki (I/O), będziesz pisać wolno, niezależnie od tego, jak szybki jest twój „pisarz” (CPU). Znacznie wydajniej jest pobierać dane blokami — zdaniami i akapitami.
2. Wewnątrz pliku: co się dzieje naprawdę?
Łańcuch wywołań przy pracy z plikiem wygląda tak:
flowchart TD
A[Twój kod C#] --> B[.NET FileStream]
B --> C[OS Windows / Linux / Mac]
C --> D[System plików: NTFS, ext4, APFS]
D --> E[Sterownik urządzenia]
E --> F[Nośnik fizyczny: HDD / SSD]
- Twój kod wywołuje np. File.ReadAllText(path).
- .NET pod maską używa FileStream, buforów i wywołań systemowych.
- OS zarządza cache i kolejkami.
- System plików znajduje bloki danych pliku.
- Sterownik komunikuje się z urządzeniem.
- Nośnik wykonuje operację fizyczną.
Każda warstwa dodaje narzut. Wąskie gardło najczęściej jest na poziomie nośnika fizycznego.
3. Przykład: wolny kod w praktyce
Antywzorzec: czytać plik po jednym bajcie przez ReadByte().
// ❌ Niewydajne czytanie pliku po bajtach
using FileStream fs = new FileStream("bigfile.txt", FileMode.Open);
int currentByte;
while ((currentByte = fs.ReadByte()) != -1)
{
// Robimy coś z bajtem
}
Dlaczego to złe? Każde wywołanie ReadByte() to osobne odwołanie do strumienia. Na dużych plikach wywołań jest miliony, a system traci czas na narzuty zamiast robić pożyteczną pracę.
Właściwie — czytać blokami:
// ✅ Wydajne czytanie pliku dużymi blokami
byte[] buffer = new byte[4096]; // 4 KB — standardowy rozmiar bufora
int bytesRead;
using FileStream fs = new FileStream("bigfile.txt", FileMode.Open);
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// Przetwarzamy otrzymany blok danych
}
Czytanie większymi porcjami pozwala OS i dyskowi efektywniej wykorzystać cache i kolejki — czas wykonania spada wielokrotnie.
4. Wpływ na realne aplikacje
Interfejs użytkownika (UI). Blokujące I/O „zamraża” okno. Ważne jest przenoszenie operacji do tła/asynchronicznie i nieblokowanie głównego wątku.
Serwery WWW i DB. Serwery ciągle czytają/piszą dane; wolny dysk lub sieć spowalniają cały serwis. Buforowanie, pula połączeń i asynchroniczne I/O — klucz do przepustowości.
Big Data. Przy gigabajtach/terabajtach każda nieefektywność się skaluje. Rozmiary bloków, dostęp sekwencyjny i przetwarzanie strumieniowe rozwiązują problem.
Gry. Długie ładowanie leveli/assetów to kwestia I/O. Poprawne pakowanie assetów i czytanie dużymi chunkami skraca czasy ładowania.
5. Typowe błędy początkujących
Częsty błąd — czytanie linia po linii lub bajt po bajcie dużych plików przez ReadByte albo za mały bufor (np. 256 bajtów). Liczba wywołań systemowych rośnie, a wydajność spada.
Jest też druga skrajność: próba wczytania całego ogromnego pliku przez File.ReadAllBytes — i spodziewany OutOfMemoryException. Lepiej trzymać „złoty środek”: rozsądne bloki (często 4–8 KB i więcej, zależnie od profilu obciążenia) i przetwarzanie strumieniowe.
GO TO FULL VERSION