C ++보다 빠른 Java 힙 할당


13

나는 이미이 질문 을 SO에 게시 했으며 괜찮 았습니다. 불행히도 닫히지 않았지만 (다시 열려면 한 번의 투표 만 필요합니다) 누군가가 여기에 더 잘 맞기 때문에 여기에 게시하도록 제안 했으므로 다음은 말 그대로 질문의 사본입니다.


나는이 답변 에 대한 의견을 읽고 있었고이 인용문을 보았습니다.

객체 인스턴스화 및 객체 지향 기능은 처음부터 설계되었으므로 사용하기가 매우 빠릅니다 (많은 경우 C ++보다 빠름). 컬렉션이 빠릅니다. 표준 Java는이 영역에서 가장 최적화 된 C 코드 인 경우에도 표준 C / C ++를 능가합니다.

한 명의 사용자 (정말로 높은 담당자 수) 가이 주장을 대담하게 변호하여

  1. Java의 힙 할당은 C ++보다 낫습니다.

  2. 그리고 자바에서 컬렉션을 방어하는이 진술을 추가했습니다.

    Java 콜렉션은 주로 다른 메모리 서브 시스템으로 인해 C ++ 콜렉션에 비해 빠릅니다.

그래서 내 질문은 이것이 사실 일 수 있고, 그렇다면 왜 자바의 힙 할당이 훨씬 빠르 냐는 것입니다.


SO 유용 / 관련 에 관한 비슷한 질문에 대한 내 대답을 찾을 수 있습니다 .
Daniel Pryden

1
Java (또는 다른 관리되고 제한된 환경)를 사용하면 객체를 이동하고 포인터를 업데이트 할 수 있습니다. 즉, 더 나은 캐시 위치를 동적으로 최적화 할 수 있습니다. 제어되지 않은 비트 캐스트를 사용하는 C ++ 및 포인터 산술을 사용하면 모든 객체가 영원히 해당 위치에 고정됩니다.
SK-logic

3
나는 누군가가 항상 메모리를 복사하기 때문에 Java 메모리 관리가 더 빠르다는 말을들은 적이 없다고 생각했습니다. 한숨.
gbjbaanb

1
@gbjbaanb, 메모리 계층 구조에 대해 들어 본 적이 있습니까? 캐시 미스 페널티? 범용 할당자가 비싸고 1 세대 할당은 단일 추가 작업이라는 것을 알고 있습니까?
SK-logic

1
이것은 어떤 경우에는 다소 사실 일 수 있지만 Java에서는 힙에 모든 것을 할당하고 c ++에서는 스택에 많은 양의 객체를 할당하여 훨씬 더 빠를 수 있다는 점을 놓칩니다.
JohnB

답변:


23

이것은 흥미로운 질문이며 답은 복잡합니다.

전반적으로 JVM 가비지 컬렉터는 매우 잘 설계되고 매우 효율적이라고 말할 수 있습니다. 아마도 가장 일반적인 범용 메모리 관리 시스템 일 것입니다.

C ++는 특정 목적을 위해 설계된 특수 메모리 할당기로 JVM GC를 능가 할 수 있습니다 . 예를 들면 다음과 같습니다.

  • 프레임 단위 메모리 할당 기. 주기적으로 전체 메모리 영역을 삭제합니다. 이들은 예를 들어 임시 메모리 영역이 프레임 당 한 번 사용되고 즉시 폐기되는 C ++ 게임에서 자주 사용됩니다.
  • 고정 크기 객체 풀을 관리하는 사용자 지정 할당 자
  • 스택 기반 할당 (JVM은 다양한 상황에서 (예 : 이스케이프 분석을 통해) 수행함 )

물론 특수 메모리 할당자는 정의에 의해 제한됩니다. 일반적으로 개체 수명주기 및 / 또는 관리 할 수있는 개체 유형에 대한 제한이 있습니다. 가비지 콜렉션이 훨씬 유연합니다.

