2014-09-13 1 views
2

(저는 컴파일러가 volatile 변수없이 레지스터에 변수를 저장하는 것을 방해하지 않아야하므로 휘발성을 사용하지 않고 실제로 작동하는 근본적인 이유를 찾고 있습니다. ... 또는 거기에 ...)Monitor를 사용하는 경우에도 모든 멤버 변수가 스레드 안전성을 위해 휘발성이 필요한 것은 아닙니까? (모델이 실제로 작동하는 이유는 무엇입니까?)

이 질문은 volatile이 없으면 컴파일러가 이론적으로 CPU 레지스터에 저장하는 것을 포함하여 다양한 변수로 모든 변수를 최적화 할 수 있다는 생각의 불일치에서 유래합니다. 변수 주위의 잠금과 같은 동기화를 사용할 때는 필요하지 않습니다. 하지만 실제로 컴파일러/jit가 코드 경로에서 사용할지 여부를 알 수있는 방법이 없습니다. 그래서 의심은 메모리 모델을 "일"하게 만들기 위해 여기에서 실제로 일어나고있는 다른 것입니다.

이 예제에서는 컴파일러/jit가 _count를 레지스터로 최적화하지 못하게하여 메모리가 아닌 레지스터에서 증분이 완료되는 것을 막습니다 (나중에 종료 호출 후 메모리에 기록). _count가 휘발성이면 모든 것이 정상적으로 보일 수 있지만 많은 코드가 휘발성없이 작성됩니다. 컴파일러가 메서드에서 잠금 또는 동기화 객체를 찾으면 레지스터에 _count를 최적화하지 않을 수 있다는 것을 알 수 있습니다. 그러나이 경우 잠금 호출은 다른 함수에 있습니다.

대부분의 문서에서는 잠금과 같은 동기화 호출을 사용하는 경우 휘발성을 사용할 필요가 없다고 말합니다.

그렇다면 컴파일러가 _count를 레지스터로 최적화하지 못하도록하고 잠겨있는 레지스터를 잠재적으로 업데이트하지 못하게하는 이유는 무엇입니까? 필자는 컴파일러가 최적화해서는 안된다는 말을하지 않는 한, 모든 멤버 변수가 정말로 휘발성 일 필요가 있기 때문에 대부분의 멤버 변수가 레지스터에 최적화되지 않는다는 느낌이 들었습니다. 그렇지 않으면 많은 코드가 실패 할 것이라고 생각합니다. . 비슷한 것을 C++에서 보았을 때 몇 년 전에 로컬 함수 변수가 레지스터에 저장되었지만 클래스 멤버 변수는 저장되지 않았습니다.

주된 질문은 컴파일러/jit이 클래스 멤버 변수를 레지스터에 넣지 않아 휘발성이 없으면 휘발성이 작동하지 않는 유일한 방법일까요?

(호출에 예외 처리 및 안전의 부족을 무시하지만 요점을 보내 주시기 바랍니다.)

public class MyClass 
{ 
    object _o=new object(); 

    int _count=0; 

    public void Increment() 
    { 
    Enter(); 
    // ... many usages of count here... 
    count++; 
    Exit(); 
    } 




//lets pretend these functions are too big to inline and even call other methods 
// that actually make the monitor call (for example a base class that implemented these) 
    private void Enter() { Monitor.Enter(_o); } 
    private void Exit() { Monitor.Exit(_o); } //lets pretend this function is too big to inline 
// ... 
// ... 
} 
+0

20 개의 'MyClass' 객체를 인스턴스화하고 하드웨어에 8 개의 범용 레지스터 만 있으면 어떻게됩니까? – Mephy

+0

