do while, exit, cycle и do concurrent
Когда число повторений заранее неизвестно, на сцену выходят do while, бесконечный do и управление потоком через exit и cycle.
Цикл do while повторяет тело, пока истинно заданное условие; exit досрочно выходит из цикла, cycle переходит к следующей итерации, минуя остаток тела.
Счётный do хорош, когда число итераций известно заранее. Но итерационные методы — Ньютона, простых итераций, бисекции — крутятся «пока не сошлось», и заранее неизвестно сколько. Для таких задач Fortran даёт условный цикл do while, бесконечный do с явным выходом, а также операторы тонкого управления exit и cycle. Этот урок завершает тему циклов самыми важными для численных методов конструкциями и знакомит с современным do concurrent для параллельных вычислений.
Два рода повторения: счёт и условие
Полезно мысленно разделить все циклы на два рода. Первый — счётные: мы заранее знаем, сколько раз повторить, и об этом был предыдущий урок. Второй — условные: мы повторяем, пока выполняется некоторое условие, а сколько это займёт шагов — выясняется только в процессе. Подавляющая часть «интересных» вычислений относится ко второму роду. Метод Ньютона сходится за разное число шагов в зависимости от начального приближения и требуемой точности; чтение данных продолжается, пока не встретится конец файла; моделирование идёт, пока система не достигнет равновесия. Заранее посчитать число итераций здесь невозможно в принципе — оно зависит от данных.
Исторически условные циклы в Fortran долго собирали «вручную» из меток и go to: ставили метку, в конце тела вычисляли условие и при необходимости прыгали назад. Это работало, но возвращало нас к спагетти-коду, от которого язык так старательно уходил в ветвлениях. Современные конструкции — do while, бесконечный do с exit, а также cycle — были введены именно для того, чтобы выразить условное повторение структурно, без единой метки и без go to. Освоив их, вы фактически закрываете последнюю нишу, где раньше требовались переходы по меткам, и получаете полный набор структурных средств управления потоком.
Цикл do while
Конструкция do while (условие) проверяет условие перед каждой итерацией и выполняет тело, пока оно истинно. Классическое применение — итерации до достижения точности.
program newton_sqrt
implicit none
integer, parameter :: dp = selected_real_kind(15)
real(dp) :: x, guess, prev
x = 2.0_dp
guess = 1.0_dp
prev = 0.0_dp
do while (abs(guess - prev) > 1.0e-12_dp)
prev = guess
guess = 0.5_dp * (guess + x / guess) ! итерация Ньютона для sqrt(x)
end do
print *, "sqrt(2) =", guess
end program newton_sqrt
Вывод:
sqrt(2) = 1.4142135623730951
Цикл повторяет формулу Ньютона, пока изменение между шагами больше порога 1e-12. Поскольку условие проверяется до тела, при изначально ложном условии тело не выполнится ни разу. Критично инициализировать переменные так, чтобы первая проверка прошла, — иначе цикл не запустится.
Здесь видна важная характеристика do while: это цикл с предусловием — проверка стоит перед телом. В C есть две формы условного цикла: while с предусловием и do ... while с постусловием, где тело гарантированно исполняется хотя бы раз перед первой проверкой. Несмотря на похожее написание, фортрановский do while соответствует именно сишному while, а не do ... while — не дайте имени себя запутать. Отдельной формы с постусловием в Fortran нет, и это сознательное упрощение: если нужно «выполнить хотя бы раз, потом проверить», естественнее взять бесконечный do и поставить exit в конце тела, о чём ниже. Так одна гибкая конструкция заменяет вторую специальную, и язык остаётся компактнее.
С предусловием связана и типичная ошибка инициализации. В нашем примере prev намеренно задано далёким от guess значением, чтобы первая разность заведомо превысила порог и цикл стартовал. Забыв об этом и инициализировав обе переменные одинаково, мы бы получили ложное условие на самом входе — и метод не сделал бы ни шага, тихо вернув начальное приближение как «ответ». Это коварно тем, что программа не падает и не зависает, а выдаёт правдоподобный, но неверный результат. В итерационных методах подобную «инициализацию ради первого прохода» приходится продумывать всегда.
Бесконечный цикл и exit
Иногда удобнее цикл без условия в заголовке, с выходом из середины. do без параметров — это бесконечный цикл; покинуть его можно оператором exit. Это идеальный шаблон, когда проверку выхода логично делать в середине тела.
program read_until
implicit none
integer :: i, value
i = 0
do
i = i + 1
value = i * i
if (value > 50) exit ! выход из середины
print *, i, value
end do
print *, "Остановились на i =", i
end program read_until
Вывод:
1 1
2 4
3 9
4 16
5 25
6 36
7 49
Остановились на i = 8
exit немедленно прекращает цикл и передаёт управление за end do. Здесь при i = 8 значение 64 > 50, и цикл прерывается до печати. Бесконечный do + exit — мощный и читаемый шаблон, часто яснее, чем хитрое условие в do while.
cycle: пропуск итерации
Оператор cycle не выходит из цикла, а пропускает остаток текущей итерации и переходит к следующей. Удобно для отсева ненужных случаев без глубокой вложенности if.
program skip_odd
implicit none
integer :: i, total
total = 0
do i = 1, 10
if (mod(i, 2) /= 0) cycle ! пропустить нечётные
total = total + i
end do
print *, "Сумма чётных от 1 до 10:", total
end program skip_odd
Вывод:
Сумма чётных от 1 до 10: 30
Когда i нечётно, cycle перепрыгивает к следующему значению, минуя total = total + i. Без cycle пришлось бы оборачивать прибавление в if (mod(i,2)==0); cycle часто читается чище, особенно при нескольких условиях отсева.
Управление вложенными циклами по имени
В одиночном цикле exit и cycle действуют на него. Но во вложенных циклах часто нужно выйти именно из внешнего. Для этого exit и cycle принимают имя цикла — здесь именование становится не украшением, а необходимостью.
program find_pair
implicit none
integer :: a, b
outer: do a = 1, 5
inner: do b = 1, 5
if (a * b == 12) then
print *, "Найдено:", a, "*", b, "= 12"
exit outer ! выход из ОБОИХ циклов
end if
end do inner
end do outer
print *, "Готово"
end program find_pair
Вывод:
Найдено: 3 * 4 = 12 Готово
Без имени exit покинул бы только внутренний цикл inner, и внешний продолжил бы работу. exit outer прерывает именованный внешний цикл целиком. Аналогично cycle outer перешёл бы к следующей итерации внешнего цикла. Это единственный чистый способ управлять вложенными циклами в Fortran — без меток и go to.
do concurrent: подсказка для параллелизма
Современный Fortran (2008) ввёл do concurrent — цикл, которым программист обещает, что итерации независимы и могут выполняться в любом порядке или параллельно. Это не команда «распараллелить», а гарантия отсутствия зависимостей, которой компилятор вправе воспользоваться для векторизации или многопоточности.
program concurrent_demo
implicit none
integer, parameter :: n = 5
real :: a(n), b(n)
integer :: i
b = [1.0, 2.0, 3.0, 4.0, 5.0]
do concurrent (i = 1:n)
a(i) = b(i) ** 2 ! итерации независимы
end do
print *, a
end program concurrent_demo
Вывод:
1.00000000 4.00000000 9.00000000 16.0000000 25.0000000
Ключевое правило: тело do concurrent не должно иметь зависимостей между итерациями — нельзя, чтобы итерация читала то, что пишет другая. Нарушение этого обещания даёт неопределённый результат. Зато при соблюдении компилятор может выполнить цикл максимально эффективно.
Зачем do concurrent в эпоху многоядерности
Появление do concurrent в стандарте 2008 года — прямой ответ на сдвиг в устройстве процессоров. Десятилетиями производительность росла за счёт тактовой частоты, и последовательный цикл сам собой ускорялся с каждым новым поколением «железа». Примерно с середины 2000-х этот рост упёрся в физические пределы, и индустрия свернула на путь параллелизма: больше ядер, векторные инструкции, графические ускорители. Чтобы использовать такие машины, цикл нужно раздать по исполнителям — но обычный do семантически последователен, и компилятор не вправе менять порядок итераций, пока сам не докажет их независимость. А доказать это автоматически мешает алиасинг — возможность того, что два разных имени массива на самом деле указывают на пересекающуюся память; в общем случае компилятор вынужден предполагать худшее и отказываться от распараллеливания.
Конструкция do concurrent разрубает этот узел, перенося ответственность на программиста: вы утверждаете, что итерации независимы, и тем самым освобождаете оптимизатор от недоказуемого. Это особенно ценно в физическом моделировании, обработке сеток, линейной алгебре — там, где один и тот же расчёт применяется к миллионам независимых точек. На практике именно do concurrent стал тем рычагом, через который современные компиляторы Fortran умеют отгружать вычисления на GPU без единой строчки сторонних библиотек: цикл, помеченный как независимый, оптимизатор вправе превратить в тысячи параллельных нитей графического ускорителя. Тем самым один и тот же исходник способен эффективно работать и на обычном процессоре, и на массивно-параллельном устройстве — редкое и ценное свойство для научного кода, который живёт десятилетиями и переезжает с одной архитектуры на другую.
Важно не переоценить обещание. do concurrent ничего не распараллеливает сам по себе — это лишь разрешение, а воспользуется ли им компилятор и как именно, зависит от его возможностей, флагов сборки и целевой платформы. На старой машине без подходящих опций тот же цикл просто выполнится последовательно. И обратная сторона: раз гарантию независимости даёт программист, цена ошибки целиком на нём. Если итерации на деле зависимы — например, накапливают сумму в общую переменную или читают соседний элемент, который параллельно перезаписывается, — результат становится неопределённым, причём он может оказаться правильным при последовательном исполнении и «поломаться» лишь при реальном распараллеливании. Такие баги крайне неприятны, потому что прячутся до смены платформы или флагов. Поэтому помечать цикл как do concurrent следует только после честной проверки, что никакая итерация не зависит от результата другой.
Как работает под капотом
Чем do concurrent отличается от обычного do на уровне модели исполнения? Обычный цикл семантически последователен: итерации идут строго по порядку, и компилятор обязан это сохранить, если не докажет независимость сам (что не всегда возможно из-за алиасинга). do concurrent переворачивает контракт: программист утверждает независимость, снимая с компилятора бремя доказательства. Это даёт оптимизатору свободу переупорядочить, векторизовать или раздать итерации по ядрам. Что касается exit/cycle: на уровне машинного кода это безусловные переходы — exit прыгает за конец цикла, cycle — к коду инкремента и проверки. Именованные варианты просто указывают, к концу/началу какого из вложенных циклов прыгать, заменяя ненавистные метки и go to структурным механизмом.
Частые ошибки
- Бесконечный
do while. Если условие никогда не станет ложным (например, забыли обновлять переменную), цикл зависнет. - Неинициализированные переменные перед
do while. Условие проверяется до тела; неверная инициализация может не запустить цикл вовсе. exitбез имени во вложенном цикле. Выйдет только из внутреннего; для внешнего нужноexit outer.- Зависимости в
do concurrent. Если итерации не независимы, результат неопределён — это нарушение контракта, а не «иногда работает». - Путаница
exitиcycle.exitпокидает цикл,cycleлишь пропускает остаток итерации; перепутать легко.
Итоги
do while (условие)повторяет тело, пока условие истинно; проверка — перед итерацией.doбез параметров — бесконечный цикл; выход из него — операторомexit.exitпокидает цикл;cycleпропускает остаток итерации и идёт к следующей.- Во вложенных циклах
exit/cycleс именем управляют нужным циклом — это заменаgo to. do concurrent— обещание независимости итераций, дающее компилятору свободу распараллеливания.- Нарушение независимости в
do concurrentведёт к неопределённому результату.