Devin.KR

Fortran · 심화

객체와 수치 해석으로 깊어지는 Fortran

결과 저장과 재시작 - 스트림·이진 입출력

access=stream 이진 파일, 체크포인트 저장과 재시작, namelist 로 설정 읽기, 결과를 CSV 로 내보내기

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서 계산 반복을 병렬로 실행할 수 있게 했다. 이제 계산 중간의 상태를 파일에 남겨 실행을 이어 갈 수 있게 한다. 계산이 빠르더라도 실행이 끝날 때까지 결과를 메모리에만 두면 작업을 중단하기 어렵다. 반대로 숫자 배열만 저장하면 어떤 설정으로 몇 단계까지 계산했는지 알 수 없어 재시작을 잘못할 수 있다.

이 장에서는 2차원 금속판의 온도 배열에 격자 크기, 완료한 단계 수, 시간과 계산 매개변수를 함께 저장한다. 저장한 상태에서 이어 계산한 결과를 처음부터 계산한 결과와 비교한다. 설정과 분석용 결과는 사람이 읽는 텍스트 파일로, 재시작 상태는 이진 파일로 구분한다.

  • 스트림 입출력(stream I/O)으로 레코드 구분 정보 없이 이진 데이터를 읽고 쓴다.
  • 체크포인트(checkpoint)에 필요한 상태를 정하고, 읽은 내용을 검증한 뒤 계산을 재개한다.
  • 네임리스트(namelist)로 이름을 붙인 설정 값을 읽는다.
  • CSV 파일로 온도 배열을 내보내며 좌표 순서와 숫자 서식을 명시한다.
  • 연속 실행과 재시작 실행의 최종 온도를 비교해 저장 경로를 점검한다.

문제 상황

금속판의 열 확산을 수천 단계 계산하는 작업을 생각해 보자. 계산 도중 컴퓨터를 재시작해야 하거나, 다른 작업을 위해 실행을 멈춰야 할 수 있다. 마지막 온도만 화면에 출력했다면 남은 계산을 이어 갈 수 없다. 온도 배열을 저장했더라도 시간 간격이나 경계 조건이 달라졌다면 이전 실행을 이어 간다고 보기 어렵다.

파일의 목적도 서로 다르다. 설정 파일은 사용자가 값 하나를 고치기 쉬워야 한다. 재시작 파일은 계산에 필요한 값을 충분한 정밀도로 보존해야 한다. 분석용 결과 파일은 스프레드시트나 다른 프로그램에서 열기 쉬워야 한다. 하나의 형식으로 세 가지 목적을 모두 해결하려 하면 읽기 편의성과 상태 보존 사이에서 선택이 모호해진다.

파일마다 맡기는 역할이 다르다
파일형식담는 내용주요 목적
settings.dat네임리스트 텍스트격자와 계산 설정실행 조건 입력
checkpoint.bin비서식 스트림 이진메타데이터와 온도 배열계산 재시작
temperature.csv서식 지정 텍스트격자 첨자와 온도결과 분석과 공유

완성 프로그램은 입력 준비 과정까지 재현할 수 있도록 settings.dat을 먼저 만든다. 그 파일을 다시 읽고, 같은 초기 상태에서 두 가지 계산을 수행한다. 하나는 네 단계를 연속 계산한다. 다른 하나는 두 단계 뒤 저장하고 파일에서 상태를 읽어 나머지 두 단계를 계산한다. 예제의 단계 수는 작지만, 저장 시점과 재시작 시점을 분리하는 구조는 긴 계산에도 그대로 적용할 수 있다.

스트림 이진 파일에는 읽는 순서까지 설계한다

비서식 입출력(unformatted I/O)은 숫자를 사람이 읽는 문자열로 바꾸지 않고 처리한다. 이때 access='stream'을 지정하면 파일을 파일 저장 단위의 연속으로 다룬다. 비서식 순차 파일에서 처리계가 사용하는 레코드 구분 정보가 스트림 파일에는 붙지 않는다. 따라서 write 문을 여러 번 실행해도 각 write의 경계가 파일에 별도 레코드로 기록되는 것은 아니다.

이 성질은 파일 구성을 명확하게 만들 때 유용하다. 그러나 읽는 쪽이 데이터의 종류와 순서를 알아야 한다. 정수 다음에 실수를 썼다면 정수 다음에 실수를 읽어야 한다. 배열의 크기도 파일 자체가 자동으로 알려 주지 않는다. 배열보다 앞에 크기를 저장하고, 그 값을 확인한 뒤 메모리를 할당해야 한다.

