Будет ли strlen вычисляться несколько раз при использовании в условии цикла?


109

Я не уверен, может ли следующий код вызывать избыточные вычисления или это зависит от компилятора?

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

Будет strlen()ли рассчитываться каждый раз при iувеличении?


14
Я собираюсь предположить, что без сложной оптимизации, которая может обнаружить, что ss никогда не меняется в цикле, тогда да. Лучше скомпилировать и посмотреть на сборку.
MerickOWA 06

6
Это зависит от компилятора, от уровня оптимизации и от того, что вы (можете) делать ssвнутри цикла.
Христо Илиев

4
Если компилятор может доказать, что ssникогда не изменялся, он может вывести вычисление из цикла.
Daniel Fischer

10
@Mike: «требуется анализ во время компиляции того, что именно делает strlen» - strlen, вероятно, является внутренним, и в этом случае оптимизатор знает, что он делает.
Стив Джессоп,

3
@MikeSeymour: Нет, может быть, нет. strlen определяется стандартом языка C, и его имя зарезервировано для использования, определенного языком, поэтому программа не может предоставить другое определение. Компилятор и оптимизатор имеют право предположить, что strlen зависит исключительно от его ввода и не изменяет его или какое-либо глобальное состояние. Задача оптимизации здесь заключается в том, чтобы определить, что память, на которую указывает ss, не изменяется никаким кодом внутри цикла. Это вполне возможно с текущими компиляторами, в зависимости от конкретного кода.
Eric Postpischil 06

Ответы:


138

Да, strlen()будет оцениваться на каждой итерации. Возможно, что в идеальных обстоятельствах оптимизатор сможет сделать вывод, что значение не изменится, но я лично не стал бы на это полагаться.

Я бы сделал что-то вроде

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

или возможно

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

при условии, что строка не изменит длину во время итерации. Если это возможно, вам нужно будет либо вызывать strlen()каждый раз, либо обрабатывать его с помощью более сложной логики.


14
Если вы знаете, что не манипулируете строкой, второй вариант гораздо предпочтительнее, поскольку это, по сути, цикл, который будет выполняться в strlenлюбом случае.
mlibby 06

26
@alk: Если строка может быть сокращена, то оба варианта неверны.
Майк Сеймур,

3
@alk: если вы меняете строку, цикл for, вероятно, не лучший способ перебора каждого символа. Я бы подумал, что цикл while более прямой и более легкий для управления счетчиком индекса.
mlibby 06

2
идеальные обстоятельства включают компиляцию с GCC под Linux, где strlenпомечено как __attribute__((pure))позволяющее компилятору исключить несколько вызовов. Атрибуты GCC
Дэвид Родригес - dribeas 06

6
Вторая версия - идеальная и самая идиоматическая форма. Он позволяет передавать строку только один раз, а не дважды, что будет иметь гораздо лучшую производительность (особенно согласованность кеша) для длинных строк.
R .. GitHub НЕ ПОМОГАЕТ ICE 06

14

Да, каждый раз, когда вы используете цикл. Тогда он будет каждый раз вычислять длину строки. так что используйте это так:

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

В приведенном выше коде каждый раз, когда цикл запускает цикл, str[i]проверяется только один конкретный символ в строке в определенном месте i, поэтому он будет занимать меньше памяти и будет более эффективным.

См. Эту ссылку для получения дополнительной информации.

В приведенном ниже коде каждый раз при запуске цикла strlenбудет подсчитываться длина всей строки, что менее эффективно, занимает больше времени и занимает больше памяти.

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

3
Я могу согласиться с «[это] более эффективно», но использовать меньше памяти? Единственная разница в использовании памяти, о которой я могу думать, будет в стеке вызовов во время strlenвызова, и если вы работаете так напряженно, вам, вероятно, следует подумать об исключении нескольких других вызовов функций ...
CVn

@ MichaelKjörling Хорошо, если вы используете «strlen», тогда в цикле он должен сканировать всю строку при каждом запуске цикла, тогда как в приведенном выше коде «str [ix]» сканирует только один элемент в течение каждого цикла цикл, расположение которого обозначено "ix". Таким образом, он занимает меньше памяти, чем strlen.
codeDEXTER 06

1
На самом деле я не уверен, что это имеет большой смысл. Очень наивная реализация strlen будет чем-то вроде того, int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }что в значительной степени соответствует тому, что вы делаете в коде своего ответа. Я не утверждаю, что итерация по строке один раз, а не два, более эффективна по времени , но я не вижу, чтобы тот или другой использовал больше или меньше памяти. Или вы имеете в виду переменную, используемую для хранения длины строки?
CVn

@ MichaelKjörling См. Отредактированный выше код и ссылку. А что касается памяти - каждый раз, когда цикл запускается, каждое повторяющееся значение сохраняется в памяти, а в случае 'strlen', поскольку он снова и снова считает всю строку, ему требуется больше памяти для хранения. а также потому, что в отличие от Java, в C ++ нет «сборщика мусора». Тогда я тоже могу ошибаться. см. ссылку об отсутствии «Сборщика мусора» в C ++.
codeDEXTER 06

