1. Ale to już było
Kilkanaście miesięcy temu na Stack Overflow nijaki Shen Feng opisał w jaki sposób udało mu się uzyskać 1.6M bezczynnych połączeń TCP na jednym komputerze[2]. Napisany przez niego serwer w C został odpalony pod linuksem na maszynie z 16 GB pamięci RAM. Niezły wynik, ale pojawia się kilka pytań. Czy da się uzyskać podobny wynik używając języka programowania wyższego poziomu? I jeszcze jedno. Czy można jakoś zaprząc ten serwer do nietrywialnej pracy (pomijam echo) tak by obsługiwał on przychodzące requesty i generował odpowiedzi każdemu z ponad miliona klientów w czasie rzeczywistym? 2x TAK!
2. Boost Asio i 400K bezczynnych połączeń TCP
Na samym początku chce zaznaczyć ze moja maszynka ma 4x mniej RAM-u od maszynki Shen Fenga dlatego zadowole się "jedynie" 400K klientami. Pozostałe dane techniczne są nieistotne, system to Ubuntu 12.04. Kluczem do sukcesu Shenga było wykorzystanie mechanizmu epoll dostępnego od wersji 2.6 Linuksa. Epoll jest wydajnym zamiennikiem select-a i w odróżnieniu od niego doskonale radzi sobie nawet przy bardzo duzej liczbie obserwowanych deskryptorów plików. My również go użyjemy ale nie bezpośrednio. Serwer oraz generator klientów zostaną zaimplementowane w C++ z użyciem biblioteki Boost Asio. Ten rozdział traktuje po części jako prosty tutorial Boost Asio.
2.1 Generator klientów
Wszystko zacznie się od przygotowania systemu, gdyż potrzebujemy wsparcia z jego strony. Przed napisaniem czegokolwiek utworzymy kilkadziesiąt wirtualnych interfejsów sieciowych oraz zwiększymy liczbę portów per interfejs.
for i in `seq 21 54`; do sudo ifconfig eth0:$i 192.168.1.$i up ; done sudo sysctl -w net.ipv4.ip_local_port_range="1025 65535"
Dodatkowo zwiększymy limit na liczbę deskryptorów do niemal miliona. Trzeba zmodyfikować plik /etc/security/limits.conf:
* - nofile 999999
Statystyki:
cat /proc/net/sockstat
Nadszedł czas na Boost Asio. Programowanie z Asio jest nieco trikowe i wymaga obycia, ponieważ:
- napisany kod sprowadza się do małego zbioru handler-ów odpalanych asynchronicznie. Mamy odwrócony flow co może utrudnić zrozumienie kodu.
- łatwo jest popełnić subtelne błedy (póżniej pokaże jakie), trudno je wyłapać.
- biblioteka intensywnie używa szablonów co w połączeniu z boost::bind i innymi wynalazkami może skutkować rozwlekłymi i nieczytelnymi komunikatami gcc podczas kompilacji.
using namespace boost::asio; const int connections_per_IP = 12500; const int log_step = 10000; const int interfaces_number = 32; io_service m_io_service; ip::tcp::resolver m_resolver(m_io_service); std::vector<ip::tcp::socket> m_sockets; std::array<char, 4096> m_buffer; void read_handler(const boost::system::error_code &, std::size_t) { std::cout << "Read_handler: We got sth from server\n"; exit(1); } void connect_handler(const boost::system::error_code &p_error_code) { static int clients_number = 0; if (!p_error_code) { ++clients_number; if (clients_number % log_step == 0) std::cout << clients_number << " clients are connected\n"; m_sockets[clients_number-1].async_read_some( buffer(m_buffer), &read_handler); } else { std::cout << "Connect_handler: Some error occured: " << p_error_code.value() << "\n"; exit(1); } } void resolve_handler(const boost::system::error_code &p_error_code, ip::tcp::resolver::iterator p_endpoint_iterator, int p_interface_id) { if (!p_error_code) { for (int i = 0; i < connections_per_IP; i++) { int socket_index = connections_per_IP * p_interface_id + i; m_sockets[socket_index].async_connect(*p_endpoint_iterator, connect_handler); } } else { std::cout << "Resolve handler: Some error occured: " << p_error_code.value() << "\n"; exit(1); } } void resolve_all_connections() { for (int interface_id = 0; interface_id < interfaces_number; interface_id++) { for (int i = 0; i < connections_per_IP; i++) m_sockets.push_back(ip::tcp::socket(m_io_service)); std::string host = "192.168.1." + std::to_string(21 + interface_id); ip::tcp::resolver::query query(ip::tcp::tcp::v4(), host, "9090"); m_resolver.async_resolve(query, boost::bind( &resolve_handler, placeholders::error, placeholders::bytes_transferred, interface_id) ); } } int main() { try { resolve_all_connections(); m_io_service.run(); } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; } return 0; }Idea jest prosta.Chcemy nawiązać po connections_per_IP połączeń TCP na każdym z interfaces_number interfejsów sieciowych. Przed wejściem do pętli głównej obsługi komunikatów zaszytej w io_service::run, w resolve_all_connections tworzymy po jednym gnieździe per połączenie oraz przygotowujemy query z danymi adresu hosta. Ostatecznie w resolverz-e rejestrujemy resolve_handler wraz z danymi hosta. Handler ten odpalony zostanie po rozwiązaniu adresu. Zadaniem resolve_handler-a jest obliczenie indeksu właściwego gniazda oraz wywołania na nim async_connect wraz z przekazaniem handlera do obsługi połączenia - connect_handler-a.
Connect_handler - jaki jest każdy widzi. Spełnia dwie funkcje tzn. zlicza i wypisuje liczbę połączonych klientów oraz odbiera potencjalne dane wysłane przez serwer. Stąd dodatkowe async_read_some. Moją intencją było jedynie podtrzymywanie połaczenia przy życiu dlatego założyłem że serwer nigdy nic nie wyśle i read_handler nigdy nie zostanie odpalony. Tutaj asynchroniczne I/O pokazuje swoją siłe. Użycie synchronicznego read-a natychmiast zablokowałoby połączenie, a co z tym idzie całą aplikacje bo mamy tutaj tylko jeden wątek. Z tych samych względów odpadają wszelkie sleep-y i inne cuda.
Teraz kilka detali.
Po pierwsze każda z operacji na sieci jest wykonywana asynchronicznie o czym świadczy prefiks async. Dodatkowo w Asio każda z takich operacji może sie nie udać (np. z powodu przerwania połączenia) stąd handler-y powinny ZAWSZE sprawdzać error_code np.
if (!error_code) { ... }Po drugie należy zwracać baczną uwagę na czas życia obiektów przekazywanych do Asio. Wszelkie handlery są przekazywane przez wartość, ale już np. bufor w funkcji buffer przez referencje (tutaj m_buffer). Ostatnio straciłem mnóstwo czasu na wychwycenie błędu spowodowanego właśnie przekazaniem lokalnego bufora do środka jednej z funkcji asio.
Ostatni detal to pytanie do czytelnika.
Odpowiedź na końcu posta [3].
2.2 Skalowalny serwer echo
W poprzednim rozdziale pokazaliśmy w jaki sposób, bez uciekania się do socket-ów napisać program do nawiązywania połączeń z serwerem. Przy okazji zobaczylismy siłę drzemiącą w bibliotece Boost Asio. Okazuje sie ze stworzenie serwer-a jest tylko odrobine trudniejsze, co więcej nie będziemy musieli ręcznie zarządzać obserwowaniem deskryptor-ów za pomocą epoll-a. Klasa epoll_reactor[4] zrobi to za nas!
Hierarchia klas naszego serwera jest trywialna. Funkcjonalność została rozbita na 2 proste klasy: server i connection.
class server { public: server(short port); void run(); void remove_connection(std::shared_ptr<connection> p_connection); private: void start_accept(); void handle_accept(std::shared_ptrServer składa się z tcp::acceptor-a, io_service-u oraz dodatkowo zawiera zbiór aktywnych połaczeń w postaci obiektów klasy connection. Serwer nasłuchuje na porcie 9090 oraz za pomocą pary metod start_accept + handle_accept tworzy obiekt connection i oddelegowuje pracę do tego obiektu po czym kontynuuje nasłuch.new_connection, const boost::system::error_code& error); io_service m_io_service; tcp::acceptor m_acceptor; std::set<std::shared_ptr<connection>> m_connections; };
class connection : public std::enable_shared_from_this<connection> { public: connection(const connection&) = delete; connection& operator=(const connection&) = delete; connection(io_service& io_service, server &p_server); tcp::socket& socket(); void start(); private: void handle_read(const boost::system::error_code& error, size_t bytes_transferred); void handle_write(const boost::system::error_code& error); server &m_server; tcp::socket m_socket; const static int max_length = 1024; static char data[max_length]; };Klasa connection zawiera statyczny bufor na requesty i responsy, gniazdo oraz agreguje klasę server. OK. Póki co wszystko wydaje się być oczywiste i całkowicie naturalne. Serwer utrzymuje zbiór połączeń TCP, każde połączenie ma swój socket oraz wszystkie połączenia współdzielą jeden bufor wiadomości. Skąd jednak zależność klasy connection od klasy server? Czy server nie jest czasem właścicielem obiektów klasy connection?
Kluczem do zrozumienia są niewinnie wyglądające linijki z connection::handle_read i connection::handle_write:
m_server.remove_connection(shared_from_this());O to w tym wszystkim chodzi. Tylko klasa connection zna moment przerwania połączenia, zatem tylko obiekt klasy connection wie kiedy zakończyć swój żywot. Obiekt klasy nadrzędnej - serwer nie ma tym pojęcia. Można by tutaj wykonać samobója przez delete this. Z drugiej strony jest to nieelegackie tym bardziej że serwer nie zostałby o tym poinformowany. O wiele rozsądniej dodać zależność do serwera, przekazać mu this-a i liczyć na to że serwer zajmie się usunięciem połączenia. Tak też zrobiono, przy czym zamiast przekazywania gołego this-a przekazujemy this-a opakowanego w shared_ptr aby ustrzec się przed niebezpieczeństwem podwójnego ususnięcia obiektu [5].
Spójrzmy jeszcze raz na deklaracje connection:
class connection : public std::enable_shared_from_this<connection> { ... };Aby skorzystać z dobrodziejstw shared_from_this konieczne jest dziedziczenie po enable_shared_from_this. Warto zauważyć tutaj występowanie idiomu CRTP(Curiously recurring template pattern), który ma dość szerokie zastosowanie w C++[6].
Nasz prosty serwer jest w stanie utrzymać nawet 400K bezczynnych połączeń. Fajnie by było jednak wykonać jakąś, choćby minimalną pracę dla każdego z klientów. Uznałem, że całkiem prostym i jednocześnie ciekawym problemem będzie problem parsowania adresów URL. Połączony klient wyśle do serwera adres URL, ten zaś odeśle poszczególne fragmenty adresu takie jak scheme, userinfo, port, query itp. Oczywiście zadanie jest na tyle proste, że o wiele lepiej było by je wykonać po prostu lokalnie, jednak tak jak wspomniałem chce w ten sposób obciążyć serwer nietrywialną pracą jednocześnie utrzymując b. dużą liczbę połączeń.
3. Parser URL
Wielokrotnie zarówno w pracy jak i na różnej maści forach internetowych spotkałem się ze stwierdzeniem w stylu: "Nie ma sensu optymalizować tego kodu ponieważ obecnie kompilatory są tak dobre że zrobią to za Ciebie". Jest to bzdura totalna co udowodnie w dalszej części posta. W niniejszym rozdziale na przykładzie parsowania postaram się pokazać na czym polega i jak przebiega optymalizacja kodu oraz dlaczego diabeł tkwi w szczegółach.
Mamy adres URL o następującej gramatyce:
URL = scheme "://" [ userinfo "@" ] host [ ":" port ] "/" path [ "?" query ] [ "#" fragment ]
Zadanie polega na wyciągnięciu wszystkich zaznaczonych wyżej części tj. scheme, userinfo, host, port, path, query i fragment. Dodatkowo chcemy to zrobić WYDAJNIE tzn. tak szybko jak to tylko możliwe. Wkrótce zobaczymy że z poziomu C++ nie jest to tak proste jak się wydaje...
3.1 Naiwne parsowanie
Całkowicie naturalnym podejściem dla programisty C++ jest wykorzystanie w tym miejscu kombinacji std::string + funkcji z <algorithm>. Dorzucając do tego keyword auto dostajemy całkiem zwięzły i elegancki kod:
inline void naive_parse(const string &str_in) { input_str = str_in; scheme = userinfo = host = query = hash = port = ""; auto scheme_it = search(input_str.begin(), input_str.end(), separator_scheme.begin(), separator_scheme.end()); scheme.reserve(distance(input_str.begin(), scheme_it)); transform(input_str.begin(), scheme_it, back_inserter(scheme),ptr_funW skrócie. Wykorzystując find-a wielokrotnie skanujemy nasz napis wejściowy i wyszukujemy separatory rozdzielające interesujace nas fragmenty URL-a. Przy okazji w dwóch miejscach niezbędne jest użycie funkcji tolower tj. dla schematu i dla nazwy hosta. Warto zwrócić uwagę na możliwości jakie dają iteratory.(tolower)); if( scheme_it == input_str.end() ) return; advance(scheme_it, separator_scheme.length()); auto userinfo_it = find(scheme_it, input_str.end(), '@'); if (userinfo_it != input_str.end()) { userinfo.assign(scheme_it, userinfo_it); scheme_it = ++userinfo_it; } auto host_end_it = find(scheme_it, input_str.end(), '/'); host.reserve(distance(scheme_it, host_end_it)); transform(scheme_it, host_end_it, back_inserter(host),ptr_fun (tolower)); auto port_begin_it = find(host.begin(), host.end(), ':'); if (port_begin_it != host.end()) { port.assign(++port_begin_it, host.end()); } auto query_begin_it = find(host_end_it, input_str.end(), '?'); auto hash_begin_it = query_begin_it; if (query_begin_it != input_str.end()) { hash_begin_it = find(query_begin_it, input_str.end(), '#'); query.assign(++query_begin_it, hash_begin_it); } else { hash_begin_it = find(host_end_it, input_str.end(), '#'); } if (hash_begin_it == input_str.end()) return; hash.assign(++hash_begin_it, input_str.end()); }
Odpowiedź na pytanie na końcu posta[7].
Czy powyższy kod jest szybki? Zanim odpowiemy na to pytanie musimy przygotować benchmark który pozwoli nam stwierdzić na ile kod jest szybki/wolny. Rozmowa o szybkości bez dobrego testu wydajnościowego nie ma sensu. Przygotowany test jest na tyle prosty by nie wprowadzać dodatkowych narzutów do wywołań metody parse i jednocześnie na tyle złożony by wyeliminować możliwość usunięcia przez kompilator fragmentów parse przy agresywnych optymalizacjach. Test kompilowany z flagą -O2.
void simple_performance_test() { std::string str1("HTTP://stackoverflow.com/questions/2616011/parse-a.py?url=1"); std::string str2("scheme://username:password@subdomain.domain.tld:port/path/file-name.suffix?query-string#hash"); std::string str3("http://blogs.yandex.ru/cachedcopy.xml?f=3dadaafe9e5bfdb5fd2c22e7155441aa&i=91&m=http%3A" "%2F%2Fmewild.livejournal.com%2F24304.html&cat=theme&id=12843#fooobar123"); std::string str4("scheme://subdomain.domain.tld:0321/path/file-name.suffix?query-string"); uri u; int empty_hash_number = 0; for (size_t iteration_number = 100000; iteration_number > 0; iteration_number--) { u.parse(str1); empty_hash_number += (u.hash.empty()); u.parse(str2); empty_hash_number += (u.hash.empty()); u.parse(str3); empty_hash_number += (u.hash.empty()); u.parse(str4); empty_hash_number += (u.hash.empty()); } assert(empty_hash_number == 200000); }Wróćmy do zasadniczego pytania. Czy powyższy kod jest szybki? Niekoniecznie.
Przyczyny:
- główna przyczyna to ogromna liczba alokacji dynamicznych. Aby ją zmierzyć wystarczy użyć valgrinda:
sudo valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --num-callers=20 --track-fds=yes ./naive_uri_parser
Dla powyższego scenariusza testowego (tj. 400000 wywołań parse) valgrind zaraportował 3000000 alokacji na stercie! OMG, to prawie 8 dla każdego pojedynczego URL-a.
- liczba kopiowań też nie jest mała. Każde wywołanie transform wykonuje pod maską kod zbliżony do std::copy/memcpy + dodatkową pracę. Teoretycznie sytuację może nieco ratować mechanizm copy-on-write[8] który jest w stanie odwlec w czasie takie kopiowanie.
- find-y nie pozwalają na przerwanie wyszukiwania po natrafieniu na jakiś dodatkowy znak, np. dla
auto userinfo_it = find(scheme_it, input_str.end(), '@');
może się zdarzyć że userinfo w ogóle nie występuje w URL-u (jest to opcjonalny fragment). Oznacza to, że nie znajdziemy znaku @ a zatem będziemy przeszukiwać URL do samego końca, chociaż pojawienie się znaku / mogłoby przerwać wyszukiwanie.
- bardzo ważną kwestią jest to na ile kod jest cache-friendly. W tym momencie tego nie wiemy, zajmiemy się tym nieco poźniej po rozwiązaniu pierwszych 3 problemów.
Przede wszystkim należy mocno ograniczyć liczbę alokacji oraz liczbę kopiowań czyli trzeba zająć się dwoma pierwszymi punktami. Oto co należy zrobić:
- zrezygnować z std::string::operator= w []. Operacja tak prosta jak przypisanie pustego stringa wymusza zwolnienie pamięci starego bufora, zaalokowanie nowego bufora oraz skopiowanie "". Zamiast tego zaalokujmy pamięć jeden raz w konstruktorze a w naive_parse użyjmy clear/assign.
- zrezygnować całkowicie z reserve
- zastąpić transform + back_inserter przez assign + transform
- usunąć "zmniejszanie znaków" dla hosta
Zatrzymaliśmy się na 135ms. Zastanówmy się czy jest to dobry czas. Ile zajmuje przetwarzanie pojedynczego URL-a? 0.135s/400000 = ~340ns. Jest to całkiem niezły czas. Pojedynczy rdzeń mojego CPU potrafi wykonać w tym czasie około 3700 instrukcji arytmetycznych w około 900 cyklach zegara. My jednak chcemy wiedzieć czy można jeszcze lepiej. Perf prawdę nam powie:
perf stat ./naive_uri_parser
sudo perf stat -e cycles,instructions,cache-misses ./naive_uri_parserOutput:
Performance counter stats for './naive_uri_parser': 134.925923 task-clock # 0.997 CPUs utilized 14 context-switches # 0.104 K/sec 2 cpu-migrations # 0.015 K/sec 318 page-faults # 0.002 M/sec 429,456,353 cycles # 3.183 GHz 97,661,246 stalled-cycles-frontend # 22.74% frontend cycles idle 53,600,326 stalled-cycles-backend # 12.48% backend cycles idle 850,989,218 instructions # 1.98 insns per cycle # 0.11 stalled cycles per insn 213,892,182 branches # 1585.256 M/sec 4,366,554 branch-misses # 2.04% of all branches 0.135361927 seconds time elapsedOraz dodatkowo
417,777,346 cycles # 0.000 GHz 850,802,868 instructions # 2.04 insns per cycle 7,648 cache-misses 0.134220801 seconds time elapsedZ powyższych statystyk można wysnuć następujące wnioski:
- ogólna wydajność jest całkiem niezła. Wykonywanych jest ~2 instrukcje/cykl. Z uwagi na sekwencyjny dostęp do pamięci (skanując URL pracujemy na kolejnych adresach) problemu nie sprawia również liczba cache miss-ów wynosząca jedynie ~7600. W obliczu 400000 wywołań parse jest to zaniedbywalna wartość.
- wąskim gardłem jest tutaj frontend procesora (przestoje na poziomie ~23%) co wynika zapewne ze zjawiska branch misprediction. Perf raportuje aż 4 mln branch miss-ów co daje 10 na wywołanie parse. Średni koszt miss-a to nawet kilkanaście cyklów zegara co daje max. 200 cyklów per parse. 200/900 ~ 20% . Rzeczywiście jest to wartość odpowiadająca zaraportowanym wcześniej przestojom na poziomie 23%.
3.2 Szybkie parsowanie
A może by tak spróbować nieco innego podejścia? Algorytm parsowania sam w sobie jest OK, trzeba jednak ulepszyć implementacje. Pokazaliśmy że eliminując branch misprediction możemu uzyskać ok. 20% zysku wydajnościowego.Czy to wszystko co możemy zrobić?
Nie od dzisiaj wiadomo, że std::string ma problemy z wydajnością, głównie ze względu na to że śmieci w pamięci poprzez tworzenie dużej liczby obiektów tymczasowych. Oprócz tego niektóre metody wprowadzają spore narzuty czasowe[9]. Z tych względów np. programiści gier zdecydowanie bardziej preferują stare dobre char stringi [10]. My również użyjemy ich w nowej implementacji parsera.
Kluczowe kwestie na które chcemy zwrócić uwagę to:
- proste skanowanie URL-a polegające na sekwencyjnym wyszukiwaniu kolejnych fragmentów tzn. najpierw scheme, następnie userinfo, później host itd. CPU preferuje kilka krótkich iteracji z drobnymi if-ami od długiego for-a z długim switcho-casem (branch-misprediction). Nie jest to nowość - takiego podejścia użyliśmy już wcześniej. Nowością jest skanowanie napisu z ograniczonym "zawracaniem". Zawracania nie można całkowicie wyeliminować ponieważ niektóre fragmenty są opcjonalne i po prostu trzeba się cofnąć. Ważne żeby zrobić to jak najwcześniej a nie dopiero na końcu URL-a.
- ograniczenie kopiowania. Chcemy to zrobić tylko raz na początku parsowania tak by stać się właścicielem wejściowego napisu. W naive_parse typem każdego fragmentu był std::string. Teraz będzie to char*. Chcielibyśmy trzymać wskaźniki na początki naszych fragmentów, co jednak z końcówkami? Wykorzystamy separatory. Struktura URL-a pozwala nam w miejscu separatora wstawić znak 0. Kopiowanie fragmentów nastąpi tylko na żądanie.
char input_str[str_size]; const char *scheme, *userinfo, *host, *path, *query, *hash, *port; inline void fast_parse(const char *str) { strcpy(input_str, str); scheme = userinfo = host = path = query = hash = port = 0; int i = 0; int old_i = 0; i = parse_scheme(&old_i, i); i = parse_userinfo(&old_i, i); i = parse_host_and_port(&old_i, i); parse_query_and_hash(&old_i, i); }Link do pełnego kodu źródłowego fast_parse znajduje się na końcu posta. Spójrzmy zatem na osiągi naszego zoptymalizowanego parsera.
Performance counter stats for './fast_uri_parser': 46.583753 task-clock # 0.992 CPUs utilized 7 context-switches # 0.150 K/sec 2 cpu-migrations # 0.043 K/sec 120 page-faults # 0.003 M/sec 147,551,153 cycles # 3.167 GHz 26,831,425 stalled-cycles-frontend # 18.18% frontend cycles idle 9,712,240 stalled-cycles-backend # 6.58% backend cycles idle 390,444,647 instructions # 2.65 insns per cycle # 0.07 stalled cycles per insn 95,951,406 branches # 2059.761 M/sec 107,114 branch-misses # 0.11% of all branches 0.046959240 seconds time elapsed
Jest moc! Czas testu spadł do 47s, średni czas fast_parse do ok. 120 ns. Uzyskaliśmy niemal trzykrotne przyspieszenie i to bez zmiany algorytmu! Jak to możliwe? Rezygnując z klasy std::string oraz STL-a przede wszystkim udało nam się dwukrotnie zmniejszyć liczbę instrukcji wysyłanych na CPU (z ~800 mln do ~400 mln). Dodatkowo bardzo mocno spadła liczba branch miss-ów.
Kod jest już obecnie mocno zoptymalizowany. 120 ns to czas porównywalny z czasem kopiowania przez memcpy 512B kawałka pamięci na moim komputerze.
4. Podsumowanie
Podsumowując. W paragrafie drugim utworzyliśmy prostą, skalowalną infrastrukturę klient - serwer z użyciem Boost Asio. Pokazaliśmy, że echo serwer skaluje się do 400 tysięcy połączeń. Nie obeszło się bez wsparcia ze strony systemu operacyjnego. Przy okazji zaprezentowaliśmy niektóre smaczki nowego standardu C++11.
W paragrafie trzecim napisaliśmy prosty parser URL a następnie zajeliśmy się jego tuningiem. Na początku pod wpływem drobnych modyfikacji proces parsowania udało nam się przyspieszyć 2x, a po przepisaniu na język C kolejne 3x. Ostatecznie parser przyspieszył aż sześciokrotnie. Przy okazji zaprezentowaliśmy linuksowe narzędzia pomocne przy optymalizacji.
Na razie to by było na tyle. W następnej części postaram się zebrać wszystko do kupy i zaprezentować działający serwer udostępniający parsowanie URL-ów. Nie zabraknie również statystyk oraz osiągów serwera takich jak latency i throughput.
C.D.N...
[link do boost_server + boost_client]
[link do naive_parse + fast_parse]
1. Dogłębna analiza tematu znajduje się tutaj: http://www.kegel.com/c10k.html
2. http://stackoverflow.com/questions/651665/how-many-socket-connections-possible/9676852#9676852
3. Nie. Nie można zamienić boost::bind na std::bind w linijce ?. Wynika to z ?. Więcej na temat różnic tu: http://stackoverflow.com/questions/10555566/is-there-any-difference-between-c11-stdbind-and-boostbind
4. http://www.boost.org/doc/libs/1_43_0/boost/asio/detail/epoll_reactor.hpp
5. Podwóje usunięcie obiektu może wystąpić np. w następującej sytuacji:
shared_ptr<Foo> foo(new Foo());
shared_ptr<Foo> foo2(foo.get());
Aby zapobiec takiej sytuacji wystarczy nigdy nie tworzyć więcej niż jednego shared_ptr-a z ustalonego raw pointer-a (zamiast tego używać konstruktora kopiującego itp.).
W przypadku this-a obiektu wskazywanego przez jakiś shared_ptr podwójne usunięcie nastąpi jeśli tylko przekażemy this-a do shared_ptr-a tzn:
shared_ptr<Foo> foo(this);
Rozwiązaniem jest użycie enable_shared_from_this, które opakowuje this w shared_ptr w bezpieczny sposób. Więcej: http://stackoverflow.com/questions/712279/what-is-the-usefulness-of-enable-shared-from-this?rq=1
6. CRTP znajduje zastosowanie m. in. w zliczaniu liczby obiektów klasy, w symulacji dynamicznego wiązania w czasie kompilacji oraz w implementacji polimorficznego clone-a. Więcej pod adresem
http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
7. Zamiana dużego znaku na mały sprowadza się jedynie do zapalenia 5-tego bitu co jest operacją natychmiastową. Można jednak całkowiecie wyeliminować obliczenia poprzez stablicowanie wyniku. Tak też zrobiono w implementacji std::tolower z libstdc++ na moim systemie. Proste testy wykazały, że stablicowanie przyspiesza fast_parse o ~5%. Wszystko co przed chwilą opisałem działa tylko dla napisu z kodowaniem ASCII, którym jest właśnie std::string
8. http://en.wikipedia.org/wiki/Copy-on-write.Z linku wynika, że w C++11 std::string z różnych powodów nie używa Copy-on-write.
9. O tym jak bolesne dla wydajności mogą być std::string-i przekonał się Fabian Giesen w swoim genialnym cyklu o tunigu demka technologicznego Intel-a. Link: http://fgiesen.wordpress.com/2013/01/30/a-string-processing-rant/
10. Przykładowy, efektywny zamiennik klasy std::string dostępny jest pod tym adresem https://home.comcast.net/~tom_forsyth/blog.wiki.html w sekcji "A sprintf that isn't as ugly"
No comments:
Post a Comment