Czy strlen zostanie obliczony wiele razy, jeśli zostanie użyty w warunku pętli?


109

Nie jestem pewien, czy poniższy kod może spowodować zbędne obliczenia, czy też jest specyficzny dla kompilatora?

for (int i = 0; i < strlen(ss); ++i)
{
    // blabla
}

Będzie strlen()obliczany za każdym razem, gdy iwzrośnie?


14
Zgaduję, że bez wyrafinowanej optymalizacji, która może wykryć, że „ss” nigdy nie zmienia się w pętli, to tak. Najlepiej skompilować i spojrzeć na zestaw, aby zobaczyć.
MerickOWA

6
Zależy to od kompilatora, poziomu optymalizacji i tego, co możesz (możesz) zrobić sswewnątrz pętli.
Hristo Iliev

4
Jeśli kompilator może udowodnić, że ssnigdy nie jest modyfikowany, może wyciągnąć obliczenia z pętli.
Daniel Fischer,

10
@Mike: „wymaga analizy w czasie kompilacji dokładnie tego, co robi strlen” - strlen jest prawdopodobnie elementem wewnętrznym, w którym to przypadku optymalizator wie, co robi.
Steve Jessop

3
@MikeSeymour: Nie ma może, może nie. strlen jest zdefiniowane w standardzie języka C, a jego nazwa jest zarezerwowana do użytku zdefiniowanego przez język, więc program nie może swobodnie podawać innej definicji. Kompilator i optymalizator mają prawo przyjąć, że strlen zależy wyłącznie od jego danych wejściowych i nie modyfikuje go ani żadnego stanu globalnego. Wyzwaniem dla optymalizacji jest tutaj określenie, że pamięć wskazywana przez ss nie jest zmieniana przez żaden kod wewnątrz pętli. Jest to całkowicie wykonalne w przypadku obecnych kompilatorów, w zależności od konkretnego kodu.
Eric Postpischil

Odpowiedzi:


138

Tak, strlen()zostaną ocenione w każdej iteracji. Jest możliwe, że w idealnych okolicznościach optymalizator mógłby wywnioskować, że wartość się nie zmieni, ale ja osobiście nie polegałbym na tym.

Zrobiłbym coś takiego

for (int i = 0, n = strlen(ss); i < n; ++i)

lub ewentualnie

for (int i = 0; ss[i]; ++i)

pod warunkiem, że łańcuch nie zmieni długości podczas iteracji. Jeśli tak, będziesz musiał albo dzwonić za strlen()każdym razem, albo obsługiwać to za pomocą bardziej skomplikowanej logiki.


14
Jeśli wiesz, że nie manipulujesz łańcuchem, druga jest o wiele bardziej preferowana, ponieważ jest to zasadniczo pętla, która i tak zostanie wykonana strlen.
mlibby

26
@alk: Jeśli ciąg może się skrócić, to oba są nieprawidłowe.
Mike Seymour

3
@alk: jeśli zmieniasz ciąg, pętla for prawdopodobnie nie jest najlepszym sposobem na iterację po każdym znaku. Myślę, że pętla while jest bardziej bezpośrednia i łatwiejsza do zarządzania licznikiem indeksu.
mlibby

2
Idealne okoliczności to kompilacja z GCC pod linuxem, gdzie strlenjest oznaczona jako __attribute__((pure))pozwalająca kompilatorowi na uniknięcie wielu wywołań. GCC Attributes
David Rodríguez - dribeas

6
Druga wersja to forma idealna i najbardziej idiomatyczna. Pozwala na przekazanie ciągu tylko raz, a nie dwa razy, co będzie miało znacznie lepszą wydajność (szczególnie spójność pamięci podręcznej) dla długich ciągów.
R .. GitHub PRZESTAŃ POMÓC NA LODZIE

14

Tak, za każdym razem, gdy używasz pętli. Wtedy za każdym razem obliczy długość struny. więc użyj tego w ten sposób:

char str[30];
for ( int i = 0; str[i] != '\0'; i++)
{
//Something;
}

