Fortran · 심화
객체와 수치 해석으로 깊어지는 Fortran
배열 심화 - 포인터·할당·마스크
allocatable 과 move_alloc, pointer 와 target, 배열 구간 포인터, where·merge·pack, 형상 맞추기
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 배열을 담은 파생 타입에 제네릭 인터페이스와 연산자를 붙였다. 이제 그 배열이 어디에 저장되고, 다른 이름으로 어떻게 접근되며, 계산에 필요한 원소만 어떻게 골라지는지 살펴본다. 같은 온도 배열을 다루더라도 저장 공간을 소유하는 변수와 일부 원소를 가리키는 변수의 역할은 다르다. 이 차이를 이해하면 배열을 복사할 때와 복사 없이 접근할 때를 구분할 수 있다.
이번 장의 예제는 작은 금속판의 온도 자료를 정리하는 프로그램이다. 판 전체의 온도를 조건에 따라 조정하고, 내부 구간을 별도로 살펴보고, 선택한 온도만 일차원 배열에 모은다. 열 방정식의 시간 적분은 수행하지 않는다. 여기서는 이후 계산에 사용할 배열의 저장 공간과 접근 방식을 정돈하는 데 집중한다.
- 할당 배열(allocatable)의 생명 주기를 관리하고 move_alloc으로 할당을 이전한다.
- 포인터(pointer)와 대상(target)의 관계를 이해하고 배열 구간에 연결한다.
- where, merge, pack으로 조건에 따른 배열 처리와 원소 선별을 표현한다.
- 배열의 계수와 형상을 확인하고 하한, 연속성, 별칭에 관한 실수를 피한다.
문제 상황
금속판의 온도를 temperature(nx, ny)에 저장한다고 하자. 전체 배열은 판의 모든 격자점을 나타내지만, 어떤 작업은 가장자리를 제외한 내부만 필요하다. 내부 온도를 매번 별도 배열에 복사하면 코드는 단순해 보여도 원본과 복사본 중 어느 쪽을 갱신했는지 추적해야 한다. 내부를 가리키는 다른 이름이 있다면 원본을 직접 수정할 수 있다.
반대로 두 시점의 온도를 보관하는 배열은 서로 독립된 저장 공간이 필요하다. 새 상태를 계산하는 동안 이전 상태를 읽어야 하기 때문이다. 계산이 끝난 뒤에는 두 배열의 역할을 바꾼다. 이때 전체 원소를 다시 복사하는 대신 할당 자체를 옮길 수 있다. 다만 이전 저장 공간을 가리키던 포인터가 남아 있다면 역할 교환 이후의 접근이 혼란스러워진다.
결과를 분석할 때는 조건에 맞는 원소만 필요할 수도 있다. 일정 온도 이상인 점의 개수, 그 점의 온도 목록, 위치별 상태 표시가 서로 다른 결과다. 개수는 스칼라이고, 온도 목록은 길이가 조건에 따라 바뀌는 일차원 배열이며, 상태 표시는 판과 같은 형상의 배열이다. 같은 조건식을 사용해도 결과의 형상은 목적에 따라 달라진다.
이번 프로그램은 4행 3열의 온도 배열로 이 문제를 드러낸다. 첫 번째 첨자를 가로 위치로 생각해도 되지만, 출력에서는 두 번째 첨자를 고정한 채 첫 번째 첨자 방향의 네 값을 한 줄에 쓴다. 첨자의 의미와 출력 방향을 먼저 정하면 메모리 순서와 화면 배치를 혼동하지 않는다.
저장 공간을 소유하고 옮기기
allocatable의 할당 상태
allocatable 배열은 자신의 할당 상태를 관리한다. 선언만 했을 때는 저장 공간이 없고, allocate가 성공하면 원소를 읽고 쓸 수 있다. allocated는 현재 할당되어 있는지 알려 준다. deallocate는 저장 공간을 해제한다. 일반적인 지역 할당 변수는 수명이 끝날 때 자동으로 해제되므로, 소유 관계가 분명한 작업 배열에 잘 맞는다.
크기와 값의 준비는 별개다. allocate(a(4,3))은 형상만 정한다. 그 직후 a의 원소에 어떤 온도가 들어 있다고 가정해서는 안 된다. 예제에서는 allocate 다음에 plate = 20.0_rk로 모든 원소를 초기화한다. 부분 구간만 먼저 쓰고 나머지를 읽는 실수를 막기 위한 순서다.
할당 배열에 배열식을 대입하면 필요한 경우 자동으로 할당되거나 재할당된다. 예를 들어 할당되지 않은 selected에 selected = pack(plate, mask)를 실행하면 결과 길이에 맞춰 공간이 생긴다. 이미 할당된 selected의 형상이 결과와 다르면 보통 대입 과정에서 재할당된다. 간결한 기능이지만, 포인터가 그 저장 공간을 가리키고 있다면 재할당 가능성을 함께 검토해야 한다.
배열 전체에 대한 대입과 배열 구간에 대한 대입은 구별해야 한다. a = expression은 할당 배열 a의 자동 재할당을 허용할 수 있지만, a(:,:) = expression은 기존 구간의 원소를 바꾸는 대입이다. 오른쪽 배열의 형상은 그 구간과 맞아야 한다. 기존 크기를 지키려는 코드에서는 이 차이가 유용하다.
move_alloc은 원소 대입이 아니다
move_alloc(from, to)는 from이 가진 할당을 to로 이전한다. 호출 뒤 from은 할당되지 않은 상태가 된다. to가 이미 할당되어 있었다면 그 기존 할당은 먼저 해제된다. 따라서 이 프로시저는 두 배열을 그대로 남겨 두는 복사와 다르다. 두 인수는 서로 다른 할당 변수여야 하며, 타입과 종류 매개변수 및 계수가 맞아야 한다.
할당 이전은 원소별 대입을 요구하지 않는다. 배열의 형상과 경계도 이전된 할당을 따른다. 예제에서는 같은 크기의 plate와 work를 세 번의 move_alloc 호출로 교환한다. spare는 중간에 한쪽 할당을 맡는 변수다. 마지막 호출이 끝나면 spare는 다시 할당되지 않은 상태다.
move_alloc 자체가 배열을 확장해 주는 것은 아니다. 길이가 여섯인 목록에 두 칸을 더하려면 먼저 길이가 여덟인 grown을 할당하고, 기존 여섯 값을 복사한 뒤 나머지를 초기화한다. 그 다음 grown의 할당을 selected로 이전한다. 기존 원소를 보존하기 위한 복사는 이 과정에 남아 있다. 마지막 할당 이전에서 추가적인 원소 대입을 하지 않는다는 점이 핵심이다.
| 표현 | 수행하는 일 | 이후 상태 |
|---|---|---|
| b = a | 원소 값을 대입한다 | a와 b를 각각 사용할 수 있다 |
| p => a | 대상에 포인터를 연결한다 | p를 통한 변경이 대상에 반영된다 |
| call move_alloc(a, b) | a의 할당을 b로 이전한다 | a는 할당되지 않은 상태가 된다 |
| allocate(b(n)) | 지정한 크기의 공간을 마련한다 | 원소 값은 별도로 초기화해야 한다 |
포인터로 배열 구간 바라보기
연결과 대입의 차이
포인터는 저장 공간을 소유하는 이름으로 사용하기보다 이미 존재하는 대상을 바라보는 이름으로 사용하는 편이 관리하기 쉽다. 실수 배열 포인터 p를 선언할 때 p(:,:)처럼 계수는 정하고 경계는 나중에 연결할 대상에서 얻도록 한다. 선언에 => null()을 붙이면 처음부터 연결 해제 상태라는 점이 분명해진다.
일반 변수를 포인터의 대상으로 삼으려면 target 속성을 갖추어야 한다. 예제의 plate는 allocatable과 target을 함께 가진다. allocatable이 저장 공간을 관리하고, target이 그 공간을 포인터 연결의 대상으로 허용한다. target을 붙인다고 저장 공간이 새로 생기거나 포인터가 자동으로 연결되지는 않는다.
core => plate(2:3,2:2)는 두 원소로 이루어진 이차원 구간에 core를 연결한다. 이후 core = 30.0_rk는 연결을 바꾸는 문장이 아니라 대상 원소의 값을 바꾸는 문장이다. 연결에는 =>를 쓰고 값 대입에는 =를 쓴다는 구분이 중요하다. 포인터에 값 대입을 하려면 먼저 유효한 대상에 연결되어 있어야 한다.
이 예의 구간 연결에서 core의 하한은 두 차원 모두 1이다. 원본의 첨자 2를 그대로 가져온다고 생각하면 안 된다. 따라서 core(1,1)은 plate(2,2)를, core(2,1)은 plate(3,2)를 가리킨다. 원본 좌표를 유지해야 한다면 포인터의 경계를 명시하는 연결 문법을 별도로 사용하거나 원본 배열과 좌표를 함께 전달하는 설계를 선택한다.
nullify(core)는 연결만 끊는다. plate의 저장 공간을 해제하지 않는다. 반대로 plate를 deallocate하면 그 공간을 가리키던 포인터는 더 이상 유효한 대상에 접근할 수 없다. 이런 포인터가 자동으로 null 상태가 된다고 기대해서는 안 된다. 유효성을 잃은 포인터에 associated를 호출하는 방식도 안전한 검사 전략이 아니다. 대상의 수명 변화 전에 연결을 명시적으로 정리해야 한다.
연속성과 별칭
Fortran 배열에서는 첫 번째 첨자가 가장 빠르게 변한다. plate(:,2)는 한 열 전체이므로 메모리에서 연속된 구간이다. 반면 plate(2,:)는 열마다 두 번째 원소를 골라내므로 일반적으로 원소 사이에 간격이 있다. 포인터는 이런 비연속 구간에도 연결될 수 있지만, contiguous 속성이 붙은 포인터에는 그 속성의 조건을 만족하는 대상을 연결해야 한다.
is_contiguous는 현재 배열이나 구간이 연속인지 확인하는 데 쓸 수 있다. 비연속 구간을 프로시저에 전달하면 더미 인수의 선언과 호출 방식에 따라 임시 배열이 필요할 수도 있다. 포인터 연결이 복사를 하지 않는다는 사실과, 그 포인터를 사용하는 모든 후속 작업이 임시 저장 공간 없이 수행된다는 주장은 다르다.
두 포인터가 겹치는 원소를 가리키면 별칭이 생긴다. 한쪽 이름으로 값을 바꾸었는데 다른 이름으로 읽은 결과도 달라지는 것은 정상이다. 그러나 같은 대상에 연결된 두 인수를 하나의 프로시저로 전달하고 한쪽을 수정하면서 다른 쪽을 읽으면 인수 결합 규칙을 위반할 수 있다. 읽기와 쓰기 역할이 겹칠 가능성이 있는 인터페이스는 피하는 편이 좋다.
이번 코드에서는 할당을 교환하기 전에 core를 nullify한다. 할당 이전 과정의 포인터 결합 상태를 추적하는 대신, 역할 교환 뒤 현재 plate의 내부에 다시 연결한다. 이 규칙을 지키면 core라는 이름이 항상 현재 판의 내부를 뜻한다. 저장 공간의 정체성과 프로그램에서 부여한 역할을 함께 관리하는 방법이다.
마스크와 형상으로 배열 계산 표현하기
where, merge, pack의 결과는 서로 다르다
논리 마스크(logical mask)는 각 원소를 선택할지 나타내는 논리 배열이다. mask = plate >= 40.0_rk를 실행하면 plate와 같은 형상의 논리 배열을 얻는다. 여기서는 첫 번째 첨자가 1인 가장자리와 4인 가장자리가 선택되어 총 여섯 원소가 참이 된다.
where는 마스크가 참인 위치에서 배열 대입을 수행한다. elsewhere를 붙이면 거짓인 위치에도 다른 대입을 수행할 수 있다. 완성 코드에서는 선택된 온도에서 5를 빼고, 선택되지 않은 온도에는 2를 더한다. 오른쪽은 이전 상태인 plate에서 읽고 왼쪽은 별도 배열 work에 쓰므로, 새 값과 이전 값의 역할이 분명하다.
merge(tsource, fsource, mask)는 위치마다 참일 때 첫 번째 값, 거짓일 때 두 번째 값을 고른다. 예제의 merge(1, 0, plate >= 50.0_rk)는 판과 같은 형상의 정수 배열을 만든다. 1과 0은 스칼라이므로 각 위치에 적용된다. 두 값 인수의 타입과 종류 매개변수는 맞아야 하며, 배열 인수가 여러 개면 형상도 맞아야 한다.
merge를 조건문처럼 사용해 위험한 식의 평가를 막으려 해서는 안 된다. 예를 들어 merge(1.0_rk / x, 0.0_rk, x /= 0.0_rk)는 x가 0인 위치에서 나눗셈이 평가되지 않는다고 보장하지 않는다. 평가 자체를 제한해야 하는 배열 대입에는 where를 사용하고, 스칼라 계산에는 if를 사용한다.
pack(array, mask)는 선택된 원소를 일차원 배열로 모은다. 선택 순서는 배열 원소 순서다. 이차원 배열이라면 첫 번째 첨자 방향을 먼저 진행하고 두 번째 첨자를 바꾼다. 위치 정보를 함께 저장하지 않으면 pack의 결과만으로 원래 좌표를 복원할 수 없다. 온도와 좌표가 모두 필요한 분석에서는 좌표 목록도 함께 구성해야 한다.
완성 코드의 mask는 온도를 조정하기 전에 만든 선택 결과다. 할당을 교환한 뒤에도 이 논리 배열의 값은 그대로 유지된다. 따라서 pack(plate, mask)는 처음 선택했던 위치의 새 온도를 모은다. 새 온도가 40 이상인 점을 다시 고르려면 mask = plate >= 40.0_rk를 다시 실행해야 한다. 마스크는 조건의 실시간 연결이 아니라 계산해 저장한 값이다.
원소 수가 같아도 형상은 다를 수 있다
배열의 계수는 첨자의 개수이고, 형상은 각 차원의 크기를 나열한 것이다. 형상 [4,3]과 [3,4]는 원소 수가 모두 12지만 서로 맞지 않는다. 원소별 연산에서 여러 비스칼라 배열을 함께 사용하려면 계수와 각 차원의 크기가 같아야 한다. 스칼라는 각 원소에 적용할 수 있으므로 배열과 스칼라의 덧셈은 가능하다.
shape는 차원별 크기를 일차원 정수 배열로 반환한다. 두 배열의 계수가 같은 것을 이미 알고 있다면 all(shape(a) == shape(b))로 형상이 같은지 확인할 수 있다. 계수가 다른 일반적인 비교까지 이 식 하나로 처리할 수는 없다. shape가 반환한 두 벡터의 길이부터 달라지기 때문이다.
하한은 형상에 포함되지 않는다. a(0:3,1:3)과 b(1:4,1:3)는 형상이 모두 [4,3]이다. 두 배열의 원소별 연산은 각 차원에서의 순서로 대응하지만, 직접 첨자를 만들어 접근하는 반복문은 각각의 경계를 따라야 한다. 좌표 의미가 있는 코드에서는 형상 검사와 경계 검사를 분리한다.
where의 마스크와 대입 대상은 형상이 맞아야 한다. pack에서 배열 마스크를 쓸 때도 입력 배열과 형상이 맞아야 한다. 예제는 allocate(mask(nx,ny))로 이를 명시하고, 계산 전에 plate와 work 및 mask의 형상을 확인한다. 실제 프로그램에서는 배열을 만들고 전달하는 경계에서 검사하면 오류가 발생한 위치를 찾기 쉽다.
| 표현 | 결과 | 예제에서의 크기 | 용도 |
|---|---|---|---|
| count(mask) | 정수 스칼라 | 값 6 | 선택된 점의 개수 |
| where (mask) | 선택 위치의 대입 | 대상 형상 [4,3] | 조건별 온도 조정 |
| merge(1,0,조건) | 정수 배열 | 형상 [4,3] | 위치별 상태 표시 |
| pack(plate,mask) | 일차원 실수 배열 | 길이 6 | 선택 온도 목록 |
완성 코드
다음 코드는 main.f90 한 파일로 저장한다. 입력 파일과 표준 입력은 필요하지 않다. 온도 조정 규칙은 배열 기능을 확인하기 위한 것으로, 시간에 따른 열 확산 모델을 의미하지 않는다. 할당 배열의 역할 교환과 목록 확장을 각각 확인할 수 있도록 구성했다.
main.f90
program plate_arrays
use iso_fortran_env, only: real64
implicit none
integer, parameter :: rk = real64
integer, parameter :: nx = 4, ny = 3
real(rk), allocatable, target :: plate(:,:)
real(rk), allocatable :: work(:,:), spare(:,:)
real(rk), allocatable :: selected(:), grown(:)
real(rk), pointer :: core(:,:) => null()
logical, allocatable :: mask(:,:)
integer, allocatable :: flags(:,:)
integer :: j, n
allocate(plate(nx,ny), work(nx,ny))
allocate(mask(nx,ny), flags(nx,ny))
plate = 20.0_rk
plate(1,:) = 80.0_rk
plate(nx,:) = 40.0_rk
core => plate(2:nx-1,2:ny-1)
core = 30.0_rk
write(*,'(A,F8.2)') 'Initial core sum: ', sum(core)
if (.not. all(shape(plate) == shape(work))) then
error stop 'plate/work shape mismatch'
end if
if (.not. all(shape(plate) == shape(mask))) then
error stop 'plate/mask shape mismatch'
end if
mask = plate >= 40.0_rk
where (mask)
work = plate - 5.0_rk
elsewhere
work = plate + 2.0_rk
end where
nullify(core)
call move_alloc(plate, spare)
call move_alloc(work, plate)
call move_alloc(spare, work)
core => plate(2:nx-1,2:ny-1)
write(*,'(A,F8.2)') 'Updated core sum: ', sum(core)
write(*,'(A,L1)') 'Spare allocated: ', allocated(spare)
write(*,'(A)') 'Updated plate:'
do j = 1, ny
write(*,'(4F8.2)') plate(:,j)
end do
flags = merge(1, 0, plate >= 50.0_rk)
write(*,'(A,I0)') 'Original mask count: ', count(mask)
write(*,'(A,I0)') 'Hot cell count: ', sum(flags)
selected = pack(plate, mask)
n = size(selected)
allocate(grown(n+2))
grown = 0.0_rk
grown(1:n) = selected
call move_alloc(grown, selected)
write(*,'(A)') 'Selected values and two spare slots:'
write(*,'(8F8.2)') selected
write(*,'(A,L1)') 'Grown allocated: ', allocated(grown)
nullify(core)
deallocate(plate, work, mask, flags, selected)
end program plate_arrays
줄별 해설
use iso_fortran_env, only: real64부터 종류 매개변수 선언까지는 모든 온도 배열에서 같은 실수 종류를 사용하도록 정한다. 이번 장에서는 정밀도 선택의 판단 기준을 확장하지 않는다. 상수의 _rk 접미사와 배열 선언이 일치하도록 만드는 데 사용한다.
plate는 실제 온도를 소유하면서 포인터 연결도 허용한다. work는 새 상태를 담고, spare는 교환 도중 할당을 잠시 맡는다. selected와 grown은 판과 계수가 다른 일차원 배열이다. 이처럼 소유 역할과 계산 역할을 이름에 드러내면 포인터를 무분별하게 사용할 필요가 줄어든다.
core의 선언에는 두 개의 콜론이 있으므로 이차원 포인터다. null()로 초기화되어 아직 대상이 없다. mask는 논리 배열이고 flags는 정수 배열이다. 선택 조건과 출력용 상태 값을 같은 변수에 섞지 않는다.
두 allocate 문은 판과 작업 공간 및 보조 배열을 만든다. 이어지는 세 대입으로 판 전체를 20으로 채우고 양쪽 가장자리를 각각 80과 40으로 바꾼다. 초기화 순서상 가장자리 값이 전체 초기값을 덮어쓴다. 내부 포인터 연결은 plate가 할당된 뒤에 수행한다.
core => plate(2:nx-1,2:ny-1)에서 선택된 구간의 형상은 [2,1]이다. core = 30.0_rk는 그 두 원소를 수정한다. 그래서 첫 출력의 합계는 60이다. 별도 내부 배열을 만들거나 판에 다시 복사하는 문장은 필요하지 않다.
두 if 문은 같은 계수의 배열들이 형상도 같은지 확인한다. 이 예제에서는 모두 같은 크기로 할당했으므로 검사에 통과한다. 다른 프로시저에서 전달받은 배열이나 크기가 바뀔 수 있는 배열이라면 이런 검사가 원소별 계산의 전제 조건을 드러낸다.
mask = plate >= 40.0_rk는 현재 조건을 저장한다. where와 elsewhere는 work의 모든 위치를 나누어 채운다. 선택된 가장자리는 75와 35가 되고, 나머지는 기존 값보다 2 높아진다. work의 모든 원소가 대입되므로 처음 할당할 때 work를 따로 초기화하지 않아도 이후 읽기는 유효하다.
세 move_alloc 호출 앞의 nullify는 내부를 바라보던 이름을 정리한다. 첫 호출 뒤 plate는 미할당이고 spare가 이전 판을 가진다. 두 번째 호출 뒤 plate는 새 상태를 가지며 work가 미할당이다. 마지막 호출 뒤 work가 이전 판을 가지며 spare가 미할당이다. 각 단계에서 이전 대상의 값 대입을 수행하지 않는다.
새 plate에 core를 다시 연결하면 내부 두 값은 각각 32다. 합계는 64가 된다. allocated(spare)가 거짓이라는 출력은 교환용 변수에 할당이 남지 않았음을 확인한다. 반복문은 plate(:,j)를 출력하므로 화면의 각 줄은 첫 번째 첨자 방향의 네 값이다.
merge는 새 판에서 50 이상인 위치를 1로 표시한다. 첫 번째 가장자리의 세 점만 이 조건을 만족하므로 sum(flags)는 3이다. 기존 mask에는 여섯 개의 참이 남아 있다. 두 개수가 다른 이유는 조건의 기준과 마스크를 계산한 시점이 모두 다르기 때문이다.
pack의 결과는 75, 35가 세 번 반복되는 길이 여섯의 배열이다. grown 전체를 0으로 채운 뒤 앞쪽 여섯 칸에 selected를 대입한다. move_alloc으로 grown의 할당을 넘기면 selected의 길이는 여덟이 되고 grown은 미할당이 된다. 마지막에는 core의 연결을 끊고 소유 변수의 공간을 해제한다.
실행 결과
GNU Fortran 16에서 다음 명령으로 컴파일하고 실행한다. 자유 형식 소스이며 표준 Fortran 기능만 사용한다. 실수는 F8.2로 출력하므로 각 값이 여덟 칸을 차지하고 소수점 아래 두 자리가 표시된다.
gfortran -std=f2018 -Wall main.f90 -o plate_arrays
./plate_arrays
예상 출력은 다음과 같다. 가장자리의 새 온도 35는 기존 마스크로 선택되지만, 새 상태의 50 이상 조건에는 포함되지 않는다.
Initial core sum: 60.00
Updated core sum: 64.00
Spare allocated: F
Updated plate:
75.00 22.00 22.00 35.00
75.00 32.00 32.00 35.00
75.00 22.00 22.00 35.00
Original mask count: 6
Hot cell count: 3
Selected values and two spare slots:
75.00 35.00 75.00 35.00 75.00 35.00 0.00 0.00
Grown allocated: F
실무에서 자주 틀리는 것
연결되지 않은 포인터에 값을 대입한다
다음 코드의 =는 대상 연결을 만들지 않는다. p가 null 상태이므로 값을 쓸 대상이 없다. 컴파일러가 문법을 받아들이더라도 유효한 실행이 아니다.
real(rk), pointer :: p(:) => null()
real(rk), target :: edge(4)
edge = 20.0_rk
p = edge
edge의 다른 이름이 필요하다면 포인터 연결을 사용한다. 독립된 복사본이 필요하다면 p 대신 할당 배열을 선언하고 값 대입을 사용한다. 두 목적을 먼저 구분해야 한다.
real(rk), pointer :: p(:) => null()
real(rk), target :: edge(4)
edge = 20.0_rk
p => edge
p(1) = 80.0_rk
nullify(p)
대상을 해제한 뒤 포인터를 검사한다
대상의 할당을 해제했다고 포인터가 자동으로 연결 해제 상태가 되는 것은 아니다. 다음 코드는 해제 뒤 p의 결합 상태가 정의되어 있다고 가정한다.
allocate(a(4))
p => a
deallocate(a)
if (associated(p)) print *, p
대상을 해제하기 전에 포인터를 정리한다. 같은 대상을 가리키는 포인터가 여러 개면 모두 관리해야 한다. 포인터를 소유 변수의 짧은 작업 구간 안에서만 사용하면 이 관계를 추적하기 쉽다.
allocate(a(4))
a = 20.0_rk
p => a
nullify(p)
deallocate(a)
원소 개수만 보고 where를 사용한다
다음 배열과 마스크는 원소 수가 모두 12다. 그러나 형상 [4,3]과 [3,4]가 달라 where에 사용할 수 없다. 일부 컴파일러가 진단하지 못하는 경우에도 표준에 맞는 계산이 아니다.
allocate(a(4,3), mask(3,4))
a = 20.0_rk
mask = .true.
where (mask) a = 0.0_rk
마스크를 대상의 차원별 크기로 만든다. 자동 할당 대입으로 조건식의 결과를 받는 방법도 있지만, 이미 할당된 배열을 유지하려면 형상을 명시적으로 맞추는 편이 의도가 분명하다.
allocate(a(4,3))
allocate(mask(size(a,1),size(a,2)))
a = 20.0_rk
mask = a >= 40.0_rk
where (mask) a = 0.0_rk
merge로 0 나눗셈을 막으려 한다
merge는 원소별 선택 함수지만, 선택되지 않을 값 인수가 평가되지 않는다고 보장하지 않는다. 다음 식을 안전한 역수 계산으로 사용하면 안 된다.
inverse = merge(1.0_rk / values, 0.0_rk, values /= 0.0_rk)
같은 형상의 inverse가 이미 할당되어 있다는 전제에서 where로 대입을 나눈다. 여기의 오른쪽 나눗셈은 원소별 연산이므로 활성화된 위치에서 수행된다. where 안에 넣었다고 임의의 함수 호출 전체가 조건부 평가되는 것은 아니라는 점도 구별한다.
where (values /= 0.0_rk)
inverse = 1.0_rk / values
elsewhere
inverse = 0.0_rk
end where
한눈에 보기
| 기능 | 주된 역할 | 확인할 조건 |
|---|---|---|
| allocatable | 저장 공간을 소유한다 | 할당 상태와 초기화 여부 |
| move_alloc | 할당을 다른 변수로 이전한다 | 이전 후 from의 미할당 상태 |
| pointer와 target | 같은 저장 공간에 다른 이름으로 접근한다 | 대상 수명과 연결 상태 |
| 배열 구간 포인터 | 일부 원소를 복사 없이 바라본다 | 경계, 연속성, 겹치는 원소 |
| where | 선택 위치에 배열 대입을 수행한다 | 마스크와 대상의 형상 |
| merge | 위치마다 두 값 중 하나를 고른다 | 값 인수의 타입과 평가 안전성 |
| pack | 선택 원소를 일차원으로 모은다 | 원소 순서와 좌표 정보의 필요성 |
| shape | 차원별 크기를 확인한다 | 계수와 하한은 별도로 판단 |
저장 공간을 소유하는 배열은 allocatable로 두고, 포인터는 필요한 구간에 짧게 연결하는 방식이 이 예제의 기준이다. 계산 결과의 역할이 바뀌는 지점에서는 연결을 정리하고 다시 만든다. 조건별 대입은 where, 위치별 값 선택은 merge, 선택된 값의 목록은 pack으로 표현하면 결과의 형상이 코드에 드러난다.
다음 장에서는 배열에 담긴 수치가 얼마나 믿을 만한지 살펴본다. 저장 공간이 올바르게 관리되고 형상이 맞는다는 사실만으로 수치 오차까지 작다고 결론 내릴 수는 없다. 여기서 정리한 배열 구조를 바탕으로 정밀도와 오차를 따로 검토하게 된다.
연습 문제
- 완성 코드에서 첫 번째 내부 포인터 연결 직후 core의 shape와 lbound를 구하라. core(2,1)이 가리키는 plate의 원소도 쓰라.
- 할당 교환 뒤 pack을 실행하기 전에 mask를 새 온도가 40 이상인 위치로 다시 계산하라. 선택 개수와 확장 전 selected의 값을 구하라.
- 목록을 확장하는 코드를 별도 조각으로 작성하라. 길이 여섯인 selected를 길이 열로 늘리고, 기존 값 뒤의 네 칸을 -1로 채운다. move_alloc을 사용하고 호출 뒤 grown의 상태를 설명하라.
- plate(2,:)를 가리키는 일차원 포인터 line을 선언하고 연결하라. 이 구간이 연속인지 판단하고, line의 값을 모두 25로 바꾸었을 때 수정되는 원본 위치를 설명하라.
정답과 해설
-
shape(core)는 [2,1]이고 lbound(core)는 [1,1]이다. core(2,1)은 plate(3,2)를 가리킨다. 구간의 원본 시작 첨자와 연결된 포인터의 하한을 구분해야 한다. 포인터를 반복문으로 순회할 때는 원본 첨자를 추측하기보다 lbound와 ubound 또는 size를 사용한다.
-
다음 대입으로 마스크를 갱신한다. 새 판에서 40 이상인 값은 75 세 개뿐이다. count(mask)는 3이고 pack 결과는 [75.00,75.00,75.00]이다. 기존 가장자리의 40은 조정 뒤 35가 되었으므로 새 마스크에서 제외된다.
mask = plate >= 40.0_rk selected = pack(plate, mask)완성 코드의 이후 확장 부분을 그대로 사용하면 selected는 길이 다섯이 되고 끝의 두 값은 0이다. 고정된 여덟 칸 서식은 출력할 값이 더 적어도 그 다섯 값만 출력한다.
-
기존 selected가 길이 여섯이고 grown이 미할당이라는 전제에서 다음과 같이 작성한다. 먼저 전체를 -1로 초기화하고 기존 값으로 앞부분을 덮어쓴다. 초기화 누락 없이 추가 공간을 구분할 수 있다.
n = size(selected) allocate(grown(n+4)) grown = -1.0_rk grown(1:n) = selected call move_alloc(grown, selected)호출 뒤 selected의 길이는 열이고 grown은 미할당이다. selected의 기존 할당은 이전 과정에서 해제되므로 먼저 deallocate(selected)를 호출할 필요는 없다. 기존 여섯 값은 앞의 구간 대입에서 보존되었다.
-
선언부에 line을 추가하고 plate가 할당된 뒤 연결한다. 이 판은 첫 번째 차원의 크기가 4이므로 plate(2,:)의 원소들은 메모리에서 네 원소 간격으로 놓인다. 구간은 비연속이며 line에 contiguous 속성을 붙이지 않는다.
real(rk), pointer :: line(:) => null() ! plate의 할당과 초기화 뒤에 실행한다. line => plate(2,:) line = 25.0_rk nullify(line)수정되는 원본 위치는 plate(2,1), plate(2,2), plate(2,3)이다. 포인터의 일차원 형상은 [3]이며 원본의 첫 번째 첨자는 연결식에서 이미 고정되었다. 다른 내부 포인터가 이 원소와 겹친다면 그 포인터로 읽는 값도 함께 달라진다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.