Devin.KR

Fortran · 기본

계산으로 배우는 현대 Fortran

변수와 종류(kind) - 정밀도를 직접 고르는 언어

integer·real·logical·character, iso_fortran_env 의 int32·real64, 리터럴 접미사 _real64, parameter 상수, 단정도 기본값 함정

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서는 프로그램을 컴파일하고 실행해 첫 계산 결과를 화면에 찍었다. 이번 장에서는 계산에 쓰는 값을 담는 그릇, 곧 변수를 다룬다. Fortran 에서 변수는 "무엇을 담는가"(자료형)뿐 아니라 "얼마나 정밀하게 담는가"(종류, kind)까지 프로그래머가 직접 정한다. 이 선택을 건너뛰면 계산은 돌아가지만 숫자가 조용히 어긋난다. 이 장은 그 어긋남이 어디서 생기는지 보이고, 피하는 습관을 만든다.

예제는 작은 기상 관측소의 자료다. 네 시각의 기온을 읽어 평균을 내고, 영하로 내려간 시각이 있었는지 판단하고, 강수량과 하늘 상태를 기록한다. 이 프로그램은 뒤 장에서 배열, 파일, 파생 타입을 얹으며 계속 자란다.

  • integer, real, logical, character 네 가지 기본 자료형을 선언하고 값을 넣을 수 있다.
  • iso_fortran_env 모듈의 int32 와 real64 로 정수와 실수의 종류를 이식성 있게 고를 수 있다.
  • 리터럴 접미사 _real64 를 붙여 상수의 정밀도를 맞출 수 있다.
  • parameter 로 바뀌지 않는 값에 이름을 붙일 수 있다.
  • 접미사 없는 실수 리터럴이 단정도로 해석되는 함정을 설명하고 피할 수 있다.

문제 상황

관측소 담당자가 하루치 기온의 평균을 구하는 프로그램을 짰다고 하자. 값은 소수점 아래 한두 자리뿐이라 문제될 것이 없어 보인다. 그런데 어느 날 같은 자료를 다른 컴퓨터에서 돌렸더니 평균이 소수점 일곱 번째 자리쯤에서 다르게 나왔다. 코드를 아무리 읽어도 계산식은 같다.

원인은 대개 변수와 상수의 정밀도다. 한쪽 컴퓨터에서는 실수가 4바이트로, 다른 쪽에서는 8바이트로 저장됐거나, 한 프로그램 안에서 어떤 값은 4바이트로 만들어진 뒤 8바이트 변수에 들어갔을 수 있다. 4바이트 실수는 유효 숫자가 6자리 남짓이어서, 한 번 거친 값은 8바이트 변수에 넣어도 잃은 자릿수가 돌아오지 않는다. 관측값 하나하나는 문제가 없어 보여도 수천 개를 더하면 오차가 눈에 띈다.

Fortran 은 이 선택을 숨기지 않고 코드에 드러내게 하는 언어다. 선언에 종류를 적고, 상수에도 종류를 적는다. 처음에는 글자가 늘어 번거롭지만, 어느 컴퓨터에서 돌려도 같은 결과를 얻게 해 준다.

자료형 네 가지와 종류

기본 자료형

Fortran 의 기본 자료형은 네 가지다. 정수(integer)는 소수점이 없는 수, 실수(real)는 소수점이 있는 수, 논리형(logical)은 참과 거짓, 문자형(character)은 글자의 나열이다. 변수는 사용하기 전에 선언문으로 자료형과 이름을 밝힌다.

네 가지 기본 자료형과 값을 쓰는 방법
자료형담는 값리터럴 예이 장의 용도
integer정수4_int32관측 횟수
real실수3.5_real64기온, 강수량
logical참 또는 거짓.true.영하 여부
character글자"rain"관측소 이름, 하늘 상태

리터럴(literal)은 코드에 직접 적은 값 자체를 가리키는 말이다. 3.5 나 "rain" 처럼 이름 없이 값만 적은 것이다. 논리형의 참과 거짓은 앞뒤에 점을 붙여 .true., .false. 로 쓴다.

모든 프로그램 단위의 맨 앞에는 implicit none 을 둔다. 이 문장이 없으면 Fortran 은 선언하지 않은 이름을 첫 글자로 자료형을 짐작하는 옛 규칙을 적용해, 오타로 쓴 변수까지 새 변수로 받아들인다. 이 문장을 두면 선언하지 않은 이름은 컴파일 오류가 된다.