예제는 파일 앞부분에 식별 문자열과 형식 버전을 둔다. 식별 문자열은 다른 파일을 잘못 지정했는지 판단하는 첫 단서다. 형식 버전은 저장 항목이나 순서가 바뀌었을 때 이전 파일을 구분하는 값이다. 이후에는 격자 크기, 완료한 단계 수, 시간, 시간 간격, 열 확산 계수, 격자 간격을 쓴다. 이 앞부분을 파일 헤더(header)라고 부른다.

체크포인트는 식별 정보와 계산 상태를 먼저 저장하고 온도 배열을 뒤에 저장한다
체크포인트의 저장 순서가 읽기 순서를 결정한다
순서항목Fortran 선언검사 내용
1식별 문자열character(len=8)HTPLATE1인지 확인
2버전, 가로·세로 크기, 단계integer(int32) 네 개버전과 범위 확인
3시간, 시간 간격, 확산 계수, 격자 간격real(real64) 네 개유한한 값과 설정 일치 확인
4온도 배열real(real64) 배열읽기 성공과 유한성 확인

배열 전체를 입출력 목록에 적으면 Fortran의 배열 요소 순서로 처리된다. a(nx,ny)에서는 첫 번째 첨자가 먼저 변한다. 따라서 a(1,1), a(2,1)부터 a(nx,1)까지 처리한 다음 a(1,2)로 진행한다. 파일을 다른 언어로 읽을 때는 이 순서까지 전달해야 한다.

int32와 real64는 사용할 종류를 분명하게 하지만, 이 파일을 모든 컴퓨터에서 읽을 수 있는 교환 형식으로 만들지는 않는다. 바이트 순서, 실수 표현, 문자 표현 등은 별도 약속이 필요하다. 이 예제의 이진 파일은 같은 실행 환경에서 재시작하는 용도로 한정한다. 파일 위치를 바이트 수로 가정하지 않고 같은 종류와 순서의 입출력 목록으로 읽는다.

파일의 세부 처리와 GNU Fortran의 입출력 관련 설명은 GNU Fortran 공식 문서에서 확인할 수 있다. 실제 장기 보관 형식을 정할 때는 컴파일러와 파일 형식의 버전도 함께 기록하는 편이 좋다.

재시작은 읽기 성공 뒤에 상태를 교체한다

체크포인트에는 다음 단계를 계산하는 데 필요한 값이 들어 있어야 한다. 여기서는 온도 배열, 완료한 단계 수, 현재 시간과 계산 매개변수가 그 상태다. 고정된 경계 온도는 배열에도 들어 있으며, 계산 규칙은 프로그램이 알고 있다. 경계 온도를 사용자가 바꾸거나 시간에 따라 변화시키는 프로그램으로 확장한다면 그 설정도 저장 형식에 추가해야 한다.

단계 수의 의미를 먼저 정한다. saved_step=2는 두 단계를 이미 계산했다는 뜻이다. 재시작 반복은 saved_step+1에서 시작한다. 저장된 단계부터 다시 계산하면 한 단계를 중복 수행한다. 최종 단계까지 완료한 체크포인트라면 반복을 수행하지 않고 결과만 내보낼 수 있다.

파일 읽기는 두 부분으로 나눈다. 먼저 헤더를 읽어 식별 정보, 크기, 단계 범위와 매개변수를 검사한다. 그다음 임시 배열을 할당하고 온도를 읽는다. 읽기와 파일 닫기가 성공하고 온도에 유한하지 않은 값이 없을 때만 move_alloc으로 현재 배열을 교체한다. 덜 읽힌 배열이 계산에 들어가는 경로를 막는 구조다.

재시작은 헤더와 임시 배열을 검증한 뒤 현재 상태를 교체하고 다음 단계부터 계산한다

iostat은 입출력 문이 끝난 상태를 정수로 돌려준다. 이 예제에서는 0만 성공으로 받아들이며, 나머지는 오류 메시지를 출력한 뒤 종료한다. newunit으로 얻은 장치 번호는 값이 음수일 수도 있으므로 양수인지 검사하지 않는다. open의 성공 여부는 장치 번호 대신 iostat으로 판단한다.

