Optymalizacja wczesnej alokacji pamięci

0

Czy jest jakiś sposób, by kompilator zamieniał

std::cout << "test" << " " << "2" << "\n";

na

std::cout << "test 2\n";

czy też

std::string s_1 = "a";
s_1 += "b";
std::string s_2 = "z";
s_1 += "cdefghijklmnopqrstuvwxy";
s_1 += "bcdefghijklmnopqrstuvwxy";
s_1 += "abcdefghijklmnopqrstuvwxy";
s_1 += "cdefghijklmnopqrstuvw";
s_1 += "cdefghijklmnopqrstuvwxy";
s_1 += "cdefghijklmnopqrstuvwxyz";

na

std::string s_1 = "abcdefghijklmnopqrstuvwxybcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxycdefghijklmnopqrstuvwcdefghijklmnopqrstuvwxycdefghijklmnopqrstuvwxyz";
std::string s_2 = "z";

?

I która opcja szczegółowa odpowiada za optymalizację tego drugiego w -O3 do postaci kopiowania pamięci?
A może w C++ dane i tak byłyby wrzucane na stos?

To jest przykład większego problemu polegającego na wcześniejszym przydzieleniu pamięci na dane i późniejszym składowaniu tych danych, a zapisie wszystkiego przy pomocy różnych operatorów.

W C można by zapisać tak, przedzielając dowolnymi instrukcjami.

char *s = ( char * )malloc( 10 + 1 );
strcpy( s, "abc" );
strcpy( s + 3, "def" );
strcpy( s + 3 + 3, "ghij" );

Ale to powyższe nie jest bezpieczne, ze względu na użycie liczb zależnych od tekstów. Dlatego pytam o operatory.

2

jaki jest powód tego że?

to
std::cout << "test" << " " << "2" << "\n";
 na to
std::cout << "test 2\n";

W czym to ma być bardziej zooptymalizowane? w wywołaniu << który większego wpływu na wydajność miec nie będzie?

co do dodawania stringów szybko na oko zeby zredukować zbędne alokacje

  1. std::string jak pamiętam ma reserve
  2. użyć stringstream zamiast std::string. Stringstream działa inaczej.
1

Brakuje kontekstu.
W zależności od szczegółów to tak/nie lub są różne sposoby.
Nejlpeij jednak to polecam używać std::format albo lepiej fmt::print.

Ta cześć

overcq napisał(a):
std::string s_1 = "a";
s_1 += "b";
std::string s_2 = "z";
s_1 += "cdefghijklmnopqrstuvwxy";
s_1 += "bcdefghijklmnopqrstuvwxy";
s_1 += "abcdefghijklmnopqrstuvwxy";
s_1 += "cdefghijklmnopqrstuvw";
s_1 += "cdefghijklmnopqrstuvwxy";
s_1 += "cdefghijklmnopqrstuvwxyz";

Można zamienić na:

std::string s_2 = "z";
std::string s_1 = "a"
    "b"
    "cdefghijklmnopqrstuvwxy"
    "bcdefghijklmnopqrstuvwxy"
    "abcdefghijklmnopqrstuvwxy"
    "cdefghijklmnopqrstuvw"
    "cdefghijklmnopqrstuvwxy"
    "cdefghijklmnopqrstuvwxyz";

Zależnie od tego co robisz dużo monza zrobić za pomcą constexpr

0

znazłem jeszcze inny przykład z c++ 20 na stacku

string fmt_s = 
    fmt::format("{}{}{}{}{}{}{}{}{}{}{}{}",
                s1,s2,s3,s4,  // load all handles into memory
                s5,s6,s7,s8,  // then make only one call!
                s9,s10,s11,s12);

Jeśli pracujesz a dużych stringach, Można poszukać zewnętrrznej libki taki przykład z brzegu
https://github.com/btwael/SuperString

0
overcq napisał(a):

Czy jest jakiś sposób, by kompilator zamieniał

std::cout << "test" << " " << "2" << "\n";

na

std::cout << "test 2\n";

Było by to "dośc ambitne", bo chodziło by o wyd... wy-optymalziację operatora << , który choć wygląda znajomo, ale może miec wiele niuansów a nawet zuuupełnie odmienne implementacje (np licznik ile razy został odpalony). Gdybym ja konstruował kompilator / bibliotekę, NIGDY bym nie optymalizował operacji IO, bo nie.
(w praktyce prawdopodobnie zachodzi cachowanie / buforowanie w bibliotece, ale "sztuk wywołań" na pewno nie jest zoptymalizowane, cztery razy dla czterech elementów).

O wiele mniej ambitne by było:
std::cout << "test" + " " + "2" + "\n";

