[Python] negative index를 사용하자

일반적으로 random access 가능한 자료 구조에서 뒤에서 k번째 원소를 가져오는 방법은 다음과 같다.

  1. 자료구조의 크기를 가져온다.
  2. 크기를 기반으로 접근할 인덱스를 구한다.
  3. 원하는 자료를 가져온다.

이를 코드로 표현하면 다음과 같다.

파이썬에서는 이를 보다 간결하게 표현하기 위해 negative indexing을 지원한다. negative indexing은 말 그대로 음수를 이용해서 뒤에서부터 자료에 접근하는 것이다. negative indexing을 이용하면 위의 코드는 아래와 같이 표현된다.

파이썬에서는 뒤에서부터 접근할 때 negative indexing을 사용하는 것이 권장되지만 다른 언어로 코딩을 처음 배운 사람들은 파이썬에서도 길이를 가져와 인덱스를 계산하는 방식을 사용하는 경우가 종종 있다. 이런 사람들은 negative indexing이 단순한 syntactic sugar라고 생각하는 것 같다. 하지만 negative indexing은 syntactic sugar가 아니다.

negative indexing이 syntactic sugar라면 negative indexing을 쓴 것과 같은 바이트 코드가 생성돼야 한다. 하지만 둘은 서로 다른 바이트 코드가 생성된다. 위의 각 코드가 실제로 생성하는 바이트 코드는 다음과 같다.

첫 번째 코드는 global memory에서 len 함수를 찾아 호출하여 그 결과에 k를 뺀 값을 인덱스로 사용하는 코드를 생성하고, 두 번째 코드는 k를 음수로 만들어 바로 인덱스로 사용한다. 인덱싱을 통해 값을 가지고 오는 바이트 코드는 BINARY_SUBSCR인데, 물론 인터프리터가 BINARY_SUBSCR 를 실행할 때 주어진 인덱스가 음수이면 내부적으로 리스트의 길이를 가져와 인덱스를 다시 계산하는 과정을 거친다. 하지만 Global Interpreter Lock(a.k.a. GIL) 때문에 이 둘의 동작은 같지 않다.

파이썬의 GIL은 바이트 코드 단위의 atomic만을 보장한다. LOAD_FAST 같은 몇몇 바이트 코드는 GIL을 릴리즈하지 않지만, CALL_FUNCTION을 포함한 대부분의 바이트 코드는 실행 후 GIL을 릴리즈 할 수도 있다.

또한, 두 코드는 생성되는 바이트 코드가 다르므로 수행 시간도 다르다. 첫 번째 코드는 global memory에서 len 함수를 가지고 와 호출하는 비용이 더 든다. 게다가 negative indexing을 사용할 때 BINARY_SUBSCR가 내부적으로 계산한 인덱스와 다르게 첫 번째 코드에서 직접 계산한 인덱스는 단순한 정수가 아니라 정수를 들고 있는 PyObject이다. 따라서 값을 할당하고 해제하는 비용이 더 들기 때문에 negative indexing을 사용하는 것이 더 빠르다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API