가비지 콜렉션은 성능 측면에서 몇 가지 중요한 이점 을 제공합니다 .

  • 객체 인스턴스화 는 실제로 매우 빠릅니다. 새로운 객체가 메모리에 순차적으로 할당되는 방식 때문에 종종 하나 이상의 포인터 추가가 필요하며 이는 일반적인 C ++ 힙 할당 알고리즘보다 확실히 빠릅니다.
  • 당신은 라이프 사이클 관리 비용에 대한 필요성 피 (일반적으로 훨씬 더 GC 이상) 자주 증분 이후 성능 관점에서 매우 가난하고 참조 횟수의 감소시키는 것은 성능 오버 헤드를 많이 추가 (때로는 GC의 대안으로 사용) 예를 들어, 참조 카운트를 - .
  • 불변 개체를 사용하는 경우 구조 공유 를 활용 하여 메모리를 절약하고 캐시 효율성을 향상시킬 수 있습니다. 이것은 Scala 및 Clojure와 같은 JVM의 기능적 언어에 의해 많이 사용됩니다. 공유 객체의 수명을 관리하기가 매우 어렵 기 때문에 GC 없이는이 작업을 수행하기가 매우 어렵습니다. 불변성과 구조 공유가 대규모 동시 응용 프로그램을 구축하는 데 중요하다고 생각한다면 이것이 GC의 가장 큰 성능 이점 일 것입니다.
  • 모든 유형의 객체와 해당 수명주기가 동일한 가비지 수집 시스템에서 관리되는 경우 복사피할 수 있습니다 . 대상에 다른 메모리 관리 방법이 필요하거나 개체 수명주기가 다르기 때문에 전체 데이터 사본을 가져와야하는 C ++과는 대조적입니다.

Java GC에는 한 가지 주요 단점이 있습니다. 가비지 수집 작업이 주기적으로 작업 단위로 연기되고 수행되기 때문에 가끔 GC 일시 중지 로 인해 가비지가 수집되어 대기 시간에 영향을 줄 수 있습니다. 일반적으로 일반적인 응용 프로그램에서는 문제가되지 않지만, 실시간어려운 상황 (예 : 로봇 제어) 에서는 Java를 배제 할 수 있습니다 . 소프트 실시간 (예 : 게임, 멀티미디어)은 일반적으로 정상입니다.


C ++ 영역에는 해당 문제를 해결하는 특수 라이브러리가 있습니다. 아마도 가장 유명한 예는 SmartHeap입니다.
Tobias Langner

5
소프트 실시간이라고해서 보통 중지해도 괜찮다는 의미는 아닙니다 . 이는 중단 / 충돌 / 실패 대신 실제 상황 에서 일시 중지 / 재 시도 할 수 있음을 의미합니다 . 일반적으로 일시 중지 된 음악 플레이어를 사용하고 싶은 사람은 없습니다. GC 일시 정지의 문제는 발생이다 일반적으로 하고 예측할 . 이러한 방식으로 소프트 실시간 애플리케이션에서도 GC 일시 정지가 허용되지 않습니다. GC 일시 중지는 사용자가 애플리케이션 품질을 신경 쓰지 않는 경우에만 허용됩니다. 그리고 오늘날 사람들은 더 이상 순진하지 않습니다.
Eonil

1
귀하의 주장을 뒷받침하기 위해 성능 측정을 게시하십시오. 그렇지 않으면 사과와 오렌지를 비교합니다.
JBR 윌킨슨

1
@Demetri 그러나 실제로는 비현실적인 제약을 충족시킬 수 없다면 사건이 너무 많이 발생 하는 경우 (그리고 다시 예측할 수없는 경우 까지만 해당 )입니다. 즉, C ++은 모든 실시간 상황에서 훨씬 더 쉽습니다.
Eonil

