Секции, шаги и условный where
Секция массива — это «вырезка» из него, ведущая себя как самостоятельный массив: можно читать, писать и передавать.
Секция массива (array section) — подмножество элементов массива, заданное диапазоном индексов вида
low:high:stride; секция сама является массивом и может стоять и слева, и справа от присваивания.
Умение работать с целым массивом — половина дела. Вторая половина — гибко вырезать из него куски: строку матрицы, каждый второй элемент, прямоугольный блок. Fortran даёт для этого выразительный синтаксис секций, который превращает сложные манипуляции в одну строку. Этот урок учит вырезать секции одномерных и многомерных массивов, использовать шаг, понимать триплет low:high:stride и применять условные присваивания через where. Это инструменты, делающие численный код Fortran компактным и читаемым.
Триплет индексов: low:high:stride
Секция задаётся триплетом начало:конец:шаг. Любую часть можно опустить: пропущенное начало означает нижнюю границу, конец — верхнюю, шаг — 1. Голое двоеточие : — «весь диапазон».
program sections_1d
implicit none
integer :: a(10), i
a = [(i, i = 1, 10)] ! 1..10
print *, "Весь: ", a(:)
print *, "Со 2 по 5: ", a(2:5)
print *, "Каждый 2-й:", a(1:10:2)
print *, "С конца: ", a(10:1:-1)
a(3:5) = 0 ! секция СЛЕВА: обнулить элементы 3..5
print *, "После a(3:5)=0:", a
end program sections_1d
Вывод:
Весь: 1 2 3 4 5 6 7 8 9 10 Со 2 по 5: 2 3 4 5 Каждый 2-й: 1 3 5 7 9 С конца: 10 9 8 7 6 5 4 3 2 1 После a(3:5)=0: 1 2 0 0 0 6 7 8 9 10
Ключевая идея: секция — полноценный массив. a(2:5) можно печатать, передавать в функцию, присваивать. А a(3:5) = 0 показывает секцию слева: присваивание затрагивает только указанные элементы. Шаг -1 в a(10:1:-1) разворачивает массив.
Многомерные массивы и их секции
Двумерный массив (матрица) объявляется двумя размерами. Секции по каждому измерению позволяют вырезать строки, столбцы и блоки — основа линейной алгебры.
program sections_2d
implicit none
integer :: m(3, 4), i, j
do i = 1, 3
do j = 1, 4
m(i, j) = i * 10 + j ! заполняем: m(2,3) = 23
end do
end do
print *, "Вторая строка:", m(2, :) ! фиксируем строку, все столбцы
print *, "Третий столбец:", m(:, 3) ! все строки, фикс. столбец
print *, "Блок 1:2, 2:3:"
print *, m(1:2, 2:3)
end program sections_2d
Вывод:
Вторая строка: 21 22 23 24 Третий столбец: 13 23 33 Блок 1:2, 2:3: 12 13 22 23
Здесь m(2, :) — вся вторая строка (фиксируем первый индекс, второй — весь диапазон), m(:, 3) — весь третий столбец. m(1:2, 2:3) вырезает прямоугольный блок 2×2. Обратите внимание на порядок вывода блока — он связан с хранением по столбцам, о чём в следующем уроке.
Условное присваивание: where
Конструкция where — это «if для массивов»: она применяет присваивание только к тем элементам, где маска истинна. Это устраняет циклы при условной обработке данных.
program where_demo
implicit none
real :: data(6)
data = [-3.0, 2.0, -1.0, 5.0, -4.0, 0.0]
where (data < 0.0)
data = 0.0 ! обнулить отрицательные (ReLU)
end where
print *, "После обнуления:", data
data = [-3.0, 2.0, -1.0, 5.0, -4.0, 0.0]
where (data > 0.0)
data = sqrt(data) ! корень только из положительных
elsewhere
data = -1.0 ! остальным -1
end where
print *, "where/elsewhere:", data
end program where_demo
Вывод:
После обнуления: 0.00000000 2.00000000 0.00000000 5.00000000 0.00000000 0.00000000 where/elsewhere: -1.00000000 1.41421354 -1.00000000 2.23606801 -1.00000000 -1.00000000
Маска data < 0.0 — это логический массив; where применяет тело только там, где маска .true.. Блок elsewhere обрабатывает остальные элементы. Это естественный способ записать, например, функцию активации ReLU или обработку пропусков в данных — без единого явного цикла и проверки.
forall и его судьба
Конструкция forall позволяет индексированное присваивание сразу по диапазону индексов. Она выглядит как цикл, но семантически — массивное присваивание: порядок не определён. Важно знать: forall в новых стандартах объявлена устаревшей в пользу do concurrent, но в существующем коде встречается.
program forall_demo
implicit none
integer :: a(5), i
forall (i = 1:5)
a(i) = i * i
end forall
print *, a
! современная замена — do concurrent:
do concurrent (i = 1:5)
a(i) = i * i * i
end do
print *, a
end program forall_demo
Вывод:
1 4 9 16 25
1 8 27 64 125
Семантическая разница между forall и обычным do тонка, но важна. Цикл do выполняется строго по порядку, и каждая итерация видит результаты предыдущих; forall же ближе к массивному присваиванию — правые части как бы вычисляются все сразу, до записи, поэтому зависимость a(i) = a(i-1) + 1 внутри него ведёт себя не так, как в цикле. Именно эта «массивная» семантика и делала forall привлекательным когда-то, но она же порождала неоднозначности и мешала компиляторам оптимизировать код. do concurrent решает задачу честнее: он не притворяется массивом, а прямо обещает компилятору, что итерации независимы и их можно исполнять в любом порядке или параллельно. Это обещание даёт программист — и отвечает за него.
Зачем языку синтаксис секций
Стоит задуматься, почему вырезание кусков массива вообще встроено в язык, а не отдано библиотеке или ручным циклам. Ответ тот же, что и со всей массивной моделью Fortran: численные алгоритмы по своей природе оперируют не отдельными числами, а блоками — строками и столбцами матриц, подобластями сетки, окнами данных. Метод Гаусса вычитает одну строку матрицы из других; обновление сетки в задаче теплопроводности затрагивает прямоугольный блок ячеек; фильтр скользит окном по сигналу. Если бы для каждой такой операции приходилось писать цикл с индексами, код тонул бы в служебной механике, а намерение терялось. Синтаксис секций поднимает уровень разговора: вы говорите «вторая строка», «внутренние ячейки», «каждый второй отсчёт» — и язык понимает вас буквально.
Сравнение с другими языками снова показывает, насколько это родная черта Fortran. В C никаких секций нет в принципе: чтобы взять столбец матрицы, вы пишете цикл и вручную считаете смещения, а «развернуть массив» — это ещё один цикл. В Python та же выразительность есть у срезов NumPy (a[1:10:2], m[:, 3]) — и это снова прямое заимствование из Fortran, вплоть до синтаксиса триплета. Но в NumPy срез — это объект-представление, создаваемый библиотекой в рантайме, тогда как в Fortran секция — конструкция языка, которую компилятор разворачивает в эффективный код без накладных расходов на создание объекта. Тот, кто привык к срезам NumPy, освоит секции Fortran почти мгновенно; разница будет лишь в нумерации с единицы и в том, что граница в триплете Fortran включается, а в Python — нет.
Секции в реальных задачах линейной алгебры
Чтобы увидеть силу секций, рассмотрим прямой ход метода исключения Гаусса — алгоритм, лежащий в основе решения систем линейных уравнений. На каждом шаге нужно вычесть из всех нижележащих строк ведущую строку, домноженную на коэффициент. В терминах элементов это двойной цикл; в терминах секций — почти одна строка мысли: «к блоку строк ниже текущей прибавить внешнее произведение столбца множителей на ведущую строку». Запросив секцию a(k+1:n, k+1:n) как обновляемый блок, a(k+1:n, k) как столбец множителей и a(k, k+1:n) как ведущую строку, расчётчик выражает суть шага компактно и близко к матричной записи метода. Так же естественно секциями вырезаются подматрицы для блочных алгоритмов, диагонали через шаговую индексацию, верхние и нижние треугольники.
В задачах на сетках секции не менее уместны. Простейший пятиточечный шаблон Лапласа — усреднение каждой внутренней ячейки по четырём соседям — записывается как операция над сдвинутыми секциями: u(2:n-1, 2:n-1) обновляется через u(1:n-2, 2:n-1), u(3:n, 2:n-1) и аналогичные сдвиги по второму индексу. Одно присваивание над секциями заменяет двойной цикл по всей внутренней области и при этом яснее показывает, что именно вычисляется — соседи слева, справа, сверху, снизу. Это типичный приём в явных разностных схемах, и читается он как прямая запись шаблона.
where как инструмент маскирования
Конструкция where заслуживает более широкого взгляда, чем «if для массивов». По сути это механизм маскированных вычислений — той же идеи, что лежит в основе предикатного исполнения в современных процессорах и в булевой индексации NumPy. Маска позволяет применять разную обработку к разным частям массива без ветвления по каждому элементу: положительные — под корень, отрицательные — заменить, пропуски (специальные значения вроде NaN или сигнальной константы) — отдельно. Типичные применения — функции активации в машинном обучении (ReLU как where (x < 0) x = 0), отбраковка выбросов, обработка отсутствующих данных, кусочно-заданные функции. Важно держать в уме, что условие в where вычисляется для всего массива целиком в виде логической маски, и тело применяется выборочно; это отличается от досрочного выхода в цикле и иногда означает, что вычисляются и отбрасываются «лишние» значения — плата за векторность.
Как работает под капотом
Что физически представляет собой секция вроде a(1:10:2)? Это не копия данных, а описание: компилятор формирует дескриптор, указывающий на тот же блок памяти, но с другим начальным смещением, длиной и шагом. Шаг (stride) — ключевое понятие: для a(1:10:2) шаг равен 2 размерам элемента, поэтому секция «перескакивает» через один. Столбец матрицы m(:, 3) при хранении по столбцам — непрерывен (шаг 1), а строка m(2, :) — разрежена (шаг равен числу строк): это объясняет, почему обход по столбцам в Fortran эффективнее. Когда секция передаётся в процедуру, ожидающую непрерывный массив, компилятор может незаметно создать временную копию (copy-in/copy-out) — это бывает источником скрытых затрат. Конструкция where вычисляет логическую маску в виде временного массива, затем применяет присваивание только к отмеченным позициям — модель «маска плюс выборочная запись», которую процессор реализует через предикатные SIMD-операции.
Частые ошибки
- Путаница порядка границ в триплете.
a(5:2)с шагом по умолчанию +1 — пустая секция; для разворота нуженa(5:2:-1). - Несовпадение форм при присваивании секций.
a(1:3) = b(1:5)— ошибка форм; число элементов слева и справа должно совпадать. - Ожидание копии от секции. Секция ссылается на исходные данные; изменение через секцию меняет оригинал.
- Использование устаревшего
forall. В новом коде предпочитайтеwhere(по условию) иdo concurrent(по индексам). - Скрытые копии при передаче секций. Передача нерегулярной секции в процедуру может вызвать копирование — следите за производительностью в горячем коде.
Итоги
- Секция задаётся триплетом
low:high:stride; пропущенные части берут границы и шаг 1. - Секция — полноценный массив: стоит и справа, и слева от присваивания.
- В матрице
m(2,:)— строка,m(:,3)— столбец,m(1:2,2:3)— блок. where (маска) ... elsewhere ... end where— условное присваивание по элементам.forallустарел; используйтеwhereпо условию иdo concurrentпо индексам.- Секция ссылается на исходные данные через шаг (stride), а не копирует их.