CPU 설계, 그 첫 번째 이야기 | 데이터 의존성  By junukwon7

Language :

이 글은 언어로 작성되어 있습니다.
사용하실 언어를 선택하십시오.

This post is written in Language.
Select the language you want to use.

この文は言語で作成されています。
使用する言語を選択してください。


2명의 계산원, 철수와 영희가 있다고 하자. 한 작업은 한 명이 수행하며, 한 계산원은 두 작업을 동시에 수행할 수 없다. 철수와 영희는 주어진 계산을 최대한 빠르게 수행하고자 한다.


여기 순서대로 3개의 작업이 들어왔다.

  1. 10*20을 계산하시오 (1초 소요)
  2. 797*3874를 계산하시오. (10초 소요)
  3. 1+2를 계산하시오. (2초 소요)

 

가장 빠르게 해결하려면 어떻게 해야 할까? 1-철수, 2-영희, 3-철수 순서로 작업하면 된다. 철수가 1번, 3번 작업을 미리 하는 사이 영희가 2번 작업을 수행하면 되기 때문. 이 경우 걸리는 시간은 10초다. 1, 2, 3번 작업의 순서가 상관 없기 때문이다.

 

 

다른 작업이 들어왔다.

  1. 10*20을 계산하시오 (1초 소요)
  2. 797*3874를 계산하시오. (10초 소요)
  3. 1.의 답과 2.의 답의 합을 구하시오. (2초 소요)

 

이 경우 걸리는 시간은 최소 12초다. 3번 작업을 하려면 1, 2번 작업이 모두 완료되어야 하기 때문이다. 설령 철수가 1번 작업을 1초만에 끝냈더라도, 3번 작업에 2번의 결과를 요구하므로 9초를 더 기다려야 하는 것이다.

 

데이터 의존성

데이터 의존성은 여러 작업 간에 선행관계를 나타낸다. 데이터 의존성은 크게 flow dependency, name dependency, control dependency로 구분할 수 있다.

조금 더 구체적으로, 데이터 의존성은 아래와 같이 나타낼 수 있다. 컴퓨터 과학자 A. J. Bernstein의 이름을 따서 '번스타인 조건'(Bernstein Condition)이라고 한다.

$$[I(S_1) \cap O(S_2​)] \cup [O(S_1) \cap I(S_2)] \cup [O(S_1) \cap O(S_2)] \neq \varnothing$$

  • $I(S_n)$은 구문 $S_n$이 읽는 메모리의 집합이며
  • $O(S_n)$은 구문 $S_n$이 쓰는 메모리의 집합이다.

 

 

이때, $S_1$이 실행되고 어느 정도의 시간이 흐른 뒤 $S_2$가 실행된다. 즉, 이전 연산이 완벽하게 실행된 후 다음 연산이 실행된다.

 

따라서, Bernstein 조건이 성립하지 않는 경우는 세 유형으로 분류할 수 있다.

  • Anti Dependence(Write-After-Read): $[I(S_1) \cap O(S_2​)] \neq \varnothing$, $S_2$에서 특정 값을 쓰기 전 $S_1$에서 해당 값을 읽는 경우
  • Flow Dependence(Read-After-Write): $[O(S_1) \cap I(S_2​)] \neq \varnothing$, $S_2$에서 특정 값을 읽기 전 $S_1$에서 해당 값을 쓰는 경우
  • Output Dependence(Write-After-Write): $[O(S_1) \cap O(S_2​)] \neq \varnothing$, $S_2$에서 특정 값을 쓰기 전 $S_1$에서 해당 값을 쓰는 경우

 

  • Input Dependence(Read-After-Read): $[I(S_1) \cap I(S_2​)] \neq \varnothing$, 일반적으로 종속관계가 없다.

더 자세한 예시를 들어 보자.

 

Input Dependency

읽기 이후에 다시 읽기 연산이 일어나는 경우다.

1. A = C + 1
2. B = C + 2


위 두 구문의 순서는 일반적인 경우 상관이 없다. 그러나 Memory-Mapped I/O와 같이 읽기 연산이 그 값을 바꾸거나 다음 값을 가져오는 경우 순서 의존성이 있다.

 

Flow Dependency

쓰기 이후에 읽기 연산이 일어나는 경우다.

1. A = 1
2. B = A
3. C = B

구문 2는 구문 1에, 구문 3은 구문 2에 의존적이다. 이 경우처럼 제거할 수 없는 의존성을 True Dependency라고 부르며 Instruction-Level 병렬 연산을 불가능하다.

 

Anti Dependency

읽기 이후에 쓰기 연산이 일어나는 경우다.

1. A = 1
2. B = A + 1
3. A = 3

구문 2와 구문 3을 바꾸면 그 결과 또한 달라진다. A에 대한 읽기 연산과 쓰기 연산이 모두 포함되어 있기 때문이다.

1. A = 1
#. C = A
2. B = C + 1
3. A = 3

종속성을 제거하기 위해 # 구문을 추가할 수 있다. CPU가 이러한 Register Renaming 작업을 자동으로 수행하기도 하며, 이제 구문 2와 구문 3을 병렬로 처리할 수 있다.

 

Output Dependency

읽기 이후에 쓰기 연산이 일어나는 경우다.

1. A = 1
2. B = A + 1
3. A = 3

이 예시에서 구문 1과 구문 3이 Output Dependency를 가지고 있다. 따라서 구문 1과 3은 병렬로 처리될 수 없다.

1. C = 1
2. B = C + 1
3. A = 3

종속성을 제거하기 위해 구문 1, 2의 A를 C로 바꿀 수 있다. 그러나 멀티스레드 프로그램에서 공유하는 변수의 경우 함부로 그 이름을 바꿔서는 안 된다.

 

Input Dependency와 Output Dependency는 변수명을 Rewrite하는 방식으로 제거할 수 있기에 Name Dependency라고도 부른다.

 

Control Dependency

앞선 구문의 결과에 따라 다음 구문의 수행 여부가 변하는 경우다.

1. if A > B
2.     A = B
3. C = A

위 예시에서 구문 2의 수행 여부는 구문 1의 결과에 따라 변한다.

 

Memory Dependency

포인터 변수를 사용하는 경우 그 의존성을 쉽게 확인하기 어렵다.

*X = *Y + 1
*A = *B + 1

위 예시는 얼핏 보면 의존성이 없어 보이지만, Y와 A가 같은 곳을 가리킨다면 Flow Dependency, X와 B가 같은 곳을 가리킨다면 Anti Dependency가, X와 A가 같은 곳이라면 Output Dependency가 발생한다.

 

 

댓글()