Wyniki testów wydajnościowych do projektu FileGuard Rafał Wijata Sposób wykonania testów: Wyniki testów są podane w taktach procesora. Prędkość procesora, na którym zostały wykonane testy była równa 572476000Hz. Do testów użylem prostej proceury zczytującej licznik taktów procesora od włączenia, wziętej z http://www.eecs.harvard.edu/vino/perf/hbench/. Proceura jest jednym poleceniem asemblera, i wymaga co najmniej procesora Pentium. Wygląda tak: #define read_cycle_counter(res_p) \ __asm __volatile ( \ ".byte 0xf; .byte 0x31 # RDTSC instruction \n\ movl %%edx, %1 # High order 32 bits \n\ movl %%eax, %0 # Low order 32 bits" \ : "=g" (*(int *)(res_p)), "=g" (*(((int *)res_p)+1)) \ : /* no input regs */ \ : "eax", "edx") Funkcja open() W funkcji dentry_open() (fs/open.c) umieściłem kawałek kodu sprawdzający czas trwania wywołania funkcji open() specyficznej dla danego pliku - czyli tej z file_operations. Ten czas był logowany do pliku za pomocą kernel logera (klogd). Następnie po zadanej liczbie powtórzeń otwarcia tego samego pliku, log systemowy był analizowany pod kątem wyników tylko interesujących nas plików. Pliki były rozpoznawane po numerze metryczki pliku. Oto wyniki, wyniki podane po znaku /, są wynikami po zignorowaniu 10 najlepszych i 10 najgorszych wyników, wyniki w nawiasach są wynikami w mikrosekundach (1/1 000 000 sek) Open(), nazwa absolutna, 10000 prób +------------+-----------------+--------------------+------------------------+----------------+ | | nie pilnowany | bez funkcji open() | pusta funkcja open() | brak strażnika | +------------+-----------------+--------------------+------------------------+----------------+ | minimalny | 83 | 280 | 290 | 227 | +------------+-----------------+--------------------+------------------------+----------------+ | maksymalny | 232/114 | 693/338 | 11980/374 | 716/247 | +------------+-----------------+--------------------+------------------------+----------------+ | średni | 92(0.16) | 319(0.55) | 319(0.55) | 228(0.39) | +------------+-----------------+--------------------+------------------------+----------------+ Z powyższego wynika fakt, że ładowanie modułu do pamięci nie ma większego znaczenia, szczególnie jest był on już wcześniej załadowany i nie musi być wczytywany z dysku. Można to było zaobserwować przy ponowieniu testu. Przyrost czasu wykonywania funkcji jest razy 3, być może to dużo, ale gdy się porówna czasy, które w obu przypadkach są poniżej 1 mikrosekundy, ten narzut czasowy wydaje się jak najbardziej akceptowalny. Jest to tak naprawdę narzut podwójnego wywołania funkcji, wyszukania odpowiednich struktur dla aktualnego strażnika i kilku sprawdzeń warunku [if ()]. Open(), nazwa absolutna, 5000 prób, strażnik z przestrzeni użytkownika +-------------+---------------------+-------------------------+----------------------------+ | | bez funkcji open() | z pustą funkcją open() | open() z zakładaniem sesji | +-------------+---------------------+-------------------------+----------------------------+ | minimalny | 2350 | 18800 | 3000 (?) | +-------------+---------------------+-------------------------+----------------------------+ | maksymalny | 3100/2500 | 74000/42300 | 5782000/128000 | +-------------+---------------------+-------------------------+----------------------------+ | średni | 2450(4.2) | 21100(36.8) | 47000(82) | +-------------+---------------------+-------------------------+----------------------------+ Tym razem narzut na otwarcie pliku jest duży, i dla niektórych osób może być nawet nieakceptowalny. Jednak jest to normalne następstwo wymiany danych między procesem i jądrem. Każde otwarcie pliku wymaga przesłania co najmniej dwóch komunikatów (żądanie - odpowiedź), a przesyłanie danych przez barierę jądro/użytkownik kosztuje. Kosztuje również fakt, że aby całość mogła dojść do skutku, należy kilka razy przełaczyć kontekst. Jest tu dużo więcej do zrobienia niż proste wywołanie funkcji i dodanie kilku linijek kodu. Tym razem do zadania zaprzęgamy cały system operacyjny. Całość jest niezbędna, i sądze, że trudno będzie znaleźć optymalniejszy sposób. Jeśli ktoś faktycznie potrzebuje strażników w trybie użytkownika, to opóźnienia są akceptowalne, choć duże. Jeśli jednak zadanie można wykonać w trybie jądra, to polecałbym napisanie go w module. Dla lepszego poparcia tej tezy wykonałem testy czytania (read()) tego samego pliku, raz pilnowanego przez moduł, raz przez proces. Sposób wykonania pomiarów był taki sam jak dla funkcji open() Strażnik jako wynik funkcji read() dawał aktualny czas systemowy (to samo co polecenie date). Read(), 5000 prób, czas pobrany z sys_time() + konwersja do ciagu znaków +-------------+-------------+-------------+ | | moduł | proces | +-------------+-------------+-------------+ | minimalny | 1400/3100 | 31000/34000 | +-------------+-------------+-------------+ | maksymalny | 17000/5100 | 34000 | +-------------+-------------+-------------+ | średni | 3900(6.8) | 34000(59.3) | +-------------+-------------+-------------+ Tym razem wyniki różnią się o rząd wielkości. W przypadku napisania tej samej funkcjonalności jako moduł jądra można dużo zyskać na obciążeniu systemu. Na koniec zrobiłem jeszcze test dla modułu crypt. Jest to proste szyfrowanie danych za pomocą mechanizmu XOR. Każdy bajt pliku zanim zostanie zwrócony użytkownikowi jest XORowany z wybraną liczbą. Tak samo dzieje się przy zapisie danych. Ten moduł również napisałem w wersji modułu i procesu. Testowane były tylko te wywołania funkcji read(), ktore zwracaly 4Kbajty danych. Kolumna brak, oznacza czytanie bez odszyfrowywania danych. Read(), 5000 prób, moduł cryp, 4KB +-------------+--------------+--------------+---------------------+ | | brak | moduł | proces | +-------------+--------------+--------------+---------------------+ | minimalny | 3090/3180 | 19600/20600 | 6300/12000 | +-------------+--------------+--------------+---------------------+ | maksymalny | 22000/10800 | 45500/37800 | 30mln/5.7mln(9956!) | +-------------+--------------+--------------+---------------------+ | średni | 5560(9.7) | 23200(40.5) | 55000(96) | +-------------+--------------+--------------+---------------------+ Test ten dowodzi jak nieprzewidywalnie duży może być czas oczekiwania na odpowiedź w przypadku procesu. Natomiast fakt, że bez strażnika jest szybciej nic nie wnosi, ponieważ opóźnienia są generowane głownie przez samą funkcję read(). Narzut samego mechanizmu jest prawdopodobnie taki sam jak przy funkcji open(). Fakt, że tym razem nie ma różnic rzędu wielkości dla wyników srednich, można wytłumaczyć tym, że w poprzednim teście obliczenie daty systemowej kosztuje bardzo mało czasu, więc wieszość czasu wykonania funkcji jest poświęcana na komunikację moduł - proces. Swiadczą o tym niewiele wieksze czasy dla sredniego wykonania funkcji przez proces, i stosunkowo wyraźnie większe czasy dla modułu jądra. W obu przypadkach narzut jest około 30000 taktów zegara czyli jakies 50 mikrosekund. Czyli ostateczny wniosek jest taki: - dla małych funkcji (niewiele robiących) bardzo się nie opłaci umieszczać strażnika w przestrzeni użytkownika (bardzo duże narzuty na obsługę samego strażnika) - dla dużych funkcji (zajmujących sporo czasu) może się opłacić wynieść funkcjonalność poza jądro systemu, zyskując duże możliwości jakie daje pisanie programów dla przestrzeni użytkownika. Narzut czasowy dla funkcji wykonującej jedną operacje na każdym bajcie danych jest razy dwa. Dla bardziej skomplikowanych funkcji, będzie jeszcze mniejszy, i zawsze około 50 mikrosekund (dla mojego procesora). Wynika to z faktu, że narzut na wywołanie funkcji z procesu-strażnika kosztuje właściwie zawsze tyle samo (20tyś taktów dla open, 30tyś dla read). Jest jednak zagrożenie, że przy bardzo dużych/(czasochłonnych) funkcjach, znów może przestać się opłacać wynosić strażników poza jądro. Powodem jest fakt, iż kod jądra wykonuje sie szybciej (brak opóźnień związanych z szeregowaniem procesów). Ale oczywiście trzeba będzie sie zastanowić czy blokowanie systemu operacyjnego na tak dlugie czasy (w czasie wykonywania funkcji strażnika z modułu, proces w imieniu, którego była wywołana funkcja, nie może zostać wywłaszczony do jej zakończenia), nie spowoduje ogólnego pogorszenia stabilności i funkcjonalności systemu.