[번역] JVM Anatomy Park #4: TLAB 할당

6279 단어
원본 주소: JVM Anatomy Park #4: TLAB allocation

문제.


TLAB 할당은 무엇입니까?포인터 충돌(Pointer-bump) 분배는 또 무엇입니까?어쨌든 분배 대상은 누가 책임지나요?

이론


우리가 new MyClass() 실행할 때, 실행할 때 환경은 새로 만든 대상에게 메모리를 분배합니다.할당용 GC(메모리 관리자) 예제 인터페이스는 매우 간단합니다.
 ref Allocate(T type);
 ref AllocateArray(T type, int size);

물론 메모리 관리자를 작성하는 언어는 일반적으로 실행하는 언어와 다르기 때문에 (예를 들어 JVM은 자바 언어를 실행하지만 HotSpot JVM은 C++로 작성됨) 코드의 실제 인터페이스는 비교적 어렵다.새 객체의 Java 코드를 로컬 VM 코드로 변환해야 합니다.이 과정의 원가가 높습니까?이 가능하다, ~할 수 있다,...메모리 관리자는 다중 스레드를 처리하고 메모리를 신청해야 합니까?그럼요.
다중 스레드를 최적화하고 신청하는 장면을 최적화하기 위해 우리는 스레드에 전체 메모리를 한꺼번에 분배하고 다 사용한 후에 VM에 새로운 메모리를 신청한다.Hotspot에서 이러한 메모리 블록은 루트 로컬 분배 버퍼(TLABs)라고 불리며 메모리 블록을 기반으로 하는 분배를 지원하기 위해 복잡한 메커니즘을 구축했다.공간적으로 볼 때 TLABs는 로컬 라인에 있습니다. 이것은 TLAB가 로컬 라인의 분배를 받는 버퍼입니다.TLAB는 여전히 자바 더미의 일부이며, 로컬에서 새로 만든 대상의 인용을 TLAB 외부의 필드에 전달할 수 있습니다.
모든 유명한 OpenJDK GCs는 TLAB 할당을 지원합니다.이 부분의 VM 코드는 상당히 추상적이다.모든 Hotspot 컴파일러는 TLAB 할당을 지원하며 객체 할당 논리로 생성된 시스템 코드는 일반적으로 다음과 같습니다.
0x00007f3e6bb617cc: mov    0x60(%r15),%rax        ; TLAB "current"
0x00007f3e6bb617d0: mov    %rax,%r10              ; tmp = current
0x00007f3e6bb617d3: add    $0x10,%r10             ; tmp += 16 (object size)
0x00007f3e6bb617d7: cmp    0x70(%r15),%r10        ; tmp > tlab_size?
0x00007f3e6bb617db: jae    0x00007f3e6bb61807     ; TLAB is done, jump and request another one
0x00007f3e6bb617dd: mov    %r10,0x60(%r15)        ; current = tmp (TLAB is fine, alloc!)
0x00007f3e6bb617e1: prefetchnta 0xc0(%r10)        ; ...
0x00007f3e6bb617e9: movq   $0x1,(%rax)            ; store header to (obj+0)
0x00007f3e6bb617f0: movl   $0xf80001dd,0x8(%rax)  ; store klass to (obj+8)
0x00007f3e6bb617f7: mov    %r12d,0xc(%rax)        ; zero out the rest of the object

분배 논리는 생성 코드에 연결되어 있기 때문에 GC를 호출해서 대상을 분배할 필요가 없습니다.할당을 신청한 대상이 TLAB를 다 소모했거나 대상이 TLAB보다 크다면, 우리는 '느린 경로' 를 사용해서 이곳에서 직접 분배하거나 새로운 TLAB로 되돌아간다.대부분의 정상적인 분배 논리는 TLAB의 현재 포인터를 대상의 크기를 늘린 다음에 계속 실행하는 것이다.
이런 분배 메커니즘이 때때로'포인터 충돌 분배(pointer bump allocation)'라고 불리는 이유다.바늘 충돌 분배는 분배에 사용되는 연속적인 메모리를 필요로 하지만, 이것은 메모리 압축의 수요를 도입했다.CMS가 오래된 시대에 어떻게 빈 목록에서 메모리를 분배했는지, 바늘 충돌 분배 방식에 따라 병렬 제거가 가능해졌는지 유의하세요.신세대의 더 적은 생존 대상은 빈 리스트 분배 비용을 지불할 것이다.
실험에서 우리는 -XX:-UseTLAB를 통해 TLAB 메커니즘을 닫을 수 있다.모든 객체 할당은 다음과 같은 로컬 코드를 실행합니다.
-   17.12%     0.00%  org.openjdk.All  perf-31615.map
   - 0x7faaa3b2d125
      - 16.59% OptoRuntime::new_instance_C
         - 11.49% InstanceKlass::allocate_instance
              2.33% BlahBlahBlahCollectedHeap::mem_allocate  

...그러나 통상적으로 이것은 좋은 생각이 아니다.

실험