1
완전성 : GC 성능 측면에서 또 다른 단점이 있습니다. 대부분의 기존 GC에서 다른 코어에서 실행될 가능성이있는 다른 스레드에서 메모리 해제가 발생하므로 GC가 동기화에 심각한 캐시 무효화 비용이 발생 함을 의미합니다. 다른 코어들 사이의 L1 / L2 캐시; 또한 주로 NUMA 인 서버에서 L3 캐시도 동기화해야합니다 (Hypertransport / QPI, ouch (!)).
No-Bugs Hare

3

이것은 과학적 주장이 아닙니다. 이 문제에 대해 생각할만한 음식을 제공하고 있습니다.

하나의 시각적 비유는 이것입니다. 카펫이 깔린 아파트 (주거 단위)가 제공됩니다. 카펫이 더럽습니다. 아파트 바닥을 깨끗하게 만드는 가장 빠른 방법은 몇 시간입니까?

답 : 단순히 오래된 카펫을 말아 올리십시오. 버리십시오; 새 카펫을 펴세요.

우리는 여기서 무엇을 무시하고 있습니까?

  • 기존 개인 소지품을 옮긴 다음 입주하는 비용.
    • 이것을 "세계의 중지"가비지 수집 비용이라고합니다.
  • 새로운 카펫의 비용.
    • RAM과 동시에 무료입니다.

가비지 콜렉션은 큰 주제이며 Programmers.SE와 StackOverflow에 많은 질문이 있습니다.

부수적 인 문제로, 객체 참조 카운팅과 함께 TCMalloc으로 알려진 C / C ++ 할당 관리자는 이론적 으로 모든 GC 시스템의 최고의 성능 요구를 충족시킬 수 있습니다.


실제로 c ++ 11에는 가비지 수집 ABI가 있습니다 . 이것은 내가 얻은 답변 중 일부와 매우 유사합니다.
aaronman

C ++에서 언어 혁신의 진행을 방해하는 기존 C / C ++ 프로그램 (Linux 커널 및 libaic_but_still_still_economically_important 라이브러리와 같은 코드 기반)과 같은 기존 C / C ++ 프로그램을 손상시킬 우려가 있습니다.
rwong

이해가되지만, C ++ 17에서는 더 완벽 할 것이라고 생각하지만 사실 일단 C ++로 프로그래밍하는 법을 배우면 더 이상 원하지 않아도됩니다. 아마도 두 관용구를 결합하는 방법을 찾을 수 있습니다. 멋지게
aaronman

세상을 멈추지 않는 가비지 수집기가 있다는 것을 알고 있습니까? 압축 (GC 측) 및 힙 조각화 (일반 C ++ 할당 자)의 성능 영향을 고려 했습니까?
SK-logic

2
이 비유의 주된 단점은 GC가 실제로하는 것은 더티 비트를 찾아서 잘라낸 다음 나머지 비트를 다시보고 새 카펫을 만드는 것입니다.
svick

3

주된 이유는 Java에 새로운 대량의 메모리를 요청할 때 힙의 끝 부분으로 곧장 가서 블록을 제공하기 때문입니다. 이런 식으로, 메모리 할당은 스택에 할당하는 것만 큼 빠릅니다 (C / C ++에서 대부분의 시간을 할애하는 방법입니다.)

따라서 할당은 빠르지 만 메모리를 확보하는 비용은 계산하지 않습니다. 훨씬 나중에까지 아무것도 해지하지 않는다고해서 비용이 많이 들지 않는다는 의미는 아니며 GC 시스템의 경우 비용이 '정상적인'힙 할당보다 훨씬 더 큽니다. GC는 모든 객체를 검사하여 객체가 살아 있는지 여부를 확인한 다음 객체를 비워야하며 (큰 비용) 힙을 압축하기 위해 메모리를 복사해야하므로 결국 빠른 할당이 가능합니다 메커니즘 (또는 메모리가 부족한 경우 C / C ++는 모든 할당에서 힙을 걸어 객체에 맞는 다음 여유 공간 블록을 찾습니다).