종류(kind)는 정밀도를 고르는 값이다

같은 real 이라도 컴퓨터 안에서 몇 바이트를 쓰느냐에 따라 표현할 수 있는 정밀도가 다르다. 이 선택지를 Fortran 은 종류라는 정수로 부른다. 종류의 번호는 컴파일러마다 다를 수 있어서, 코드에 real(8) 처럼 숫자를 직접 적으면 다른 컴파일러에서 뜻이 달라질 수 있다. 표준이 보장하는 방법은 iso_fortran_env 모듈이 주는 이름을 쓰는 것이다.

use, intrinsic :: iso_fortran_env, only: int32, real32, real64

여기서 int32 는 32비트(4바이트) 정수의 종류, real32 는 32비트 실수(단정도), real64 는 64비트 실수(배정도)의 종류를 나타내는 이름이다. only: 는 모듈에서 이 이름들만 가져오겠다는 뜻이다. 선언은 real(real64) :: t1 처럼 자료형 뒤 괄호에 종류를 적는다.

real64 는 real32 의 두 배인 8바이트를 쓰며 유효 숫자가 약 15자리로 훨씬 많다.
종류별 크기와 정밀도 (gfortran 기준, 이름은 iso_fortran_env 에서 가져온다)
종류 이름크기유효 숫자표현 범위
int324바이트정수 전부약 ±21억 (2147483647 까지)
real324바이트약 6자리약 1e-38 ~ 1e38
real648바이트약 15자리약 1e-308 ~ 1e308

유효 숫자는 앞에서부터 믿을 수 있는 자릿수다. 기온을 소수 둘째 자리까지 보는 용도라면 real32 로도 충분해 보인다. 그러나 수천 개의 값을 더하고 나누는 순간 작은 오차가 쌓인다. 과학 계산에서는 real64 를 기본으로 삼는 것이 안전하며, 이 책의 예제도 그렇게 한다.

parameter 로 이름 붙인 상수

영하를 가르는 기준 0도, 하루의 시간 수 24 처럼 프로그램이 도는 동안 바뀌지 않는 값은 선언에 parameter 를 붙인다. 이렇게 하면 값이 선언 자리에서 정해지고, 뒤에서 대입하려 하면 컴파일 오류가 난다. 코드 중간에 24 나 0.0 이 이유 없이 나타나는 것보다 이름이 붙은 상수가 읽기 쉽고, 값을 바꿀 때도 한 군데만 고치면 된다.

real(real64), parameter :: frost_limit = 0.0_real64
character(len=*), parameter :: station = "Gwanak"

문자형 상수의 len=* 는 길이를 대입된 문자열에서 가져오라는 표시다. 길이를 직접 세지 않아도 된다.

리터럴 접미사와 단정도 기본값 함정

변수에 종류를 붙였으면 상수에도 붙여야 한다. 실수 리터럴은 _real64 를 붙여 0.1_real64 처럼 쓴다. 정수 리터럴은 _int32 를 붙인다. 밑줄 뒤의 이름은 종류 이름이며, 앞에서 가져온 이름과 같아야 한다.

접미사를 빼면 리터럴은 컴파일러의 기본 종류가 된다. gfortran 에서 기본 실수는 4바이트 단정도다. 그래서 0.1 이라고만 쓰면 이 수는 먼저 단정도로 만들어진다. 이것을 real64 변수에 대입해도 이미 반올림된 값이 옮겨질 뿐이다. 변수 선언은 real64 로 했으니 안심이라는 생각이 이 함정의 출발점이다.

이 차이를 직접 보려고 이 장의 프로그램에 같은 0.1 을 두 방식으로 저장해 17자리까지 찍는 부분을 넣었다. 단정도로 만든 0.1 은 소수 아홉째 자리부터 참값에서 벗어난다. 배정도로 만든 0.1 은 소수 열일곱째 자리에서야 반올림 차이가 드러난다.

같은 0.1 이라도 접미사가 없으면 단정도 값이 되어 소수 아홉째 자리부터 어긋난다.

실수 값을 이진수로 저장하기 때문에 0.1 은 어느 종류에서도 정확히 담기지 않는다. 문제는 그 오차가 종류에 따라 크게 달라진다는 점이다. 이 오차를 다루는 방법은 다음 장에서 식과 내장 함수를 배우며 더 살펴본다.

완성 코드

아래는 앞의 내용을 모두 담은 main.f90 이다. 변수와 상수를 선언하고, 평균과 영하 여부를 계산하고, 마지막에 종류에 대한 정보와 0.1 의 두 표현을 출력한다.

