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 처럼 자료형 뒤 괄호에 종류를 적는다.
| 종류 이름 | 크기 | 유효 숫자 | 표현 범위 |
|---|---|---|---|
| int32 | 4바이트 | 정수 전부 | 약 ±21억 (2147483647 까지) |
| real32 | 4바이트 | 약 6자리 | 약 1e-38 ~ 1e38 |
| real64 | 8바이트 | 약 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 은 어느 종류에서도 정확히 담기지 않는다. 문제는 그 오차가 종류에 따라 크게 달라진다는 점이다. 이 오차를 다루는 방법은 다음 장에서 식과 내장 함수를 배우며 더 살펴본다.
완성 코드
아래는 앞의 내용을 모두 담은 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) :: n | n = 4_int32 | 범위는 약 ±21억 |
| 실수 선언 | real(real64) :: x | x = 3.5_real64 | 상수에도 접미사를 붙인다 |
| 논리형 | logical :: flag | .true. | 앞뒤에 점을 붙인다 |
| 문자형 | character(len=6) :: s | s = "rain" | 짧으면 공백으로 채워지고 길면 잘린다 |
| 상수 | parameter | frost_limit | 나중에 대입하면 컴파일 오류 |
| 정밀도 확인 | precision(x), huge(n) | precision(0.0_real64) | 인자는 종류를 알려 주는 용도 |
| 적은 모양 | 해석되는 종류 | real64 변수에 대입한 뒤 | 권장 |
|---|---|---|---|
0.1 | 기본 실수(단정도) | 단정도 값이 그대로 옮겨진다 | 피한다 |
0.1_real32 | real32 | 단정도 값이 그대로 옮겨진다 | 의도했을 때만 |
0.1_real64 | real64 | 배정도 값이 된다 | 기본으로 쓴다 |
연습 문제
- 하루의 시간 수 24 를 int32 상수
hours_per_day로, 강수 기준 0.1 mm 를 real64 상수rain_threshold_mm로 선언하라. 그리고 변수rain_mm에 2.5 를 넣고, 기준 이상이면 참이 되는 논리 변수raining에 결과를 담는 코드를 써라. - 다음 선언에서 어떤 점이 문제인지 설명하고 고쳐라.
real(real64) :: xx = 0.3 character(len=4) :: sky에sky = "cloudy"를 대입하면 sky 에 저장되는 값은 무엇인가? 이를 막으려면 선언을 어떻게 고쳐야 하는가?- int32 변수에
3000000000_int32를 대입하려 하면 어떻게 되는가? 이유를huge의 결과와 연결해 설명하라.
정답과 해설
- 예시 답은 다음과 같다. 상수 선언은 변수 선언과 같은 구역에 둔다.
2.5 는 0.1 보다 크므로 raining 은 참이다. 모든 리터럴에 종류 접미사가 붙어 있는 점을 확인한다. 선언 앞에는 iso_fortran_env 에서 int32 와 real64 를 가져오는 use 문과 implicit none 이 있어야 한다.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 - 문제는
0.3이 접미사 없는 리터럴이어서 단정도로 해석된다는 점이다. 그 단정도 값이 x 에 옮겨지므로 x 는 0.3 에 가장 가까운 배정도 값이 아니다.x = 0.3_real64로 고친다. 이 값에 대한 경고는-Wall에서 나올 수 있다. - 저장되는 값은 앞 네 글자인
"clou"다. 오른쪽이 잘린다. 선언을character(len=6) :: sky이상으로 고친다. 가능한 값 가운데 가장 긴 것의 길이에 맞추는 것이 기준이다. - int32 의 최댓값
huge(0_int32)는 2147483647 이다. 3000000000 은 이보다 크므로 담을 수 없다. gfortran 은 상수가 종류의 범위를 벗어나면 컴파일 단계에서 오류로 알려 준다. 이런 크기의 정수가 필요하면 iso_fortran_env 에 있는 더 큰 정수 종류인 int64 를 쓴다. 변수의 종류를 정할 때는 담을 값의 범위를 먼저 따져야 한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.