검증에도 한계가 있다. 식별 정보와 유한성 검사는 모든 파일 손상을 발견하지 못한다. 온도의 일부 비트가 바뀌어 다른 유한한 실수가 되면 통과할 수 있다. 또한 status='replace'로 같은 파일을 덮어쓰다가 실행이 중단되면 기존 체크포인트도 잃을 수 있다. 예제는 입출력 실패 시 즉시 종료하며, 저장 실패 후 복구까지 제공하지는 않는다. 실제 운영에서는 서로 다른 파일명으로 세대를 남기고 검증된 파일을 선택하는 정책을 추가할 수 있다.

설정은 이름으로 읽고 결과는 행으로 내보낸다

네임리스트는 변수 이름과 값을 연결한다. namelist /run/ nx, ny, dt처럼 그룹을 선언하면 입력 파일에서 &run으로 그룹을 시작하고 /로 끝낸다. 변수의 순서는 선언 순서와 같을 필요가 없으며, 생략한 변수는 읽기 직전의 값을 유지한다. 따라서 읽기 전에 기본값을 정하고 읽기 뒤에 범위를 검사해야 한다.

변수 이름을 잘못 쓰거나 구문을 틀리면 읽기에서 오류가 날 수 있다. 반면 nx=1처럼 구문상 올바르지만 계산에 부적절한 값은 프로그램이 직접 거부해야 한다. 예제는 격자 크기를 3부터 200까지로 제한하고, 단계 수와 저장 시점도 검사한다. 시간 간격과 격자 간격은 양수이며 확산 계수는 음수가 아니어야 한다.

열 확산 계산은 앞서 사용한 명시적 갱신을 그대로 둔다. 두 방향의 격자 간격이 같은 경우 r=alpha*dt/dx**2를 사용하며, 여기서는 r이 0.25 이하인 설정만 허용한다. 입출력 기능을 붙인다고 수치 계산에 허용되는 설정 범위가 달라지는 것은 아니다.

CSV는 i,j,temperature라는 세 열을 가진다. 배열을 한 번에 텍스트로 쓰지 않고 격자점마다 한 행을 쓴다. 바깥 반복은 j, 안쪽 반복은 i로 두어 이진 배열 출력과 같은 요소 순서로 내보낸다. i와 j는 격자 첨자이며 물리 좌표가 아니다. 물리 좌표가 필요하다면 원점과 격자 간격을 정한 뒤 별도 열을 추가한다.

CSV의 온도에는 F12.6을 사용한다. 값 앞의 공백도 파일에 포함되지만, 열 구분은 쉼표가 맡는다. 소수 여섯 자리로 반올림한 텍스트 결과이므로 재시작의 입력으로 쓰지 않는다. 분석용 출력의 자리 수와 내부 계산의 정밀도는 서로 다른 선택이다. decimal='point'를 지정해 소수점 기호를 점으로 정한다.

완성 코드

다음 프로그램은 main.f90 하나로 구성된다. 실행할 때 settings.dat, checkpoint.bin, temperature.csv를 현재 디렉터리에 만들며 같은 이름의 기존 파일을 교체한다. 예제에서 설정 값을 바꾸려면 make_settings의 문자열을 수정한다. 기존 설정 파일을 직접 편집해 사용하려면 call make_settings()를 제거한 뒤 실행한다.

main.f90