1
@ aashis2s Отсутствие сборщика мусора играет роль только при создании объектов в куче. Объекты в стеке уничтожаются, как только область видимости и заканчивается.
Ikke

9

Хороший компилятор может не вычислять его каждый раз, но я не думаю, что вы можете быть уверены, что каждый компилятор это делает.

Кроме того, компилятор должен знать, что strlen(ss)это не меняется. Это верно, только если ssне изменяется в forцикле.

Например, если вы используете функцию только для чтения ssв forцикле in, но не объявляете параметр ss-параметр as const, компилятор даже не может знать, что ssне было изменено в цикле, и должен производить вычисления strlen(ss)на каждой итерации.


3
+1: не только ssнельзя изменять в forцикле; он не должен быть доступен и изменен любой функцией, вызываемой в цикле (либо потому, что он передается как аргумент, либо потому, что это глобальная переменная или переменная области видимости файла). Const-квалификация также может быть фактором.
Джонатан Леффлер

4
Я думаю, что очень маловероятно, что компилятор мог знать, что ss не меняется. Могут быть случайные указатели, указывающие на память внутри 'ss', о которых компилятор не знает, которые могут изменить 'ss'
MerickOWA 06

Джонатан прав, локальная константная строка может быть единственным способом убедиться в том, что компилятор не сможет изменить ss.
MerickOWA 06

2
@MerickOWA: действительно, это одна из вещей, которые restrictесть в C99.
Стив Джессоп,

4
Что касается вашего последнего параграфа: если вы вызываете функцию только для чтения ssв цикле for, то, даже если ее параметр объявлен const char*, компилятору все равно нужно пересчитать длину, если только (а) он не знает, что ssуказывает на объект const, вместо того, чтобы быть просто указателем на константу, или (б) он может встроить функцию или иным образом увидеть, что она доступна только для чтения. Принятие const char*параметра не является обещанием не изменять указанные данные, потому что приведение char*и изменение допустимо при условии, что измененный объект не является константой и не является строковым литералом.
Стив Джессоп,

4

Если ssимеет тип, const char *и вы не отбрасываете constсущность цикла, компилятор может вызвать только strlenодин раз, если включена оптимизация. Но точно не на такое поведение можно рассчитывать.

Вы должны сохранить strlenрезультат в переменной и использовать эту переменную в цикле. Если вы не хотите создавать дополнительную переменную, в зависимости от того, что вы делаете, возможно, вам удастся перевернуть цикл и выполнить итерацию назад.

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

1
Звонить strlenвообще - ошибка . Просто зацикливайтесь, пока не дойдете до конца.
R .. GitHub НЕ ПОМОГАЕТ ICE 06

i > 0? Разве этого не должно быть i >= 0здесь? Лично я бы также начал с того, что strlen(s) - 1если перебирать строку в обратном направлении, то завершение не \0требует особого рассмотрения.
CVn

2
@ MichaelKjörling i >= 0работает только в том случае, если вы инициализируете его strlen(s) - 1, но затем, если у вас есть строка нулевой длины, начальное значение исчезает
преторианец

@ Prtorian, хорошее замечание о строке нулевой длины. Я не учел этот случай, когда писал свой комментарий. Оценивает ли C ++ i > 0выражение при входе в начальный цикл? Если нет, то вы правы, случай нулевой длины обязательно разорвет цикл. Если это так, вы «просто» получите знаковый i== -1 <0, поэтому в цикле не будет записи, если условие есть i >= 0.
CVn

@ MichaelKjörling Да, условие выхода оценивается перед выполнением цикла в первый раз. strlenТип возвращаемого значения беззнаковый, поэтому (strlen(s)-1) >= 0принимает значение true для строк нулевой длины.
Praetorian

3

Формально да, strlen()ожидается, что будет вызываться на каждой итерации.

В любом случае я не хочу отрицать возможность существования некоторой умной оптимизации компилятора, которая оптимизирует любой последующий вызов strlen () после первого.


3

Код предиката в целом будет выполняться на каждой итерации forцикла. Чтобы запомнить результат strlen(ss)вызова, компилятор должен знать, что по крайней мере

  1. Функция не strlenимела побочных эффектов
  2. Память, на которую указывает ss, не изменяется в течение цикла.

Компилятор не знает ни одной из этих вещей и, следовательно, не может безопасно запомнить результат первого вызова.


Что ж, он мог бы знать эти вещи с помощью статического анализа, но я думаю, что вы думаете, что такой анализ в настоящее время не реализован ни в каких компиляторах C ++, да?
GManNickG 06

@GManNickG определенно может оказаться №1, но №2 сложнее. Для одного потока да, это определенно можно доказать, но не для многопоточной среды.
JaredPar 06

1
Может быть, я проявляю упрямство, но я думаю, что номер два возможен и в многопоточных средах, но определенно не без чрезвычайно сильной системы вывода. Просто размышляю здесь; определенно выходит за рамки любого текущего компилятора C ++.
GManNickG 06