program main
  use, intrinsic :: iso_fortran_env, only: int32, real32, real64
  implicit none

  character(len=*), parameter :: station = "Gwanak"
  integer(int32), parameter :: n_obs = 4_int32
  real(real64), parameter :: frost_limit = 0.0_real64

  real(real64) :: t1, t2, t3, t4
  real(real64) :: t_sum, t_mean, rain_mm
  logical :: has_frost
  character(len=6) :: sky
  real(real32) :: tenth_single
  real(real64) :: tenth_widened, tenth_double

  t1 = 3.5_real64
  t2 = 1.25_real64
  t3 = -0.75_real64
  t4 = 2.0_real64
  rain_mm = 12.4_real64
  sky = "rain"

  t_sum = t1 + t2 + t3 + t4
  t_mean = t_sum / real(n_obs, real64)
  has_frost = t3 < frost_limit

  tenth_single = 0.1_real32
  tenth_widened = tenth_single
  tenth_double = 0.1_real64

  print "(a, a)", "station: ", station
  print "(a, i0)", "observations: ", n_obs
  print "(a, f8.2)", "sum (C):", t_sum
  print "(a, f8.2)", "mean (C):", t_mean
  print "(a, l1)", "frost: ", has_frost
  print "(a, f8.1)", "rain (mm):", rain_mm
  print "(a, a, a)", "sky: [", sky, "]"
  print "(a, a, a)", "sky: [", trim(sky), "]"
  print "(a, i0, a, i0)", "int32 kind=", int32, " real64 kind=", real64
  print "(a, i0, a, i0)", "digits real32: ", precision(0.0_real32), " real64: ", precision(0.0_real64)
  print "(a, i0)", "max int32: ", huge(0_int32)
  print "(a, f20.17)", "single 0.1:", tenth_widened
  print "(a, f20.17)", "double 0.1:", tenth_double
end program main

줄별 해설

use, intrinsic :: iso_fortran_env, only: ... 는 컴파일러가 기본으로 제공하는 모듈에서 종류 이름 세 개를 가져온다. intrinsic 은 직접 만든 모듈이 아니라 언어 처리계에 들어 있는 모듈이라는 표시다. 모듈은 뒤 장에서 직접 만든다. 그다음 줄의 implicit none 은 선언 없는 이름을 오류로 만든다. 이 두 줄은 선언문보다 앞에 와야 한다.

character(len=*), parameter :: station = "Gwanak" 는 관측소 이름 상수다. 한글 대신 영문을 쓴 것은 문자 하나가 차지하는 바이트 수가 환경마다 다를 수 있어 길이 계산이 어긋날 수 있기 때문이다. n_obs 는 정수 상수이며 값에 _int32 를 붙여 종류를 맞췄다. frost_limit 는 영하 기준이다.

변수 선언에서 t1 부터 t4 는 네 시각의 기온이고, 쉼표로 이어 한 줄에 선언할 수 있다. 같은 종류의 변수끼리 묶어 두면 읽기 편하다. logical :: has_frost 는 영하 시각이 있었는지를 담는다. character(len=6) :: sky 는 정확히 6글자를 담는 문자 변수다. 문자 변수는 길이가 정해져 있어서 더 짧은 문자열을 넣으면 오른쪽이 공백으로 채워지고, 더 긴 문자열을 넣으면 잘린다.

값 대입에서 모든 실수에 _real64 를 붙였다. 음수는 -0.75_real64 처럼 부호를 앞에 둔다. 계산 부분의 real(n_obs, real64) 는 정수 4 를 real64 실수 4.0 으로 바꾸는 내장 함수 호출이다. 실수를 정수로 나누는 식의 규칙은 다음 장에서 자세히 다루고, 여기서는 나누는 쪽을 실수로 맞춰 둔다고만 이해하면 된다. 합은 3.5 + 1.25 - 0.75 + 2.0 = 6.0 이고 평균은 1.5 다. has_frost = t3 < frost_limit 는 비교의 결과인 참 또는 거짓을 논리 변수에 넣는다. -0.75 는 0 보다 작으므로 참이 된다.

0.1 을 다루는 부분에서 tenth_single 은 단정도 변수이고 0.1_real32 는 단정도 상수다. tenth_widened = tenth_single 은 이 단정도 값을 배정도 변수에 옮긴다. 값은 그대로이며 잃은 자릿수가 복구되지는 않는다. tenth_double 은 처음부터 배정도 상수로 만들었다. 두 값을 비교하면 접미사 없는 리터럴이 왜 문제인지 알 수 있다.

