인덱스 할당 최적화
인덱스 할당 최적화 (Indexed Assignment Optimization)
사용자 정의 클래스의 subsasgn 메서드에서 성능 문제가 생기는 경우가 있어요. Octave의 게으른 복사(lazy copy) 방식 탓인데, Octave가 이 문제를 어떻게 풀어내는지 살펴볼게요. Octave 매뉴얼 34.3.2절 내용을 정리해 봤어요.
본문
Octave의 전반적인 게으른 복사·값 전달(pass-by-value) 의미론은 사용자 정의 subsasgn 메서드의 성능에 문제를 일으켜요. 다음 subsasgn 호출을 상상해 보죠.
ss = substruct ("()", {1});
x = subsasgn (x, ss, 1);
여기서 대응하는 메서드가 이렇게 생겼다고 해요.
function x = subsasgn (x, ss, val)
...
x.myfield (ss.subs{1}) = val;
endfunction
문제는 subsasgn 메서드에 들어갈 때 x가 여전히 호출자의 스코프에서 참조된다는 점이에요. 즉 메서드는 할당을 수행하기 전에 먼저 x와 x.myfield를 unshare(복사)해야 하죠. 호출이 끝나면 오류가 나지 않는 한 결과가 즉시 호출자 스코프의 x에 할당되므로, 이전 x.myfield의 값은 잊혀집니다. 그래서 Octave 언어는 N개 원소(N은 x.myfield의 크기)의 복사를 강제하는데, 실제로는 단일 원소 하나만 수정하면 되는 상황이에요. 다시 말해 상수 시간(constant-time) 연산이 선형 시간(linear-time) 연산으로 떨어져 버립니다. 큰 배열을 내부에 저장하는 사용자 클래스라면 실제로 문제가 될 수 있어요.
이 문제를 부분적으로 해결하기 위해 Octave는 m-파일로 작성된 사용자 정의 subsasgn 메서드에 특별한 최적화를 적용해요. 메서드가 내장 할당 구문의 결과로 호출될 때(위에서 본 직접 subsasgn 호출이 아니라 x(1) = 1 같은 형태이면서), 그리고 위 예시처럼 subsasgn 메서드가 입력·출력 인자를 동일하게 선언했다면, Octave는 호출자 스코프 안의 x 복사를 무시합니다. 그래서 메서드 실행 중 x에 가해진 모든 변경이 호출자의 복사본에도 직접 영향을 미치죠.
이 덕분에 예를 들어 단일 원소 수정이 상수 시간에 일어나는 다항식(polynomial) 클래스를 정의할 수 있어요.
이 최적화가 가져오는 함의를 이해하는 게 중요해요. 호출자 스코프에 x의 추가 복사본이 존재하지 않으므로, 실행 중 오류가 발생했을 때 x를 잘못된 상태로 남기지 않을 책임은 전적으로 호출되는 쪽(callee)에 있어요. 또 메서드가 x를 부분적으로 바꾼 뒤 오류를 내면, 그 변경이 호출자 스코프의 x에도 영향을 미칩니다. 다만 subsasgn 안에서 x를 삭제하거나 완전히 교체해도 아무 효과가 없어요. 인덱스 할당만 영향을 줍니다.
이 최적화는 코드 동작 방식을 바꿀 수 있어서(특히 잘못 작성된 코드에서), 이를 제어하는 optimize_subsasgn_calls 함수가 제공돼요. 이 기능은 기본적으로 켜져 있어요. 최적화를 피하는 또 다른 방법은 subsasgn 메서드를 입력·출력 인자가 서로 다르게 선언하는 거예요.
function y = subsasgn (x, ss, val)
...
endfunction
더 알아보기
- octave-methods — 메서드 블록과 접근 제어
- octave-index-expressions — 인덱스 표현식과
subsasgn - octave-expressions — 표현식과 연산자