Android Document  SDK old PDF 파일
Android SMP(2/7) - 이론:데이터 메모리 장벽
작성자
작성일 2011-05-02 (월) 22:19
ㆍ추천: 0  ㆍ조회: 14632      
IP: 121.xxx.76

 
 
목차
 
1. 도입
2. 이론
   2.1. 메모리 일관성 모델(Memory Consistency Models)
   2.2. 데이터 메모리 장벽(Data Memory Barriers)
   2.3. 원자적 오퍼레이션들(Atomic Operations)
3. 사례
   3.1. C에서 하지 말 것
   3.2. Java에서 하지 말 것.
   3.3. 해야 할 것
4. 부록
 
 

2.2 데이터 메모리 장벽(Data Memory Barriers)

 
메모리 장벽은 여러분의 코드가 CPU에게 메모리 접근 순서 문제를 지시하는 방법을 제공한다. ARM/x86
단일 프로세서는 순차적 일관성을 제공한다. 따라서 이것들은 필요가 없다. (장벽 명령어를 실행할 수는
있으나 유용하지 않다; 최소한 그것이 너무 비싸서 SMP 타겟용으로 별도로 빌드할 동기가 있는 경우에만
필요하다.)

 
고려할 4가지 기본적 상황이 있다.

  1. 다른 store를 수반하는 store
  2. 다른 load를 수반하는 load
  3. store를 수반하는 load
  4. load를 수반하는 store 
 
2.2.1 store/store load/load
 
 
우리들의 이전 예제를 상기하라:

쓰레드 1

쓰레드 2

A = 41

B = 1 // “A is ready”

loop_until (B == 1)

reg = A

 
쓰레드 1 은 B에 store(B=1) 이전에 A에 store(A=41)가 일어나도록 하는 것이 필요하다. 이것이 “store/store”
상황이다. 비슷하게 쓰레드 2 는 A에 load(reg=A) 이전에 B에 load(B==1)가 일어나도록 하는 것이 필요하다.
이것이 "load/load" 상황이다. 앞에서 언급했듯이, load 와 store는 임의의 순서로 관측될 수 있다.

캐쉬 논의로 다시 돌아가서, A와 B가 최소한의 캐쉬 일관성을 사용하는 분리된 캐쉬 라인에 있다고
가정하자. 만약 A에 store(A=41)가 로컬에 남아있는데 B에 store(B=1)가 배포된다면, core 2는 B=1은
보겠지만 A에 업데이트는 보지 못한다. 다른 측면에서, 우리가 이전에 A를 read 했거나 또는 그것이
마치 최근에 read 했던 다른 어떤 것들처럼 동일한 캐쉬 라인에 있다고 가정하자. Core 2는 B에
update가 보일 때까지 돌 것이며, 그런 다음에 로컬 캐쉬에서 A를 load 하게 되면, 그 값은 여전히 0 이다.
우리는 이것을 다음과 같이 바로 잡을 수 있다.

쓰레드 1

쓰레드 2

A = 41

store/store barrier

B = 1 // “A is ready”

loop_until (B == 1)

load/load barrier

reg = A

 
store/store 장벽은 모든 관측자들이 B에 write 전에 A에 write를 관측할 것을 보장한다. 그것은 쓰레드 1
에서의 load의 순서를 보장하지는 않는다. 하지만, 우리는 어떤 load도 하지 않는다 그러므로 문제는 없다.
쓰레드 2 에서의 load/load 장벽은 그곳에서 load에 대해 동일한 보장을 한다.
 
store/store 장벽이 쓰레드 2 가 프로그램 순서대로 store를 관측하는 것을 보장하는데, 쓰레드 2 에서
왜 load/load 장벽이 필요한가? 왜냐하면 우리는 또한 쓰레드 1 이 프로그램 순서대로 load를 관측하게끔
보장할 필요가 있기 때문이다.

