둘 다 비트를 한쪽으로 밀고 밀려난 비트를 CF에 넣는다. 가르는 질문은 하나다. “오른쪽으로 밀 때 맨 위에 생긴 빈칸을 무엇으로 채우는가.” 0으로 채우면 논리 시프트(SHR), 원래 맨 위 비트를 한 번 더 써서 채우면 산술 시프트(SAR)다. 그 답은 다시 “이 비트들을 부호 없는 수로 읽는가, 부호 있는 수로 읽는가”로 정해진다.
같은 비트 11001111을 두 명령으로 오른쪽으로 한 칸 밀어 본다.
7 6 5 4 3 2 1 0 CF
밀기 전 [1 1 0 0 1 1 1 1]
SHR 뒤 [0 1 1 0 0 1 1 1] 1 맨 위에 0이 들어왔다
SAR 뒤 [1 1 1 0 0 1 1 1] 1 맨 위에 원래 부호 비트 1이 들어왔다
부호 없는 수로 읽으면 207이 103이 됐다(SHR). 부호 있는 수로 읽으면 −49가 −25가 됐다(SAR). 둘 다 “2로 나눈 값”인데, 같은 비트를 어떻게 읽느냐에 따라 맞는 명령이 다르다1.
어느 쪽일까
C1 (a) AL에 든 픽셀 밝기(0~255)를 절반으로 줄인다. (b) AL에 든 온도 변화량(−128~127)을 절반으로 줄인다. 각각 SHR과 SAR 중 무엇을 쓰는가? 반대로 쓰면 어떻게 되는가?
답: (a) SHR. 밝기 200(11001000)에 SAR을 쓰면 맨 위에 1이 들어와 11100100 = 228로 오히려 커진다. (b) SAR. −80(10110000)에 SHR을 쓰면 맨 위에 0이 들어와 01011000 = 88, 부호가 뒤집힌다.
이유: 빈칸을 채우는 값이 “이 수의 윗자리가 원래 무엇이었어야 하는가”와 맞아야 한다. 부호 없는 수의 윗자리는 0이고, 음수의 윗자리는 1이다1.
C2 (c) EAX의 위쪽 16비트를 아래쪽으로 내려 그 값만 꺼낸다. (d) 왼쪽으로 민다. 각각 논리와 산술 중 무엇인가?
답: (c) shr eax,16. 비트 묶음을 꺼낼 때는 수의 부호와 상관없이 빈칸이 0이어야 한다. SAR을 쓰면 맨 위 비트가 1일 때 윗자리가 전부 1로 찬다. (d) 고를 것이 없다. SAL과 SHL은 같은 명령이다. 왼쪽으로 밀 때는 아래쪽 빈칸에 늘 0이 들어온다.
결정적 차이
논리 오른쪽 시프트 SHR |
산술 오른쪽 시프트 SAR |
|
|---|---|---|
| 맨 위 빈칸 | 0 | 원래 부호 비트의 복사 |
| 맞는 읽기 | 부호 없는 수, 비트 묶음 | 부호 있는 수(2의 보수) |
| 계산하는 값 | \(\lfloor u / 2^n \rfloor\) | \(\lfloor s / 2^n \rfloor\) |
| 부호 | 결과는 늘 0 이상으로 읽힌다 | 그대로다 |
| 맨 위 비트가 0인 값 | 두 명령의 결과가 같다 | 같다 |
| 왼쪽 방향 | SHL |
SAL(= SHL) |
| Java 연산자 | >>> |
>> |
\(u\)는 비트를 부호 없는 수로 읽은 값, \(s\)는 부호 있는 수로 읽은 값, \(n\)은 민 칸 수다. \(\lfloor\ \rfloor\)는 작은 쪽 정수로 내린다는 뜻이다. 선택을 가르는 줄은 “맞는 읽기”다. 맨 위 비트가 0인 값(8비트에서 0~127)에서는 둘이 같은 결과를 내므로, 차이는 맨 위 비트가 1일 때만 드러난다1.
SHR은 \(\lfloor u/2^n \rfloor\), SAR은 \(\lfloor s/2^n \rfloor\), −80에 SHR을 쓰면 88, −5를 SAR로 한 칸 밀면 −3 — 37_shift-instructions_verify.py (파이썬 흉내로 확인)산술 시프트는 아래쪽으로 내린다. −5(11111011)를 SAR로 한 칸 밀면 11111101 = −3이다. −5 ÷ 2 = −2.5를 0 쪽(−2)이 아니라 작은 쪽(−3)으로 내린다. −1은 몇 칸을 밀어도 −1이다. C나 Java의 정수 나눗셈 -5 / 2는 −2이므로, 음수에서는 “오른쪽으로 밀기”와 “나누기”의 결과가 다를 수 있다1.
둘 다 아닐 때
C3 논리 시프트도 산술 시프트도 맞지 않는 상황 두 가지와 그때 쓸 명령을 쓰라.
답: ① 밀려난 비트를 잃으면 안 될 때(비트 순서만 바꾸고 싶을 때)는 밀려난 비트가 반대쪽 끝으로 돌아오는 회전 명령을 쓴다. ② 빈칸을 0이나 부호가 아니라 이웃 레지스터의 비트로 채워야 할 때(두 레지스터에 걸친 큰 수를 밀 때)는 이중 시프트 SHLD·SHRD나 CF를 끼워 돌리는 RCL·RCR을 쓴다.
연결
- 작은 값을 큰 칸으로 옮길 때도 같은 갈림이 있다. 윗자리를 0으로 채우는
MOVZX와 부호로 채우는MOVSX: 제로 확장과 부호 확장 - 비교한 뒤 점프를 고를 때도 같은 갈림이 있다: 부호 있는 비교 점프와 부호 없는 비교 점프