이것이 Java / .NET 벤치 마크가 이와 같이 우수한 성능을 보여 주지만 실제 응용 프로그램이 그렇게 나쁜 성능을 보여주는 이유 중 하나입니다. 휴대 전화의 앱만 살펴 봐야합니다. 정말 빠르고 반응이 빠른 앱은 모두 NDK를 사용하여 작성되었으므로 놀라웠습니다.

모든 객체가 로컬로 할당 된 경우 (예 : 단일 연속 블록) 현재는 수집 속도가 빠릅니다. 이제 Java에서는 객체가 힙의 자유 끝에서 한 번에 하나씩 할당되므로 연속 블록을 얻지 못합니다. 운이 좋을 때만 행복하게 연속적으로 끝낼 수 있습니다 (즉, GC 압축 루틴의 변덕과 대상을 복사하는 방법). 반면에 C / C ++는 스택을 통해 연속 할당을 명시 적으로 지원합니다. 일반적으로 C / C ++의 힙 객체는 Java의 BTW와 다르지 않습니다.

이제 C / C ++를 사용하면 메모리를 절약하고 효율적으로 사용하도록 설계된 기본 할당 자보다 더 나아질 수 있습니다. 할당자를 고정 블록 풀 세트로 교체 할 수 있으므로 할당중인 객체에 맞는 크기의 블록을 항상 찾을 수 있습니다. 힙을 걷는 것은 빈 블록의 위치를 ​​확인하기 위해 비트 맵 조회의 문제가되며 할당 해제는 단순히 해당 비트 맵에서 비트를 재설정합니다. 비용은 고정 크기 블록에 할당 할 때 더 많은 메모리를 사용하므로 4 바이트 블록, 16 바이트 블록 등의 힙이 있습니다.


2
GC를 전혀 이해하지 못하는 것 같습니다. 가장 일반적인 시나리오를 고려하십시오-수백 개의 작은 객체가 지속적으로 할당되지만 수십 개의 객체 만 1 초 이상 생존합니다. 이렇게하면 메모리를 확보하는 데 드는 비용이 전혀 없습니다.이 수십 개는 어린 세대에서 복사하고 추가 혜택으로 압축하고 나머지는 무료로 버립니다. 그리고 한심한 Dalvik GC는 적절한 JVM 구현에서 찾을 수있는 최신의 최신 GC와 관련이 없습니다.
SK-logic

1
해제 된 오브젝트 중 하나가 힙의 중간에 있으면 나머지 힙이 압축되어 공간을 확보합니다. 또는 GC 압축은 설명하는 최선의 경우가 아니면 발생하지 않는다고 말하는 것입니까? 후기 세대 중반에 객체를 놓지 않으면 세대 GC가 훨씬 더 잘 수행됩니다.이 경우 영향이 상대적으로 클 수 있습니다. GC를 위해 일한 Microsoftie가 세대 GC를 만들 때 GC 트레이드 오프를 설명하는 글이있었습니다. 다시 찾을 수 있는지 살펴 보겠습니다.
gbjbaanb

1
무슨 "힙"을 말하는거야? 쓰레기의 대부분은 젊은 세대 단계에서 재생되며, 대부분의 성능 이점은 이러한 압축에서 비롯됩니다. 물론, 기능 프로그래밍 (많은 짧은 수명의 작은 객체)에 전형적인 메모리 할당 프로파일에서 대부분 볼 수 있습니다. 물론 아직 최적화되지 않은 수많은 최적화 기회가 있습니다. 예를 들어, 특정 경로의 힙 할당을 스택 또는 풀 할당으로 자동 전환 할 수있는 동적 영역 분석이 있습니다.
SK-logic

3
힙 할당이 '스택만큼 빠름'이라는 주장에 동의하지 않습니다. 힙 할당에는 스레드 동기화가 필요하지만 스택은 정의하지 않습니다.
JBRWilkinson

1
나는 추측하지만 Java와 .net을 사용하면 내 요점을 알 수 있습니다. 다음 프리 블록을 찾기 위해 힙을 걸을 필요가 없으므로 그 점에서 훨씬 빠릅니다. 그렇습니다-맞습니다. 스레드 된 앱을 손상시킬 수 있습니다.
gbjbaanb

