Java와 Python의 bytecode

L-cloud·2023년 6월 7일
0
post-thumbnail

bytecode가 무엇인지 알고있는 독자를 대상으로 합니다. JVM과 PVM을 설명하지 않습니다.

본 글에서 알아볼 내용
1. Java의 complie은 왜 파이썬 보다 느릴까?
2. JIT와 interpreter는 무엇이 다를까?
3. CPython은 왜 JIT를 도입하지 않을까?

Java를 공부하다 문득 의문이 들었다. 보통 자바 책의 1장에는 Java가 C, C++ 같은 언어와 어떻게 다르게 플랫폼 독립적으로 동작하는지가 나와 있다. 모두가 아는 사실이지만 처음 컴파일을 통해 bytecode를 생성하고 JVM이 해당 bytecode를 기계어로 번역해 주기 때문에 플랫폼에 맞는 JVM만 설치되어 있으면 OS와 상관없이 동작한다.
CPython도 bytecode를 생성하고 PVM위에서 돌아가는데 왜 Java의 경우 bytecode를 생성하는 compile이 느릴까. Java의 경우 JIT를 통해 상당한 속도 개선을 하였다고 하는데, CPython도 Java와 유사한 방식으로 작동하니 JIT를 도입할 수는 없을까? 천천히 알아보자

그래서 자바와 파이썬은 어떻게 동작하는데?

간단히 살펴보자. 아래 Test.java 코드를 실행하고 싶으면

public class Test {

	public static void main(String[] args) {
		System.out.println("Hello World!!");
	}
}

javac Test.java를 통해 bytecode를 생성한다. java Test 명령어를 입력하면 JREJVM 위에서 해당 코드가 동작하도록 한다. 그런데 이 동작 과정에서 JVM에 따라 파이썬처럼 bytecode를 기계어로 한 줄 한 줄 번역해 실행하는 인터프리터 방식을 사용하기도 하고, JIT를 사용하기도 하고, C나 C++처럼 AOT를 사용하기도 하고, processor의 도움을 받아 bytecode를 번역없이 실행하기도 한다.

CPython도 유사하다. compile을 통해 bytecode를 생성하고 (.pyc, .pyo가 bytecode ) PVM이 이를 한 줄 한 줄 인터프리터 방식으로 기계어로 번역한다. 파이썬의 재미있는 특징은 bytecode를 생성하고 실행하는 단위를 조정할 수 있다는 점이다. 아래 코드를 예시로 살펴보자.

#test.py
print("Hello world")
12var = "I'm error" # 변수 명 오류

python test.py를 실행하면 당연히 오류가 발생하며 Hello world도 출력되지 않는다.

 File "/private/tmp/test.py", line 2
 12var = "I'm error"
 ^
SyntaxError: invalid syntax

하지만 cmd에서 python을 실행하여 동일한 코드를 작성할 경우 아래처럼 bloc 단위로 bytecode를 생성하고 실행하기 때문에 Hello world를 출력하고 오류가 발생한다.

>>> print("Hello world")
Hello world
>>> 12var = "I'm error"
 File "<stdin>", line 1
 12var = "I'm error"
 ^
SyntaxError: invalid syntax

(정확하지는 않지만 jupyter notebook도 이를 활용하지 않을까 한다.)

bytecode를 생성한다는 점은 동일한데 왜 Java의 컴파일 속도가 더 느릴까?

이는 각 언어의 특성을 생각해 보면 이해하기 쉽다. 가상 떠올리기 쉬운 차이는 타입이다.
우선 자바의 경우 정적 타입 언어이다. 컴파일 단계에서 이를 다 확인 해줘야 한다. 예를 들어보자. 아래 코드는 컴파일조차 되지 않는다.

 int test(String a){
 return a;
 }

