Неформатный и потоковый (stream) доступ

Неформатный ввод-вывод сохраняет числа в двоичном виде без потери точности, а stream-доступ даёт побайтовый поток, совместимый с файлами других языков.

Неформатный (unformatted) I/O записывает внутреннее двоичное представление данных без конверсии в текст. Stream-доступ (access='stream') трактует файл как непрерывную последовательность байтов без записей-разделителей.

Зачем отказываться от текста

Форматный вывод удобен человеку, но платит за это двумя ценами: он медленный (конверсия двоичное→десятичное→строка для каждого числа) и неточный (печатается ограниченное число знаков, остальное теряется). Для сохранения промежуточных результатов большого расчёта — массивов на миллионы элементов, контрольных точек симуляции, сеточных полей — обе цены неприемлемы. Здесь применяют неформатный I/O: число записывается ровно так, как лежит в памяти, байт в байт. Это на порядок быстрее (нет конверсии) и абсолютно точно (восстанавливается бит в бит). Платой служит нечитаемость глазом и привязка к платформе: порядок байтов (endianness) и размеры типов должны совпадать при записи и чтении. Поэтому неформатные файлы — это формат «для себя» и для обмена между совместимыми сборками, а не универсальный архив.

Неформатная последовательная запись и record-маркеры

Классический неформатный доступ — последовательный (access='sequential', он же по умолчанию для неформатных файлов в старом стиле). Каждый оператор write создаёт одну запись (record). Важная тонкость: Fortran-runtime окружает каждую запись служебными маркерами длины — обычно по 4 (или 8) байт до и после данных, хранящих размер записи. Это позволяет читать записи переменной длины и «перематывать» назад, но означает, что файл не является «голым» дампом чисел — в нём есть невидимые маркеры.

program unf_seq
  implicit none
  integer :: u, i
  real(8) :: a(1000)
  a = [(real(i, 8) * 0.5d0, i = 1, 1000)]

  open(newunit=u, file="data.bin", form="unformatted", &
       access="sequential", status="replace")
  write(u) a                 ! одна запись: 1000 чисел real(8)
  close(u)

  ! чтение обратно — без потери точности:
  open(newunit=u, file="data.bin", form="unformatted", &
       access="sequential", status="old")
  read(u) a
  close(u)
  print *, "Прочитано, a(1000) =", a(1000)
end program unf_seq

Вывод:

 Прочитано, a(1000) =    500.00000000000000

Здесь весь массив записан и прочитан как единая запись, мгновенно и точно. Обратите внимание на & в конце строк — это символ продолжения длинного оператора на следующую строку (в исходнике это просто амперсанд; здесь он экранирован для отображения). Запись массива целиком эффективнее поэлементной: один системный вызов вместо тысячи.

Stream-доступ: чистый поток байтов

Record-маркеры мешают, когда файл должен читаться другой программой — на C, Python или просто другим инструментом, который не знает про Fortran-маркеры. Для таких случаев Fortran 2003 ввёл stream-доступ (access='stream'). Stream-файл — это непрерывная последовательность байтов без всяких разделителей записей, ровно как файл, открытый в C в бинарном режиме. Что записали, то и лежит — никаких лишних байт.

program stream_io
  implicit none
  integer :: u
  integer(4) :: header = 1000
  real(8)    :: x(1000)
  integer :: i
  x = [(real(i, 8), i = 1, 1000)]

  open(newunit=u, file="raw.dat", form="unformatted", &
       access="stream", status="replace")
  write(u) header            ! ровно 4 байта
  write(u) x                 ! ровно 8000 байт, без маркеров
  close(u)
end program stream_io

Файл raw.dat теперь содержит ровно 4 + 8000 = 8004 байта: четырёхбайтовый заголовок и подряд тысячу восьмибайтовых чисел. Такой файл без проблем прочитает программа на C, зная раскладку. Stream-доступ — мост между Fortran и остальным миром бинарных данных: именно его используют для совместимости форматов, для чтения чужих бинарных файлов и для записи данных, которые потом обработает не-Fortran инструмент.

Позиционирование в stream-файле