W powyższym kodzie str[i]weryfikuje tylko jeden określony znak w ciągu w lokalizacjii każdym razem, gdy pętla rozpoczyna cykl, dzięki czemu zajmie mniej pamięci i jest bardziej wydajna.

Zobacz ten link, aby uzyskać więcej informacji.

W poniższym kodzie za każdym razem, gdy pętla strlenzostanie uruchomiona , policzy długość całego ciągu, który jest mniej wydajny, zajmuje więcej czasu i zajmuje więcej pamięci.

char str[];
for ( int i = 0; i < strlen(str); i++)
{
//Something;
}

3
Zgadzam się z twierdzeniem „[to] jest bardziej wydajne”, ale zużywa mniej pamięci? Jedyna różnica w zużyciu pamięci, o której mogę pomyśleć, dotyczyłaby stosu wywołań podczas strlenpołączenia, a jeśli pracujesz tak napięty, prawdopodobnie powinieneś pomyśleć o usunięciu kilku innych wywołań funkcji ...
a CVn

@ MichaelKjörling Cóż, jeśli używasz "strlen", to w pętli musi on skanować cały ciąg za każdym razem, gdy pętla jest uruchomiona, podczas gdy w powyższym kodzie "str [ix]" skanuje tylko jeden element podczas każdego cyklu pętla, której położenie jest reprezentowane przez „ix”. Dzięki temu zajmuje mniej pamięci niż „strlen”.
codeDEXTER

1
Nie jestem pewien, czy to ma sens. Bardzo naiwna implementacja strlen byłaby czymś w rodzaju tego, int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }co jest dokładnie tym, co robisz w kodzie w swojej odpowiedzi. Nie twierdzę, że iterowanie po ciągu raz, a nie dwa, jest bardziej wydajne czasowo , ale nie widzę, aby jedno lub drugie wykorzystywało mniej lub więcej pamięci. A może odnosisz się do zmiennej używanej do przechowywania długości łańcucha?
CVn

@ MichaelKjörling Proszę zobaczyć powyższy edytowany kod i link. A jeśli chodzi o pamięć - za każdym razem, gdy pętla jest uruchomiona, każda iterowana wartość jest przechowywana w pamięci, aw przypadku 'strlen', ponieważ zlicza cały ciąg raz po raz, wymaga więcej pamięci do przechowywania. a także dlatego, że w przeciwieństwie do Javy, C ++ nie ma „garbage collectora”. Wtedy też mogę się mylić. zobacz odnośnik dotyczący braku "Garbage Collectora" w C ++.
codeDEXTER

1
@ aashis2s Brak garbage collectora odgrywa rolę tylko podczas tworzenia obiektów na stercie. Obiekty na stosie zostają zniszczone, gdy tylko zasięg i kończy się.
Ikke,

9

Dobry kompilator może nie obliczyć tego za każdym razem, ale nie sądzę, aby można było być pewnym, że robi to każdy kompilator.

Poza tym kompilator musi wiedzieć, że strlen(ss)to się nie zmienia. Jest to prawdą tylko wtedy, gdy ssnie jest zmieniane w forpętli.

Na przykład, jeśli używasz funkcji tylko do odczytu ssw forpętli, ale nie zadeklarujesz ssparametru -parameter as const, kompilator nie może nawet wiedzieć, że ssnie jest zmieniany w pętli i musi obliczyć strlen(ss)w każdej iteracji.


3
+1: Nie tylko ssnie można zmieniać w forpętli; nie może być dostępny i zmieniany przez żadną funkcję wywoływaną w pętli (albo dlatego, że jest przekazywany jako argument, albo ponieważ jest zmienną globalną lub zmienną o zasięgu plikowym). Kwalifikacja Const również może być czynnikiem.
Jonathan Leffler

4
Myślę, że jest bardzo mało prawdopodobne, aby kompilator wiedział, że „ss” się nie zmienia. Mogą istnieć zbłąkane wskaźniki wskazujące na pamięć wewnątrz 'ss', o czym kompilator nie ma pojęcia, że ​​mogą zmienić 'ss'
MerickOWA

Jonathan ma rację, lokalny ciąg znaków const może być jedynym sposobem, aby kompilator miał pewność, że nie ma możliwości zmiany „ss”.
MerickOWA

