Est-ce que strlen sera calculé plusieurs fois s'il est utilisé dans une condition de boucle?


109

Je ne sais pas si le code suivant peut provoquer des calculs redondants ou est-il spécifique au compilateur?

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

Sera strlen()calculé à chaque fois que iaugmente?


14
Je vais deviner que sans une optimisation sophistiquée capable de détecter que cela ne change jamais dans la boucle, alors oui. Il est préférable de compiler et de regarder l'assemblage pour voir.
MerickOWA

6
Cela dépend du compilateur, du niveau d'optimisation et de ce que vous (pourriez) faire à l' ssintérieur de la boucle.
Hristo Iliev

4
Si le compilateur peut prouver que ce ssn'est jamais modifié, il peut sortir le calcul de la boucle.
Daniel Fischer

10
@Mike: "nécessite une analyse à la compilation de ce que fait exactement strlen" - strlen est probablement un intrinsèque, auquel cas l'optimiseur sait ce qu'il fait.
Steve Jessop

3
@MikeSeymour: Il n'y a pas de peut-être, peut-être pas. strlen est défini par le standard du langage C, et son nom est réservé à l'usage défini par le langage, donc un programme n'est pas libre de fournir une définition différente. Le compilateur et l'optimiseur sont en droit de supposer que strlen dépend uniquement de son entrée et ne le modifie pas ni aucun état global. Le défi de l'optimisation ici est de déterminer que la mémoire pointée par ss n'est altérée par aucun code à l'intérieur de la boucle. C'est tout à fait faisable avec les compilateurs actuels, en fonction du code spécifique.
Eric Postpischil

Réponses:


138

Oui, strlen()sera évalué à chaque itération. Il est possible que, dans des circonstances idéales, l'optimiseur puisse en déduire que la valeur ne changera pas, mais je ne me fierais personnellement pas à cela.

Je ferais quelque chose comme

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

ou éventuellement

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

tant que la chaîne ne changera pas de longueur pendant l'itération. Si c'est le cas, vous devrez soit appeler à strlen()chaque fois, soit le gérer selon une logique plus compliquée.


14
Si vous savez que vous ne manipulez pas la chaîne, la seconde est bien plus préférable car c'est essentiellement la boucle qui sera exécutée de strlentoute façon.
mlibpar

26
@alk: Si la chaîne peut être raccourcie, alors les deux sont faux.
Mike Seymour

3
@alk: si vous changez la chaîne, une boucle for n'est probablement pas le meilleur moyen d'itérer sur chaque caractère. Je pense qu'une boucle while est plus directe et plus facile à gérer le compteur d'index.
mlibpar le

2
les circonstances idéales incluent la compilation avec GCC sous Linux, où strlenest marqué comme __attribute__((pure))permettant au compilateur d'élider plusieurs appels. Attributs GCC
David Rodríguez - dribeas

6
La deuxième version est la forme idéale et la plus idiomatique. Il vous permet de passer la chaîne une seule fois plutôt que deux, ce qui aura de bien meilleures performances (en particulier la cohérence du cache) pour les longues chaînes.
R .. GitHub STOP HELPING ICE

14

Oui, chaque fois que vous utilisez la boucle. Ensuite, il calculera à chaque fois la longueur de la chaîne. alors utilisez-le comme ceci:

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

Dans le code ci-dessus str[i]ne vérifie qu'un caractère particulier dans la chaîne à l'emplacement ichaque fois que la boucle démarre un cycle, donc cela prendra moins de mémoire et sera plus efficace.

Consultez ce lien pour plus d'informations.

Dans le code ci-dessous, chaque fois que la boucle s'exécute strlen, la longueur de la chaîne entière sera comptée, ce qui est moins efficace, prend plus de temps et prend plus de mémoire.

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

3
Je suis d'accord avec "[c'est] plus efficace", mais utilise moins de mémoire? La seule différence d'utilisation de la mémoire à laquelle je peux penser serait dans la pile d'appels pendant l' strlenappel, et si vous êtes aussi serré, vous devriez probablement penser à éliminer quelques autres appels de fonction également ...
un CVn