출력 부분의 따옴표 안 문자열은 서식(format)이다. a 는 문자열, i0 는 정수를 필요한 만큼의 폭으로, l1 은 논리값을 한 글자(T 또는 F)로 찍는다. f8.2 는 전체 폭 8칸에 소수점 아래 2자리, f20.17 은 폭 20칸에 17자리다. 폭이 값보다 넓으면 왼쪽에 공백이 채워진다. trim(sky) 는 문자열 뒤쪽의 공백을 떼어 낸 결과를 돌려준다. precision 은 해당 종류의 유효 십진 자릿수를, huge 는 해당 종류가 담을 수 있는 최댓값을 돌려주는 내장 함수다. 인자는 값 자체가 아니라 종류를 알려 주는 용도이므로 0.0_real32 처럼 아무 값이나 쓴다.

실행 결과

터미널에서 다음 두 명령을 차례로 실행한다. -std=f2018 는 표준 Fortran 2018 규칙만 허용하게 하고, -Wall 은 흔한 실수에 경고를 낸다. 이 코드는 경고 없이 컴파일된다.

$ gfortran -std=f2018 -Wall -o main main.f90
$ ./main
station: Gwanak
observations: 4
sum (C):    6.00
mean (C):    1.50
frost: T
rain (mm):    12.4
sky: [rain  ]
sky: [rain]
int32 kind=4 real64 kind=8
digits real32: 6 real64: 15
max int32: 2147483647
single 0.1: 0.10000000149011612
double 0.1: 0.10000000000000001

종류 번호 4 와 8 은 gfortran 에서의 값이다. 다른 컴파일러에서는 다른 숫자가 나올 수 있어서, 코드에는 번호 대신 항상 이름을 쓴다.

실무에서 자주 틀리는 것

접미사 없는 실수 리터럴을 배정도 변수에 넣는다

변수만 real64 로 선언하고 상수는 그냥 적으면 값이 단정도로 만들어진다. gfortran 은 -Wall 에서 값이 바뀐다는 경고를 내기도 하지만, 경고를 읽지 않고 넘기기 쉽다.

! 틀린 코드: 0.1 은 단정도로 해석된다
real(real64) :: dt
dt = 0.1

! 고친 코드
real(real64) :: dt
dt = 0.1_real64

습관으로 삼을 것은 단순하다. 코드에 쓰는 모든 실수 리터럴 뒤에 변수와 같은 종류 이름을 붙인다. 정수 0 이나 1 처럼 값이 정확히 표현되는 경우에도 같은 습관을 들이면 읽을 때 의심이 줄어든다.

implicit none 을 빼먹는다

이 줄이 없으면 오타가 오류로 잡히지 않고 새 변수로 처리된다. 아래 코드에서 t_mean 을 t_men 으로 잘못 썼는데도 컴파일이 통과하고, 출력되는 값은 의미 없는 수가 된다.

! 틀린 코드
program main
  real(8) :: t_mean
  t_men = 1.5
  print *, t_mean
end program main

! 고친 코드
program main
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  real(real64) :: t_mean
  t_mean = 1.5_real64
  print "(f8.2)", t_mean
end program main

고친 코드에서 t_men 으로 오타를 내면 컴파일러가 "선언되지 않은 이름"이라고 알려 준다. 오류는 실행 뒤가 아니라 컴파일할 때 잡는 편이 훨씬 싸다.

종류 번호를 코드에 직접 적는다

real(8) 은 gfortran 에서는 배정도로 동작하지만, 표준이 그렇게 보장하지는 않는다. 종류 번호를 다르게 매기는 컴파일러에서는 다른 정밀도가 되거나 컴파일되지 않는다.

! 틀린 코드
real(8) :: t
t = 3.5d0

! 고친 코드
use, intrinsic :: iso_fortran_env, only: real64
real(real64) :: t
t = 3.5_real64

3.5d0 처럼 지수 문자 d 로 배정도를 쓰는 옛 방식도 같은 이유로 피한다. 이름으로 쓴 종류는 읽는 사람에게 뜻이 분명하게 전해진다.

문자 변수의 길이를 문자열보다 짧게 잡는다

문자 변수는 길이가 고정이라 긴 문자열을 넣으면 오른쪽이 조용히 잘린다. 컴파일러가 대입을 보고 경고를 낼 때도 있지만, 입력이 실행 때 정해지는 경우에는 알려 주지 못한다.