Stream-доступ позволяет читать и писать с произвольной позиции через спецификатор pos= — номер байта (нумерация с 1). Это даёт прямой доступ к любому месту файла без чтения предыдущих данных, что удобно для больших файлов с известной раскладкой.

integer :: u
real(8) :: value
open(newunit=u, file="raw.dat", form="unformatted", access="stream", status="old")
! пропустить 4-байтовый заголовок и прочитать 10-е число (по 8 байт):
! байт начала = 5 (после заголовка) + (10-1)*8 = 5 + 72 = 77
read(u, pos=77) value
close(u)
print *, "10-й элемент =", value

Спецификатор pos= превращает stream-файл в подобие массива на диске с произвольным доступом по смещению. Это фундамент для собственных бинарных форматов с заголовком и таблицей смещений.

Прямой доступ (direct access)

Существует и третья модель — прямой доступ (access='direct') с фиксированной длиной записи recl=. Файл делится на пронумерованные записи равного размера, и к записи номер n обращаются через rec=n. Это классический способ хранить таблицы фиксированных строк (например, базу частиц, где каждая запись — одна частица). Stream-доступ во многом гибче (произвольная позиция в байтах, а не в записях), поэтому в новом коде чаще выбирают stream, но direct-access остаётся в ходу в легаси и там, где удобна именно записевая модель.

Режимaccess=ЕдиницаМаркеры записейСовместимость с C
Последовательныйsequentialзапись (record)да (служебные)нет
Прямойdirectзапись фикс. длинынетчастично
Потоковыйstreamбайтнетда

Как работает под капотом

При неформатном выводе библиотека просто копирует байты из памяти переменной в буфер файла — без всякой интерпретации. Для real(8) это 8 байт IEEE-754, для integer(4) — 4 байта дополнительного кода. Отсюда зависимость от платформы: на машине с другим порядком байтов те же 8 байт прочитаются как другое число. Маркеры записей в sequential-режиме runtime добавляет сам, оборачивая каждый write; их размер (4 или 8 байт) — деталь реализации компилятора, поэтому неформатные sequential-файлы не переносимы даже между разными компиляторами Fortran. Stream-режим маркеров не добавляет, и потому stream-файл «честен»: его содержимое полностью определяется тем, что вы записали. Это и делает stream предпочтительным для межъязыкового обмена и для форматов, чью раскладку вы документируете и контролируете сами.

Контрольные точки и большие данные

Главное практическое применение неформатного и stream-ввода-вывода — сохранение состояния больших расчётов, или контрольные точки (checkpoints). Представьте симуляцию, считающуюся неделями на суперкомпьютере: климатическую модель, расчёт турбулентности, молекулярную динамику. Прерывание (сбой узла, исчерпание квоты времени, плановая остановка) не должно означать потерю всех вычислений. Решение — периодически сбрасывать на диск полное состояние: все поля на сетке, время, счётчики. При перезапуске программа читает последнюю контрольную точку и продолжает с неё. Здесь форматный вывод немыслим: поля могут занимать гигабайты, и конверсия в текст была бы и невыносимо медленной, и неточной. Только неформатный бинарный вывод даёт нужную скорость (прямое копирование памяти на диск) и точность (бит в бит, чтобы продолженный расчёт был идентичен непрерывному).

subroutine write_checkpoint(filename, time, u, v, p)
  character(len=*), intent(in) :: filename
  real(8), intent(in) :: time
  real(8), intent(in) :: u(:,:), v(:,:), p(:,:)
  integer :: unit
  open(newunit=unit, file=filename, form="unformatted", &
       access="stream", status="replace")
  write(unit) time
  write(unit) shape(u)          ! сохранить размеры для проверки при чтении
  write(unit) u
  write(unit) v
  write(unit) p
  close(unit)
end subroutine write_checkpoint

Обратите внимание на запись shape(u) перед самими данными: сохраняя размеры массивов, при чтении можно проверить, что контрольная точка соответствует текущей конфигурации, и аккуратно выделить массивы нужного размера. Это типичный приём — бинарный файл с небольшим «заголовком» метаданных (время, размеры, версия формата), за которым следуют объёмные данные. Stream-доступ здесь предпочтительнее sequential, потому что даёт полный контроль над раскладкой и совместим с инструментами пост-обработки на других языках.

