[java] 휘발성은 비쌉니까?

컴파일러 작성자를위한 JSR-133 쿡북을 읽은 후 휘발성의 구현, 특히 부분에 대해 “원자 지침과 상호 작용”나는 그것을 업데이트하지 않고 휘발성 변수를 읽는 것은 LoadLoad 또는 LoadStore 장벽을 필요로한다고 가정합니다. 페이지 아래로 내려 가면 LoadLoad 및 LoadStore가 X86 CPU에서 효과적으로 작동하지 않는 것을 알 수 있습니다. 이것은 x86에서 명시적인 캐시 무효화없이 휘발성 읽기 작업을 수행 할 수 있으며 일반 변수 읽기만큼 빠르다는 것을 의미합니까 (휘발성의 재정렬 제약 조건 무시)?

나는 이것을 올바르게 이해하지 못한다고 생각합니다. 누군가 나를 깨우쳐 줄 수 있습니까?

편집 : 다중 프로세서 환경에 차이가 있는지 궁금합니다. 단일 CPU 시스템에서 CPU는 John V.가 말한 것처럼 자체 스레드 캐시를 볼 수 있지만 다중 CPU 시스템에서는 이것이 충분하지 않고 메인 메모리에 도달해야하는 CPU 구성 옵션이 있어야 휘발성이 느려집니다. 다중 CPU 시스템에서 그렇죠?

추신 : 이것에 대해 더 배우기 위해 나는 다음과 같은 훌륭한 기사에 대해 우연히 발견했습니다.이 질문이 다른 사람들에게 흥미로울 수 있기 때문에 여기에 내 링크를 공유하겠습니다.



답변

Intel에서 경합되지 않는 휘발성 읽기는 매우 저렴합니다. 다음과 같은 간단한 경우를 고려하면 :

public static long l;

public static void run() {
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Java 7의 어셈블리 코드 인쇄 기능을 사용하면 run 메소드가 다음과 같이 보입니다.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

getstatic에 대한 2 개의 참조를 살펴보면 첫 번째는 메모리에서로드를 포함하고 두 번째는 이미로드 된 레지스터에서 값이 재사용되므로로드를 건너 뜁니다 (long은 64 비트이고 내 32 비트 랩톱에서는 2 개의 레지스터를 사용합니다).

l 변수를 휘발성으로 만들면 결과 어셈블리가 다릅니다.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

이 경우 변수 l에 대한 getstatic 참조는 모두 메모리에서로드를 포함합니다. 즉, 값은 여러 휘발성 읽기에서 레지스터에 보관 될 수 없습니다. 원자 적 읽기가 있는지 확인하기 위해 값을 주 메모리에서 MMX 레지스터 movsd 0x6fb7b2f0(%ebp),%xmm0로 읽어 읽기 작업을 단일 명령으로 만듭니다 (이전 예제에서 64 비트 값은 일반적으로 32 비트 시스템에서 두 개의 32 비트 읽기가 필요함을 보았습니다).

따라서 휘발성 읽기의 전체 비용은 대략 메모리로드와 동일하며 L1 캐시 액세스만큼 저렴할 수 있습니다. 그러나 다른 코어가 휘발성 변수에 쓰는 경우 캐시 라인은 주 메모리 또는 L3 캐시 액세스를 요구하는 무효화됩니다. 실제 비용은 CPU 아키텍처에 따라 크게 달라집니다. Intel과 AMD 간에도 캐시 일관성 프로토콜이 다릅니다.


답변

일반적으로 대부분의 최신 프로세서에서 휘발성로드는 일반로드와 비슷합니다. 휘발성 저장소는 모니터 진입 / 모니터 종료 시간의 약 1/3입니다. 이는 캐시 일관성이있는 시스템에서 볼 수 있습니다.

OP의 질문에 답하기 위해 휘발성 쓰기는 비용이 많이 드는 반면 읽기는 일반적으로 그렇지 않습니다.

이는 x86에서 명시 적 캐시 무효화없이 휘발성 읽기 작업을 수행 할 수 있고 일반 변수 읽기만큼 빠르다는 것을 의미합니까 (휘발성의 재정렬 제한 사항 무시)?

예, 때로는 필드의 유효성을 검사 할 때 CPU가 주 메모리에 도달하지 않고 대신 다른 스레드 캐시를 감시하고 거기에서 값을 가져옵니다 (매우 일반적인 설명).

그러나 여러 스레드에서 필드에 액세스하는 경우 AtomicReference로 래핑한다는 Neil의 제안을 두 번째로하겠습니다. AtomicReference이기 때문에 읽기 / 쓰기에 대해 거의 동일한 처리량을 실행하지만 필드가 여러 스레드에 의해 액세스되고 수정된다는 것이 더 분명합니다.

OP의 편집에 응답하도록 편집 :

캐시 일관성은 약간 복잡한 프로토콜이지만 간단히 말해서 CPU는 메인 메모리에 연결된 공통 캐시 라인을 공유합니다. CPU가 메모리를로드하고 다른 CPU가없는 경우 CPU는 가장 최신 값으로 간주합니다. 다른 CPU가 동일한 메모리 위치를로드하려고하면 이미로드 된 CPU가이를 인식하고 실제로 요청 CPU에 대한 캐시 된 참조를 공유합니다. 이제 요청 CPU는 CPU 캐시에 해당 메모리의 복사본을 갖게됩니다. (참조를 위해 메인 메모리를 볼 필요가 없었습니다)

a

꽤 많은 프로토콜이 관련되어 있지만 이것은 무슨 일이 일어나고 있는지에 대한 아이디어를 제공합니다. 또한 여러 프로세서가없는 경우 다른 질문에 답하기 위해 휘발성 읽기 / 쓰기가 실제로 여러 프로세서를 사용할 때보 다 빠를 수 있습니다. 실제로 단일 CPU에서 여러 개로 동시에 더 빠르게 실행되는 일부 응용 프로그램이 있습니다.


답변

Java 메모리 모델 (JSR 133에서 Java 5+에 대해 정의 됨)의 말에 따르면 volatile변수 에 대한 모든 작업 (읽기 또는 쓰기) 은 동일한 변수에 대한 다른 작업과 관련하여 발생 전 관계를 생성 합니다. 이는 컴파일러와 JIT가 스레드 내에서 명령어 순서를 변경하거나 로컬 캐시 내에서만 작업을 수행하는 것과 같은 특정 최적화를 피해야 함을 의미합니다.

일부 최적화를 사용할 수 없기 때문에 결과 코드는 아마도 그다지 많지는 않지만 필연적으로 느려질 것입니다.

그럼에도 불구하고 블록 volatile외부의 여러 스레드에서 액세스된다는 것을 알지 못하는 한 변수를 만들면 안됩니다 synchronized. 심지어 당신은 휘발성이 대 최선의 선택인지 여부를 고려해야한다 synchronized, AtomicReference그 친구, 명시 적 및 Lock등 클래스,


답변

휘발성 변수에 액세스하는 것은 동기화 된 블록에서 일반 변수에 대한 액세스를 래핑하는 것과 여러면에서 유사합니다. 예를 들어, 휘발성 변수에 대한 액세스는 CPU가 액세스 전후에 명령을 재정렬하는 것을 방지하며 일반적으로 실행 속도가 느려집니다 (얼마나 말할 수는 없지만).

보다 일반적으로, 다중 프로세서 시스템에서는 휘발성 변수에 대한 액세스가 페널티없이 수행 될 수있는 방법을 알지 못합니다. 프로세서 A의 쓰기가 프로세서 B의 읽기와 동기화되도록 보장하는 방법이 있어야합니다.


답변


댓글 달기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다