store/store 장벽은 다른 코어가 이후의 store를 보기 전에 그것을 보도록, 로컬 캐쉬 밖으로 모든
dirty entries를 flush하는 작업이 될 수 있다. load/load 장벽은 이후의 load가 이전의 load 이후에
관측되도록, 로컬 캐쉬를 완전히 제거하고 나서 진행중인 load가 끝나는 것을 기다리는 것일 수 있다.
적절한 보장들이 유지되는 한, CPU가 실제로 하는 것은 아무것도 없다. 만약 우리가 코어 1에서
장벽을 사용하고 core 2에서 그렇지 않다면, core 2는 여전히 그것의 로컬 캐쉬에서 A를 읽을 수 있다.
 
아키텍처들이 서로 다른 메모리 모델을 가지기 때문에, 이러한 장벽들은 ARM SMP상에서는 필요하지만,
x86 SMP상에는 필요치 않다.


2.2.2 load/store store/load

 
이전에 본 Dekker의 알고리즘 코드 조각은 store/load 장벽의 필요성을 묘사한다. 여기에는 load/store
장벽이 요구되는 예제가 있다.


쓰레드 1

쓰레드 2

reg = A

B = 1 // “A 잠금

loop_until (B == 1)

A = 41 // 업데이트 A

 
쓰레드 2 쓰레드 1 의 A에서 load(reg=A)를 관측하기 전에 쓰레드 1 의 B=1 store를 관측할 수 있다.
그리고 그 결과 쓰레드 1 이 A를 read할 기회를 갖기 전에 A=41을 store한다. 각각의 쓰레드에 load/store
장벽을 삽입하는 것은 이 문제를 해결한다.

쓰레드 1

쓰레드 2

reg = A

load/store barrier

B = 1 // “A 잠금

loop_until (B == 1)

load/store barrier

A = 41 // 업데이트 A


로컬 캐쉬에 store(B=1)는 메인 메모리에서 load(reg=A) 이전에 관측될 수 있다. 왜냐하면 메인
메모리에 대한 접근이 너무 느리기 때문이다. 이 경우에, core 1의 캐쉬가 B에 대한 캐쉬라인을
가지고 있지만 A는 없다고 가정하자. A에서 load가 시작되고 그 동안 실행은 계속 진행된다.
B에 store는 로컬 캐쉬에서 일어나고 어떻게든 A에서 load가 여전히 진행되는 동안 코어 2에서
사용할 수 있게 된다. 쓰레드 2 쓰레드 1 의 A에서 load가 관측되기 전에 loop를 빠져나갈 수 있다.
 
더 난해한 질문은 쓰레드 2 에 장벽이 필요한가 이다. 만약 CPU가 자기 멋대로 write를 수행하지
않고, 그래서 순서와 상관없이 명령어를 실행하지 않는다는 조건에서, 만약 쓰레드 1 이 load/store
순서를 보장한다면 쓰레드 2쓰레드 1 의 read(reg=A) 전에 A에 store(A=41) 할 수 있는가?
(답은 없다임.) 만약 세 번째 core가 A와 B를 지켜보고 있다면 무엇인가? (답은 이제 필요하다이며,
그것이 없다면 여러분은 세 번째 core에서 B==0/A==41을 관측할 수 있다.)
두 위치 모두에 장벽을
삽입하는 것이 가장 안전하다. 그리고 세부적으로는 걱정하지 마라.
 
앞에서 언급했듯이, store/load 장벽은 x86 SMP 상에서만 요구되는 종류이다.

 
2.2.3 장벽 명령어(Barrier Instructions)

 
서로 다른 CPU들은 다른 형태의 장벽 명령어를 제공한다. 예를 들어 다음과 같다.


  • Sparc V8은 4개의 엘리먼트 비트 벡터를 가지는 “membar” 명령어를 가진다. 
    4가지 종류의 장벽을 각각 따로 지정할 수 있다.
  • Alpha는 “rmb(load/load)”, “wmb(store/store)”, 그리고 “mb(full)”을 제공한다. 
    (상식: 리눅스 커널은 이러한 이름과 동작을 갖는 3개의 메모리 장벽을 제공한다.)
  • x86은 다양한 옵션을 갖는다. “mfence”(SSE2에서 도입됨)는 전체 장벽full barrier를 제공한다.
  • ARMv7은 “dmb st”(store/store)와 “dmb sy”(full)을 갖는다. 