! 틀린 코드: "drizzle" 은 7글자인데 6칸뿐이다
character(len=6) :: sky
sky = "drizzle"        ! 저장되는 값은 "drizzl"

! 고친 코드
character(len=7) :: sky
sky = "drizzle"

가능한 값 가운데 가장 긴 것에 맞춰 길이를 잡는다. 반대로 짧은 문자열은 공백으로 채워지므로, 비교하거나 출력할 때 trim 으로 뒤쪽 공백을 떼는 것을 기억한다.

한눈에 보기

이 장에서 쓴 문법과 주의점
항목쓰는 법예주의
종류 이름 가져오기use, intrinsic :: iso_fortran_env, only: ...int32, real64선언보다 앞에 둔다
정수 선언integer(int32) :: nn = 4_int32범위는 약 ±21억
실수 선언real(real64) :: xx = 3.5_real64상수에도 접미사를 붙인다
논리형logical :: flag.true.앞뒤에 점을 붙인다
문자형character(len=6) :: ss = "rain"짧으면 공백으로 채워지고 길면 잘린다
상수parameterfrost_limit나중에 대입하면 컴파일 오류
정밀도 확인precision(x), huge(n)precision(0.0_real64)인자는 종류를 알려 주는 용도
접미사 유무에 따른 리터럴의 종류
적은 모양해석되는 종류real64 변수에 대입한 뒤권장
0.1기본 실수(단정도)단정도 값이 그대로 옮겨진다피한다
0.1_real32real32단정도 값이 그대로 옮겨진다의도했을 때만
0.1_real64real64배정도 값이 된다기본으로 쓴다

연습 문제

  1. 하루의 시간 수 24 를 int32 상수 hours_per_day 로, 강수 기준 0.1 mm 를 real64 상수 rain_threshold_mm 로 선언하라. 그리고 변수 rain_mm 에 2.5 를 넣고, 기준 이상이면 참이 되는 논리 변수 raining 에 결과를 담는 코드를 써라.
  2. 다음 선언에서 어떤 점이 문제인지 설명하고 고쳐라.
    real(real64) :: x
    x = 0.3
  3. character(len=4) :: sky 에 sky = "cloudy" 를 대입하면 sky 에 저장되는 값은 무엇인가? 이를 막으려면 선언을 어떻게 고쳐야 하는가?
  4. int32 변수에 3000000000_int32 를 대입하려 하면 어떻게 되는가? 이유를 huge 의 결과와 연결해 설명하라.

정답과 해설

  1. 예시 답은 다음과 같다. 상수 선언은 변수 선언과 같은 구역에 둔다.
    integer(int32), parameter :: hours_per_day = 24_int32
    real(real64), parameter :: rain_threshold_mm = 0.1_real64
    real(real64) :: rain_mm
    logical :: raining
    
    rain_mm = 2.5_real64
    raining = rain_mm >= rain_threshold_mm
    
    2.5 는 0.1 보다 크므로 raining 은 참이다. 모든 리터럴에 종류 접미사가 붙어 있는 점을 확인한다. 선언 앞에는 iso_fortran_env 에서 int32 와 real64 를 가져오는 use 문과 implicit none 이 있어야 한다.
  2. 문제는 0.3 이 접미사 없는 리터럴이어서 단정도로 해석된다는 점이다. 그 단정도 값이 x 에 옮겨지므로 x 는 0.3 에 가장 가까운 배정도 값이 아니다. x = 0.3_real64 로 고친다. 이 값에 대한 경고는 -Wall 에서 나올 수 있다.
  3. 저장되는 값은 앞 네 글자인 "clou" 다. 오른쪽이 잘린다. 선언을 character(len=6) :: sky 이상으로 고친다. 가능한 값 가운데 가장 긴 것의 길이에 맞추는 것이 기준이다.
  4. int32 의 최댓값 huge(0_int32) 는 2147483647 이다. 3000000000 은 이보다 크므로 담을 수 없다. gfortran 은 상수가 종류의 범위를 벗어나면 컴파일 단계에서 오류로 알려 준다. 이런 크기의 정수가 필요하면 iso_fortran_env 에 있는 더 큰 정수 종류인 int64 를 쓴다. 변수의 종류를 정할 때는 담을 값의 범위를 먼저 따져야 한다.
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

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

READER FEEDBACK

질문·의견

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

댓글 0

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

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