program heat_restart
  use iso_fortran_env, only: int32, real64
  use, intrinsic :: ieee_arithmetic, only: ieee_is_finite
  implicit none

  integer :: nx, ny, nsteps, save_step
  integer :: u, ios, k, loaded_step
  real(real64) :: dt, alpha, dx, r, loaded_time
  real(real64) :: difference
  real(real64), allocatable :: reference(:,:), plate(:,:)
  namelist /run/ nx, ny, nsteps, save_step, dt, alpha, dx

  nx = 5
  ny = 5
  nsteps = 4
  save_step = 2
  dt = 0.1_real64
  alpha = 1.0_real64
  dx = 1.0_real64

  call make_settings()
  open(newunit=u, file='settings.dat', status='old', &
       action='read', form='formatted', decimal='point', iostat=ios)
  call check_io(ios, 'open settings')
  read(u, nml=run, iostat=ios)
  call check_io(ios, 'read settings')
  close(u, iostat=ios)
  call check_io(ios, 'close settings')

  call require(nx >= 3 .and. nx <= 200, 'nx out of range')
  call require(ny >= 3 .and. ny <= 200, 'ny out of range')
  call require(nsteps >= 1 .and. nsteps <= 100000, &
               'nsteps out of range')
  call require(save_step >= 1 .and. save_step <= nsteps, &
               'save_step out of range')
  call require(ieee_is_finite(dt), 'dt is not finite')
  call require(ieee_is_finite(alpha), 'alpha is not finite')
  call require(ieee_is_finite(dx), 'dx is not finite')
  call require(dt > 0.0_real64, 'dt must be positive')
  call require(alpha >= 0.0_real64, 'alpha must be nonnegative')
  call require(dx > 0.0_real64, 'dx must be positive')
  r = alpha * dt / dx**2
  call require(ieee_is_finite(r), 'r is not finite')
  call require(r >= 0.0_real64 .and. r <= 0.25_real64, &
               'r out of range')

  allocate(reference(nx,ny), plate(nx,ny))
  call initialize(reference)
  plate = reference

  do k = 1, nsteps
    call advance(reference, r)
  end do

  do k = 1, save_step
    call advance(plate, r)
  end do
  call write_checkpoint('checkpoint.bin', plate, save_step, &
                        real(save_step,real64) * dt)

  ! Discard the in-memory state before loading the file.
  deallocate(plate)
  call read_checkpoint('checkpoint.bin', plate, &
                       loaded_step, loaded_time)

  do k = loaded_step + 1, nsteps
    call advance(plate, r)
  end do

  difference = maxval(abs(reference - plate))
  call require(difference <= 100.0_real64 * epsilon(1.0_real64), &
               'restart comparison failed')
  call write_csv('temperature.csv', plate)

  write(*,'(A,I0,A,F8.3)') 'loaded step=', loaded_step, &
                           ', time=', loaded_time
  write(*,'(A,I0,A,F8.3)') 'final step=', nsteps, &
                           ', time=', real(nsteps,real64) * dt
  write(*,'(A,F12.6)') 'center=', plate((nx+1)/2,(ny+1)/2)
  write(*,'(A,F12.6)') 'mean=', sum(plate) / real(nx*ny,real64)
  write(*,'(A,F12.8)') 'max difference=', difference
  write(*,'(A)') 'CSV written: temperature.csv'