하지만 파이썬의 경우 동적 타입 언어이기 때문에 컴파일 시 자바보다 확인해야 하는 것이 적다. print 문을 컴파일하는 과정을 여기에서 간단히 볼 수 있다. 하지만 실행 시점의 속도는 당연히 자바가 더 빠르다. PVM은 실행 시 객체의 데이터 타입을 정해주여야 하므로 JVM보다 해야 할 일이 많다. 당연히 컴파일 과정에서 최적화도 Java가 상대적으로 더 잘 된다. 조금 더 자세히 알고 싶다면 여기를 참고해 보자. 옛날 글에다 비유가 종종 있어서 어렵기는 하지만 꼭 한 번 읽어보기를 추천한다.

JIT는 무엇이고 Cpython은 왜 이를 도입하지 않았을까?

검색을 하다보먼 Java가 bytecode를 기계어로 번역해 주는 과정에서 JIT를 사용해서 속도향상이 되었다는 글을 많이 볼 수 있다. 어떻게 Java의 JIT이 속도를 실행 속도를 높여주는지 정말 간단히 이야기해 보고자 한다.

인터프리터는 한 줄 한 줄 번역하여 실행한다. JIT은 실행 시간에 compile을 하여 bytecode를 기계코드로 번역해 주고 중복되는 부분이 있으면 인터프리터가 다시 번역하지 않도록 캐싱을 해주거나 여러 방식으로 최적화를 진행한다. AOT 처럼 미리 컴파일하면 특정 환경에 종속 되지만, bytecode를 실행 환경에서 JVM이 현재 환경에 맞도록 컴파일하면 환경에 구애받지 않고 실행할 수 있다. 물론 총 실행 시간에 JIT으로 컴파일하는 시간이 포함되기 때문에 코드의 재사용성이 떨어지면 오히려 인터프리터보다 속도가 떨어질 수도 있다. Java의 경우 목적에 따라 c1, c2 컴파일러 등 다양한 컴파일러이 있고 이를 선택해서 설치할 수 있다. 또한 JVM에 따라 인터프리터와 JIT을 둘 다 사용하는 경우도 있고, JIT만 사용하는 경우 등 다양하다.

cpython도 속도 향상을 위해 JIT을 도입하면 안 될까? 실제 Merging Unladen Swallow 라는 프로젝트로 시도는 있었다! 하지만 스폰서 구글의 관심, 개발자들의 관심 밖, LLVM의 문제 등으로 적용되지는 않았다. (자세한 이야기, LLVM) 그 외의 이유에도 사실 속도 개선이 필요하다면 C/C++ 등 extention(기계어로 컴파일 되어있음)을 사용하면 되고, extention된 기계어와 bytecode를 JIT로 만드는 것은 어렵다고 한다.

번외)
CPython 버전 3.6부터는 사용자 정의 프레임 평가 함수를 인터프리터에 설정하고 JIT 컴파일된 코드를 코드 객체에 저장할 수 있도록 한다. 이 기능은 JIT를 지원하기 위해 별도의 모듈 형태로 구현되었으며, PEP 523에 자세히 설명되어 있다. PEP 523

JIT을 도입한 파이썬 인터프리터도 있다!

pypy가 대표적인 예시이다.

  • JIT 컴파일러를 사용한다.
  • GC에서 reference count를 사용하지 않는다.
  • python처럼 cycle 탐지를 한 번에 하지 않고 여러 조각으로 분할하고 각 조각을 실행한다.
  • C extention을 최적화할 수 없기 때문에 C extention의 속도는 cpython보다 느리고 몇몇 모듈은 사용할 수 없다.
  • 오버헤드가 있어서 오래 실행되는 프로그램에 어울린다.

더 자세한 내용은 docs를 참고 바란다.

결론

Java 와 cpython의 기본 원리는 비슷하지만 수행 작업은 조금씩 다르다. Java는 프로그래머가 해야하는 것이 더 많지만 실행 속도가 빠르고 Python은 상대적으로 VM에 위임하는 것(동적 타입 등)이 많다. 어떤 것을 선택할지는 목적에 따라 다르겠다. 개발 속도 자체는 Python이 빠르기에 초기에 Python으로 개발을 해서 반응이 좋으면 Java로 옮기는 일이 많아보인다.

참고자료

더 알아보면 좋을 내용

  • JVM 구조
  • PVM 구조

지적은 언제나 환영입니다.

profile
내가 배운 것 정리

0개의 댓글