CodeGym /Kursy /C# SELF /Problem wydajności wejścia-wyjścia (

Problem wydajności wejścia-wyjścia ( I/O)

C# SELF
Poziom 41 , Lekcja 0
Dostępny

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.20.5 nanosekund
  • Odczyt z RAM: 10100 nanosekund
  • Odczyt z SSD: 50100 mikrosekund
  • Odczyt z HDD: 510 milisekund
  • Zapytanie sieciowe: 10100 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 48 KB i więcej, zależnie od profilu obciążenia) i przetwarzanie strumieniowe.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION