2017-10-23 3 views
1

그의 놀라운 책 C#을 간단히 요약하면 (무료 챕터는 온라인으로 제공됩니다), 메모리 장벽을 통해 "새로 고침"가치를 얻는 방법에 대해 이야기합니다. 그의 예는 다음과 같습니다.MemoryBarrier는 실제로 새로 고침 값을 보장합니까?

static void Main() 
    { 
     bool complete = false; 
     var t = new Thread(() => 
     { 
      bool toggle = false; 
      while (!complete) 
      { 
       toggle = !toggle; 
      } 
     }); 
     t.Start(); 
     Thread.Sleep(1000); 
     complete = true; 
     t.Join();  // Blocks indefinitely 
    } 

릴리스 모드로 빌드하면 제안 된대로 무기한 차단됩니다. 그는이 블록을 해결할 수있는 몇 가지 솔루션을 제공합니다. while 루프에서는 Thread.MemoryBarrier를 사용하고, lock을 사용하거나 "complete"volatile static 필드를 사용하십시오.

휘발성 필드 솔루션에 동의하는 이유는 휘발성 메모리가 JIT 용 레지스터 읽기가 아닌 직접 메모리 읽기를 시행하기 때문입니다. 그러나 나는이 최적화가 펜스와 메모리 장벽과 아무 관련이 없다고 생각한다. JIT 최적화가 메모리 나 레지스터에서 읽는 것을 선호하는 것과 마찬가지로 JIT 최적화의 문제입니다. 실제로 대신 메모리 배리어를 사용하는 모든 메소드 호출에서와 같이 모든 레지스터를 사용하지 JIT "확신"

class Program 
    { 
     [MethodImpl(MethodImplOptions.NoInlining)] 
     public static bool Toggle(bool toggle) 
     { 
      return !toggle; 
     } 
     static void Main() 
     { 
      bool complete = false; 
      var t = new Thread(() => 
      { 
       bool toggle = false; 
       while (!complete) 
       { 
        toggle = Toggle(toggle); 
       } 
      }); 
      t.Start(); 
      Thread.Sleep(1000); 
      complete = true; 
      t.Join();  // Blocks indefinitely 
     } 
    } 
여기

내가 더미 토글 호출을하고있다. 생성 된 어셈블리 코드에서 JIT가 "완전한"지역 변수를 읽는 직접 메모리 액세스를 사용하고 있음을 분명히 알 수 있습니다. 따라서 필자의 가정은 인텔 CPU에서 적어도 컴파일러 최적화를 고려하면 MemoryBarrier는 "새로움"측면에서 아무런 역할을하지 않습니다. MemoryBarrier가 주문한 울타리를 주문합니다. 나는 그렇게 생각하는 것이 맞습니까? 나는 휘발성과 휘발성 필드 솔루션 동의

+0

* 두 가지 * 요구 사항이 있습니다. 첫 번째는 변수를 레지스터에 저장하지 못하게하는 지터를 설득하는 것입니다. 꽤 쉽게 포기하고, 메서드 호출로 충분할 수 있습니다. 선언 * volatile *은 x86에서 효과적인 해커입니다. 또는 잠금 또는 연동과 같이 동기화 된 민감한 코드 조각으로부터의 자연스러운 방법. Interlocked.Equals (toggle, false)를 삽입하는 것만으로도 충분합니다. 다른 하나는 장벽에서 가져온 메모리 액세스 순서입니다. Volalite.Read/Write가 가장 효과적입니다. 지터가 휘발성 변수를 처리하도록 강요하므로 충분합니다. –

+1

저는 여러분에게 동의합니다 : 이것은 메모리 장벽과는 아무런 관련이 없습니다. –

+0

@HansPassant,'volatile'은'Volatile.Read' /'Volatile.Write'와 마찬가지로 좋은 접근법 중 하나입니다. 그러나 그것을 잊어 버릴 수는 없습니다. 또한,'Interlocked.Equals'는 단지 정적 인'Object.Equals' 일뿐입니다. 동기화와 관련해서는 충분하지 않습니다. – acelent

답변

1

는 JIT에 대한 읽기 레지스터보다는 읽기 직접 메모리를 적용합니다. 그러나 나는이 최적화가 펜스와 메모리 장벽과 아무 관련이 없다고 생각한다.

휘발성 읽기 및 쓰기가 ECMA-335 I.12.6.7 설명된다. 이 섹션의 중요한 부분은 다음과 같습니다.

휘발성 읽기에는 CIL 명령어 시퀀스의 읽기 명령어 다음에 발생하는 메모리 참조보다 먼저 읽음이 발생한다는 보장이 있습니다. 휘발성 쓰기는 "릴리스 의미"를 가지며, 이는 CIL 명령어 시퀀스의 쓰기 명령어 이전에 메모리 참조 이후에 쓰기가 보장된다는 의미입니다.

CLI에의 순응 구현

휘발성 동작의 의미를 보장한다.

휘발성 제거 작업을하지 않는다 네이티브 코드 에 CIL 변환 않으며는 한 번의 작업으로 여러 휘발성 작업을 병합해야 좋은 최적화 컴파일러.

획득 및 메모리 장벽을 필요로하지 않습니다 x86 및 x86-64의 아키텍처에 대한 의미를 해제 (하드웨어 메모리 모델은 휘발성 의미에서 요구하는 것보다하지 약한 때문에). 그러나 ARM 아키텍처의 경우 JIT는 하프 펜스 (한 방향 메모리 장벽)를 방출해야합니다.

휘발성이있는 예제에서는 최적화 제한으로 인해 모든 것이 작동합니다. 그리고 MemoryBarrier를 사용하면 컴파일러가이 변수가 MemoryBarrier를 통과 할 수 없기 때문에 루프 외부에서 단일 변수의 읽기를 최적화 할 수 없기 때문에 작동합니다.

그러나 코드

while (!complete) 
{ 
    toggle = Toggle(toggle); 
} 

은 이런 식으로 최적화 할 수 있습니다 :

var tmp = complete; 
while (!tmp) 
{ 
    toggle = Toggle(toggle); 
} 

이 메서드 호출의 경우에는 발생하지 않는 이유는 그 몇 가지 이유 최적화가 적용되지 않았습니다 (그러나 적용될 수 있음). 따라서 표준에 의존하지 않고 변경 될 수있는 구현 세부 사항에 의존하기 때문에이 코드는 취약하고 구현에 따라 다릅니다.

+0

좋은 답변입니다! MemoryBarrier가 어떻게 JIT의 동작을 변경시키는 지에 대한 문서가 있었으면 좋겠습니다. 그렇게 말하면, 당신이 말하는 것은 논리적입니다. 값을 레지스터에 저장하면 메모리 경계를 넘어선 읽기 값이 유지되어 처음에는 장벽의 목적을 무효화합니다. –

+1

jit이 어떻게 동작하는지에 대한 @OnurGumus 세부 사항은 구현 세부 사항이지만 RyuJIT의 소스 코드 또는 github의 설명서를 읽을 수 있습니다. 어쨌든 당신이 의지 할 수있는 것과 가능한 일을 설명하는 ECMA-335 규격이 있습니다. –

+0

또한 C# 컴파일러가'while (! complete)'문을 최적화하지 않더라도 JIT 컴파일러는 여전히 그러한 최적화를 수행 할 수 있으며, 그렇지 않은 경우 약한 메모리 모델 하드웨어 아키텍처가 여전히 동일한 값을 읽을 수 있습니다. 동기화가 부족할 때마다 '완료'됩니다. – acelent

관련 문제