Endianness и переносимость бинарных данных

У бинарного ввода-вывода есть фундаментальное ограничение, которое нужно понимать ясно: бинарные данные привязаны к платформе. Многобайтовое число хранится в памяти в определённом порядке байтов — endianness. На процессорах x86 и ARM (в обычном режиме) используется little-endian: младший байт первым. Некоторые другие архитектуры и сетевые протоколы используют big-endian: старший байт первым. Если записать real(8) на little-endian машине и прочитать те же байты на big-endian, получится совершенно другое число — байты переставлены. Для checkpoint-файлов, читаемых на той же машине, проблемы нет. Но при переносе бинарных данных между разными платформами endianness становится критичным.

Подход к переносимостиСуть
Опции компилятора-fconvert=big-endian и подобные — конверсия на лету
Спецификатор convert=в open указать порядок байтов файла
Стандартные форматыHDF5, NetCDF — сами решают endianness
Текст для обменапожертвовать скоростью/точностью ради переносимости

Для серьёзного обмена научными данными между системами обычно не пишут «сырой» бинарный формат вручную, а используют специализированные библиотеки — NetCDF и HDF5. Они хранят данные бинарно (быстро и точно), но при этом самоописывающе и переносимо: файл содержит метаданные о типах, размерностях, порядке байтов, единицах измерения, и читается одинаково на любой платформе любым инструментом. По сути это «бинарный формат, сделанный правильно»: скорость и точность бинарного ввода-вывода плюс переносимость и самоописываемость, которых лишён ручной неформатный вывод. В мире наук о Земле NetCDF фактически обязателен. Понимание этой иерархии — от быстрого, но платформенно-зависимого неформатного вывода через stream к самоописывающим переносимым форматам — позволяет осознанно выбирать инструмент: «сырой» бинарный для временных контрольных точек на одной машине, NetCDF/HDF5 для данных, которыми делятся и которые хранят надолго.

Частые ошибки

  • Ожидать, что неформатный sequential-файл прочитает программа на C. Из-за служебных record-маркеров это не «голый» дамп. Для совместимости с C используйте access='stream'.
  • Игнорировать endianness при обмене файлами между платформами. Неформатные данные зависят от порядка байтов; при переносе между big- и little-endian нужна конверсия (опции компилятора или явная перестановка байт).
  • Смешивать форматный и неформатный доступ к одному юниту. Юнит открывается либо как form='formatted', либо 'unformatted'; смешать в одном open нельзя.
  • Забывать про pos= с нумерацией от 1. В stream-доступе первый байт имеет номер 1, а не 0; смещения считайте аккуратно.
  • Поэлементная запись больших массивов. write(u) a для всего массива куда быстрее цикла по элементам — меньше системных вызовов и накладных расходов.

Итоги

  • Неформатный I/O пишет двоичное представление: быстро и без потери точности, но привязано к платформе.
  • Последовательный неформатный доступ добавляет служебные record-маркеры — такой файл не «голый» дамп.
  • Stream-доступ (access='stream') даёт чистый поток байтов без маркеров — совместим с C и другими языками.
  • pos= в stream-режиме даёт произвольный доступ по номеру байта (с 1); direct-access — по номеру записи фиксированной длины.
  • При обмене бинарными файлами помните про endianness и размеры типов.
Проверьте себя
1. Почему неформатный sequential-файл Fortran нельзя напрямую прочитать программой на C как простой массив?
AЧисла в нём зашифрованы
BRuntime окружает каждую запись служебными маркерами длины, которых C не ожидает
CC не поддерживает двоичные файлы
DФайл всегда сжат
2. Что обеспечивает access='stream' в Fortran?
AАвтоматическое сжатие данных
BЧтение и запись файла как непрерывного потока байтов без записей-разделителей
CФорматный вывод с дескрипторами
DЗащиту от изменения endianness