@GManNickG Я не думаю, что это возможно в C / C ++. Я мог бы очень легко спрятать адрес ssв size_tили разделить его на несколько byteзначений. Тогда мой коварный поток мог бы просто записать байты в этот адрес, и компилятор знал бы способ понять, к чему это относится ss.
JaredPar 06

1
@JaredPar: извините за удар, вы можете заявить, что int a = 0; do_something(); printf("%d",a);это не может быть оптимизировано, на основании того, что это do_something()может сделать вашу неинициализированную вещь int или может сканировать резервную копию стека и aсознательно изменять . Фактически, gcc 4.5 оптимизирует его до do_something(); printf("%d",0);-O3
Стив Джессоп,

2

Да . strlen будет вычисляться каждый раз, когда i увеличивается.

Если вы не изменили ss с помощью в цикле, это не повлияет на логику, иначе это повлияет.

Безопаснее использовать следующий код.

int length = strlen(ss);

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

2

Да, strlen(ss)длина будет вычисляться на каждой итерации. Если вы ssкаким-то образом увеличиваете, а также увеличиваете i; был бы бесконечный цикл.


2

Да, strlen()функция вызывается каждый раз при оценке цикла.

Если вы хотите повысить эффективность, всегда не забывайте сохранять все в локальных переменных ... Это займет время, но очень полезно ..

Вы можете использовать следующий код:

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

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


2

В настоящее время это не распространено, но 20 лет назад на 16-битных платформах я бы рекомендовал это:

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

Даже если ваш компилятор не очень умен в оптимизации, приведенный выше код все же может дать хороший ассемблерный код.


1

Да. Тест не знает, что ss не изменяется внутри цикла. Если вы знаете, что это не изменится, я бы написал:

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

1

Да, черт возьми, даже при идеальных обстоятельствах!

На сегодняшний день (январь 2018 г.) и gcc 7.3 и clang 5.0, если вы скомпилируете:

#include <string.h>

void bar(char c);

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

Итак, имеем:

  • ss постоянный указатель.
  • ss отмечен __restrict__
  • Тело цикла никоим образом не может касаться памяти, на которую указывает ss(ну, если это не нарушает __restrict__).

и тем не менее оба компилятора выполняют strlen() каждую итерацию этого цикла . Удивительный.

Это также означает, что намек / принятие желаемого за действительное со стороны @Praetorian и @JaredPar не срабатывает.


0

ДА, простыми словами. И есть небольшое «нет» в редких случаях, когда компилятор желает этого, в качестве шага оптимизации, если он обнаруживает, что никаких изменений не было сделано ssвообще. Но в безопасном состоянии вы должны думать как ДА. Есть некоторые ситуации, такие как multithreadedпрограмма, управляемая событиями и событиями, она может работать с ошибками, если вы считаете это НЕТ. Будьте осторожны, так как это не сильно улучшит сложность программы.


0

Да.

strlen()рассчитывается каждый раз, когда iувеличивается и не оптимизируется.

Код ниже показывает, почему компилятор не должен оптимизировать strlen().

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

Я думаю, что эта конкретная модификация никогда не изменяет длину ss, а только ее содержимое, поэтому (действительно, очень умный) компилятор все еще может оптимизировать strlen.
Даррен Кук

0

Мы легко можем это проверить:

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

Состояние цикла оценивается после каждого повторения перед перезапуском цикла.

Также будьте осторожны с типом, который вы используете для обработки длины строк. это должно быть то, size_tчто было определено как unsigned intв stdio. его сравнение и приведение intможет вызвать серьезную проблему уязвимости.


0

ну, я заметил, что кто-то говорит, что он по умолчанию оптимизирован любым «умным» современным компилятором. Кстати посмотрите результаты без оптимизации. Я пробовал:
Минимальный код C:

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

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

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

Мой компилятор: g ++ (Ubuntu / Linaro 4.6.3-1ubuntu5) 4.6.3
Команда для генерации кода сборки: 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
     .....

Я бы не хотел доверять любому компилятору, который пытался его оптимизировать, если адрес строки не был restrictопределен. Хотя есть некоторые случаи, когда такая оптимизация была бы законной, усилия, необходимые для надежного выявления таких случаев, в отсутствие restrictкаких-либо разумных мер, почти наверняка превысят выгоду. Однако, если бы адрес строки имел const restrictквалификатор, этого было бы достаточно, чтобы оправдать оптимизацию, не обращая внимания ни на что другое.
supercat

0

Разрабатывая ответ преторианца, я рекомендую следующее:

for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
  • autoпотому что вам не нужно заботиться о том, какой тип возвращает strlen. Компилятор C ++ 11 (например gcc -std=c++0x, не полностью C ++ 11, но работают автоматические типы) сделает это за вас.
  • i = strlen(s)потому что вы хотите сравнить 0(см. ниже)
  • i > 0 потому что сравнение с 0 (немного) быстрее, чем сравнение с любым другим числом.

Недостатком является то, что вы должны использовать i-1для доступа к строковым символам.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.