전체 장벽Full Barrier은 4개의 카테고리 모두를 포함하는 것을 의미한다.
 
장벽 명령어에 의해 보장되는 것은 오직 순서라는 것을 인지하는 것이 중요하다. 이것을 캐쉬 일관성
“sync points” 또는 비동기적 “flush” 명령으로 취급하지 마라. ARM “dmb” 명령은 다른 코어들에
직접적인 어떤 영향도 끼치지 않는다. 어느 곳에서 장벽 명령어를 제기할 필요가 있는지를 파악해야
할 때, 이것을 이해하는 것은 중요하다.

 

2.2.4 주소 의존성과 인과적 일관성(Address Dependencies and Causal Consistency)

  
(이것은 조금 더 고급 주제이며 건너뛰어도 된다.)
 
ARM CPU는 load/load 장벽을 사용하지 않아도 되는 특별한 경우를 제공한다. 이전보다 조금 수정된
다음의 예제를 생각해 보라. 

쓰레드 1

쓰레드 2

[A+8] = 41

store/store barrier

B = 1 // “A 준비됨

loop:

reg0 = B

if (reg0 == 0) goto loop

reg1 = 8

reg2 = [A + reg1]

 
이 예제에는 새로운 표기법을 도입되었다. 만약 “A”가 메모리 주소를 가리킨다면, “A+n”은 A로부터의
n 바이트 오프셋 메모리 주소를 가리킨다. 만약 A가 오브젝트 또는 배열의 베이스 주소라면, [A+8]은
오브젝트의 필드나 배열의 엘리먼트가 될 것이다.

이전 예제에서 봤던 “loop_until”은 B의 load가 reg0에서 보이도록(이전 예제에서는 B==1이었음) 확장되었다.
reg1에는 숫자 값 8 이 설정되었다. 그리고 reg2는 주소 [A+reg1] (쓰레드 1이 접근하는 동일한 위치) 에서
load 되었다.
 
이것은 B에서 load(reg0=B)가 [A+reg1]에서 load(reg2=[A+reg1]) 이후에 관측될 수 있기 때문에 올바르게
동작하기 않을 것이다. 우리는 loop 뒤에 load/load 장벽을 사용함으로써 이것을 해결할 수 있지만,
ARM 상에서는 그냥 이렇게 할 수도 있다.
 

쓰레드 1

쓰레드 2

[A+8] = 41

store/store barrier

B = 1 // “A 준비됨

loop:

reg0 = B

if (reg0 == 0) goto loop

reg1 = 8 + (reg0 & 0)

reg2 = [A + reg1]


우리는 여기에서 reg1에 대한 설정을 상수(8)에서 B에서 load 된 것에 의존하는 값으로 바꾸었다. 이
경우에 있어서, 우리는 0 으로 B에서 load된 값에 bitwise AND를 한다. 그 결과는 0 이다. 그러므로 
reg1은 여전히 값 8을 갖게 된다. 하지만, ARM CPU는 [A+reg1]에서 load(reg2=[A+reg1])가 B에서
load(reg0=B)에 의존한다고 믿으며, 그래서 프로그램 순서대로 두 개가 관측되도록 할 것이다.
 
이것은 주소 의존성address dependency이라 일컬어 진다. 주소 의존성은 load에 의해 리턴된 값이 
수반되는 load 또는 store의 주소를 계산하기 위해 사용되는 될 때 존재한다. 이것은 특정 상황에서
명시적 장벽에 대한 필요를 여러분이 없앨 수 있게 할 수 있다.
 
