Чи буде струн обчислюватися в кілька разів, якщо його використовувати в умовах циклу?


109

Я не впевнений, чи може наступний код викликати надмірні обчислення, чи це специфічний для компілятора?

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

Чи strlen()буде обчислюватися кожен раз, коли iзбільшуватиметься?


14
Я буду здогадуватися, що без складної оптимізації, яка може виявити, що 's' ніколи не змінюється в циклі, тоді так. Найкраще скласти і подивитися на збірку, щоб побачити.
MerickOWA

6
Це залежить від компілятора, рівня оптимізації та того, що ви (можливо) зробите ssвсередині циклу.
Христо Ілієв

4
Якщо компілятор може довести, що ssніколи не змінюється, він може підняти обчислення з циклу.
Даніель Фішер

10
@Mike: "вимагає аналізу часу компіляції саме того, що робить strlen" - strlen, ймовірно, суттєвий, і в цьому випадку оптимізатор знає, що робить.
Стів Джессоп

3
@MikeSeymour: Неможливо, може, й ні. strlen визначається стандартом мови C, а його ім'я зарезервоване для використання, визначеного мовою, тому програма не вільна для надання іншого визначення. Компілятор і оптимізатор мають право припускати, що струн залежить виключно від його введення та не змінює його чи будь-який глобальний стан. Завданням для оптимізації тут є визначення того, що пам'ять, на яку вказує ss, не змінюється жодним кодом всередині циклу. Це цілком можливо з поточними компіляторами, залежно від конкретного коду.
Eric Postpischil

Відповіді:


138

Так, strlen()буде оцінено по кожній ітерації. Можливо, за ідеальних обставин оптимізатор може виявити, що значення не зміниться, але я особисто на це не покладався.

Я б робив щось подібне

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

або можливо

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

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


14
Якщо ви знаєте, що ви не маніпулюєте рядком, другий є набагато кращим, оскільки це по суті цикл, який буде виконуватися strlenбудь-яким чином.
mlibby

26
@alk: Якщо рядок може скоротитися, вони обидва помиляються.
Майк Сеймур

3
@alk: якщо ви змінюєте рядок, цикл for для циклу, ймовірно, не є найкращим способом ітерації над кожним символом. Я думаю, що певний час цикл є більш прямим і простішим в управлінні лічильником індексів.
mlibby

2
Ідеальні обставини включають компіляцію з GCC під Linux, де strlenпозначено як __attribute__((pure))дозволяє компілятору ухилятися від декількох викликів. Атрибути GCC
David Rodríguez - dribeas

6
Друга версія - це ідеальна і найбільш ідіоматична форма. Це дозволяє переходити по рядку лише один раз, а не двічі, що матиме набагато кращі показники (особливо кохерентність кешу) для довгих рядків.
R .. GitHub ЗАСТАНОВИТИ ДІЯ

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

1
Я не впевнений, що це має багато сенсу. Дуже наївна реалізація strlen була б чимось на кшталт того, int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }що в значній мірі саме те, що ви робите в коді. Я не стверджую , що ітерація рядок один раз , а не в два рази більше часу -efficient, але я не бачу один або інший , використовуючи більш-менше пам'яті. Або ви маєте на увазі змінну, яка використовується для утримання довжини рядка?
CVn

@ MichaelKjörling Перегляньте вище відредагований код та посилання. Що стосується пам’яті - щоразу, коли цикл працює, кожне ітераційне значення зберігається в пам’яті, а у випадку «strlen», оскільки він знову і знову рахує цілий рядок, для зберігання потрібно більше пам’яті. а також тому, що на відміну від Java, у C ++ немає "сміттєзбірника". Тоді я можу помилитися також. дивіться посилання щодо відсутності "сміттєзбірника" в C ++.
codeDEXTER

1
@ aashis2s Відсутність сміттєзбірника відіграє лише роль при створенні об'єктів на купі. Об'єкти в стеці руйнуються, як тільки область і закінчується.
Ikke

9

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

Крім того, компілятор повинен знати, що strlen(ss)не змінюється. Це справедливо лише в тому випадку, якщо ssце не змінюється в forциклі.

Наприклад, якщо ви використовуєте функцію лише для читання ssу forциклі, але не оголошуєте параметр ssяк const, компілятор навіть не може знати, що ssце не змінюється в циклі, і він повинен обчислювати strlen(ss)кожну ітерацію.


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

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

Джонатан має рацію, локальний рядок const може бути єдиним способом впевнитись у компіляторі, що немає можливості зміни 'ss'.
MerickOWA

2
@MerickOWA: дійсно, це одна з речей, що restrictстосуються C99.
Стів Джессоп

4
Щодо останнього параграфа: якщо ви викликаєте функцію лише для читання ssу циклі for-циклу, то навіть якщо її параметр оголошений const char*, компілятору все-таки потрібно перерахувати довжину, якщо будь-який (a) не знає, що ssвказує на об'єкт const, на відміну від просто вказівника на const або (b) він може вбудовувати функцію або іншим чином бачити, що вона є лише для читання. Прийняття const char*параметра - це не обіцянка не змінювати вказані дані, оскільки воно може бути char*передано та змінено за умови, що змінений об'єкт не const і не є літеральним рядком.
Стів Джессоп

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 СТОП ДОПОМОГА ВІД

i > 0? Хіба цього не повинно бути i >= 0тут? Особисто я також би почав, strlen(s) - 1якби повторювати рядок назад, тоді закінчення \0не потребує особливого розгляду.
CVn

2
@ MichaelKjörling i >= 0працює лише в тому випадку, якщо ви ініціалізуєтесь strlen(s) - 1, але тоді, якщо у вас є рядок на нульовій довжині, початкове значення переливається
Преторіан

@ Prætorian, хороша точка в рядку нульової довжини. Я не розглядав цей випадок, коли писав свій коментар. Чи оцінює C ++ i > 0вираз при початковому введенні циклу? Якщо це не так, значить, ви праві, нульова довжина справи обов'язково порушить цикл. Якщо це так, ви "просто" отримуєте підписаний i== -1 <0, тому немає запису циклу, якщо це умовно i >= 0.
CVn

@ MichaelKjörling Так, умова виходу оцінюється перед першим виконанням циклу. strlenтип повернення не підписаний, тому (strlen(s)-1) >= 0обчислюється як "true" для рядків нульової довжини.
Преторіан

3

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

У всякому разі, я не хочу заперечувати можливість існування якоїсь розумної оптимізації компілятора, яка оптимізує будь-який наступний виклик strlen () після першого.


3

Код предиката в повному обсязі буде виконуватися на кожній ітерації forциклу. Для запам'ятовування результату strlen(ss)виклику компілятору потрібно знати, щонайменше

  1. Функція не strlenбула побічною дією
  2. Пам'ять, на яку ssвказує, не змінюється протягом тривалості циклу

Компілятор не знає жодної з цих речей, а тому не може безпечно запам'ятати результат першого дзвінка


Ну, це могло б знати ті речі зі статичним аналізом, але я думаю, ваш твір полягає в тому, що такий аналіз наразі не реалізується в жодних компіляторах C ++, так?
GManNickG

@GManNickG це однозначно можна довести №1, але №2 важче. Для одного потоку так, це точно можна довести, але не для багатопотокового середовища.
JaredPar

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

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

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

2

Так . strlen буде обчислюватися щоразу, коли я збільшується.

Якщо ви не змінювали сс з в петльових засобах це не впливатиме на логіку в іншому випадку це буде впливати.

Безпечніше використовувати наступний код.

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.