Лучший способ написать большой массив для файла в fortran? Текст против другого

Я хотел знать, что лучший способ написать большой массив fortran (5000 x 5000 реальных одиночных чисел точности) в файл. Я пытаюсь сохранить результаты численного расчета для последующего использования, поэтому их не нужно повторять. Из расчета 5000 х 5000 х 4 байта на номер номера 100 Мб, можно ли сохранить его в форме, которая составляет всего 100 Мб? Есть ли способ сохранить fortran-массивы в виде двоичного файла и прочитать его для дальнейшего использования?

Я заметил, что сохранение номеров в текстовом файле приводит к тому, что файл намного больше размера сохраняемого типа данных. Это потому, что числа сохраняются как символы?

Единственный способ, которым я знаком с записью в файл, -

open (unit=41, file='outfile.txt')

do  i=1,len
    do j=1,len

        write(41,*) Array(i,j)
    end do
end do

Хотя я бы предположил, что есть лучший способ сделать это. Если кто-нибудь может указать мне на некоторые ресурсы или примеры, чтобы подтвердить мою способность эффективно писать и читать большие файлы (с точки зрения памяти), это было бы здорово. Спасибо!

Ответ 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

Ответ 2

ОТКРЫТЬ файл для чтения и записи как "неформатированный", а также читать и записывать данные без предоставления формата, как показано в приведенной ниже программе.

program xunformatted
integer, parameter :: n = 5000, inu = 20, outu = 21
real               :: x(n,n)
integer            :: i
character (len=*), parameter :: out_file = "temp_num"
call random_seed()
call random_number(x)
open (unit=outu,form="unformatted",file=out_file,action="write")
do i=1,n
   write (outu) x(i,:) ! write one row at a time
end do
print*,"sum(x) =",sum(x)
close (outu)
open (unit=inu,form="unformatted",file=out_file,action="read")
x = 0.0
do i=1,n
   read (inu) x(i,:)   ! read one row at a time
end do
print*,"sum(x) =",sum(x)
end program xunformatted