@ MichaelKjörling Eh bien, si vous utilisez "strlen", alors dans une boucle, il doit parcourir toute la chaîne à chaque fois que la boucle s'exécute, alors que dans le code ci-dessus le "str [ix]", il ne scanne qu'un seul élément à chaque cycle du boucle dont l'emplacement est représenté par "ix". Ainsi, il faut moins de mémoire que "strlen".
codeDEXTER

1
Je ne suis pas sûr que cela ait beaucoup de sens, en fait. Une implémentation très naïve de strlen serait quelque chose commeint strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; } qui est à peu près exactement ce que vous faites dans le code de votre réponse. Je ne dis pas qu'itérer sur la chaîne une fois plutôt que deux est plus efficace en temps , mais je ne vois pas l'un ou l'autre utilisant plus ou moins de mémoire. Ou faites-vous référence à la variable utilisée pour contenir la longueur de la chaîne?
un CVn le

@ MichaelKjörling Veuillez consulter le code édité ci-dessus et le lien. Et en ce qui concerne la mémoire, chaque fois que la boucle s'exécute, chaque valeur itérée est stockée en mémoire et en cas de «strlen», car il compte la chaîne entière encore et encore, il nécessite plus de mémoire pour stocker. et aussi parce que contrairement à Java, C ++ n'a pas de "Garbage Collector". Alors je peux me tromper aussi. voir le lien concernant l'absence de "Garbage Collector" en C ++.
codeDEXTER

1
@ aashis2s L'absence de garbage collector ne joue un rôle que lors de la création d'objets sur le tas. Les objets sur la pile sont détruits dès que la portée et se termine.
Ikke

9

Un bon compilateur peut ne pas le calculer à chaque fois, mais je ne pense pas que vous puissiez être sûr que chaque compilateur le fait.

En plus de cela, le compilateur doit savoir que strlen(ss)cela ne change pas. Ceci n'est vrai que s'il ssn'est pas modifié en forboucle.

Par exemple, si vous utilisez une fonction en lecture seule sur ssdansfor loop mais que vous ne déclarez pas le ssparamètre -paramètre comme const, le compilateur ne peut même pas savoir que ce ssn'est pas changé dans la boucle et doit calculer strlen(ss)à chaque itération.


