Dwie linijki kodu i +5% IOPS: jak przesunięcie memset przyspieszyło SSD w Linuksie
Czasem nie trzeba pisać nowego sterownika ani przepisywać całego podsystemu, żeby przyspieszyć jądro. Wystarczy przenieść w odpowiednie miejsce kilka linijek, które przez lata robiły zbędną robotę. Dokładnie to zrobił Fengnan Chang z ByteDance: przesunął wywołanie memset() w warstwie iomap i wycisnął około 5% więcej IOPS na ext4 i XFS. Zmiana trafia do jądra Linux 7.2.
Co to jest iomap
iomap to wspólna warstwa mapowania we/wy w jądrze. Systemy plików takie jak ext4, XFS czy exFAT używają jej, żeby przełożyć offset w pliku na konkretne miejsce na urządzeniu blokowym. Sercem tej warstwy jest pętla iomap_iter(): dla zadanego zakresu pliku prosi system plików o wypełnienie struktury struct iomap opisującej kolejny ciągły fragment, wykonuje operację we/wy, po czym pyta o następny fragment, aż skończy się zakres.
Co właściwie zmieniono
Na początku każdej iteracji kod czyścił struktury iomap i srcmap przez memset(..., 0, sizeof(...)). Robił to bezwarunkowo, również po ostatniej iteracji, kiedy nie ma już nic do zmapowania, a wywołujący i tak porzuca iterator.
Wcześniej jedna funkcja pomocnicza robiła dwie rzeczy naraz: sprzątała batch folio i zerowała struktury:
static inline void iomap_iter_reset_iomap(struct iomap_iter *iter)
{
if (iter->iomap.flags & IOMAP_F_FOLIO_BATCH) {
folio_batch_release(iter->fbatch);
folio_batch_reinit(iter->fbatch);
iter->iomap.flags &= ~IOMAP_F_FOLIO_BATCH;
}
iter->status = 0;
memset(&iter->iomap, 0, sizeof(iter->iomap));
memset(&iter->srcmap, 0, sizeof(iter->srcmap));
}
Po zmianie sprzątanie batcha zostało oddzielone od zerowania, a oba memset() przeniesiono tak, żeby wykonywały się tylko wtedy, gdy iteracja trwa dalej. Kiedy iomap_iter() kończy pracę i zwraca wartość <= 0, zerowanie po prostu się nie odbywa, bo nikomu już nie jest potrzebne.
Dlaczego dwie linijki dają aż 5%
Na nowoczesnym dysku NVMe pojedyncza operacja 4k jest mikroskopijna, a wąskim gardłem staje się procesor i przepustowość zapisu do pamięci, nie sam dysk. Przy obciążeniu rzędu setek tysięcy czy milionów operacji na sekundę każdy zbędny memset dwóch struktur, wykonywany na każdą operację, sumuje się w zauważalny narzut. Usunięcie tego marnotrawstwa z gorącej ścieżki to właśnie te kilka procent.
Najlepiej widać to w syntetycznym, ale realistycznym scenariuszu: losowy odczyt bloków 4k przez io_uring w trybie polling, na szybkim NVMe. To workload, w którym liczy się każdy cykl, a różnica około 5% IOPS jest dobrze mierzalna i powtarzalna zarówno na ext4, jak i na XFS.
Kto i gdzie
Patch napisał Fengnan Chang z ByteDance, a do drzewa VFS/iomap przyjął go Christian Brauner. Commit bd6a1379a41a siedzi w pliku fs/iomap/iter.c i wejdzie wraz z jądrem Linux 7.2, spodziewanym mniej więcej w sierpniu 2026. Media policzyły zmianę różnie, jako dwie albo trzy linijki, zależnie od tego, jak traktować przeniesione wiersze. Sedno jest jedno: to przesunięcie istniejącego kodu, a nie nowy algorytm.
Wniosek
To dobry przykład na to, że w dojrzałym kodzie wciąż siedzą takie smaczki. Defensywny memset na każdej iteracji wyglądał niewinnie i przez lata nikomu nie przeszkadzał, dopóki ktoś nie zaprofilował gorącej ścieżki pod kątem ekstremalnych IOPS. Jeśli sam optymalizujesz, zacznij od profilera i od prostego pytania: czy to, co robisz w pętli, na pewno musi się wykonać za każdym razem.
Źródła: Phoronix, commit na git.kernel.org, wątek na lore.kernel.org, XDA, Linux Magazine