contains

  subroutine require(condition, message)
    logical, intent(in) :: condition
    character(len=*), intent(in) :: message

    if (.not. condition) then
      write(*,'(A)') 'ERROR: ' // message
      error stop 1
    end if
  end subroutine require

  subroutine check_io(status_code, operation)
    integer, intent(in) :: status_code
    character(len=*), intent(in) :: operation

    if (status_code /= 0) then
      write(*,'(A,A,A,I0)') 'I/O ERROR: ', operation, &
                            ', iostat=', status_code
      error stop 1
    end if
  end subroutine check_io

  subroutine make_settings()
    integer :: unit, stat

    open(newunit=unit, file='settings.dat', status='replace', &
         action='write', form='formatted', iostat=stat)
    call check_io(stat, 'create settings')
    write(unit,'(A)',iostat=stat) &
      '&run', &
      ' nx=5, ny=5, nsteps=4, save_step=2,', &
      ' dt=0.1, alpha=1.0, dx=1.0', &
      '/'
    call check_io(stat, 'write settings')
    close(unit, iostat=stat)
    call check_io(stat, 'close generated settings')
  end subroutine make_settings

  subroutine initialize(a)
    real(real64), intent(out) :: a(:,:)

    a = 0.0_real64
    a((size(a,1)+1)/2,(size(a,2)+1)/2) = 100.0_real64
  end subroutine initialize

  subroutine advance(a, ratio)
    real(real64), intent(inout) :: a(:,:)
    real(real64), intent(in) :: ratio
    real(real64) :: next(size(a,1),size(a,2))
    integer :: i, j

    next = a
    do j = 2, size(a,2) - 1
      do i = 2, size(a,1) - 1
        next(i,j) = a(i,j) + ratio * &
          (a(i-1,j) + a(i+1,j) + a(i,j-1) + a(i,j+1) &
           - 4.0_real64 * a(i,j))
      end do
    end do
    a = next
  end subroutine advance

  subroutine write_checkpoint(path, a, step, time)
    character(len=*), intent(in) :: path
    real(real64), intent(in) :: a(:,:), time
    integer, intent(in) :: step
    character(len=8), parameter :: magic = 'HTPLATE1'
    integer(int32), parameter :: version = 1_int32
    integer :: unit, stat

    open(newunit=unit, file=path, status='replace', &
         access='stream', form='unformatted', &
         action='write', iostat=stat)
    call check_io(stat, 'open checkpoint for write')
    write(unit,iostat=stat) magic, version, &
      int(size(a,1),int32), int(size(a,2),int32), int(step,int32), &
      time, dt, alpha, dx
    call check_io(stat, 'write checkpoint header')
    write(unit,iostat=stat) a
    call check_io(stat, 'write checkpoint array')
    close(unit, iostat=stat)
    call check_io(stat, 'close written checkpoint')
  end subroutine write_checkpoint

  subroutine read_checkpoint(path, a, step, time)
    character(len=*), intent(in) :: path
    real(real64), allocatable, intent(inout) :: a(:,:)
    integer, intent(out) :: step
    real(real64), intent(out) :: time
    character(len=8) :: magic
    integer(int32) :: version, file_nx, file_ny, file_step
    real(real64) :: file_time, file_dt, file_alpha, file_dx
    real(real64) :: tolerance
    real(real64), allocatable :: candidate(:,:)
    integer :: unit, stat

    open(newunit=unit, file=path, status='old', &
         access='stream', form='unformatted', &
         action='read', iostat=stat)
    call check_io(stat, 'open checkpoint for read')
    read(unit,iostat=stat) magic, version, file_nx, file_ny, &
      file_step, file_time, file_dt, file_alpha, file_dx
    call check_io(stat, 'read checkpoint header')

    call require(magic == 'HTPLATE1', 'wrong checkpoint magic')
    call require(version == 1_int32, 'unsupported checkpoint version')
    call require(file_nx == int(nx,int32), 'checkpoint nx mismatch')
    call require(file_ny == int(ny,int32), 'checkpoint ny mismatch')
    call require(file_step >= 0_int32 .and. &
                 file_step <= int(nsteps,int32), 'invalid saved step')
    call require(all(ieee_is_finite( &
      [file_time, file_dt, file_alpha, file_dx])), &
      'checkpoint metadata is not finite')
    call require(abs(file_dt - dt) <= 0.0_real64, 'dt mismatch')
    call require(abs(file_alpha - alpha) <= 0.0_real64, &
                 'alpha mismatch')
    call require(abs(file_dx - dx) <= 0.0_real64, 'dx mismatch')
    tolerance = 32.0_real64 * epsilon(1.0_real64) * &
      max(1.0_real64, abs(file_time))
    call require(abs(file_time - real(file_step,real64)*dt) &
                 <= tolerance, 'saved time mismatch')

    allocate(candidate(nx,ny))
    read(unit,iostat=stat) candidate
    call check_io(stat, 'read checkpoint array')
    close(unit, iostat=stat)
    call check_io(stat, 'close read checkpoint')
    call require(all(ieee_is_finite(candidate)), &
                 'checkpoint temperature is not finite')

    call move_alloc(candidate, a)
    step = int(file_step)
    time = file_time
  end subroutine read_checkpoint

  subroutine write_csv(path, a)
    character(len=*), intent(in) :: path
    real(real64), intent(in) :: a(:,:)
    integer :: unit, stat, i, j

    open(newunit=unit, file=path, status='replace', &
         action='write', form='formatted', decimal='point', &
         iostat=stat)
    call check_io(stat, 'open CSV')
    write(unit,'(A)',iostat=stat) 'i,j,temperature'
    call check_io(stat, 'write CSV header')
    do j = 1, size(a,2)
      do i = 1, size(a,1)
        write(unit,'(I0,",",I0,",",F12.6)',iostat=stat) i, j, a(i,j)
        call check_io(stat, 'write CSV row')
      end do
    end do
    close(unit, iostat=stat)
    call check_io(stat, 'close CSV')
  end subroutine write_csv

end program heat_restart

줄별 해설

use와 선언부. iso_fortran_env에서 파일에 사용할 정수와 실수 종류를 가져온다. ieee_is_finite는 메타데이터와 온도 배열에서 무한대와 NaN을 거부하는 데 쓴다. namelist 선언은 새로운 변수를 만드는 문장이 아니다. 이미 선언한 변수들을 run이라는 입력 그룹으로 묶는다.

기본값 설정과 설정 읽기. 먼저 모든 설정 변수에 값을 준다. make_settings가 입력 파일을 만들고, status='old'인 open이 그 파일의 존재를 요구한다. read(u,nml=run)은 이름에 대응하는 값을 읽는다. 읽기 뒤에 close도 검사하므로 파일을 열고 읽는 단계만 성공했다고 전체 작업의 성공을 가정하지 않는다.

입력 검증. 격자와 단계 수를 제한한 다음 실수 값의 유한성과 부호를 검사한다. r을 계산한 뒤에도 유한성을 확인한다. 양수인 설정이라도 극단적으로 큰 값이나 작은 값은 중간 계산의 표현 범위를 벗어날 수 있기 때문이다. 이 코드는 잘못된 설정을 복구하지 않고 설명을 출력한 뒤 종료한다.

기준 실행과 저장 실행. initialize는 배열을 0으로 채우고 중앙 격자점만 100으로 둔다. reference는 끝까지 계산하는 기준 배열이다. plate는 save_step까지만 계산한 다음 파일에 저장한다. 두 배열은 동일한 갱신 프로시저를 사용하므로 비교가 입출력 경로의 영향을 드러낸다.

메모리 상태 폐기. 저장 뒤 deallocate(plate)를 호출한다. 파일 읽기를 생략하거나 잘못 구현했는데도 이전 메모리 값으로 계산이 계속되는 상황을 피하려는 장치다. 재시작 후 반복의 시작값은 loaded_step+1이다. loaded_time은 확인용으로 출력하며, 최종 시간은 완료한 단계 수와 dt의 곱으로 계산한다.

advance. next=a로 경계를 포함한 현재 값을 복사하고 내부 격자점만 바꾼다. 이 예제의 경계는 초기의 0을 유지한다. 갱신식을 별도 배열에 계산한 뒤 a에 대입하는 구조를 유지하므로, 파일 저장 기능을 붙여도 한 단계의 계산 의미가 바뀌지 않는다.

헤더 쓰기. magic은 길이 8의 문자이며 version은 int32 정수다. 배열 크기와 단계 수도 int32로 변환해 쓴다. 현재 설정의 크기와 단계 수를 제한했으므로 이 변환에 필요한 범위가 확보된다. dt, alpha, dx는 내부 프로시저가 주 프로그램의 값을 참조한다.

헤더 읽기. 쓰기와 동일한 순서 및 종류로 변수를 나열한다. 헤더 read가 실패하면 그 뒤의 변수 검사를 수행하지 않는다. 크기는 검증된 현재 설정과 같아야 하며, 단계 수는 0부터 nsteps까지 허용한다. 따라서 파일이 제시한 크기를 검증 없이 allocate에 넘기지 않는다.

실수 설정 비교. 매개변수 비교는 차이의 절댓값이 0 이하인지 검사한다. 같은 환경에서 저장한 설정과 수치적으로 같은 값을 요구하는 조건이다. 허용 오차로 설정 변경을 받아들이려는 검사가 아니다. 반면 저장 시간의 일관성은 epsilon을 바탕으로 작은 반올림 차이를 허용한다. 비교의 목적에 따라 두 기준을 구분한다.

임시 배열과 상태 교체. candidate에 온도를 읽고 close를 마친 뒤 모든 요소의 유한성을 확인한다. move_alloc은 candidate의 할당을 a로 이전한다. a가 이미 할당되어 있더라도 그 기존 할당을 해제하고 이전할 수 있다. 마지막에 step과 time을 돌려주므로 정상 반환 시 배열과 메타데이터가 함께 준비된다.

결과 비교와 CSV. maxval(abs(reference-plate))는 모든 격자점 가운데 가장 큰 차이를 찾는다. 비교를 통과한 뒤 CSV를 작성한다. 이 검사는 저장과 재시작 경로의 일관성을 확인하며, 열 방정식 해법 자체의 정확도를 입증하는 검사는 아니다.

실행 결과

main.f90을 저장한 디렉터리에서 다음과 같이 컴파일하고 실행한다. 프로그램은 표준 입력을 읽지 않는다.

gfortran -std=f2018 -Wall main.f90 -o heat_restart
./heat_restart

기본 설정의 예상 표준 출력은 다음과 같다. 필드 너비에 따른 공백도 출력의 일부다.

loaded step=2, time=   0.200
final step=4, time=   0.400
center=   21.920000
mean=    3.283200
max difference=  0.00000000
CSV written: temperature.csv

프로그램이 만든 설정 파일은 다음 내용을 가진다. 네임리스트의 실수 문자열은 읽을 때 해당 변수의 real64 종류로 변환된다.

settings.dat

&run
 nx=5, ny=5, nsteps=4, save_step=2,
 dt=0.1, alpha=1.0, dx=1.0
/

CSV 파일에는 머리글 한 행과 격자점 25개의 행이 들어 있다. 다음 명령으로 가운데 행을 확인할 수 있다.