Gdzie optymalizacji podlegał by operator +, o wiele silniej zdefiniowany, na dobrze znanym wbudowanym typie string, i tu kompilator mógłby poczuć się uprawniony.

Nie jestem aktywny w C++, ale nawet w takiej niechętnej Javie operator + na stringach podlega optymalizacji kompilatora

0

Z szybkich testów czasu wykonywania się wynika, że kod z fmt wykonuje się najdłużej, następnie kod z wielokrotnym wypisywaniem na wyjście. Najkrócej (prawie 2 razy) trwa łączenie string, a jeszcze krócej kod w C z użyciem my_strcpy i puts, gdzie nawet wywoływane są malloc/free. Natomiast jeśli użyć strcpy, to jeszcze 2 razy krócej.
Te wyniki skłaniają do przemyślenia, jak definiować funkcje, ponieważ może okazać się, że ze static zamiast funkcji bibliotecznej będzie najszybciej: kompilator zoptymalizuje przy małych wywołaniach na równoważny kod.

0
overcq napisał(a):

Te wyniki skłaniają do przemyślenia, jak definiować funkcje, ponieważ może okazać się, że ze static zamiast funkcji bibliotecznej będzie najszybciej: kompilator zoptymalizuje przy małych wywołaniach na równoważny kod.

Zwyczajnie gramatycznie tego nie rozumiem.

overcq napisał(a):

Z szybkich testów czasu wykonywania się wynika, że kod z fmt wykonuje się najdłużej, następnie kod z wielokrotnym wypisywaniem na wyjście. Najkrócej (prawie 2 razy) trwa łączenie string, a jeszcze krócej kod w C z użyciem my_strcpy i puts, gdzie nawet wywoływane są malloc/free. Natomiast jeśli użyć strcpy, to jeszcze 2 razy krócej

Pewnie że różnice będą, ale jak porównujesz do strcpy a nawet nie do strncpy to są zupełnie inne właściwości kodu co do bezpieczeństwa, konserowalnosci itd ...
Silny formater BĘDZIE MNIEJ WYDAJNY o bardziej prostego konkatenatora (a tym samym poziomie rozwoju technicznego) , bo więcej po prostu interpretuje

Np w C nie masz żadnej oficjalnej wykładni, aby mieć "swoje FILE " - to boli jakby chcieć poczarować, np output nie do pliku, w C++ podmienialne jest niemal wszystko, łącznie z alokatorami
"Coś tam" zdarzyło się grzebać w alokatorach C++, jak RAM było 1 kB, ale to zupełnie nieprofesjonalny projekt i bez kontynuacji - tym niemniej fajne wrażenia

0

@ZrobieDobrze: W tym bezpieczeństwie przeszkadza głównie składnia języka, w której nie ma jednoznaczności (liczba znaków jest rozdzielona od tekstu), ale przecież mogłaby być składnia, która pozwalałaby użyć tekstu i mieć wczesną alokację. (Poza tym jeszcze są wymienione constexpr, ale poszukam o nich później.)
Akurat podmiana jest dzięki składni C++ i niekoniecznie musi być widoczna w czasie wykonywania, a w takim razie jest optymalna.
Jeśli chodzi o niegramatyczne zdanie, to miałem na myśli umieszczanie funkcji w bibliotece dynamicznie łączonej a umieszczenie w pliku nagłówkowym programu zadeklarowanej jako static.

0

zrobiłem krótki test na quick bench. Specjalnie dodałem no optimize, scenariusz niestety kiepski. Dopisz swoje
https://quick-bench.com/q/8Uxr1HALh_K_3jwbu5bJAsCwkbA
z i bez optymalizacji zmiennych.
wniosek taki, bardzo dużo pomaga reserve i nieco stringstream. Ale mogą się zdarzyć optymalizacje co spowoduje że zwykły string wyprzedzi sstrignsatream.
edit: żeby nie było to syntetyki, konkretny kod i flagi będą mieć wpływ. Oczywiście jak chcesz dziergać w czystym C to będziesz miał szybko.

4pconcat.png

2

@overcq jescze raz proszę o kontekst tego kodu. Twój przykład na razie wygląda dziwnie/bezsensu.
Zrzędzenie na język nie pozwoli nam zrozumieć co właściwie chcesz osiągnąć.

0

Niestety nie mam kodu w C++, ponieważ dopiero się nad nim zastanawiałem:

W C można by zapisać tak, przedzielając dowolnymi instrukcjami.

char *s = ( char * )malloc( 10 + 1 );
strcpy( s, "abc" );
strcpy( s + 3, "def" );
strcpy( s + 3 + 3, "ghij" );

Ale to powyższe nie jest bezpieczne, ze względu na użycie liczb zależnych od tekstów. Dlatego pytam o operatory.

