Ответ 1
Записывайте файлы данных в двоичные файлы, если только вы не собираетесь читать результат - и вы не будете читать массив размером в 2,5 миллиона элементов.
Причины использования двоичных файлов в три раза снижаются:
- Точность
- Производительность
- Размер данных
Точность может быть наиболее очевидной. Когда вы конвертируете (двоичный) число с плавающей запятой в строковое представление десятичного числа, вы неизбежно собираетесь усечь в какой-то момент. Это нормально, если вы уверены, что когда вы прочитаете текстовое значение обратно в значение с плавающей запятой, вы, безусловно, получите такое же значение; но это на самом деле тонкий вопрос и требует тщательного выбора формата. Используя форматирование по умолчанию, различные компиляторы выполняют эту задачу с различной степенью качества. Это сообщение в блоге, написанное с точки зрения программиста игр, отлично справляется с проблемами.
Рассмотрим небольшую программу, которая для разных форматов записывает реальный номер с одной точностью в строку и затем снова считывает ее обратно, отслеживая максимальную ошибку, с которой он сталкивается. Мы просто перейдем от 0 до 1, в единицах машинного эпсилона. Код следует:
program testaccuracy
character(len=128) :: teststring
integer, parameter :: nformats=4
character(len=20), parameter :: formats(nformats) = &
[ '( E11.4)', '( E13.6)', '( E15.8)', '(E17.10)' ]
real, dimension(nformats) :: errors
real :: output, back
real, parameter :: delta=epsilon(output)
integer :: i
errors = 0
output = 0
do while (output < 1)
do i=1,nformats
write(teststring,FMT=formats(i)) output
read(teststring,*) back
if (abs(back-output) > errors(i)) errors(i) = abs(back-output)
enddo
output = output + delta
end do
print *, 'Maximum errors: '
print *, formats
print *, errors
print *, 'Trying with default format: '
errors = 0
output = 0
do while (output < 1)
write(teststring,*) output
read(teststring,*) back
if (abs(back-output) > errors(1)) errors(1) = abs(back-output)
output = output + delta
end do
print *, 'Error = ', errors(1)
end program testaccuracy
и когда мы запустим его, получим:
$ ./accuracy
Maximum errors:
( E11.4) ( E13.6) ( E15.8) (E17.10)
5.00082970E-05 5.06639481E-07 7.45058060E-09 0.0000000
Trying with default format:
Error = 7.45058060E-09
Обратите внимание, что даже использование формата с 8 цифрами после десятичной точки - что, по нашему мнению, было бы большим, учитывая, что операции с одиночной точностью верны с точностью до 6-7 знаков после запятой - мы не получаем точных копий назад, примерно на 1е-8. И этот формат по умолчанию для компилятора не дает нам точных значений плавающей запятой в оба конца; введена некоторая ошибка! Если вы программист видеоигры, этого уровня точности вполне может быть достаточно. Однако, если вы выполняете зависящие от времени симуляции турбулентных флюидов, это может быть абсолютно не в порядке, особенно если есть какое-то предвзятое отношение к тому, где вводится ошибка, или если ошибка возникает в том, что должно быть сохраненной величиной.
Обратите внимание, что если вы попробуете запустить этот код, вы заметите, что для завершения требуется довольно долгое время. Это потому, что, может быть, удивительно, что производительность - еще одна реальная проблема с текстовым выходом чисел с плавающей запятой. Рассмотрим следующую простую программу, которая просто выписывает ваш пример 5000 и раз; 5000 реального массива в виде текста и как неформатированный двоичный файл:
program testarray
implicit none
integer, parameter :: asize=5000
real, dimension(asize,asize) :: array
integer :: i, j
integer :: time, u
forall (i=1:asize, j=1:asize) array(i,j)=i*asize+j
call tick(time)
open(newunit=u,file='test.txt')
do i=1,asize
write(u,*) (array(i,j), j=1,asize)
enddo
close(u)
print *, 'ASCII: time = ', tock(time)
call tick(time)
open(newunit=u,file='test.dat',form='unformatted')
write(u) array
close(u)
print *, 'Binary: time = ', tock(time)
contains
subroutine tick(t)
integer, intent(OUT) :: t
call system_clock(t)
end subroutine tick
! returns time in seconds from now to time described by t
real function tock(t)
integer, intent(in) :: t
integer :: now, clock_rate
call system_clock(now,clock_rate)
tock = real(now - t)/real(clock_rate)
end function tock
end program testarray
Здесь выходы синхронизации, для записи на диск или в ramdisk:
Disk:
ASCII: time = 41.193001
Binary: time = 0.11700000
Ramdisk
ASCII: time = 40.789001
Binary: time = 5.70000000E-02
Обратите внимание, что при записи на диск двоичный вывод 352 раза с точностью до ASCII и ramdisk его ближе к 700 раз. Для этого есть две причины: одна заключается в том, что вы можете выписывать данные сразу, а не в цикле; другой заключается в том, что генерация строкового десятичного представления числа с плавающей запятой является удивительно тонкой операцией, которая требует значительного количества вычислений для каждого значения.
Наконец, это размер данных; текстовый файл в приведенном выше примере выводится (в моей системе) примерно в 4 раза больше размера двоичного файла.
Теперь существуют реальные проблемы с двоичным выходом. В частности, исходный бинарный выход Fortran (или, если на то пошло, C) очень хрупкий. Если вы измените платформы или измените размер своих данных, ваш результат больше не будет хорош. Добавление новых переменных на выходе приведет к поломке формата файла, если вы не всегда добавляете новые данные в конце файла, и вы не можете заранее знать, какие переменные находятся в двоичном блобе, который вы получаете от своего соавтора (кто может быть вы, три месяца назад). Большинство недостатков двоичного вывода можно избежать, используя библиотеки, такие как NetCDF, которые пишут самоописывающие двоичные файлы, которые намного более "будущие" доказательство ", чем исходное двоичное. Еще лучше, так как это стандарт, многие инструменты читают файлы NetCDF.
В Интернете много обучающих программ NetCDF; наш здесь. Простой пример использования NetCDF дает одинаковые времена для исходного двоичного файла:
$ ./array
ASCII: time = 40.676998
Binary: time = 4.30000015E-02
NetCDF: time = 0.16000000
но дает хороший самоописывающий файл:
$ ncdump -h test.nc
netcdf test {
dimensions:
X = 5000 ;
Y = 5000 ;
variables:
float Array(Y, X) ;
Array:units = "ergs" ;
}
и размер файлов примерно такой же, как и исходный двоичный файл:
$ du -sh test.*
96M test.dat
96M test.nc
382M test.txt
код следует:
program testarray
implicit none
integer, parameter :: asize=5000
real, dimension(asize,asize) :: array
integer :: i, j
integer :: time, u
forall (i=1:asize, j=1:asize) array(i,j)=i*asize+j
call tick(time)
open(newunit=u,file='test.txt')
do i=1,asize
write(u,*) (array(i,j), j=1,asize)
enddo
close(u)
print *, 'ASCII: time = ', tock(time)
call tick(time)
open(newunit=u,file='test.dat',form='unformatted')
write(u) array
close(u)
print *, 'Binary: time = ', tock(time)
call tick(time)
call writenetcdffile(array)
print *, 'NetCDF: time = ', tock(time)
contains
subroutine tick(t)
integer, intent(OUT) :: t
call system_clock(t)
end subroutine tick
! returns time in seconds from now to time described by t
real function tock(t)
integer, intent(in) :: t
integer :: now, clock_rate
call system_clock(now,clock_rate)
tock = real(now - t)/real(clock_rate)
end function tock
subroutine writenetcdffile(array)
use netcdf
implicit none
real, intent(IN), dimension(:,:) :: array
integer :: file_id, xdim_id, ydim_id
integer :: array_id
integer, dimension(2) :: arrdims
character(len=*), parameter :: arrunit = 'ergs'
integer :: i, j
integer :: ierr
i = size(array,1)
j = size(array,2)
! create the file
ierr = nf90_create(path='test.nc', cmode=NF90_CLOBBER, ncid=file_id)
! define the dimensions
ierr = nf90_def_dim(file_id, 'X', i, xdim_id)
ierr = nf90_def_dim(file_id, 'Y', j, ydim_id)
! now that the dimensions are defined, we can define variables on them,...
arrdims = (/ xdim_id, ydim_id /)
ierr = nf90_def_var(file_id, 'Array', NF90_REAL, arrdims, array_id)
! ...and assign units to them as an attribute
ierr = nf90_put_att(file_id, array_id, "units", arrunit)
! done defining
ierr = nf90_enddef(file_id)
! Write out the values
ierr = nf90_put_var(file_id, array_id, array)
! close; done
ierr = nf90_close(file_id)
return
end subroutine writenetcdffile
end program testarray