sed -n '12,16p' temperature.csv
1,3,    0.000000
2,3,   10.560000
3,3,   21.920000
4,3,   10.560000
5,3,    0.000000

중앙 온도는 100에서 21.92로 내려가고 주변으로 열이 퍼진다. 경계 온도를 0으로 고정했으므로 판 전체의 평균 온도도 내려간다. 최대 차이가 표시 자리에서 0이라는 출력에만 의존하지 않고, 프로그램 내부의 허용 오차 검사도 함께 수행한다. 기본 실행에서는 저장 전후에 같은 계산 순서를 사용하므로 두 최종 배열이 같은 값을 얻는다.

실무에서 자주 틀리는 것

쓰기와 읽기의 종류를 다르게 사용한다

실수를 문자열로 읽는 경우와 달리 비서식 파일은 종류 변환을 자동으로 해 주는 입력 형식이 아니다. real64 값을 쓴 자리에 기본 실수를 읽으면 소비하는 파일 저장 단위와 이후 위치가 맞지 않을 수 있다.

틀린 코드는 다음과 같다.

real(real64) :: saved_dt
real :: loaded_dt

write(u) saved_dt
! A different unit must be positioned at the same field for reading.
read(v) loaded_dt

고친 코드는 저장한 값과 같은 종류로 읽는다. 단위 u와 v는 각각 쓰기와 읽기 용도로 연 파일이라고 가정한다.

real(real64) :: saved_dt, loaded_dt

write(u) saved_dt
read(v) loaded_dt

정수와 문자열에도 같은 원칙이 적용된다. 종류나 문자열 길이를 바꾸면 파일 형식 버전도 함께 검토해야 한다.

읽기 오류를 무시하고 재시작한다

잘린 파일은 일부 요소만 읽은 상태로 오류를 돌려줄 수 있다. 그 배열을 바로 계산에 사용하면 정상적으로 복원한 상태라고 볼 수 없다.

틀린 코드는 다음과 같다.

read(u, iostat=ios) plate
call advance(plate, r)

고친 코드는 임시 배열의 읽기 결과를 검사한 뒤 상태를 바꾼다. candidate와 plate는 할당 배열이라고 가정한다.

allocate(candidate(nx,ny))
read(u, iostat=ios) candidate
call check_io(ios, 'read temperature')
call require(all(ieee_is_finite(candidate)), 'invalid temperature')
call move_alloc(candidate, plate)
call advance(plate, r)

완성 코드에서는 파일 닫기까지 성공한 뒤 상태를 교체한다. 오류에서 복귀해 기존 계산을 계속하려면 종료 대신 오류 상태를 호출자에게 돌려주는 구조도 필요하다.

저장한 단계를 다시 계산한다

저장 단계가 완료한 단계 수인지 다음에 계산할 단계인지 혼용하면 결과가 한 단계씩 어긋난다. 예제의 파일에는 완료한 단계 수를 기록한다.

틀린 코드는 다음과 같다.

do k = loaded_step, nsteps
  call advance(plate, r)
end do

고친 코드는 다음 단계부터 시작한다.

do k = loaded_step + 1, nsteps
  call advance(plate, r)
end do

시간도 같은 의미로 저장해야 한다. 두 단계를 완료했다면 저장 시간은 2*dt이며, 재시작 직후 아직 세 번째 단계는 수행하지 않은 상태다.

서식 없는 목록 출력으로 CSV를 만든다

목록 지향 출력은 쉼표로 구분된 열과 일정한 소수 자릿수를 보장하는 CSV 작성 규칙이 아니다. 읽는 프로그램이 우연히 받아들였다는 이유로 파일 규칙을 생략하지 않는다.

틀린 코드는 다음과 같다.

write(u, *) i, j, plate(i,j)

고친 코드는 구분 문자와 숫자 서식을 명시한다.

write(u, '(I0,",",I0,",",F12.6)', iostat=ios) i, j, plate(i,j)
call check_io(ios, 'write CSV row')

F12.6의 폭을 넘는 값은 별표로 출력될 수 있다. 예제의 온도 범위에는 충분한 폭이지만, 출력 대상의 규모가 바뀌면 필드 폭이나 지수 표기 서식을 다시 선택해야 한다.

한눈에 보기

