Секции, шаги и условный 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), а не копирует их.
Проверьте себя
1. Что означает секция a(1:10:2)?
AЭлементы с 1 по 10 и ещё раз с 2
BКаждый второй элемент с 1 по 10 (шаг 2)
CЭлементы 1, 10 и 2
DОшибка синтаксиса
2. Что делает конструкция where (data < 0.0) data = 0.0?
AОбнуляет весь массив
BПрисваивает 0 только тем элементам, где маска (data < 0) истинна
CВызывает ошибку для массива
DСравнивает массив с числом и возвращает logical
3. Какую конструкцию рекомендуют вместо устаревшего forall?
Ago to
Bобычный do для условий и do concurrent для индексов
Cselect case
Dтолько ручные циклы