휘발성은 비쌉니까?


111

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

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

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

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


1
여러 CPU가 참조하는 구성에 대한 내 편집을 읽을 수 있습니다. 단기 참조를 위해 다중 CPU 시스템에서 더 이상 주 메모리에 대한 단일 읽기 / 쓰기가 발생하지 않을 수 있습니다.
John Vint 2011 년

2
휘발성 읽기 자체는 비싸지 않습니다. 주요 비용은 최적화를 방지하는 방법입니다. 실제로 휘발성이 엄격한 루프에서 사용되지 않는 한 평균 비용도 그리 높지 않습니다.
평판이 좋지 않습니다.

2
infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) 에 대한이 기사 도 관심을 가질 수 있으며, 다양한 아키텍처에 대해 생성 된 코드에 대한 휘발성 및 동기화의 영향을 보여줍니다. 이것은 또한 jvm이 단일 프로세서 시스템에서 실행 중인지 여부를 알고 일부 메모리 장벽을 생략 할 수 있기 때문에 사전 컴파일러보다 더 나은 성능을 발휘할 수있는 경우입니다.
Jörn Horstmann

답변:


123

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 간에도 캐시 일관성 프로토콜이 다릅니다.


참고로 Java 6은 어셈블리를 표시하는 것과 동일한 기능을 가지고 있습니다 (이를 수행하는 핫스팟)
bestsss

+1에서 JDK5 휘발성 캔에 대해 재 배열 될 임의 (예를 들어, 이중 체크 잠금 고정) 읽기 / 쓰기. 그것은 비 휘발성 필드가 조작되는 방식에도 영향을 미친다는 것을 의미합니까? 휘발성 및 비 휘발성 필드에 대한 액세스를 혼합하는 것은 흥미로울 것입니다.
ewernli

@evemli, 조심해야합니다.이 진술을 한 번 직접 만들었지 만 잘못된 것으로 판명되었습니다. 엣지 케이스가 있습니다. Java 메모리 모델은 휘발성 상점보다 먼저 상점을 재정렬 할 수있는 경우 로치 모텔 의미를 허용합니다. IBM 사이트의 Brian Goetz 기사에서 이것을 선택했다면이 기사가 JMM 사양을 단순화한다는 점을 언급 할 가치가 있습니다.
Michael Barker 2012 년

20

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

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

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

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

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

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

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

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


5
AtomicReference는 getAndSet, compareAndSet 등과 같은 추가 기능을 제공하는 네이티브 함수가 추가 된 휘발성 필드에 대한 래퍼 일 뿐이므로 성능 관점에서 사용하면 추가 된 기능이 필요한 경우에만 유용합니다. 하지만 여기서 OS를 언급하는 이유가 궁금합니다. 이 기능은 CPU opcode에서 직접 구현됩니다. 그리고 이것은 하나의 CPU가 다른 CPU의 캐시 내용에 대해 알지 못하는 다중 프로세서 시스템에서 CPU가 항상 주 메모리에 도달해야하기 때문에 휘발성이 더 느리다는 것을 의미합니까?
Daniel

당신이 맞습니다. OS가 CPU를 작성 했어야했는데, 지금 고쳐야한다는 얘기를 놓쳤습니다. 그리고 예, AtomicReference가 단순히 휘발성 필드에 대한 래퍼라는 것을 알고 있지만 필드 자체가 여러 스레드에 의해 액세스된다는 일종의 문서로 추가됩니다.
John Vint 2011 년

@John, AtomicReference를 통해 다른 간접 참조를 추가하는 이유는 무엇입니까? CAS가 필요한 경우-그래도 AtomicUpdater가 더 나은 옵션이 될 수 있습니다. 내가 기억하는 한 AtomicReference에 대한 내장 함수는 없습니다.
bestsss 2011 년

@bestsss 모든 일반적인 목적을 위해 AtomicReference.set / get과 휘발성로드 및 저장소간에 차이가 없습니다. 그것은 내가 언제 어떤 것을 사용 해야하는지에 대해 동일한 느낌을 가지고 있다고 말하고 있습니다. 이 응답은 좀 더 자세히 설명 할 수 있습니다 . stackoverflow.com/questions/3964317/… 둘 중 하나를 사용하는 것이 더 선호됩니다. 단순한 휘발성 대신 AtomicReference를 사용하는 유일한 주장은 명확한 문서화를위한 것입니다. 그 자체가 내가 이해하는 가장 큰 주장은 아닙니다
John Vint

참고로 일부는 휘발성 필드 / AtomicReference (CAS가 필요 없음)를 사용하면 버그가있는 코드가 발생 한다고 주장
John Vint

12

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

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

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


4

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

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


4
휘발성 변수를 읽는 것은 명령의 재정렬 가능성과 관련하여 모니터 입력을 수행하는 것과 동일한 패널티를 가지며 휘발성 변수를 작성하는 것은 모니터 종료와 같습니다. 차이점은 어떤 변수 (예 : 프로세서 캐시)가 플러시되거나 무효화되는지 일 수 있습니다. 동기화가 모든 것을 플러시하거나 무효화하는 동안 휘발성 변수에 대한 액세스는 항상 캐시를 무시해야합니다.
다니엘

12
-1, 휘발성 변수에 액세스하는 것은 동기화 된 블록을 사용하는 것과 상당히 다릅니다. 동기화 된 블록에 들어가려면 잠금을 해제하기위한 원자 compareAndSet 기반 쓰기와 잠금 해제를위한 휘발성 쓰기가 필요합니다. 잠금이 만족 스러우면 잠금을 조정하기 위해 제어가 사용자 공간에서 커널 공간으로 전달되어야합니다 (이는 값 비싼 비트입니다). 휘발성에 액세스하는 것은 항상 사용자 공간에 남아 있습니다.
Michael Barker 2011 년

@MichaelBarker : 모든 모니터가 앱이 아닌 커널에 의해 보호되어야한다고 확신합니까?
Daniel

@Daniel : 동기화 된 블록 또는 잠금을 사용하는 모니터를 나타내는 경우 예,하지만 모니터가 만족할 경우에만 가능합니다. 커널 중재없이이를 수행하는 유일한 방법은 동일한 논리를 사용하지만 스레드를 파킹하는 대신 바쁜 스핀을 사용하는 것입니다.
Michael Barker 2011 년

@MichaelBarker : 좋아요, 만족스러운 잠금에 대해 이해합니다.
Daniel
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.