Неформатный и потоковый (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 и размеры типов.