여느 때와 마찬가지로 TLAB의 분배를 관찰하기 위한 실험을 구축합시다.모든 GC 구현에는 이러한 특성이 있기 때문에 운영 환경의 영향을 최소화하기 위해 실험적인 Epsilon GC를 사용합니다.사실상 분배만 이루어졌고 실험에 좋은 연구 플랫폼을 제공했다.
신속한 워크로드 설계: 50M의 개체를 할당합니다(왜 그렇지 않습니까?).JMH에서 실행되는 SingleShot 모드에서는 성능 통계 및 분석이 수행되지 않습니다.너도 이런 테스트를 단독으로 수행할 수 있다. 단지 싱크샷을 사용하는 것은 정말 편리하다.
@Warmup(iterations = 3)
@Measurement(iterations = 3)
@Fork(3)
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class AllocArray {
    @Benchmark
    public Object test() {
        final int size = 50_000_000;
        Object[] objects = new Object[size];
        for (int c = 0; c < size; c++) {
            objects[c] = new Object();
        }
        return objects;
    }
}

이 테스트 용례는 단일 라인에 50M개의 대상을 분배했다.경험을 바탕으로 우리는 20GB의 메모리를 배치하고 적어도 6번을 반복해서 실행했다.실험적-XX:EpsilonTLABSize 구성 항목은 TLAB의 크기를 정확하게 제어하는 데 사용됩니다.다른 OpenJDK GC는 자체 적응된 TLAB 크기 조정 전략을 사용하는데 이런 방식은 분배 압력과 기타 요소를 바탕으로 TLAB의 크기를 조정할 것이다.
잡담은 그만하고 결과를 봅시다.
Benchmark                     Mode  Cnt     Score    Error   Units

# Times, lower is better                                            # TLAB size
AllocArray.test                 ss    9   548.462 ±  6.989   ms/op  #      1 KB
AllocArray.test                 ss    9   268.037 ± 10.966   ms/op  #      4 KB
AllocArray.test                 ss    9   230.726 ±  4.119   ms/op  #     16 KB
AllocArray.test                 ss    9   223.075 ±  2.267   ms/op  #    256 KB
AllocArray.test                 ss    9   225.404 ± 17.080   ms/op  #   1024 KB

# Allocation rates, higher is better
AllocArray.test:·gc.alloc.rate  ss    9  1816.094 ± 13.681  MB/sec  #      1 KB
AllocArray.test:·gc.alloc.rate  ss    9  2481.909 ± 35.566  MB/sec  #      4 KB
AllocArray.test:·gc.alloc.rate  ss    9  2608.336 ± 14.693  MB/sec  #     16 KB
AllocArray.test:·gc.alloc.rate  ss    9  2635.857 ±  8.229  MB/sec  #    256 KB
AllocArray.test:·gc.alloc.rate  ss    9  2627.845 ± 60.514  MB/sec  #   1024 KB

우리는 단일 스레드에서 2.5GB/sec의 분배 속도를 실현한다.한 대상이 16바이트이기 때문에 초당 160백만 개의 대상을 의미한다.멀티스레드 워크로드에서 할당 속도는 초당 수십 GB에 달할 수 있습니다.물론 TLAB가 작아지면 분배 원가가 높아지고 분배 속도가 떨어진다.불행하게도 우리는 TLAB를 1KB이하로 설정할 수 없다. 이것은 Hotspot의 실현에 약간의 공간을 낭비해야 하기 때문이다. 그러나 우리는 TLAB 메커니즘을 완전히 닫고 성능에 미치는 영향을 볼 수 있다.
Benchmark                      Mode  Cnt     Score   Error    Units

# -XX:-UseTLAB
AllocArray.test                  ss    9  2784.988 ± 18.925   ms/op
AllocArray.test:·gc.alloc.rate   ss    9   580.533 ±  3.342  MB/sec

내가 헤벌쭉하게 갈게!분배 속도가 5배, 집행 시간이 10배로 늘었다!이것은 다중 스레드가 없어서 메모리를 신청하는 경우(원자적 경쟁이 발생할 수 있음), 또는 사용 가능한 메모리 위치를 찾아야 하는 장면(예를 들어 빈 목록에서 분배를 시도해야 함)이다.Epsilon에 대해 말하자면, 분배의 논리는 단지 하나의 compare-and-set일 뿐이다. 왜냐하면 바늘을 통해 메모리를 이동하기 때문이다.총 2개의 실행 스레드를 추가하고 TLAB를 종료하면 성능이 저하됩니다.
Benchmark                            Mode  Cnt           Score       Error   Units

# TLAB = 4M (default for Epsilon)
AllocArray.test                        ss    9         407.729 ±     7.672   ms/op
AllocArray.test:·gc.alloc.rate         ss    9        4190.670 ±    45.909  MB/sec

# -XX:-UseTLAB
AllocArray.test                        ss    9        8490.585 ±   410.518   ms/op
AllocArray.test:·gc.alloc.rate         ss    9         422.960 ±    19.320  MB/sec

현재 성능이 20배 떨어졌다.스레드 수가 증가함에 따라 성능은 더욱 떨어질 것이다!

관찰하다.


TLAB는 메모리 분배 메커니즘의 핵심이다. 분배기의 병목 현상을 없애고 더욱 빠른 분배 방식을 제공하며 전체적인 성능을 개선한다.흥미로운 생각이 하나 있다. TLAB는 더욱 빈번한 GC를 만들 것이다. 그 이유는 단지 너무 빨리 분배되었기 때문이다!반면 메모리 관리자가 메모리를 신속하게 분배하는 방식이 없으면 메모리 회수 성능 문제가 숨겨진다.메모리 관리자를 비교할 때 분배와 회수 두 부분, 그리고 두 가지 간의 관계를 충분히 이해해야 한다.

좋은 웹페이지 즐겨찾기