Chodzi o to, by zastąpić to powyższe czymś, co byłoby odporne na błędy, ale bez zwiększania czasu wykonywania. Użycie reserve wymuszałoby podawanie liczb oderwanych od stałych tekstowych.
Mam kod w C+, gdzie również nie używam powyższych funkcji, ale własne.
Możliwe, że są lepsze przykłady, których teraz nie znalazłem, ale teraz podam następujące:

  • W module ‘clienta’ HTTP używam zagnieżdżonych procedur kopiowania E_text_Z_s_P_copy_s0, po których następuje obliczenie długości tekstu.
  • W module ‘clienta’ HTTP/2 używam wczesnej alokacji pamięci procedurą M, po której następuje kopiowanie serią zagnieżdżonych procedur E_text_Z_s_P_copy_s0 i E_text_Z_s_P_copy_s0_0.
  • W programie ‘servera’ HTTP często używam wczesnej alokacji pamięci i kopiowania jak wyżej.

Jeśli procedury takie jak E_text_Z_s_P_copy_s0 byłyby w pliku nagłówkowym jako static, to kompilator zoptymalizuje ich wykonanie, zamieniając przypuszczalnie na lepszy kod ‘inline’ dla krótkich stałych tekstowych, dlatego porównanie wychodzi na niekorzyść operatorów wywoływanych w standardowy sposób.

6

Znasz taki termin "przedwczesna optymalizacja"?

Napisz kod, uruchom profiler i dopiero wtedy ewentualnie zacznij się zastanawiać co optymalizować.

3
Bartłomiej Golenko napisał(a):

Znasz taki termin "przedwczesna optymalizacja"?

Napisz kod, uruchom profiler i dopiero wtedy ewentualnie zacznij się zastanawiać co optymalizować.

Popieram, jak jeszcze widać, że to warstwa sieciowa (HTTP) to sorry, ale nieważne jak będziesz to optymalizował to IO jest wąskim gardłem i jest to oczywiste nawet bez profilowania kodu.
Możesz przyspieszyć budowanie nagłówków żądania HTTP abo body z JSon 1000x, a program przyspieszy dzięki temu najwyżej o 1%.

1
MarekR22 napisał(a):

Popieram, jak jeszcze widać, że to warstwa sieciowa (HTTP) to sorry, ale nieważne jak będziesz to optymalizował to IO jest wąskim gardłem i jest to oczywiste nawet bez profilowania kodu.
Możesz przyspieszyć budowanie nagłówków żądania HTTP abo body z JSon 1000x, a program przyspieszy dzięki temu najwyżej o 1%.

Umknęło mi, że sieciówka i HTTP.
Głupotą jest szukanie pojedynczych nanosekund, a używanie tak nieoptymalnych protokołów.
Protokoły binarne (co nie oznacza że naiwne, bo są całkiem 'sophisticated') mają duuuże możliwości, im słabsze architektury, tym zyzk nad http/rest większy

0

Nie mam co komentować, ponieważ widzę, że nie czytacie kodu, a tam jest schemat:

for( wszystkie sockety, na których są dane lub na które wysyłamy dane )
    przetwórz dane

Przy takim schemacie widać, że proces nie czeka głównie na dane, ale na przykład przy 1000 otwartych połączeń je przetwarza, a więc przy każdym doklejaniu ciągu powiązanym z szukaniem wolnego bloku w menedżerze pamięci jest duża strata czasu.

2

a więc przy każdym doklejaniu ciągu powiązanym z szukaniem wolnego bloku w menedżerze pamięci jest duża strata czasu.

W sensie, że to właśnie Ci mówi perf / valgrind / cachegrind w testach end-to-end (a nie benchmarku syntetycznym)? Byłoby to trochę niespodziewane imo.

0
overcq napisał(a):

Nie mam co komentować, ponieważ widzę, że nie czytacie kodu, a tam jest schemat:

for( wszystkie sockety, na których są dane lub na które wysyłamy dane )
    przetwórz dane

Przy takim schemacie widać, że proces nie czeka głównie na dane, ale na przykład przy 1000 otwartych połączeń je przetwarza, a więc przy każdym doklejaniu ciągu powiązanym z szukaniem wolnego bloku w menedżerze pamięci jest duża strata czasu.

Nie czytamy, bo ten kod nie nadaje się do czytania (tak, zerknąłem i na githuba i na kanał na youtube...).
Udowodnij (profilerem), że przy tym tysiącu połączeń proces będzie więcej czekać na sklejanie stringów niż na obsługę socketów - wtedy można dalej dyskutować.

1