ARM은 제어 의존성control dependency과 관련된 보장들을 제공하지 않는다. 이것을 실증하기 위해
잠시 ARM 코드로 들어갈 필요가 있다.

LDR r1, [r0]

CMP r1, #55

LDRNE r2, [r3]  


r0 와 r3에서 load는, 심지어는 [r0]가 55를 보유하지 않는다면 r3에서 load가 전혀 수행되지 않음에도
불구하고 순서가 바뀌어 관측될 수 있다. 아래와 같이 AND r1, r1, #0을 삽입하고 마지막 명령을
LADRNE r2,[r3,r1]으로 대체하는 것은 명시적 장벽 없이 제대로 된 순서를 가능하게 할 것이다.
(이것이 여러분이 명령어 실행에 관한 일관성 이슈들을 생각할 수 없는 이유에 대한 기본적인 예제이다.
항상 메모리 접근에 관해서 생각하라.)


LDR r1, [r0]

CMP r1, #55

AND r1, r1, #0

LDRNE r2, [r3,r1]    

 
우리가 깊게 다루고는 있지만, ARM이 인과적 일관성Causal Consistency을 제공하지 않는다는 것을 
언급할 가치가 없다.

쓰레드 1

쓰레드 2

쓰레드 3

A = 1

loop_until (A == 1)

B = 1

loop:

reg0 = B

if (reg0 == 0) goto loop

reg1 = reg0 & 0

reg2 = [A+reg1]

 
여기에서 쓰레드 1 은 A를 설정(A=1)하고, 쓰레드 2 에 신호를 보낸다. 쓰레드 2 는 그것을 보고 B를
설정(B=1)하고 쓰레드 3 에 신호를 보낸다. 쓰레드 3 은 그것을 보고 B의 load(reg0=B)와 A의 load가
프로그램 순서대로 관측되도록 하기 위해 주소 의존성을 사용해서 A를 load(reg2=[A+reg1])한다.
 
이것이 끝났을 때, reg2가 0 을 보유하는 것이 가능하다. 쓰레드 1 에 store가 쓰레드 2 에서 발생할
어떤 것을 초래하고, 그것이 쓰레드 3 에서 발생할 어떤 것을 초래한다는 사실이 쓰레드 3 이 그 순서대로
store를 관측할 것이라는 것을 의미하지는 않는다. (쓰레드 2에 load/store 장벽 추가는 이것을 해결한다.)
 

2.2.5 메모리 장벽 요약(Memory Barrier Summary)

 
장벽은 상황에 따라 다른 형태가 된다. 올바른 장벽 유형을 정확하게 사용하는 것이 성능 장점이 될 수
있기 때문에, 이러한 것을 함에 있어서 코드 관리 리스크가 있다 - 개인이 그것을 완전하게 이해하고
코드를 업데이트 하지 않는 한, 이것들은 잘못된 유형의 오퍼레이션을 만들고 그래서 알 수 없는
훼손을 초래할 수 있다. 이것 때문에, 그리고 ARM이 장벽을 선택함에 있어서 넓은 다양성을 제공하지
않기 때문에, Android User-space Atomic Primitive(안드로이드에서 만들어서 사용하는 원자적
함수원형들)는 장벽이 필요할 때 전체 장벽full barrier을 사용한다..
 
기억해야 할 핵심은 장벽이 순서를 정의한다는 것이다. 이것을 발생하게 될 일련의 액션들을 초래하는
“flush” 호출로 생각하지 마라. 대신에, 오퍼레이션들을 현재 CPU 코어 상에 시간에 맞춰 나누는 것으로
생각하라.

계속 :




 
 
덧글 쓰기 0
3500
※ 회원등급 레벨 0 이상 읽기가 가능한 게시판입니다.