2
@MerickOWA: rzeczywiście, to jedna z rzeczy, które restrictsą potrzebne w C99.
Steve Jessop

4
Odnośnie ostatniego paragrafu: jeśli wywołasz funkcję tylko do odczytu ssw pętli for, to nawet jeśli jej parametr jest zadeklarowany const char*, kompilator nadal musi przeliczyć długość, chyba że (a) wie, że sswskazuje na obiekt const, w przeciwieństwie do bycia wskaźnikiem do stałej lub (b) może wbudować funkcję lub w inny sposób zobaczyć, że jest tylko do odczytu. Pobranie const char*parametru nie jest obietnicą nie modyfikowania wskazanych danych, ponieważ można je rzutować char*i modyfikować, pod warunkiem, że zmodyfikowany obiekt nie jest stałą i nie jest literałem ciągu.
Steve Jessop

4

Jeśli ssjest typu const char *i nie odrzucasz tego, co constw pętli, kompilator może tylko wywołaćstrlen raz, jeśli optymalizacje są włączone. Ale z pewnością nie jest to zachowanie, na które można liczyć.

strlenWynik należy zapisać w zmiennej i użyć tej zmiennej w pętli. Jeśli nie chcesz tworzyć dodatkowej zmiennej, w zależności od tego, co robisz, możesz uciec od odwrócenia pętli w celu wykonania iteracji wstecz.

for( auto i = strlen(s); i > 0; --i ) {
  // do whatever
  // remember value of s[strlen(s)] is the terminating NULL character
}

1
W strlenogóle jest błędem dzwonić . Po prostu zapętlaj, aż dotrzesz do końca.
R .. GitHub PRZESTAŃ POMÓC NA LODZIE

i > 0? Nie powinno tego i >= 0tu być ? Osobiście chciałbym również zacząć od strlen(s) - 1iterowania łańcucha wstecz, wtedy zakończenie \0nie wymaga specjalnego rozważania.
CVn

2
@ MichaelKjörling i >= 0działa tylko wtedy, gdy do zainicjowania strlen(s) - 1, ale jeśli masz łańcuch o zerowej długości początkowych niedomiarów wartości
pretorianów

@ Prætorian, dobry punkt na łańcuchu o zerowej długości. Nie brałem pod uwagę tego przypadku, kiedy pisałem swój komentarz. Czy C ++ ocenia i > 0wyrażenie w początkowej pętli? Jeśli tak się nie stanie, masz rację, przypadek o zerowej długości na pewno przerwie pętlę. Jeśli tak, otrzymujesz „po prostu” znak ze znakiem i== -1 <0, więc nie ma wpisu w pętli, jeśli warunek jest taki i >= 0.
CVn

@ MichaelKjörling Tak, warunek wyjścia jest oceniany przed wykonaniem pętli po raz pierwszy. strlenzwracany typ jest bez znaku, więc zwraca (strlen(s)-1) >= 0wartość true dla łańcuchów o zerowej długości.
Praetorian

3

Formalnie tak, strlen() oczekuje się, że będzie wywoływana przy każdej iteracji.

W każdym razie nie chcę zaprzeczać możliwości istnienia jakiejś sprytnej optymalizacji kompilatora, która zoptymalizuje każde kolejne wywołanie strlen () po pierwszym.


3

Cały kod predykatu będzie wykonywany w każdej iteracji forpętli. Aby zapamiętać wynik strlen(ss)wywołania, kompilator musiałby przynajmniej to wiedzieć

  1. Funkcja strlen była wolna od skutków ubocznych
  2. Pamięć wskazywana przez ssnie zmienia się w czasie trwania pętli

Kompilator nie wie żadnej z tych rzeczy i dlatego nie może bezpiecznie zapamiętać wyniku pierwszego wywołania


Cóż, mógłby to wiedzieć dzięki analizie statycznej, ale myślę, że chodzi o to, że taka analiza nie jest obecnie zaimplementowana w żadnym kompilatorze C ++, tak?
GManNickG,

