Q: 어떤 상황에서 Java가 C++보다 훨씬 느립니까?
답변: Ben Maurer:
이 질문에 답하려면 먼저 문제를 속도 저하의 여러 원인으로 나누어야 합니다.
가비지 컬렉터. 이것은 '양날의 검'이다. 프로그램이 "대부분의 개체는 젊은 세대에서 죽습니다" 모델을 따르는 경우 가비지 수집기는 매우 유익합니다(조각화가 적고 캐시 지역성이 향상됨). 그러나 프로그램이 이 모델을 따르지 않으면 JVM은 힙 메모리를 회수하는 데 많은 리소스를 소비하게 됩니다.
대형 물체. Java에서는 모든 객체에 vtable 포인터가 있지만 C++에서는 POD 구조를 사용할 때 추가 오버헤드가 없습니다. 또한 모든 Java 개체를 잠글 수 있습니다. 구현은 JVM에 따라 달라지며, 객체에 추가 필드를 추가해야 할 수도 있습니다. 큰 객체 == 더 적은 수의 객체를 캐시합니다 == 속도가 느립니다. (한편, Java 7에서는 압축된 포인터에 64비트 레코드를 사용하는데, 이것이 문제의 일부입니다.
인라인 객체가 부족합니다. Java에서는 모든 클래스가 포인터입니다. C++에서는 객체가 될 수 있습니다. 다른 개체와 함께 할당하거나 스택에 할당하면 캐시의 위치가 향상되어 동적 메모리 할당의 오버헤드가 줄어듭니다. Java에서는 개체를 로컬 코드로 컴파일하면 많은 오버헤드가 발생할 수 있습니다. 클라이언트의 C++ 코드를 자주 호출해야 하는 경우에는 많은 오버헤드가 추가됩니다. 예를 들어 문자열은 Java에서 비효율적인 추상화이며 XML 구문 분석기를 작성하려는 경우(char[ 없음) ]), JVM에서 할당한 추가 공간으로 인해 속도가 느려집니다. 거의 모든 함수 호출은 가상 함수 호출이지만 대부분의 경우 JVM은 이 문제를 해결할 수 없으며 🎜> 고급 컴파일이 부족합니다. 기능과 어셈블리로 변환하는 기능 어셈블리의 이점을 누릴 수 있는 코드를 작성하면 Java가 제대로 수행되지 않을 수 있습니다.
내 생각에 가장 큰 문제는 가비지 수집입니다. 대용량 메모리의 전체 GC는 Java와 C++ 간의 격차가 발생하는 가장 큰 이유 중 하나입니다. 또한 프로그램의 작업 세트가 L2 캐시 외부에 배치되면 대형 개체, 인라인 개체 부족 등과 같은 문제가 발생할 수 있습니다. 비효율적인 강제 추상화 및 플랫폼 기능도 속도 저하를 일으킬 수 있지만 이는 일반적으로 잘 작성된 Java 코드 베이스를 사용하는 경우에만 발생합니다. 일반적으로 큰 문제는 아닙니다
A: Todd Lipcon
기본적으로 Ben Maurer의 의견에 동의합니다(안녕 Ben!). 몇 가지 작은 차이점이 있습니다.
최신 JVM에서 이스케이프 분석은 할당이 (a) 로컬 함수 또는 (b) 로컬 스레드에서 절대로 이스케이프되지 않을 때 작동합니다. 즉, 할당에 잠금이 필요하지 않은 경우 일반적으로 자체 스택에서 수행됩니다. 두 경우 모두 단순한 "포인터 범프" 할당입니다. 이는 C의 스택 할당과 동일합니다.
번역자 참고 사항:
이스케이프 분석은 다음을 참조하는 컴파일 최적화 기술입니다. 포인터의 동적 범위를 분석하는 방법입니다. 객체에 대한 포인터가 여러 메서드나 스레드에 의해 참조되는 경우 포인터가 이스케이프된다고 말합니다.
Java 힙의 메모리를 범프한다고 가정합니다. 절대적으로 규칙적이며, 사용된 메모리는 모두 한쪽에 배치되고, 사용 가능한 메모리는 반대쪽에 배치되며, 할당된 메모리는 분할 지점을 나타내는 표시로 중앙에 배치됩니다. 객체의 크기와 동일한 거리만큼 여유 공간에 대한 포인터를 할당하는 방법을 "포인터 충돌"이라고 합니다.
이스케이프 분석 없이도 젊은 세대의 할당은 포인터 충돌을 통해 스레드 로컬 할당 버퍼(TLAB)에서 이루어지며 동기화가 필요하지 않습니다. 따라서 Java의 작은 개체 할당은 C 언어로 구현된 malloc() 메서드보다 더 빠른 경우가 있습니다. Google의 tcmalloc과 같은 더 나은 malloc 방법은 유사한 접근 방식을 취합니다. 그러나 C언어는 할당된 객체를 메모리에 재할당할 수 없기 때문에 일부 제한이 있다.
인라인 및 가상 함수에 문제가 있지만 실제로 어떤 경우에는 Java가 C보다 더 나은 성능을 발휘할 수도 있습니다. 특히 C에서는 인라인 처리가 런타임이 아닌 컴파일 타임에 이루어지기 때문에 동적 연결을 통한 인라인 처리를 구현할 수 없습니다. Java는 컴파일 중에 클래스의 실제 구현을 아직 사용할 수 없는 경우에도 다양한 클래스나 라이브러리의 경계를 넘어 함수를 동적으로 인라인할 수 있습니다. 많은 작업에서 이 접근 방식은 항상 가상 테이블을 호출해야 하는 C++ 가상 함수 호출보다 더 효율적입니다. JIT 컴파일러는 이전에 동적 속성이 손실된 경우(예: 새 클래스가 로드된 경우) 인라인 최적화를 지능적으로 취소할 수 있습니다.
GCC의 새 버전은 이 영역에서 "전체 프로그램 최적화" 또는 "링크 타임 최적화"라고 하는 몇 가지 최적화 기능을 제공합니다. 이를 통해 프로젝트 범위 내의 개체 파일 전체에 인라인을 적용할 수 있습니다. 그러나 기본적으로 동적 연결(예: 인라인을 통한 zlib 호출 등)을 통한 인라인 구현은 허용되지 않습니다. 많은 대규모 프로젝트는 표준 라이브러리의 기능을 코드에 복사하여 구현됩니다.