2

에덴 스페이스

그래서 내 질문은 이것이 사실 일 수 있고, 그렇다면 왜 자바의 힙 할당이 훨씬 빠르 냐는 것입니다.

Java GC가 매우 흥미로워 서 작동하는 방식에 대해 조금 연구했습니다. 나는 항상 C와 C ++에서 메모리 할당 전략 모음을 확장하려고 노력하고 있으며 (C와 비슷한 것을 구현하려는 데 관심이 있음) 많은 객체를 버스트 방식으로 버스트 방식으로 할당하는 매우 빠르고 매우 빠른 방법입니다 실용적인 관점이지만 주로 멀티 스레딩으로 인해 발생합니다.

Java GC 할당의 작동 방식은 초저가의 할당 전략을 사용하여 초기에 "Eden"공간에 객체를 할당하는 것입니다. 내가 알 수있는 것은 순차 풀 할당자를 사용하고 있습니다.

알고리즘 측면 malloc에서 C 또는 기본값의 일반적인 용도보다 강제적 인 페이지 결함을 줄이면서 operator newC ++을 던지는 것보다 훨씬 빠릅니다 .

그러나 순차적 할당자는 눈에 띄는 약점을 가지고 있습니다. 가변 크기 청크를 할당 할 수는 있지만 개별 청크를 해제 할 수는 없습니다. 정렬을 위해 패딩을 사용하여 연속적인 순차 방식으로 할당하며 한 번에 할당 한 모든 메모리 만 제거 할 수 있습니다. 그것들은 일반적으로 C와 C ++에서 프로그램이 시작되고 반복적으로 검색되거나 새로운 키가 추가 될 때만 빌드 해야하는 검색 트리와 같이 요소 삽입 만 필요하고 요소 제거가 필요없는 데이터 구조를 구성하는 데 유용합니다 ( 키가 제거되지 않음).

또한 요소를 제거 할 수있는 데이터 구조에도 사용할 수 있지만 이러한 요소는 개별적으로 할당을 해제 할 수 없으므로 실제로 메모리에서 해제되지 않습니다. 순차 할당자를 사용하는 이러한 구조 는 데이터가 별도의 순차 할당자를 사용하여 새로운 압축 사본으로 복사되는 지연된 패스가 없으면 (그리고 고정 할당자가이기는 경우에 매우 효과적인 기술입니다) 지연 되지 않는 한 더 많은 메모리를 소비 합니다. 어떤 이유로 든하지 마십시오. 데이터 구조의 새 사본을 순차적으로 할당하고 이전 구조의 모든 메모리를 덤프하십시오.

수집

위의 데이터 구조 / 순차 풀 예제에서와 같이 Java GC가 많은 개별 청크의 버스트 할당에 매우 빠르더라도이 방법으로 만 할당하면 큰 문제가됩니다. 소프트웨어가 종료 될 때까지 아무 것도 해제 할 수 없으며,이 시점에서 모든 메모리 풀을 한 번에 해제 (퍼지) 할 수 있습니다.

따라서 단일 GC주기 후에 "Eden"공간의 기존 객체를 통해 패스가 생성되고 (순차적으로 할당 됨) 여전히 참조 된 객체는 개별 청크를 해제 할 수있는보다 일반적인 할당기를 사용하여 할당됩니다. 더 이상 참조되지 않는 것은 제거 과정에서 단순히 할당 해제됩니다. 따라서 기본적으로 "아직 참조 된 경우 Eden 공간에서 객체를 복사 한 다음 제거"입니다.

일반적으로 비용이 많이 들기 때문에 별도의 백그라운드 스레드에서 수행되어 원래 모든 메모리를 할당 한 스레드가 크게 중단되는 것을 방지합니다.

메모리가 Eden 공간에서 복사되고 초기 GC주기 후 개별 청크를 해제 할 수있는이 더 비싼 체계를 사용하여 할당되면 객체는보다 영구적 인 메모리 영역으로 이동합니다. 그런 다음 해당 청크는 참조가 중단되면 후속 GC주기에서 해제됩니다.

속도

간단히 말해서 Java GC가 힙 할당에서 C 또는 C ++보다 성능이 우수한 이유는 메모리 할당을 요청하는 스레드에서 가장 비싸고 완전히 퇴화되지 않은 할당 전략을 사용하기 때문입니다. 그런 다음 malloc다른 스레드에 대해 직선형과 같은보다 일반적인 할당자를 사용할 때 일반적으로 수행해야하는 비용이 많이 드는 작업을 저장합니다 .

따라서 개념적으로 GC는 실제로 전체적으로 더 많은 작업을 수행해야하지만 전체 스레드가 단일 스레드로 선불로 지불되지 않도록 스레드 전체에 분배합니다. 메모리를 할당하는 스레드가 매우 저렴하게 작업을 수행 한 다음 개별 개체를 실제로 다른 스레드로 해제 할 수 있도록 작업을 수행하는 데 필요한 실제 비용을 연기합니다. C 또는 C ++에서 malloc또는 호출 할 때 operator new동일한 스레드 내에서 선불 비용을 선불로 지불해야합니다.

이것이 가장 큰 차이점이며 Java가 순진한 호출을 사용 malloc하거나 operator new여러 개의 작은 덩어리를 개별적으로 할당 하여 C 또는 C ++보다 성능이 우수한 이유는 무엇입니까 ? 물론 GC 사이클이 시작될 때 일반적으로 약간의 원자 연산과 잠재적 잠금이 있지만, 아마도 약간 최적화되어있을 것입니다.

기본적으로 간단한 설명은 단일 스레드에서 더 많은 비용을 malloc지불하는 것 ( )과 단일 스레드 에서 더 저렴한 비용을 지불 한 다음 병렬로 실행될 수있는 다른 스레드에 더 많은 비용을 지불하는 것으로 요약됩니다 GC. 이 방법을 사용하는 단점은 할당자가 기존 객체 참조를 무효화하지 않고 메모리를 복사 / 이동시킬 수 있도록 객체 참조에서 객체로 가져 오는 두 가지 간접 지시가 필요하다는 것을 의미하며 객체 메모리가 일단 확보되면 공간적 로컬 성을 잃을 수 있습니다 "Eden"공간에서 나갔습니다.

마지막으로 C ++ 코드는 일반적으로 힙에 개별적으로 객체의 보트로드를 할당하지 않기 때문에 비교가 약간 불공평합니다. 적절한 C ++ 코드는 인접한 블록이나 스택의 많은 요소에 메모리를 할당하는 경향이 있습니다. 무료 상점에 한 번에 하나씩 작은 물체의 보트로드를 할당하면 코드가 혼란스러워집니다.


0

속도를 측정하는 사람, 측정하는 구현 속도 및 입증하려는 대상이 모두 다릅니다. 그리고 그들이 비교하는 것.

할당 / 할당 해제를 살펴보면 C ++에서 malloc에 ​​1,000,000 건의 호출이 있고 1,000,000 건의 무료 통화가있을 수 있습니다. Java에서는 비어있는 1,000,000 개의 객체를 찾는 루프에서 실행중인 new () 및 가비지 수집기가 1,000,000 번 호출됩니다. 루프는 무료 () 호출보다 빠를 수 있습니다.

반면에 malloc / free는 다른 시간에 개선되었으며 일반적으로 malloc / free는 별도의 데이터 구조에서 1 비트를 설정하고 동일한 스레드에서 발생하는 malloc / free에 최적화되어 있으므로 다중 스레드 환경에서 공유 메모리 변수가 없습니다 많은 경우에 사용되며 잠금 또는 공유 메모리 변수는 매우 비쌉니다.

세 번째로, 가비지 수집없이 필요할 수있는 참조 카운팅과 같은 것들이 있으며, 무료로 제공되지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.