Cray 포인터(Cray pointers)

Cray 포인터(Cray pointers)

Fortran에는 원래 포인터 문법이 없는데, C처럼 저수준 메모리 주소를 직접 다루고 싶을 때 쓰던 비표준 확장이 Cray 포인터예요. 옛 DEC 계열 코드에서 자주 나오던 표기로, GNU Fortran이 호환용으로 지원해요. 이 문서는 그 문법과 동작, 그리고 주의할 제약을 정리해줘요.

출처: Cray pointers (The GNU Fortran Compiler)

본문

Cray 포인터는 Fortran에서 C 언어의 포인터와 비슷한 역할을 하는 비표준 확장이에요. 이건 두 개의 변수로 구현돼요. 하나는 메모리 주소를 담는 정수 포인터(pointer), 다른 하나는 그 포인터를 역참조(dereference)할 때 쓰는 **포인티(pointee)**예요.

포인터/포인티 쌍은 다음과 같은 형태로 선언해요.

        pointer ( <pointer> , <pointee> )

또는,

        pointer ( <pointer1> , <pointee1> ), ( <pointer2> , <pointee2> ), ...

포인터는 메모리 주소를 담기 위한 정수이고, 포인티는 배열이거나 스칼라일 수 있어요. 만약 스코핑 유닛 안에서 가정 크기 배열(assumed-size array)이 허용된다면 포인티도 가정 크기 배열일 수 있어요. 즉 마지막 차원을 값 대신 *로 남겨둘 수 있죠. 포인티는 가정 형상 배열(assumed shape array)일 수는 없어요. 그리고 포인티를 위해 별도로 공간이 할당되지는 않아요.

포인티의 타입은 포인터 문보다 앞이나 뒤 어디에서든 선언할 수 있고, 배열 명세도 앞/중간/뒤 어디서든 가능해요. 포인터도 포인터 문 이전에 정수로 선언할 수 있지만, 일부 머신에서는 기본 정수 크기가 포인터 크기와 달라서 아래 코드는 이식성이 없어요.

        integer ipt
        pointer (ipt, iarr)

포인터가 너무 작은 종류(kind)로 선언되면 컴파일러가 경고를 내요. 그렇게 하면 포인터에 저장된 메모리 주소가 잘릴 수 있어서 결과 바이너리가 제대로 동작하지 않을 가능성이 커요. 그래서 위 예시의 첫 줄을 생략하는 편이 안전해요. ipt의 타입을 명시적으로 선언하지 않으면, 컴파일러가 포인터를 담을 만큼 충분히 큰 정수 변수가 되도록 보장해 주니까요.

Cray 포인터에서 포인터 산술은 동작하지만, C 포인터 산술과는 달라요. Cray 포인터는 그냥 평범한 정수라서, 포인터를 증가시키려면 몇 바이트를 더해야 하는지는 사용자가 직접 계산해야 해요. 예를 들어:

        real target(10)
        real pointee(10)
        pointer (ipt, pointee)
        ipt = loc (target)
        ipt = ipt + 1

마지막 문장은 C 포인터 산술처럼 ipttarget(1)의 주소로 만들지 않아요. ipt에 1을 더하는 것은 ipt에 저장된 주소에 1바이트만 더하는 것이거든요. 포인티가 포함된 어떤 식이든, 포인터에 저장된 값을 기준 주소(base address)로 삼아 변환돼요.

요소의 주소를 얻으려면 이 확장이 제공하는 내장 함수 LOC() 를 쓰면 돼요. LOC()는 C의 & 연산자와 비슷한데, 주소를 정수 타입으로 바꿔준다는 점만 달라요.

        real ar(10)
        pointer(ipt, arpte(10))
        real arpte
        ipt = loc(ar)   ! arpte 는 ar 의 별칭(alias)이 돼요
        arpte(1) = 1.0  ! ar(1) 을 1.0 으로 설정

포인터는 MALLOC 내장 함수 호출로 설정할 수도 있어요.

Cray 포인티는 흔히 기존 변수의 별칭으로 쓰여요.

        integer target(10)
        integer iarr(10)
        pointer (ipt, iarr)
        ipt = loc(target)

ipt가 바뀌지 않는 한, iarr은 이제 target의 별칭이에요. 그런데 최적화기가 이 별칭을 감지하지 못해요. 그래서 iarrtarget을 동시에 사용하는 건 안전하지 않아요. Fortran의 별칭 규칙(aliasing rules)을 위반하는 방식으로 포인티를 쓰는 것은 무효예요. 이걸 피하는 건 사용자의 책임이고, 컴파일러는 그런 별칭이 없다고 가정하고 코드를 생성해요.

Cray 포인터는 별칭이 없을 때(즉 동적 할당된 메모리 블록을 접근할 때)나, 포인티는 쓰면서 그와 저장 공간을 공유하는 다른 변수는 쓰지 않는 루틴에서는 올바르게 동작해요. 이 규칙을 어긴 코드는 의도대로 실행되지 않을 수 있어요. 이건 최적화기의 버그가 아니에요. 별칭 규칙을 위반하는 코드는 애초에 무효거든요. (이건 GNU Fortran만의 문제가 아니라, Cray 포인터를 지원하는 어떤 Fortran 컴파일러라도 잘못된 별칭이 있는 코드는 '부정확하게' 최적화해요.)

Cray 포인터와 포인티에는 적용 가능한 속성에 여러 제약이 있어요.

  • 포인티에는 ALLOCATABLE, INTENT, OPTIONAL, DUMMY, TARGET, INTRINSIC, POINTER 속성을 붙일 수 없어요.
  • 포인터에는 DIMENSION, POINTER, TARGET, ALLOCATABLE, EXTERNAL, INTRINSIC 속성을 붙일 수 없고, 함수의 결과일 수도 없어요.
  • 포인티는 둘 이상의 포인터 문에 나올 수 없고, equivalence, common, data 문에도 나올 수 없어요.

Cray 포인터는 함수나 서브루틴을 가리킬 수도 있어요.

  implicit none
  external sub
  pointer (subptr,subpte)
  external subpte
  subptr = loc(sub)
  call subpte()
  [...]
  subroutine sub
  [...]
  end subroutine sub

포인터는 프로그램 진행 중 바뀔 수 있고, 그렇게 되면 포인티가 가리키는 위치도 바뀌어요. 다만 포인티가 인자로 전달될 때는 호출된 함수 안에서 평범한 변수처럼 다뤄져요. 그 후에 포인터가 바뀌어도 전달된 배열의 기준 주소는 바뀌지 않아요.

더 알아보기

  • LOC()는 이 Cray 포인터 확장이 함께 제공하는 내장 함수예요. 주소를 정수로 얻을 수 있어요.
  • 동적 메모리를 직접 할당해서 포인터로 받고 싶다면 MALLOC 내장 함수를 함께 볼 수 있어요.
  • Cray 포인터는 비표준이므로, 새 코드에서는 Fortran 90의 POINTER 속성이나 allocatable 배열, C 연동(ISO_C_BINDING)을 우선 고려하는 게 이식성 측면에서 좋아요.