@GManNickG z pewnością może udowodnić, że # 1, ale # 2 jest trudniejsze. W przypadku pojedynczego wątku tak, zdecydowanie może to udowodnić, ale nie w środowisku wielowątkowym.
JaredPar

1
Może jestem uparty, ale myślę, że numer dwa jest możliwy również w środowiskach wielowątkowych, ale zdecydowanie nie bez szalenie silnego systemu wnioskowania. Tylko rozmyślam tutaj; zdecydowanie poza zakresem jakiegokolwiek obecnego kompilatora C ++.
GManNickG,

@GManNickG Myślę, że nie jest to możliwe w C / C ++. Bardzo łatwo mogłem ukryć adres ssw a size_tlub podzielić go na kilka bytewartości. Mój przebiegły wątek mógłby wtedy po prostu zapisać bajty do tego adresu, a kompilator znałby sposób zrozumienia, z którym się on wiąże ss.
JaredPar

1
@JaredPar: Przykro mi, że uderzam, możesz twierdzić, że int a = 0; do_something(); printf("%d",a);nie można go zoptymalizować, na podstawie tego, że do_something()może zrobić twoją niezainicjowaną rzecz, lub możesz indeksować kopię zapasową stosu i acelowo modyfikować . W rzeczywistości, gcc 4.5 robi zoptymalizować go do do_something(); printf("%d",0);z -O3
Steve Jessop

2

tak . strlen będzie obliczana za każdym razem, gdy i wzrośnie.

Jeśli nie zmieniłeś ss z in the loop, oznacza to, że nie wpłynie to na logikę inaczej będzie to miało wpływ.

Bezpieczniej jest użyć następującego kodu.

int length = strlen(ss);

for ( int i = 0; i < length ; ++ i )
{
 // blabla
}

2

Tak, strlen(ss)będzie obliczyć długość w każdej iteracji. Jeśli w ssjakiś sposób zwiększasz, a także zwiększasz i; byłaby nieskończona pętla.


2

Tak, strlen()funkcja jest wywoływana za każdym razem gdy pętla jest oceniana.

Jeśli chcesz poprawić wydajność, zawsze pamiętaj, aby zapisać wszystko w zmiennych lokalnych ... To zajmie trochę czasu, ale jest bardzo przydatne ...

Możesz użyć kodu jak poniżej:

String str="ss";
int l = strlen(str);

for ( int i = 0; i < l ; i++ )
{
    // blablabla
}


2

Obecnie nie jest to powszechne, ale 20 lat temu na platformach 16-bitowych poleciłbym to:

for ( char* p = str; *p; p++ ) { /* ... */ }

Nawet jeśli Twój kompilator nie jest zbyt inteligentny w optymalizacji, powyższy kod może jeszcze dać dobry kod asemblera.


1

Tak. Test nie wie, że ss nie zmienia się w pętli. Jeśli wiesz, że to się nie zmieni to napisałbym:

int stringLength = strlen (ss); 
for ( int i = 0; i < stringLength; ++ i ) 
{
  // blabla 
} 

1

Arrgh, do cholery, nawet w idealnych okolicznościach będzie!

Na dzień dzisiejszy (styczeń 2018 r.) Oraz gcc 7.3 i clang 5.0, jeśli kompilujesz:

#include <string.h>

void bar(char c);

void foo(const char* __restrict__ ss) 
{
    for (int i = 0; i < strlen(ss); ++i) 
    {
        bar(*ss);
    }
}    

Więc mamy:

  • ss jest stałym wskaźnikiem.
  • ss jest zaznaczony __restrict__
  • Ciało pętli nie może w żaden sposób dotknąć pamięci wskazywanej przez ss(no chyba, że ​​narusza __restrict__).

a mimo to oba kompilatory wykonują strlen() każdą pojedynczą iterację tej pętli . Niesamowity.

Oznacza to również, że aluzje / pobożne życzenia @Praetorian i @JaredPar nie sprawdzają się.


0