저장과 재시작에서 확인할 약속
항목사용한 방법확인할 점
설정 입력namelist /run/기본값을 먼저 정하고 범위를 검사한다
이진 저장access='stream', form='unformatted'종류와 항목 순서를 읽기와 맞춘다
파일 식별식별 문자열과 버전다른 파일과 지원하지 않는 형식을 거부한다
배열 복원임시 배열과 move_alloc읽기 성공 뒤에 현재 상태를 교체한다
재시작 반복loaded_step+1부터 계산완료한 단계를 중복 계산하지 않는다
결과 내보내기명시적 CSV 서식열의 의미, 요소 순서, 자릿수를 정한다
실패 확인iostat 검사open, read, write, close를 모두 확인한다

다음 장에서는 설정 읽기, 계산, 저장과 결과 출력의 연결을 종합 실습에 적용한다. 여기서 정한 파일의 역할과 저장 단계의 의미가 그 연결의 기준이 된다.

연습 문제

  1. save_step을 4로 바꿔 실행하라. loaded step, loaded time과 재시작 뒤 계산 반복의 수행 횟수를 설명하라. 최종 중앙 온도가 바뀌는지도 판단하라.
  2. CSV에 물리 좌표 x와 y를 추가하라. 원점은 격자점 (1,1), 두 방향의 간격은 dx로 정한다. 머리글과 행 출력문을 작성하라.
  3. 체크포인트 저장 뒤 파일을 빈 파일로 바꾸는 코드를 임시로 넣어라. 다시 읽을 때 어느 검사에서 종료하는지 설명하라. 운영체제마다 같지 않을 수 있는 iostat 값은 답에 고정하지 않는다.
  4. 경계 온도를 설정 변수 boundary로 바꾸려 한다. 기본값, 네임리스트, 초기화, 체크포인트 형식에서 바꿀 곳과 버전 변경이 필요한 이유를 설명하라.

정답과 해설

  1. loaded step은 4, loaded time은 0.400이다. 반복 범위는 5부터 4가 되어 본문을 수행하지 않는다. 저장 파일에 최종 상태가 이미 들어 있으므로 중앙 온도는 21.920000으로 유지된다. 완성 코드가 설정 파일을 매번 다시 만들기 때문에 make_settings의 save_step 문자열을 바꿔야 한다.

  2. 좌표는 x=real(i-1,real64)*dx, y=real(j-1,real64)*dx이다. 기존 첨자 열도 남기면 격자와 물리 좌표를 함께 확인할 수 있다. write_csv의 머리글과 행 출력문을 다음처럼 바꾼다.

    write(unit,'(A)',iostat=stat) 'i,j,x,y,temperature'
    call check_io(stat, 'write CSV header')
    
    ! Place this statement inside the existing i, j loops.
    write(unit,'(I0,",",I0,3(",",F12.6))',iostat=stat) &
      i, j, real(i-1,real64)*dx, real(j-1,real64)*dx, a(i,j)
    call check_io(stat, 'write CSV row')
    

    좌표 역시 고정된 소수 자릿수로 내보낸다. 판의 원점이나 두 방향 간격이 달라지면 좌표 정의와 열의 설명도 함께 바꿔야 한다.

  3. write_checkpoint 호출 뒤에 다음 코드를 넣으면 헤더도 없는 파일이 된다. 주 프로그램에 선언된 u와 ios를 사용한다.

    open(newunit=u, file='checkpoint.bin', status='replace', &
         access='stream', form='unformatted', action='write', iostat=ios)
    call check_io(ios, 'truncate checkpoint')
    close(u, iostat=ios)
    call check_io(ios, 'close empty checkpoint')
    

    재시작의 open은 파일이 존재하므로 성공할 수 있다. 이어지는 헤더 read가 파일 끝을 만나 실패하고, check_io의 read checkpoint header 검사에서 종료한다. 따라서 식별 문자열 검사나 배열 할당에는 도달하지 않는다.

  4. boundary를 real64로 선언하고 기본값을 준 뒤 run 그룹에 추가한다. 설정 파일에도 값을 쓰고 유한성을 검사한다. initialize에서 배열 전체를 boundary로 채운 뒤 중앙 온도를 설정하면 advance가 경계를 계속 유지한다. 체크포인트 헤더에는 boundary를 쓰고 읽는 변수를 같은 위치에 추가하며 현재 설정과 일치하는지 확인한다. 읽기 항목이 늘어나므로 형식 버전을 2로 바꾸고 지원 버전 검사도 수정해야 한다. 이전 파일을 그대로 새 순서로 읽으면 온도 배열의 일부를 새 메타데이터로 오해할 수 있다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.