Динамическая память и хранение по столбцам
Когда размер массива известен только во время выполнения, на помощь приходят allocatable-массивы и их грамотное управление памятью.
Allocatable-массив — массив, память под который выделяется во время выполнения оператором
allocateи освобождаетсяdeallocate(или автоматически), что позволяет работать с данными, размер которых заранее неизвестен.
До сих пор размеры массивов задавались на этапе компиляции. Но реальная задача обычно узнаёт размер данных лишь в рантайме: сколько строк в файле, сколько частиц в симуляции, какую сетку выбрал пользователь. Для этого Fortran предлагает динамические массивы с атрибутом allocatable. В современном Fortran (начиная с 90 и особенно 2003) они стали безопасными и удобными — настолько, что почти вытеснили опасные указатели. Этот урок завершает раздел о массивах динамической памятью, хранением по столбцам и тем, как писать кэш-дружелюбный код.
Allocatable: выделение и освобождение
Динамический массив объявляют с атрибутом allocatable и пустыми границами (:). Память выделяют оператором allocate, освобождают — deallocate.
program allocatable_demo
implicit none
real, allocatable :: data(:)
integer :: n, i
n = 5 ! размер становится известен в рантайме
allocate(data(n)) ! выделяем память под n элементов
do i = 1, n
data(i) = real(i) * 2.0
end do
print *, "Размер:", size(data)
print *, "Данные:", data
deallocate(data) ! освобождаем память
end program allocatable_demo
Вывод:
Размер: 5 Данные: 2.00000000 4.00000000 6.00000000 8.00000000 10.0000000
Атрибут allocatable и форма data(:) объявляют массив, ещё не имеющий памяти. allocate(data(n)) выделяет блок под n элементов уже во время выполнения. После использования deallocate возвращает память системе. Хорошая новость: в современном Fortran allocatable-массив, объявленный в процедуре, освобождается автоматически при выходе — об утечках можно почти не думать.
Безопасное выделение и проверка
Выделение может не удаться (например, при нехватке памяти). Промышленный код проверяет это через необязательные параметры stat и errmsg. Также полезна функция allocated, проверяющая, выделен ли массив.
program safe_alloc
implicit none
real, allocatable :: big(:)
integer :: ierr
allocate(big(1000), stat=ierr)
if (ierr /= 0) then
print *, "Ошибка выделения памяти!"
stop 1
end if
print *, "Выделено:", allocated(big), " размер:", size(big)
big = 0.0
deallocate(big)
print *, "После deallocate выделен:", allocated(big)
end program safe_alloc
Вывод:
Выделено: T размер: 1000 После deallocate выделен: F
Параметр stat=ierr получает код результата: 0 — успех, иначе ошибка. Это позволяет корректно реагировать на нехватку памяти вместо аварийного завершения. allocated(big) возвращает .true./.false. — удобно, чтобы не выделить дважды и не освободить невыделенное.
Автоматическое перевыделение при присваивании
Мощная возможность современного Fortran: при присваивании массива allocatable-переменной она сама подстраивает форму. Если размеры не совпадают, массив автоматически перевыделяется. Это делает код выразительным, хотя и требует понимания скрытых затрат.
program auto_realloc
implicit none
integer, allocatable :: a(:)
a = [1, 2, 3] ! выделяется под 3 элемента автоматически
print *, "Размер:", size(a), " данные:", a
a = [10, 20, 30, 40, 50] ! перевыделяется под 5 элементов
print *, "Размер:", size(a), " данные:", a
end program auto_realloc
Вывод:
Размер: 3 данные: 1 2 3 Размер: 5 данные: 10 20 30 40 50
При первом присваивании a ещё не выделен — Fortran выделяет под 3 элемента. При втором форма меняется на 5, и массив автоматически перевыделяется. Удобно, но в горячем цикле такое неявное перевыделение может стоить производительности — об этом стоит помнить.
Хранение по столбцам и кэш
Это, возможно, самое практически важное знание о производительности в Fortran. Многомерный массив раскладывается в линейной памяти по столбцам (column-major): первым меняется самый левый индекс. Значит, элементы m(1,1), m(2,1), m(3,1), m(1,2), ... идут подряд. Чтобы обход массива «бил» по соседним адресам и попадал в кэш, самый левый индекс должен меняться во внутреннем цикле.
program column_major
implicit none
integer, parameter :: n = 1000
real, allocatable :: m(:, :)
integer :: i, j
real :: total
allocate(m(n, n))
m = 1.0
total = 0.0
! ПРАВИЛЬНО: внешний цикл по столбцам j, внутренний по строкам i
do j = 1, n
do i = 1, n
total = total + m(i, j) ! i меняется быстрее -> идём по памяти подряд
end do
end do
print *, "Сумма:", total
deallocate(m)
end program column_major
Вывод:
Сумма: 1000000.00
Порядок циклов здесь не косметика, а производительность. Внутренний цикл по i (левый индекс) идёт по последовательным адресам памяти, и процессор эффективно подкачивает кэш-линии. Поменяй циклы местами — и тот же расчёт может замедлиться в разы из-за промахов кэша. Это зеркально противоположно языку C, где массивы хранятся по строкам и быстр обратный порядок. Первое, обо что спотыкаются при переносе кода между Fortran и C.
Зачем вообще динамическая память
Массивы фиксированного размера прекрасны, пока размер действительно известен заранее. Но это редкость в реальных задачах. Программа читает матрицу из файла — её размерность станет ясна лишь после открытия файла. Симуляция частиц меняет их число по ходу дела. Пользователь сам выбирает разрешение сетки. Запасаться «с запасом», объявив real :: a(1000000) на всякий случай, — плохая идея сразу по двум причинам: для мелких задач это пустая трата памяти, а для крупных запаса всё равно не хватит. Динамические массивы снимают эту дилемму: программа берёт ровно столько памяти, сколько нужно прямо сейчас, и возвращает её, когда данные больше не нужны. Это и есть смысл атрибута allocatable — отложить решение о размере с этапа компиляции на этап выполнения.
Исторически в Fortran для динамической памяти существовали два механизма: указатели (pointer) и allocatable-массивы. Указатели гибче — они могут ссылаться на чужие данные, образовывать связные структуры, перенаправляться, — но именно эта гибкость делает их опасными: висячие ссылки, утечки, алиасинг, мешающий оптимизатору. Allocatable-массивы намеренно скромнее: переменная либо владеет своим блоком памяти, либо не выделена — третьего не дано. Зато это владение даёт сильные гарантии. Память автоматически освобождается при выходе из области видимости; невозможно случайно «потерять» блок или сослаться на освобождённый. Современный стиль Fortran однозначен: использовать allocatable везде, где достаточно владения, и приберечь указатели для тех немногих случаев, где действительно нужны разделяемые или перенаправляемые ссылки. Это прямая параллель идее «владения» в современных системных языках — Fortran пришёл к ней по-своему и рано.
Цена удобства: перевыделение и копии
Автоматическое перевыделение при присваивании — одна из самых удобных и одновременно коварных возможностей. Удобство очевидно: можно собрать результат любого размера и просто присвоить его allocatable-переменной, не заботясь о ручном allocate. Коварство — в скрытой цене. Каждое такое присваивание, меняющее форму, означает запрос новой памяти у системы, копирование данных и освобождение старого блока. В разовом коде это незаметно, но в горячем цикле, где массив «дорастает» поэлементно, накладные расходы могут затмить полезную работу. Грамотный приём — оценить итоговый размер заранее и выделить память один раз перед циклом, заполняя уже готовый блок. Там, где рост действительно непредсказуем, прибегают к классической стратегии амортизации: выделять с запасом и удваивать ёмкость по мере заполнения, чтобы перевыделений было логарифмически мало.
Скрытые копии возникают и в другом месте — при передаче нерегулярных секций или несмежных массивов в процедуры, ожидающие непрерывные данные (об этом шла речь в уроке о секциях). Сумма этих наблюдений складывается в общее правило производительного Fortran: знай, когда язык за тебя выделяет память и копирует данные, и в критичных по скорости участках управляй этим явно. Удобные неявные механизмы хороши для ясности кода, но в ядре расчёта, где время решает всё, контроль важнее лаконичности.
Column-major как контракт совместимости
Хранение по столбцам влияет не только на скорость собственных циклов, но и на стыковку Fortran с внешним миром. Когда фортрановский код вызывает библиотеку на C или обменивается массивами с Python через интерфейс, обе стороны должны одинаково понимать, как двумерные данные разложены в линейной памяти. Поскольку C и NumPy по умолчанию хранят по строкам (row-major), а Fortran — по столбцам, «матрица» с одной стороны выглядит транспонированной с другой, если просто передать сырой указатель. Это не редкая экзотика, а повседневная реальность научных стеков: многие библиотеки явно поддерживают оба порядка именно потому, что Fortran-порядок настолько распространён, что для него есть отдельное название — «Fortran order». NumPy даже хранит флаг порядка у каждого массива и умеет создавать массивы в фортрановском раскладе специально для передачи в фортрановские и BLAS-процедуры.
Практический вывод: column-major — это не внутренняя деталь компилятора, которую можно игнорировать, а часть контракта, по которому Fortran живёт и внутри себя, и на границах с другими языками. Уважение к этому порядку даёт двойную выгоду — кэш-дружелюбные циклы внутри и беспроблемную совместимость снаружи. Пренебрежение им наказывает сразу с двух сторон: медленным кодом и загадочно «перевёрнутыми» данными на стыке с чужими библиотеками. Поэтому привычка располагать левый индекс во внутреннем цикле и держать в голове, что «по столбцам — значит подряд», — одна из самых окупаемых в арсенале фортран-программиста.
Как работает под капотом
Где именно живёт память allocatable-массива и почему column-major так влияет на скорость? allocate запрашивает блок в куче (heap) — динамической области памяти, в отличие от статических массивов фиксированного размера, что могут лежать в стеке. Дескриптор массива хранит указатель на этот блок, форму и шаги; автоматическое освобождение при выходе из процедуры реализовано через скрытый вызов deallocate, вставляемый компилятором, — отсюда защита от утечек. Что до кэша: процессор читает память не байтами, а кэш-линиями по 64 байта. Обращаясь к m(i, j) с быстро меняющимся i, вы читаете соседние ячейки одной линии — каждая загрузка обслуживает несколько обращений. Если же быстро меняется j, каждое обращение прыгает на n элементов вперёд, в новую линию, и кэш постоянно промахивается. На больших матрицах это различие — разы и десятки раз. Column-major — не прихоть, а контракт, который надо уважать ради скорости.
Частые ошибки
- Использование невыделенного массива. Обращение к allocatable до
allocate— ошибка; проверяйтеallocated. - Двойное выделение/освобождение.
allocateуже выделенного илиdeallocateневыделенного — ошибка рантайма; используйтеstatиallocated. - Неверный порядок вложенных циклов. Внутренний цикл должен идти по левому индексу (column-major); обратный порядок убивает производительность.
- Скрытое перевыделение в горячем коде. Автоматическое изменение формы при присваивании удобно, но в цикле дорого; выделяйте заранее.
- Перенос порядка циклов из C без изменений. C хранит по строкам, Fortran — по столбцам; копипаст порядка циклов даёт медленный код.
Итоги
- Атрибут
allocatableи форма(:)создают динамический массив без памяти;allocateвыделяет её в рантайме. deallocateосвобождает память, но в современном Fortran allocatable-массивы освобождаются автоматически при выходе из области.- Проверяйте выделение через
stat=и состояние черезallocated(). - Присваивание массива allocatable-переменной автоматически подстраивает её форму (перевыделение).
- Fortran хранит многомерные массивы по столбцам; левый индекс меняется в памяти быстрее.
- Для скорости внутренний цикл должен идти по левому индексу — это попадает в кэш (в C — наоборот).