TAK, w prostych słowach. I nie ma małego nie w rzadkich warunkach, w których kompilator chce, jako krok optymalizacji, jeśli stwierdzi, że nie wprowadzono żadnych zmian ss. Ale w bezpiecznym stanie powinieneś pomyśleć, że TAK. Istnieją sytuacje, takie jak multithreadedprogram sterowany zdarzeniami, może on ulec błędom, jeśli uznasz to za NIE. Graj bezpiecznie, ponieważ nie poprawi to zbytnio złożoności programu.


0

Tak.

strlen() obliczane za każdym razem, kiedy i rośnie i nie jest zoptymalizowany.

Poniższy kod pokazuje, dlaczego kompilator nie powinien optymalizować strlen().

for ( int i = 0; i < strlen(ss); ++i )
{
   // Change ss string.
   ss[i] = 'a'; // Compiler should not optimize strlen().
}

Myślę, że wykonanie tej konkretnej modyfikacji nigdy nie zmienia długości ss, tylko jego zawartość, więc (naprawdę, bardzo sprytny) kompilator może nadal optymalizować strlen.
Darren Cook,

0

Z łatwością możemy to przetestować:

char nums[] = "0123456789";
size_t end;
int i;
for( i=0, end=strlen(nums); i<strlen(nums); i++ ) {
    putchar( nums[i] );
    num[--end] = 0;
}

Stan pętli jest oceniany po każdym powtórzeniu, przed ponownym uruchomieniem pętli.

Uważaj również na typ używany do obsługi długości strun. powinno być size_ttym, co zostało zdefiniowane jak unsigned intw stdio. porównywanie i przesyłanie go do intmoże spowodować poważny problem z luką w zabezpieczeniach.


0

cóż, zauważyłem, że ktoś mówi, że jest domyślnie optymalizowany przez jakikolwiek "sprytny" nowoczesny kompilator. Przy okazji spójrz na wyniki bez optymalizacji. Próbowałem:
Minimalny kod C:

#include <stdio.h>
#include <string.h>

int main()
{
 char *s="aaaa";

 for (int i=0; i<strlen(s);i++)
  printf ("a");
 return 0;
}

Mój kompilator: g ++ (Ubuntu / Linaro 4.6.3-1ubuntu5) 4.6.3
Polecenie do generowania kodu asemblera: g ++ -S -masm = intel test.cpp

Gotten assembly code at the output:
    ...
    L3:
mov DWORD PTR [esp], 97
call    putchar
add DWORD PTR [esp+40], 1
    .L2:
     THIS LOOP IS HERE
    **<b>mov    ebx, DWORD PTR [esp+40]
mov eax, DWORD PTR [esp+44]
mov DWORD PTR [esp+28], -1
mov edx, eax
mov eax, 0
mov ecx, DWORD PTR [esp+28]
mov edi, edx
repnz scasb</b>**
     AS YOU CAN SEE it's done every time
mov eax, ecx
not eax
sub eax, 1
cmp ebx, eax
setb    al
test    al, al
jne .L3
mov eax, 0
     .....

Nie chciałbym ufać żadnemu kompilatorowi, który próbował go zoptymalizować, chyba że adres ciągu został restrictzakwalifikowany. Chociaż istnieją przypadki, w których taka optymalizacja byłaby uzasadniona, wysiłek wymagany do wiarygodnej identyfikacji takich przypadków w przypadku braku takiej optymalizacji restrictprawie na pewno przewyższyłby korzyści. Gdyby jednak adres ciągu miał const restrictkwalifikator, wystarczyłoby to samo w sobie do uzasadnienia optymalizacji bez konieczności patrzenia na cokolwiek innego.
superkat

0

Rozwijając odpowiedź Prætoriana, polecam:

for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
  • autoponieważ nie chcesz przejmować się tym, który typ zwraca strlen. Kompilator C ++ 11 (npgcc -std=c++0x Nie do końca C ++ 11, ale typy automatyczne działają) zrobi to za Ciebie.
  • i = strlen(s)ponieważ chcesz porównać 0(patrz poniżej)
  • i > 0 ponieważ porównanie do 0 jest (nieco) szybsze niż porównanie z jakąkolwiek inną liczbą.

Wadą jest to, że musisz użyć i-1, aby uzyskać dostęp do znaków ciągu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.