지난 글에서는 Branch Delay Slot 이 발생하는 이유를 파이프라이닝 과정을 따라가보며 살펴보았다.
요약해보면, Branching 여부는 E 단계에서 결정되기 때문에, E 단계에서 분기를 하더라도, E 단계 시점의 F 단계에 있던 명령어가 파이프라인에 남아 계속 실행되는 문제 때문에 이를 비워둠으로써 Delay Slot 이 발생하였다.
이번 글에서는 이렇게 Delay Slot 을 비워두지 않고, 다른 명령어로 채워 활용하는 방법.
Delay Slot Optimization 에 대해 정리해보고자 한다.
최적화 개요
그렇다면 어떻게 분기 명령어를 쓴 직후, nop 를 넣지 않고 최적화를 할 수 있을까?
nop 대신 분기에 (전체 코드 실행에) 영향을 주지 않는 다른 명령어를 넣으면 된다.
즉, delay slot 최적화는 프로그램의 실행 결과에 영향을 주지 않으면서 nop 위치를 기존 명령어로 채우는 것을 말한다.
최적화 방법에는 크게 아래와 같은 방법이 있다.
From Before
From After
Annulled Branch
한번 각각에 대해서 정리해보자.
From Before
From Before 방식은 이름에서 나와있듯, 분기명령어 이전 (before) 의 명령어를 가져와 nop 에 넣는 방식이다.
대표적으로 가능한 방식이, 함수를 호출할 때 call 명령어를 사용하는데, 이 명령어도 delay slot 이 생긴다.
함수를 호출할 때는 o-register 에 인자를 넘겨주어야 하는데, 이 과정을 delay slot 에서 처리할 수 있다.
유사하게 분기 명령어 실행 전에, 레지스터에 값을 할당하는 과정을 delay slot 에서 처리할 수도 있다.
예시를 보자.
위와 같은 간단한 코드를 작성하였다.
0 을 L1 레지스터에 담고, 다시 10을 L1 레지스터에 담고 있다.
그리고 test 로 분기를 하고 있다.
L1 의 값을 1 증가시키므로 L1 에는 결과적으로 11이 담겨 있어야 한다.
한번 이 코드를 어셈블하고 next_r 시점에 break 를 걸어 L1 레지스터 값을 읽어보자.
예상한대로 잘 나온다.
그렇다면 아까 말한대로 mov 명령어를 nop 위치로 옮겨도 같은 결과가 나올까?
위와 같은 코드를 실행시켜 본다.
같은 결과가 나옴을 알 수 있다.
이것이 분기 명령어 이전의 명령어를 nop 위치로 끌고 오는 From Before 최적화 방식이다.
한번 강의록에 있는 코드로 자세히 살펴보자.
간단한 반복문이다.
이를 C 코드로 옮겨보면 아래와 같은 코드이다.
int main() {
int o0 = 0;
int l0 = 1;
while(l0 <= 10) {
o0 = l0 + o0;
l0++;
}
return;
}
10번의 반복문을 돌면서 1부터 10까지 수를 o0에 다 더해주는 프로그램이다.
이 프로그램을 실행시키고, o0 의 값을 확인해보면 55가 나올 것이다.
한번 이 코드를 from before 방식으로 최적화해보자.
아까 예시에서 본 대로 mov 위치에 있는 명령어를 nop 쪽으로 끌고 올 수 있을 것 같다.
끌고 오더라도 1부터 10까지 더하는 데에는 아무런 영향이 없다.
단순히 레지스터에 값을 할당하는 것이므로 위에 있는 mov 두개 중 어떤 것을 끌고 오더라도 상관이 없다.
한번 위에 있는 mov 명령어를 delay slot으로 가져와보았다
.결과가 동일함을 알 수 있다.
하지만 언제나 From Before 방식을 사용할 수 있는 것은 아니다.
한번 최적화를 진행하기 전 코드에서, 아래쪽 nop 부분을 봐보자.
과연 nop 를 없애겠다고 12 번째 줄 inc %l0 을 앞으로 가져올 수 있을까?
가져올 수 없다.
정확히는 가져올 수 있지만 프로그램 실행 결과가 달라진다.
inc 명령어는 반드시 add 가 수행된 이후에 발생해야 기존 실행결과와 동일한데,
반복 체크는 1부터 10으로 하므로 10번이 실행되는 것은 동일하지만, 반복문 내부에서는 체크한 값보다 1 큰 값을 더해주고 있으므로 2부터 11까지 숫자를 더하는 효과가 발생하게 될 것이다.
즉, 실행 결과 65가 나올 것이다.
또 cmp 명령어도 nop 위치로 끌고 올 수 없다.
ble 로 비교를 할 CC 를 생성하는 명령어가 cmp 인데, cc 코드를 생성하기도 전에 비교를 할 수는 없는 노릇이다.
(E 단계에서 현재 CC 코드를 보고 분기 여부를 결정할 텐데, 그 때 F 단계에 cmp 명령어가 있다면 이게 무슨 소용인가)
From After
이제 from after 방식 최적화를 한번 보자.
from before 가 분기 명령어 이전의 명령어들을 가져와 nop 대신 넣었다면, from after 는 분기 명령어 이후의 명령어를 가져와 nop 대신 넣는 것이다.
최적화를 하지 않았던 기존 코드에서 ba를 하면 무조건 분기가 발생하므로 test 의 첫번째 코드인 14번째 줄 명령어는 항상 반드시 실행된다.
그렇다면 nop 에서 굳이 기다릴 필요 없이 14번째 줄 명령어를 미리 실행하고 있으면 되지 않을까?
그렇다면 위와 같이 최적화 할 수 있을 것이다.
14번째 줄 cmp 명령어를 ba 이후 delay slot 에 복사하여 똑같이 넣어주었다.
이때 '이동' 이 아니라 '복사' 를 하였음에 유의하자.
복사를 한 이유는 cmp 가 ba 분기 이후 처음 한번만 실행되는 것이 아니라 loop 를 도는 동안에도 계속 실행되어야 하기 때문이다.
따라서 test 가 아닌 loop 내부로 cmp 를 옮겨주는 작업이 한번 더 필요하다.
이렇게 했을 때도 같은 결과 55가 나오는지 확인해보자.
55가 잘 나오는 것을 볼 수있다.
처음 ba 명령어로 분기 했을 때는 nop 를 채운 cmp 로 비교해서 반복을 계속 할지 결정하고,
이후에는 loop 로 옮겨준 cmp 로 반복 여부를 결정한다.
하지만 from after 도 언제나 사용가능한 것은 아니다.
다음과 같은 케이스를 보자.
이건 너무 당연히도 안될 것 같은 케이스이다.
ret 명령어를 16번째 줄에 넣으면 어떻게 될까?
ble 가 true 임에도 ret 명령어가 실행되면서 main 함수가 종료되어버리고 만다.
한번 실행 결과가 궁금하니 돌려보자.
일단 어셈블은 되지만, 당연하게도 어셈블러께서 친절히 이건 문제가 있다고 알려주신다.
한번 실행도 시켜보자.
실행시켰더니 break point 를 도착하기도 전에 그냥 프로그램이 종료되어버렸다.
프로그램이 종료되어버렸으니 레지스터가 없다고 뜨는 것도 당연하다.
ret 이 안되는 것은 너무나도 당연하게 받아들 일 수 있으니, 한번 다른 사례를 살펴보자.
아까 ba 에서는 어차피 분기할테니까, 그 분기한 곳의 첫번째 줄 명령어를 nop 로 넣었다고 했다.
그럼 ble 에서도 마찬가지로 분기할 곳의 첫 명령어를 당겨와서 nop 로 넣으면 되는 것 아닐까?
겉보기엔 될 것 같지만, 이것도 문제가 있다.
아까와 차이점은 ba는 '무조건' 분기. 즉, 항상 분기를 실행하지만, ble 는 작거나 같을 때만 분기를 한다는 점이다.
따라서 원래는 작거나 같을 때만 실행되었던 add 명령어가, 커서 loop 를 돌지 않게 되는 순간에도 '한번 더' 실행된다.
따라서 이 프로그램을 실행하면 66이 실행결과로 나올 것이다.
그렇다.
그렇다면 이 문제를 어떻게 해결할 수 있을까?
단순한 방법은 결과적으로 '한번 더' 도는 게 문제이므로, '같다' 라는 조건을 빼 ble 대신 bl 로 반복을 도는 방법이 있다.
하지만 10번 도는 것을 의도한 코드가, delay slot 의 실행횟수 1번을 고려해 실제론 9번만 반복문을 돌도록 짠다는 것은 너무 복잡하다.
이런 상황에 사용가능한 것이 바로 3번째 최적화 방법인 '취소 분기 (Annulled Branch)' 이다.
Annulled branch (취소 분기)
뭐 이렇게까지 최적화를 해야하나.. 싶긴 하지만, 그래도 SPARC 에서 지원하는 기능이니 한번 살펴보도록 하자.
취소 분기의 사용 방법은 간단하다.
branchInstruction,a <label>
** 반드시 분기명령어와 , 와 a 를 모두 붙여 써야한다.
띄어쓰기가 들어가면 어셈블 에러난다.
ex)
ble,a loop
분기 명령어 다음에 , 를 찍고 a 를 추가해준다음 분기할 지점의 라벨을 넣으면 된다.
이렇게 하면, 다음과 같은 효과를 얻는다.
만약 분기 조건을 만족한 경우, delay slot 을 포함하여 코드를 실행하고,
조건을 만족하지 않는다면, delay slot 코드를 실행하지 않고 이후 명령어를 실행해나간다.
즉, 이 취소 분기를 사용하면, nop 를 from after 방식으로 '반드시' 없앨 수 있게 된다.
분기할 지점의 첫번째 명령어를 무조건 delay slot 에 넣으면 되기 때문이다.
만약 조건을 만족하지 않는다면 파이프라인에 들어온 delay slot 명령어를 SPARC 가 없애줄 것이다. (annul)
아까 코드에서 한번 annulled branch 를 이용해 최적화를 해보자.
수정이 아주 간단하다.
다시 의도대로 결과가 나오는 것을 알 수 있다.
Delay Slot 최적화 시 주의사항
이런 코드가 있다고 해보자.
저 set 명령어는 str 이라는 라벨에 있는 문자열을 printf 함수의 첫번째 인자로 설정하는 명령어이다.
mov 명령어와 비슷해보이는데, 과연 저 명령어를 9 번째 줄 nop 대신 넣어 최적화를 할 수 있을까?
답은 '할 수 없다' 이다.
그 이유는 set 이 기계 명령어 2개로 이루어진 합성명령어이기 때문이다.
set 명령어는 위와같이 sethi 명령어와 or 명령어 2개가 합쳐진 명령어이다.
따라서 기계명령어로 바꿔서 실행해보면 sethi 는 delay slot 에 들어가 실행이 되지만 or 명령어는 printf 로 call 이 들어가고, 나중에 printf 함수의 실행이 끝나면 실행되게 된다.
따라서 의도대로 실행이 되지 않는다.
(다만, set 이 안되는 이유는 '합성명령어' 라서가 아니라, '2개의 기계 명령어로 구성된 합성명령어' 라서 임에 주의하자.
mov 도 합성 명령어이지만, from before 방식으로 최적화가 가능했다.)
내가 시험문제를 낸다면, 이런거 물어볼 거 같다.
set 을 넣었을 때 의도대로 실행되지 않은 이유는 set 이 '합성명령어' 이기 때문이다. (x)
TMI 로 set 명령어를 구체적으로 살펴보면,
set 명령어는 string 으로 입력한 레이블 이름으로부터 레이블의 주소를 가져오는 명령어이다.
sethi 는 레이블 이름의 상위 주소를 가져와 %o0에 넣고,
or 연산을 통해 나머지 하위 주소를 %o0 에 마저 넣어준다.
분기 명령어의 기계어 포맷도 가볍게 봐보면,
하위 22비트가 분기 명령어와 목적 라벨사이에 얼마의 주소 차이만큼 떨어져있는지 나타내는 부분이다.
이 부분을 분기 명령어가 계산하게 된다.
Annul 부분은 이 분기 명령어가 취소 분기 명령어인지 아닌지 나타내는 부분으로 ,a 가 있다면 1로 활성화 될 것 같다.
이것으로 분기에 대한 이론 정리가 모두 끝났다.
다음 글에서는 지금까지 배운 내용을 토대로 C언어의 switch case 를 구현해 보는 것으로 분기 파트 정리를 완전히 마무리 할 예정이다.
그 다음에는 드디어 레지스터에서 벗어나 메모리를 조작하는 명령어를 정리해보겠다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 21. 메모리 (0) | 2023.10.19 |
---|---|
[SPARC] 20. switch - case 구현하기 (0) | 2023.10.18 |
[SPARC] 18. Branch Delay Slot 의 발생 이유 (0) | 2023.10.17 |
[SPARC] 17. 분기 명령어 (0) | 2023.10.15 |
[SPARC] 16. 비트 연산 명령어 (0) | 2023.10.15 |