3
+1: Non seulement ne doit sspas être changé dans la forboucle; il ne doit pas être accessible depuis et modifié par une fonction appelée dans la boucle (soit parce qu'il est passé en argument, soit parce qu'il s'agit d'une variable globale ou d'une variable de portée fichier). La qualification des constants peut également être un facteur.
Jonathan Leffler

4
Je pense qu'il est hautement improbable que le compilateur sache que «ss» ne change pas. Il pourrait y avoir des pointeurs errants qui pointent vers la mémoire à l'intérieur de 'ss' dont le compilateur n'a aucune idée de qui pourraient changer 'ss'
MerickOWA

Jonathan a raison, une chaîne de const locale pourrait être le seul moyen pour le compilateur de s'assurer qu'il n'y a aucun moyen pour «s» de changer.
MerickOWA

2
@MerickOWA: en effet, c'est l'une des choses qui restrictest pour C99.
Steve Jessop

4
Concernant votre dernier para: si vous appelez une fonction en lecture seule ssdans la boucle for, alors même si son paramètre est déclaré const char*, le compilateur a encore besoin de recalculer la longueur à moins que (a) il sache que sspointe vers un objet const, au lieu d'être simplement un pointeur vers const, ou (b) il peut insérer la fonction ou voir autrement qu'elle est en lecture seule. Prendre un const char*paramètre n'est pas une promesse de ne pas modifier les données pointées, car il est valide de transtyper char*et de modifier à condition que l'objet modifié ne soit pas const et ne soit pas une chaîne littérale.
Steve Jessop

4

Si ssest de type const char *et que vous ne rejetez pas le constness dans la boucle, le compilateur ne peut appeler strlenqu'une seule fois, si les optimisations sont activées. Mais ce n'est certainement pas un comportement sur lequel on peut compter.

Vous devez enregistrer le strlenrésultat dans une variable et utiliser cette variable dans la boucle. Si vous ne souhaitez pas créer de variable supplémentaire, en fonction de ce que vous faites, vous pourrez peut-être vous en sortir en inversant la boucle pour itérer en arrière.

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

1
C'est une erreur d'appeler strlendu tout. Faites une boucle jusqu'à ce que vous atteigniez la fin.
R .. GitHub STOP HELPING ICE

i > 0? Cela ne devrait-il pas être i >= 0ici? Personnellement, je commencerais aussi par strlen(s) - 1itérer sur la chaîne à l'envers, alors la terminaison \0n'a pas besoin de considération particulière.
un CVn le

2
@ MichaelKjörling i >= 0ne fonctionne que si vous initialisez à strlen(s) - 1, mais si vous avez une chaîne de longueur nulle, la valeur initiale est inférieure
Prétorien

@ Prætorian, bon point sur la chaîne de longueur nulle. Je n'ai pas considéré ce cas lorsque j'ai écrit mon commentaire. C ++ évalue-t-il l' i > 0expression lors de l'entrée de boucle initiale? Si ce n'est pas le cas, alors vous avez raison, le cas de longueur nulle rompra définitivement la boucle. Si c'est le cas, vous obtenez "simplement" un signe i== -1 <0 donc pas d'entrée de boucle si le conditionnel est i >= 0.
un CVn du

@ MichaelKjörling Oui, la condition de sortie est évaluée avant d'exécuter la boucle pour la première fois. strlenLe type de retour de n'est pas signé, donc prend la valeur (strlen(s)-1) >= 0true pour les chaînes de longueur nulle.
Prétorien

3

Formellement oui, strlen()on s'attend à ce qu'il soit appelé à chaque itération.

Quoi qu'il en soit, je ne veux pas nier la possibilité de l'existence d'une optimisation intelligente du compilateur, qui optimisera tout appel successif à strlen () après le premier.


3

Le code du prédicat dans son intégralité sera exécuté à chaque itération de la forboucle. Afin de mémoriser le résultat de l' strlen(ss)appel, le compilateur aurait besoin de savoir qu'au moins

  1. La fonction strlenétait sans effet secondaire
  2. La mémoire pointée par ssne change pas pendant la durée de la boucle

Le compilateur ne sait ni l'une ni l'autre de ces choses et ne peut donc pas mémoriser en toute sécurité le résultat du premier appel


Eh bien, il pourrait savoir ces choses avec une analyse statique, mais je pense que votre point est que cette analyse n'est actuellement pas implémentée dans aucun compilateur C ++, oui?
GManNickG

@GManNickG cela pourrait certainement prouver le n ° 1 mais le n ° 2 est plus difficile. Pour un seul thread, oui, cela pourrait certainement le prouver, mais pas pour un environnement multi-thread.
JaredPar

1
Peut-être que je suis têtu, mais je pense que le numéro deux est également possible dans les environnements multithread, mais certainement pas sans un système d'inférence extrêmement puissant. Juste rêver ici cependant; définitivement au-delà de la portée de tout compilateur C ++ actuel.
GManNickG

@GManNickG Je ne pense pas que ce soit possible en C / C ++. Je pourrais très facilement cacher l'adresse de ssen un size_tou la diviser entre plusieurs bytevaleurs. Mon thread sournois pourrait alors simplement écrire des octets dans cette adresse et le compilateur aurait un moyen de comprendre à quoi il s'agissait ss.
JaredPar

1
@JaredPar: Désolé de cogner, vous pouvez prétendre que int a = 0; do_something(); printf("%d",a);cela ne peut pas être optimisé, car cela do_something()pourrait faire votre chose int non initialisée, ou pourrait remonter la pile et modifier adélibérément. En fait, gcc 4.5 l'optimise do_something(); printf("%d",0);avec -O3
Steve Jessop

2

Oui . strlen sera calculé à chaque fois que i augmente.

Si vous n'avez pas changé ss avec dans la boucle , cela n'affectera pas la logique, sinon cela affectera.

Il est plus sûr d'utiliser le code suivant.

int length = strlen(ss);

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

2

Oui, le strlen(ss)calculera la longueur à chaque itération. Si vous augmentez ssd'une manière ou d'une autre le i; il y aurait une boucle infinie.


2

Oui, la strlen()fonction est appelée à chaque fois que la boucle est évaluée.

Si vous voulez améliorer l'efficacité, pensez toujours à tout sauvegarder dans des variables locales ... Cela prendra du temps mais c'est très utile.

Vous pouvez utiliser le code comme ci-dessous:

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

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


2

Pas courant de nos jours mais il y a 20 ans sur les plates-formes 16 bits, je recommanderais ceci:

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

Même si votre compilateur n'est pas très intelligent en matière d'optimisation, le code ci-dessus peut encore donner un bon code d'assemblage.


1

Oui. Le test ne sait pas que ss n'est pas modifié à l'intérieur de la boucle. Si vous savez que cela ne changera pas, j'écrirais:

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

1

Arrgh, ça va, même dans des circonstances idéales, bon sang!

À partir d'aujourd'hui (janvier 2018), et gcc 7.3 et clang 5.0, si vous compilez:

#include <string.h>

void bar(char c);

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

Donc nous avons:

  • ss est un pointeur constant.
  • ss est marqué __restrict__
  • Le corps de la boucle ne peut en aucun cas toucher la mémoire pointée par ss(enfin, sauf s'il viole le __restrict__).

et encore , les deux compilateurs exécutent strlen() chaque itération de cette boucle . Incroyable.

Cela signifie également que les allusions / vœux pieux de @Praetorian et @JaredPar ne fonctionnent pas.


0

OUI, en termes simples. Et il y a un petit non dans une condition rare dans laquelle le compilateur le souhaite, comme étape d'optimisation s'il constate qu'il n'y a pas du tout de modifications apportées ss. Mais en bon état, vous devriez le penser comme OUI. Il y a des situations comme un programme in multithreadedet event, il peut devenir bogué si vous le considérez comme NON. Soyez prudent car cela n'améliorera pas trop la complexité du programme.


0

Oui.

strlen()calculé à chaque fois lorsqu'il iaugmente et n'est pas optimisé.

Le code ci-dessous montre pourquoi le compilateur ne doit pas optimiser strlen().

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

Je pense que faire cette modification particulière ne modifie jamais la longueur de ss, juste son contenu, donc (un compilateur vraiment, vraiment intelligent) pourrait encore optimiser strlen.
Darren Cook

0

Nous pouvons facilement le tester:

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

La condition de boucle est évaluée après chaque répétition, avant de redémarrer la boucle.

Faites également attention au type que vous utilisez pour gérer la longueur des chaînes. ce devrait être celui size_tqui a été défini comme unsigned intdans stdio. le comparer et le convertir intpeut causer de sérieux problèmes de vulnérabilité.


0

eh bien, j'ai remarqué que quelqu'un dit qu'il est optimisé par défaut par n'importe quel compilateur moderne "intelligent". En passant, regardez les résultats sans optimisation. J'ai essayé:
Code C minimal:

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

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

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

Mon compilateur: g ++ (Ubuntu / Linaro 4.6.3-1ubuntu5) 4.6.3
Commande pour la génération du code d'assemblage: 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
     .....

Je répugnerais à faire confiance à tout compilateur qui tenterait de l'optimiser à moins que l'adresse de la chaîne ne soit restrictqualifiée. S'il existe certains cas où une telle optimisation serait légitime, l'effort requis pour identifier de manière fiable de tels cas en l'absence de restrictserait, par toute mesure raisonnable, presque certainement dépasser l'avantage. Si l'adresse de la chaîne avait un const restrictqualificatif, cependant, ce serait suffisant en soi pour justifier l'optimisation sans avoir à regarder autre chose.
supercat

0

En élaborant sur la réponse de Prætorian, je recommande ce qui suit:

for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
  • autocar vous ne voulez pas vous soucier du type de retour de strlen. Un compilateur C ++ 11 (par exemplegcc -std=c++0x , pas complètement C ++ 11 mais les types automatiques fonctionnent) le fera pour vous.
  • i = strlen(s)parce que vous voulez comparer à 0(voir ci-dessous)
  • i > 0 parce que la comparaison à 0 est (légèrement) plus rapide que la comparaison à tout autre nombre.

L'inconvénient est que vous devez utiliser i-1pour accéder aux caractères de la chaîne.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.