logo search
Языки программирования

6.4. Цикл for

Очень часто мы знаем количество итераций цикла: это либо константа, извест­ная при написании программы, либо значение, вычисляемое перед началом цикла. Цикл со счетчиком можно запрограммировать следующим образом:

int i; /* Индекс цикла */

C

int low, high; /* Границы цикла */

i = low; /* Инициализация индекса */

while (i <= high) { /* Вычислить условие выхода */

statement;

i++: /* Увеличить индекс */

};

Поскольку это общая парадигма, постольку для упрощения программирова­ния во всех (императивных) языках есть цикл for. Его синтаксис в языке С следующий:

int i; /* Индекс цикла */

int low, high; /* Границы цикла */

C

for (i = low; i <= high; i++) {

statement;

}

В Ada синтаксис аналогичный, за исключением того, что объявление и увели­чение переменной цикла неявные:

Low, High: Integer;

Ada


for I in Low .. High loop

statement;

end loop;

Ниже в этом разделе мы обсудим причины этих различий.

Известно, что в циклах for легко сделать ошибки в значениях границ. Цикл выполняется для каждого из значений от low до high; таким образом, общее число итераций равно high - low +1. Однако, если значение low строго больше значения high, цикл будет выполнен ноль раз. Если вы хотите выполнить цикл точно Л/ раз, цикл for будет иметь вид:

Ada

forlinl ..N loop...

и число итераций равно N -1 + 1 = N. В языке С из-за того, что для массивов индексы должны начинаться с нуля, цикл со счетчиком обычно записывается так:

C

for(i = 0;i<n;i++)...

Так как запись i < п означает то же самое, что и i <= (п - 1), цикл выполняется (п -1)-0+1 =п раз, как и требуется.

Обобщения в циклах for

Несмотря на то, что все процедурные языки содержат циклы for, они значи­тельно отличаются по предоставляемым дополнительным возможностям. Две крайности — это Ada и С.

В Ada исходным является положение, что цикл for должен использоваться только с фиксированным числом итераций и что это число можно вычислить перед началом цикла. Объясняется это следующим: 1) большинство реальных циклов простые, 2) другие конструкции легко запрограммировать в явном ви­де, и 3) циклы for и сами по себе достаточно трудны для тестирования и про­верки. В языке Ada нет даже классического обобщения: увеличения перемен­ной цикла на значения, отличные от 1 (или -1). Язык Algol позволяет написать итерации для последовательности нечетных чисел:

Algol


for I := 1 to N step 2 do ...

в то время как в Ada мы должны явно запрограммировать их вычисление:

for l in 1 .. (N + 1)/2 loop

Ada


|1 =2*|-1;

end loop;

В языке С все три элемента цикла for могут быть произвольными выражения­ми:

C

for(i=j*k; (i<n)&&(j + k>m); i + = 2*j)...

В описании С определено, что оператор

C

for (expression_1 ; expression_2; expression_3) statement;

эквивалентен конструкции

C