[Sayonara volatile] (http://joeduffyblog.com/2010/12/04/sayonara-volatile/) –

+0

이 동작을 요구하는 .NET 메모리 모델에는 내가 아는 것이 하나도 없습니다. 보수적 인 지터 옵티 마이저 디자인 일 뿐이므로 클래스 필드에 대한 쓰기는 항상 외부에서 관찰 할 수 있으며 내부 호출이 변수 상태에 영향을 줄 수 있다고 가정합니다. 이러한 가정을 깨뜨리는 지터는별로 인기가 없을 것입니다. –

답변

0

이 질문에 대한 추측의 대답은 CPU 레지스터에 저장되어있는 변수는 어떤 기능을하기 전에 메모리에 저장되어 호출 될 것으로 나타납니다. 이것은 하나의 스레드에서 컴파일러 디자인 관점이 필요하기 때문에 의미가 있습니다. 그렇지 않으면 다른 함수/메소드/객체에 의해 사용 된 경우 객체가 일관성이없는 것처럼 보일 수 있습니다. 그래서 일부 사람들/기사가 컴파일러에 의해 동기화 객체/클래스가 감지되고 비 휘발성 변수가 호출을 통해 안전하다고 주장하는 경우가 많지 않을 수도 있습니다. (아마도 잠금이 사용되거나 다른 동기화 객체가 같은 메소드에서 사용되지만 일단 다른 객체에서 호출 된 동기화 객체가 없을 경우), 대신 다른 메소드를 호출한다는 사실만으로도 충분할 수 있습니다 CPU 레지스터에 저장된 값을 메모리에 저장합니다. 따라서 모든 변수가 변동될 필요는 없습니다.

또한 의심스럽고 다른 사람들은 스레딩 문제로 인해 클래스 필드가 최적화되지 않았을 것으로 의심합니다.

일부 메모 (내 이해) : Thread.MemoryBarrier()는 CPU 관점에서 쓰기/읽기가 장벽을 우회하지 않도록하는 대부분의 CPU 명령입니다. (이것은 레지스터에 저장된 값과는 직접적으로 관련이 없습니다.) 따라서 이것은 레지스터에서 메모리로 변수를 직접 저장하는 원인이되지는 않을 것입니다. (여기의 논의에 따라 메소드를 호출한다는 것만 제외하면 그럴 가능성이 있습니다 - 아마도 레지스터에서 저장되는 데 사용 된 모든 클래스 필드에 영향을 주지만 어쨌든 메서드 호출 일 수 있습니다.

이론적으로 JIT/컴파일러는 동일한 방법으로 해당 메서드를 계정으로 가져올 수도 있습니다 변수는 CPU 레지스터에서 저장됩니다. 그러나 다른 메서드 나 클래스에 대한 호출에 대한 간단한 규칙을 따르면 레지스터에 저장된 변수를 메모리에 저장하게됩니다. 누군가 다른 메소드 (어쩌면 많은 메소드가 있음)에서 해당 호출을 랩핑했다면, 컴파일러는 실행시 추측 할 수있을 정도로 깊은 분석을하지 않을 것입니다. JIT는 무언가를 할 수는 있지만 다시는 그 깊이를 분석하지 않을 것이고, 두 경우 모두 잠금/동기화 작업이 무엇이든간에 보장해야하므로 가장 간단한 최적화가 가능성있는 대답입니다.

컴파일러가이 모든 것을 추측 할 수있는 사람이 아니라면 휘발성이 필요없는 이유를 가장 잘 추측 할 수있는 사람이없는 한.

이 규칙을 따르면 동기화 객체는 메모리 바리 어를 호출 할 때 메모리 캐시를 호출하여 쓰기 캐시에서 최신 값을 가져 와서 플러시되어 올바른 값을 읽을 수 있도록합니다 . 이 사이트에 당신은이 제안 암시 적 메모리 장벽입니다 무엇을 볼 수 있습니다 : http://www.albahari.com/threading/part4.aspx

5

의 입력 및 Monitor을 떠나는 것은 전체 메모리 울타리가 발생합니다. 따라서 CLR은 Monitor.Enter/Monitor.Exit 이전의 모든 쓰기 작업이 다른 모든 스레드에서 볼 수있게하고 메서드 호출 이후의 모든 읽기 작업이 그 후에 "발생"하는지 확인합니다. 즉, 호출 이전의 명령문은 호출 후에는 이동할 수 없으며 그 반대도 마찬가지입니다.

http://www.albahari.com/threading/part4.aspx을 참조하십시오.

+0

CPU 레지스터에 변수를 등록하는 데 영향을 미칠 것이라고 나는 생각하지 않습니다. 펜스는 CPU가 펜스 이후에로드 또는 저장소를 다시 정렬하지 않도록하기위한 것입니다. CPU 주문에만 영향을주는 CPU 효과입니다. 컴파일러/JIT는 호출 경로가 장래에 펜스 (fence)를 필요로하고 매우 어려운 것처럼 보이는 레지스터에 변수를 저장하지 않을 것이라는 것을 알기에 충분히 똑똑 할 필요가 있습니다. 따라서 컴파일러/JIT를 끌어낼 수 없을 것입니다. 그래서 그 대답은 여기에 있다고 생각하지 않습니다. – user2685937

+0

JIT가 프로그램을 처음 실행했기 때문에 JIT가 충분히 능숙하다고 가정해도 프로그램이 처음 실행되었을 때 알 수있는 적절한 함수가 호출되었습니다. 처음에는 그렇게하지 않으면 프로그램이 두 번째 또는 그 다음 시간에 작동하지 않을 것입니다. 따라서 다시는 매우 희박한 구현이므로 일어나는 일은 없을 것입니다. – user2685937

+0

@ user2685937 메모리 울타리에 의해 CPU의 캐시가 플러시됩니다. 어떻게 프로세서 아키텍처에서 정확히 구현되는지 모르겠습니다. 프로세서가 레지스터의 값을 다른 모든 코어에 전달하는 동안 기다리는 것처럼 쓰기 펜스를 상상할 수 있습니다. – ominug

0

그래서 레지스터 에 _count을 최적화하고 잠재적으로 잠금 내에서 단지 레지스터를 업데이트에서 컴파일러를 방지 무엇?

문서에서 내가 그런 일이 일어나지 않도록 배제하고있는 것은 아무것도 없습니다. 요점은 Monitor.Exit에 대한 호출이 완료시 _count의 최종 값이 메모리에 커밋됨을 효과적으로 보장한다는 것입니다.

그것은 컴파일러는 방법 .. 에서 잠금 또는 동기화 개체를보고 있지만,이 경우에는 잠금 호출이 다른 함수에있는 경우 레지스터에 _count을 최적화 할 수없는 알 수 의미가 있습니다.

다른 방법으로 잠금을 획득하고 해제한다는 사실은 사용자의 관점에서 관련이 없습니다. 모델 메모리는 메모리 장벽 생성기와 관련하여 준수해야하는 매우 엄격한 규칙 집합을 정의합니다. 다른 방법으로 이러한 Monitor 호출을 넣는 유일한 결과는 JIT 컴파일러가 이러한 규칙을 준수하는 데 더 힘든 시간을 갖게된다는 것입니다. 그러나 JIT 컴파일러는 반드시 준수해야합니다. 기간. 메서드 호출이 복잡하거나 너무 깊게 호출되면 JIT 컴파일러는 이러한 점에서 경험적으로 생각할 수있는 모든 휴리스틱에 대해 의심을합니다. "잊어 버리면, 난 아무 것도 최적화하지 않을거야!"

그래서 주요 질문은, 정말이 가능 컴파일러/JIT는 레지스터에 클래스 멤버 변수를 넣어하지 않습니다 휘발성없이 작동하는 유일한 방법이며, 따라서 휘발성 다음 필요가?

프로토콜이 _count을 읽기 전에 잠금을 획득하기 때문에 작동합니다. 독자가 그렇게하지 않으면 모든 배팅이 해제됩니다.

관련 문제