Gościu a co to zmienia, że masz poll, select czy jak kolwiek równolegle obsługuejsz te 1000 połączeń. Przecież Ci napisali kawa na ławie. Że to komunikacja po sieci jest wąskim gardłem. Czego jeszcze nie rozumiesz. jak cchesz coś tu przyspieszyć to pobaw się opcją socketa nagle algorithm. Mogą być nawet dosyć widoczne efekty.

0
ksh napisał(a):

Gościu a co to zmienia, że masz poll, select czy jak kolwiek równolegle obsługuejsz te 1000 połączeń. Przecież Ci napisali kawa na ławie. Że to komunikacja po sieci jest wąskim gardłem. Czego jeszcze nie rozumiesz. jak cchesz coś tu przyspieszyć to pobaw się opcją socketa nagle algorithm. Mogą być nawet dosyć widoczne efekty.

Meh. W sieci problemem są opóźnienia. Jak masz współbieżność 1000 to szybko będziesz mieć wąskie gardło na CPU. Widać, że nigdy nie robiłeś niczego wysoce współbieżnego. Inną kwestią, że dane z profilera od @overcq by się przydały.

0

Trudno mi było uzyskać nazwy symboli. Musiałem wyłączyć ‘position independent code’ podczas generowania bibliotek programu, a i tak to nie wystarczyło do uzyskania wszystkich symboli.
Są dwie wersje z programu “perf”:

  1. Z moim menedżerem pamięci (“E_mem_Q_blk”) zastępującym malloc i resztę funkcji, który jest zoptymalizowany do szybszego przydzielania pamięci niż zwalniania, co widać po czasie pobytu w free.
  2. Z oryginalnym menedżerem pamięci z “libc”.

Dlaczego funkcje mojego menedżera pamięci wykonują się tak długo względem całego programu? Ponieważ menedżer pamięci zawiera algorytm zmniejszania fragmentacji pamięci i scalania bloków (to drugie robi właśnie funkcja E_mem_Q_blk_Q_sys_table_mf_I_unite). Natomiast funkcja E_mem_Q_blk_Q_sys_table_R_new_id szuka pustego wpisu w tablicy bloków (zmapowanych, alokowanych jak i wolnych), by nie realokować tablicy za każdym razem. Poza tym w programie ‘serwera’ korzystam dużo ze sterty.

Natomiast interesująca jest funkcja E_mem_Q_blk_I_append_, która powiększa blok pamięci o podany rozmiar, oraz funkcja E_mem_Q_blk_I_remove_, która usuwa podany rozmiar z bloku pamięci.

Jednak nie są to dane z jednoczesnymi połączeniami, lecz szybko następującymi po sobie. Niestety nie posiadam sieci, która mogłaby równocześnie wysyłać żądania, a tylko kilka rdzeni procesora. :-) Może ktoś inny mógłby zrobić test na realnym ‘serwerze’ podłączonym do sieci.

0

Być może źle interpretuję to co pokazuje perf... ale czy dobrze rozumiem, że Twój program (niezależnie od wersji) spędza ponad 70% czasu na alokacji/realokacji i zwalnianiu pamięci ?
Nie wydaje Ci się to troszkę dziwne ? Co takiego robi Twój test ?

BTW - jaki jest sens ręcznego defragmentowania pamięci w systemie z pamięcią wirtualną i stronicowaniem (chyba że planujesz to uruchamiać na czymś maleńkim bez MMU, ale wtedy te tysiące połączeń na sekundę chyba i tak są nierealne)?

0

Zamiast kleić stringi po prostu dopisuj dane bezpośrednio do bufora wyjściowego, który potem wysyłasz przez socket. A bufor możesz przecież użyć wielokrotnie. W ten sposób możesz praktycznie unikać jakichkolwiek alokacji.

0

Faktycznie implementacja mojego menedżera pamięci może być nieoptymalna (popróbuję jeszcze z sortowaniem i kopiowaniem tablic alokacji).

Natomiast w przypadku HTTP/1.1 można dopisywać do bufora wyjściowego. To byłoby rozwiązanie: szybkość kosztem pamięci w przypadku równoczesnych połączeń. Ale też można by prealokować pamięć na bufor wyjściowy na przykład na rozmiar strony pamięci (4 KB) i powiększać go tylko w razie potrzeby. To ostatnie rozwiązanie wymagałoby utworzenia nowego typu obiektu w menedżerze pamięci.
W przypadku HTTP/2 już jest inaczej: nagłówki w jakiejś formie muszą być utworzone przed ich zakodowaniem.

Co robi test? Tylko uruchamia równolegle ileś sztuk curl (z odpowiednimi opcjami) do żądania strony głównej (index).

1 użytkowników online, w tym zalogowanych: 0, gości: 1