for(expression_1 ;

while (expression_2) {

statement;

expression_3;

}

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

Ada

for I in expression_1 .. expression_2 loop

statement;

end loop;

эквивалентно

I = expression_1;

Ada

Temp = expression_2;

while (I < Temp) loop

statement;

I: = I + 1;

end loop;

Если тело цикла изменяет значение переменных, используемых при вычис­лении выражения expression_2, то верхняя граница цикла в Ada изменяться не будет. Сравните это с данным выше описанием цикла for в языке С, кото­рый заново вычисляет значение выражения expression_2 на каждой итерации.

Обобщения в языке С — нечто большее, чем просто «синтаксический сахар», поскольку операторы внутри цикла, изменяющие выражения expres-sion_2 и expression_3, могут вызывать побочные эффекты. Побочных эффек­тов следует избегать по следующим причинам.

• Побочные эффекты затрудняют полную проверку и тестирование цикла.

• Побочные эффекты неблагоприятно воздействуют на читаемость и под­держку программы.

•Побочные эффекты делают цикл гораздо менее эффективным, потому что выражения expression_2 и expression_3 нужно заново вычислять на каждой итерации. Если побочных эффектов нет, оптимизирующий ком­пилятор может вынести эти вычисления за границу цикла.

Реализация

Циклы for — наиболее часто встречающиеся источники неэффективности в программах, потому что небольшие различия в языках или небольшие изме­нения в использовании оператора могут иметь серьезные последствия. Во многих случаях оптимизатор в состоянии решить эти проблемы, но лучше их понимать и избегать, чем доверяться оптимизатору. В этом разделе мы более подробно опишем реализацию на уровне регистров.

В языке Ada цикл

Ada

for I in expression_1 .. expression_2 loop

statement;

end loop;

компилируется в

compute R1,expr_1

store R1,l Нижняя граница индексации

compute R2,expr_2

store R2,High Верхняя граница индексации

L1: load R1,l Загрузить индекс

load R2,High Загрузить верхнюю границу

jump_gt R1,R2,L2 Завершить цикл, если больше

statement Тело цикла

load R1,l Увеличить индекс

incr R1

store R1,l

jump L1

L2:

Очевидная оптимизация — это закрепление регистра за индексной перемен­ной I и, если возможно, еще одного регистра за High:

compute R1 ,ехрг_1 Нижняя граница в регистре

compute R2,expr_2 Верхняя граница в регистре

L1: jump_gt R1,R2,L2 Завершить цикл, если больше

statement

incr R1 Увеличить индексный регистр

jump L1

L2:

Рассмотрим теперь простой цикл в языке С:

C

for (i = expression_1 ; expression_2; i++)

statement;

Это компилируется в

compute R1,expr_1

store R1,i Нижняя граница индексации

L1: compute R2,expr_2 Верхняя граница внутри цикла!

jump_gt R1,R2,L2 Завершить цикл, если больше

statement Тело цикла

load R1,i Увеличить индекс

incr R1

store R1,i

jump L1

L2:

Обратите внимание, что выражение expression_2, которое может быть очень сложным, теперь вычисляется внутри цикла. Кроме того, выражение expres-sion_2 обязательно использует значение индексной переменной i, которая из­меняется при каждой итерации. Таким образом, оптимизатор должен уметь выделить неизменяющуюся часть вычисления выражения expression_2, что­бы вынести ее из цикла.

Можно ли хранить индексную переменную только в регистре для увеличения эффективности? Ответ зависит от двух свойств цикла. В Ada индексная пере­менная считается константой и не может изменяться программистом. В языке С индексная переменная — это обычная переменная; она может храниться в реги­стре только в том случае, когда абсолютно исключено изменение ее текущего значения где-либо вне цикла. Никогда не используйте глобальную переменную в качестве индексной переменной, потому что другая процедура может прочи­тать или изменить ее значение:

C

int i;

void p2(void) {

i = i + 5;

}

void p1(void) {

for (i=0; i<100; i++) /* Глобальная индексная переменная */

p2(); /* Побочный эффект изменит индекс*/

}

Второе свойство, от которого зависит оптимизация цикла, — потенциальная возможность использования индексной переменной за пределами цикла. В Ada индексная переменная неявно объявляется for-оператором и недоступна за пределами цикла. Таким образом, независимо от того, как осуществляется выход из цикла, мы не должны сохранять значение регистра. Рассмотрим сле­дующий цикл поиска значения key в массиве а:

C

inta[100];

int i, key;

key = get_key();

for(i = 0;i< 100; i++)

if (a[i] == key) break;

process(i);

Переменная i должна содержать правильное значение независимо от спо­соба, которым был сделан выход из цикла. Это может вызывать затруднения при попытке оптимизировать код. Обратите внимание, что в Ada требуется явное кодирование для достижения того же самого результата, потому что ин­дексная переменная не существует вне области цикла:

Ada

Found: Integer := False;

for I in 1 ..100 loop

if A(l) = Key then

Found = I;

exit;

end if;

end loop;

Определение области действия индексов цикла в языке C++ с годами меня­лось, но конечное определение такое же, как в Ada: индекс не существует вне области цикла:

for(int i=0;i<100;i++){

C++

// Индексная переменная является локальной для цикла

}

На самом деле в любом управляемом условием операторе (включая, if- и switch-операторы) можно задать в условии несколько объявлений; область их действия будет ограничена управляющим оператором. Это свойство может способствовать читаемости и надежности программы, предотвращая непред­намеренное использование временного имени.