Search
Duplicate

컴퓨터 구조 및 설계/ 컴퓨터 연산

서론

이 장의 목표는 실수의 표현과 연산 알고리즘, 이러한 알고리즘을 수행하는 하드웨어, 그리고 이 모든 것들이 명령어 집합에 미치는 영향 등을 풀어 나가는 것이다.

덧셈과 뺄셈

컴퓨터는 사람들이 생각하는 그대로 덧셈을 수행한다.
손으로 계산할 때처럼 오른쪽에서 왼쪽으로 한 비트씩 더하고, 이때 생기는 올림수는 바로 왼쪽 자리로 보낸다. 손으로 덧셈을 할 때와 같이 바로 왼쪽 자리에 올림수를 보내준다.
뺄셈은 덧셈을 이용한다. 뺼 값의 부호를 바꾸어 더하기만 하면 된다.

예제) 7에 6을 이진법으로 더하고, 7에서 6을 이진법으로 빼라.

7 + 6
0000 0000 0000 0000 0000 0000 0000 0111 = 7 + 0000 0000 0000 0000 0000 0000 0000 0110 = 6 ----------------------------------------------- = 0000 0000 0000 0000 0000 0000 0000 1101 = 13
Plain Text
7 – 6
0000 0000 0000 0000 0000 0000 0000 0111 = 7 - 0000 0000 0000 0000 0000 0000 0000 0110 = 6 ----------------------------------------------- = 0000 0000 0000 0000 0000 0000 0000 0001 = 1
Plain Text
2의 보수법을 이용하여 -6을 더하는 방식
0000 0000 0000 0000 0000 0000 0000 0111 = 7 + 1111 1111 1111 1111 1111 1111 1111 1010 = -6 ----------------------------------------------- = 0000 0000 0000 0000 0000 0000 0000 0001 = 1
Plain Text
연산 결과를 사용 가능한 하드웨어로 표현할 수 없을 때 오버플로가 발생함을 상기하라.
부호가 다른 피연산자를 더할 경우에는 오버플로가 발생하지 않는다. 이유는 계산 결과가 두 피연산자 중 어느 하나보다는 커질 수 없기 때문이다.
뺄셈에서도 똑같이 오버플로가 발생하지 않는 경우가 있는데, 피연산자의 부호가 같은 경우에는 오버플로가 발생할 수 없다. 이는 두 번째 피연산자의 부호를 바꾸어 더하는 방식으로 뺄셈을 처리하기 때문이다. ca=c+(a)c - a = c + (-a)
덧셈이나 뺄셈에서 오버플로가 발생할 수 없는 경우에는 걱정할 것이 없지만, 실제로 오버플로가 발생했을 때는 어떻게 탐지할 수 있을까? 32비트 수 두 개를 더하거나 뺀 결과를 완벽하게 표현하기 위해서는 33비트가 필요할 경우가 있다.
워드 크기가 32비트이므로 33번째 비트는 표시할 수 없는데, 이렇게 되면 부호 비트가 결과의 부호가 아니라 크기를 나타내는 비트 중 최상의 비트 값으로 결정된다. 딱 한 비트가 부족하므로 틀릴 수 있는 것은 부호 비트 뿐이다.
따라서 두 양수를 더한 값이 음수가 되면 오버플로가 발생한다. 이것은 부호 비트가 올림수가 올라갔음을 의미한다.
이와 반대로 두 음수를 더했는데 합이 양수가 되는 경우에도 오버플로가 발생한다.
오버플로는 양수에서 음수를 뺀 결과가 음수가 되거나, 음수에서 양수를 뺀 결과가 양수가 되는 경우에도 발생할 수 있다. 이것은 부호 비트에서 빌림수(borrow)가 발생했음을 의미한다. 그림 3.2는 오버플로가 발생한 경우의 연산의 종류, 피연산자 및 그 결과를 보여준다.
그러면 부호 없는 정수에서는 어떠한가? 부호 없는 정수는 오버플로가 무시되는 메모리 주소에 사용된다.
그러므로 컴퓨터 설계자는 어떤 경우에는 오버플로를 무시하고 다른 경우에는 이를 인식하는 방법을 제공해야 한다. MIPS는 이 두 가지 선택을 지원하기 위해 두 가지 산술 명령어를 제공한다.
add(add), add immediate(addi)와 subtract(sub) 명령어들은 오버플로가 발생하면 예외(exception)을 발생시킨다.
add unsigned(addu), add immediate unsigned(addiu), subtract unsigned(subu) 명령어들은 오버플로가 발생해도 예외를 발생시키지 않는다.
C 언어는 오버플로를 무사하기 때문에 MIPS C 컴파일러는 변수의 형에 관계 없이 언제나 부호 없는 버전인 addu, addiu, subu 명령어를 생성한다. 그러나 MIPS Fortran 컴파일러는 피연산자의 형(type)에 따라서 적절한 산술 명령어를 생성한다.
MIPS는 오버플로를 탐지하면 예외(Exception)을 발생시킨다.
예외를 인터럽트(interrupt)라고 부르는 컴퓨터도 많이 있다. 예외나 인터럽트는 본질적으로 계획되지 않은 프로시저 호출이다.
오버플로가 발생한 명령어의 주소는 레지스터에 저장되고 컴퓨터는 적절한 처리를 하기 위해 해당 루틴으로 점프한다.
인터럽트가 걸린 주소를 저장해서, 해당 처리를 한 다음에 프로그램 실행을 계속할 수 있게 한다.
MIPS에는 EPC(exception program counter)라고 불리는 레지스터가 있어서 인터럽트가 걸린 명령어의 주소를 기억하는데 이용된다. mfc0(move from system control) 명령어는 EPC를 범용 레지스터에 복사하여 MIPS 소프트웨어가 점프 레지스터 명령어를 통해 인터럽트가 걸린 명령어로 돌아갈 수 있게 해준다.

요약

부호 없는 수를 사용하는 경우에 오버플로를 탐지하는 것은 쉽다. 그러나 주소 연산 과정에서 발생하는 오버플로는 검출하기를 원하지 않기 때문에 일반적으로 무시된다.
하지만 2의 보수에서는 문제가 더 복잡하다. 어떤 소프트웨어 시스템은 오버플로 탐지를 요구하므로, 오늘날 모든 컴퓨터는 오버플로 탐지 방법을 가지고 있다.
어떤 프로그래밍 언어는 바이트나 하프워드로 선언된 변수에 대하여 2의 보수법 정수 연산을 허용한다. 그러나 MIPS는 풀워드(full word)에 대해서만 정수 연산을 허용한다. 2장에서 보았듯이 MIPS에는 바이트와 하프워드 데이터 전송 명령이 있다. 바이트나 하프워드 산술연산을 위해서는 어떤 MIPS 명령어를 사용해야 할까?
lbu, lhu를 이용한 적재; add, sub, mult, div를 이용한 연산; 그리고 sb, sh를 이용하는 저장
lb, lh를 이용한 적재; add, sub, mult, div를 이용한 연산; 그리고 sb, sh를 이용하는 저장
lb, lh를 이용한 적재; add, sub, mult, div를 이용한 연산 후에 결과를 8비트나 16비트로 마스크(mask)하기 위한 AND; 그리고 sb, sh를 이용하는 저장

곱셈

십진수 1000과 1001의 곱셈을 연필로 계산하면 다음과 같다.
  1000 - multiplicand 1001 - multiplier ---- 1000 0000 0000 1000 ------ 1001000 - product
Plain Text
첫 번째 피연산자는 피승수(multiplicand)라고 부르고, 두 번째 피연산자는 승수(multiplier)라고 부른다. 최종 결과는 곱(product)이라고 부른다.
초등학교에서 배운 알고리즘에서는 승수의 자리 수를 오른쪽에서 왼쪽으로 가면서 한 번에 하나씩 택해서 이것을 피승수와 곱한 뒤 그 곱셈의 결과를 직접의 곱보다 한 자리 왼쪽에 놓는다.
주목할 점은 최종 곱셈 결과의 자리 수가 피승수나 승수에 비교해서 상당히 크다는 것이다.
사실 부호 비트를 무시하면 n비트의 피승수에 대한 m비트의 승수의 곱셈 결과는 n+m 비트의 길이를 갖는다. 결국 가능한 모든 곱을 표현하기 위해서는 n+m 비트가 필요하다.

곰셉 알고리즘과 하드웨어의 순차적 버전

첫 번째 설계는 우리가 학교에서 배운 알고리즘과 유사하다. 그림 3.3에 필요한 하드웨어를 보였다. 이 하드웨어는 종이와 연필로 푸는 방법과 유사하게 하기 위해 데이터가 위에서 아래로 흐르도록 하였다.
승수는 32비트 승수(multiplier) 레지스터가 있고, 64비트 곱(product) 레지스터는 0으로 초기화되어 있다고 가정하자.
위의 종이와 연필을 사용한 계산 방법대로라면, 매 단계마다 피승수(multiplicand)를 왼쪽으로 한 자리씩 이동시키고 필요하면 중간 결과에 더해주는 식으로 계산되어야 함을 알 수 있다.
이렇게 32비트 단계를 거치면 32비트 피승수가 왼쪽으로 32번 이동하게 된다. 그러므로 64비트 피승수 레지스터가 필요하다.
이 레지스터의 오른쪽 절반에는 32비트 피승수가 위치하게 되고, 왼쪽 절반은 0으로 초기화된다. 이 레지스터는 64비트 곱 레지스터에 축적되는 합과 피승수의 위치를 맞추기 위해 매 단계마다 1비트 왼쪽으로 자리이동된다.
그림 3.4는 각 비트 연산에 필요한 기본 3단계를 보여준다. 승수의 최하위 비트(mutilplier0)는 피승수를 곱 레지스터에 더할지 말지를 결정한다.
단계 2에서 왼쪽으로 자리이동하는 것은 종이와 연필로 계산했을 때와 같이 피연산자를 왼쪽으로 이동시키는 역할을 한다.
단계 3에서 오른쪽 자리이동하는 이유는 다음 번 반복에서 검사할 승수의 다음 비트를 준비하기 위해서다. 최종 결과를 얻기 위해서는 이러한 세 단계를 32번 반복해야 한다. 각 단계에 한 클럭 사이클씩 필요하다면 이 알고리즘은 32비트 숫자 두 개를 곱하는데 거의 100개의 클럭 사이클이 걸린다.
곱셈과 같은 산술연산의 상대적 중요도는 프로그램에 따라 다르다. 그러나 덧셈과 뺄셈의 빈도는 곱셈의 5배와 100배 사이의 어느 값이 될 것이다. 따라서 많은 응용 프로그램에서 곱셈은 여러 클럭 사이클을 소비하면서도 성능에 별다른 영향을 끼치지 않는다.
그러나 Amdahl의 법칙은 느린 연산이 많이 사용되지 않더라도 성능을 제한할 수 있다는 것을 상기시켜 준다.
이 알고리즘과 하드웨어는 매 반복이 한 클럭 사이클만 걸리도록 쉽게 바꿀 수 있다. 연산을 병렬로 수행함으로써 이러한 속도 향상이 가능해진다.
승수 비트가 1일 때 피승수를 곱에 더하는 동안 승수와 피승수를 자리이동한다. 이때 하드웨어가 검사하는 승수의 맨 오른쪽 비트가 자리이동하기 전의 값이고 덧셈에 사용하는 값이 전반복에서 자리이동된 피승수인 것만 확실하게 하면 문제는 없다.
덧셈기와 레지스터에서 사용되지 않는 부분이 있으므로 덧셈기와 레지스터의 폭을 절반으로 줄여 더욱 최적화할 수 있다. 그림 3.5는 이렇게 개선된 하드웨어를 보여준다.
산술연산을 자리이동으로 대치시키는 것은 상수를 곱할 때도 적용할 수 있다. 몇몇 컴파일러는 작은 상수와의 곱셈을 일련의 자리이동과 덧셈으로 대치시킨다.
이진수에서 한 비트 좌측 이동은 두 배 큰 수를 나타내기 때문에, 왼쪽으로 자리이동하는 것은 2의 지수승을 곱해 주는 것과 같다. 그래서 2장에서 설명한 바와 같이 거의 모든 컴파일러가 2의 지수승을 곱하는 것을 왼쪽 자리이동으로 대치하는 강도 감소 최적화(strength reduction optimization)을 수행한다.

부호있는 곱셈

지금까지는 양수만을 다루었다. 부호있는 수의 곱셈을 가장 쉽게 이해할 수 있는 방법은 먼저 승수와 피승수를 양수로 변환하고 원래의 부호를 기억하는 것이다. 이 알고리즘은 부호를 계산에서 제외시키고 31번 반복한 후, 초등학교에서 배운대로 피연산자들의 부호가 서로 다를 때만 곱을 음수로 바꾼다.
수는 무한히 많은 자리수를 가질 수 있지만, 우리가 32비트로 나타낼 뿐임을 기억한다면 마지막 알고리즘을 부호있는 수의 곱셈에 사용할 수 있다. 부호있는 수의 경우는 자리이동 단계에서 곱의 부호를 확장해야 한다. 알고리즘이 끝나면 아래쪽 워드에 32비트 곱이 남게 된다.

더 빠른 곱셈

Moore의 법칙은 하드웨어 설계자들이 훨씬 빠른 곱셈기를 만들 수 있도록 더 많은 자원을 제공해 왔다. 승수의 32개 비트를 각각 조사하면, 곱셈을 시작하는 초기에 이미 피승수가 더해져야 하는지 아닌지를 알 수 있다.
승수의 매 비트마다 32비트 덛셈기를 하나씩 할당하면 더 빠른 곱셈이 가능하다. 덧셈기의 한 입력은 피승수를 해당 승수 비트와 AND한 것이고 다른 입력은 앞 덧셈기의 출력이다.
보다 간단한 방법은 오른쪽 덧셈기의 출력을 왼쪽 덧셈기의 입력에 연결하여 32층의 덧셈기 스택을 만드는 것이다. 또 다른 방법은 이 32번의 덧셈 시간을 기다리는 대신에 log2(32)=5\log_{2}(32) = 5의 덧셈 시간만 기다리면 된다.
올림수 저장 덧셈기(carry save adder)를 사용할 수도 있고 이렇나 설계를 파이프라이닝해서 다수의 곱셈을 동시에 수행할 수 있게 하는 것이 쉽기 때문에 실제로는 곱셈이 다섯번의 덧셈 시간보다 더 빨라질 수 있다.

MIPS에서의 곱셈

MIPS는 64비트 곱을 저장할 수 있는 한 쌍의 32비트 레지스터를 별도로 제공한다. 이것은 Hi와 Lo라고 불린다. 부호가 있는 곱셈과 없는 곱셈을 적절하게 다루기 위해 MIPS는 두 개의 명령어 mult(multiply)와 multu(multiply unsigned)를 갖고 있다.
프로그래머는 32비트 정수 곱을 범용 레지스터로 가져오기 위해 mflo(move from lo) 명령어를 사용한다. MIPS 어셈블리는 범용 레지스터 세 개를 사용하는 곱하기 의사 명령어를 제공한다. 이 의사명령어는 곱을 레지스터에 넣기 위해서 mflo와 mfhi 명령어를 사용한다.

요약

초등학교에서 배운 종이와 연필로 하는 계산처럼 곱셈 하드웨어도 단순히 자리이동과 덧셈을 수행한다.
컴파일러는 2의 멱수 곱하기를 자리이동 명령어로 대치하기도 한다. 더 많은 하드웨어를 사용하면 덧셈을 병렬로 더 빠르게 수행할 수도 있다.
MIPS 곱셈 명령어(mult, multu)는 둘 다 오버플로를 무시한다. 따라서 곱이 32비트에 들어갈 수 있을지 없을지를 검사하는 것은 소프트웨어의 몫이다. multu 수행 결과 Hi가 0이거나 mult 수행 결과 Hi가 lo의 부호와 같은 비트 32개로 채워져 있다면 오버플로가 아니다.
오버플로를 탐지하기 위해 Hi를 범용 레지스터로 보내는 데 mfhi(move from hi) 명령어를 사용할 수 있다.

나눗셈

곱셈의 역연산인 나눗셈은 사용 빈도는 낮은 반면 훨씬 까다롭다. 심지어는 0으로 나누기와 같이 수학적으로 유효하지 않은 연산을 요구하기도 한다.
피연산자의 이름과 초등학교에서 배운 나눗셈 알고리즘에 대한 기억을 되살리기 위해 십진수를 이용한 긴 나눗셈을 예로 들자.
1001 - quotient --------- divisor - 1000 | 1001010 - dividend - 1000 ------ 10 101 1010 -1000 ----- 10 - remainder
Plain Text
나눗셈에서 피연산자는 피제수(dividend)와 제수(devisor) 두 개이고, 몫(quotient)이라 불리는 결과와 나머지(remainder)라고 불리는 두 번째 결과가 있다. 이 구성 요소 간의 관계는 다음과 같다.
피제수(divdend) = 몫(quotient) x 제수(devisor) + 나머지(remainder)
Plain Text
여기서 나머지는 제수보다 작다. 몫은 무시하고 나머지만을 구하기 위해 나눗셈 명령어를 사용하는 프로그램도 가끔 있다.
피제수와 제수가 둘 다 양수이고 따라서 몫과 나머지도 음수가 아니라고 가정하자. 나눗셈의 피연산자와 결과는 32비트 값을 갖는다고 하고 부호는 당분간 무시하기로 하자.

나눗셈 알고리즘과 하드웨어

그림 3.8은 초등학교에서 배운 알고리즘을 그대로 하드웨어로 구현한 것이다. 몫 레지스터 32비트를 0으로 초기화해서 시작한다.
이 알고리즘은 매번 반복할 때마다 제수를 오른쪽으로 한 비트씩 자리이동 시켜야 하므로 제수를 64비트 제수(Divisor) 레지스터의 왼쪽 절반에 넣고 시작한다. 그리고 피제수와 자리를 맞추기 위해 반복할 때마다 오른쪽으로 한 비트씩 자리이동 시킨다. 나머지 레지스터는 피제수로 초기화된다.
그림 3.9는 첫 번째 나눗셈 알고리즘의 세 단계를 보여 준다. 사람과 달리 컴퓨터는 제수가 피제수보다 작다는 것을 먼저 알 만큼 똑똑하지 못하다. 따라서 먼저 단계에서 1에서 제수를 빼야만 한다.
이것은 set on less than 명령어에서 했던 비교와 같음을 기억하라. 만약 결과가 양수이면, 제수는 피제수와 같거나 더 작다. 따라서 몫에 1을 넣는다. (단계 2a)
만약 결과가 음수이면 제수를 나머지 레지스터에 다시 더함으로써 원래의 값을 회복하고(단계 2b) 몫에는 0을 넣는다.
제수는 오른쪽으로 자리이동 되고 다시 이 과정을 반복한다. 계산이 완료되면 나머지와 몫은 각각 해당 레지스터에 남게 된다.
이 알고리즘과 하드웨어는 훨씬 더 빠르고 싸게 개선될 수 있다. 뺄셈과 동시에 피연산자와 몫을 자리이동시키면 성능 향상이 가능하다.
레지스터와 덧셈기에 사용되지 않는 부분이 있음을 활용하여 덧셈기와 레지스터의 크기를 절반으로 줄일 수 있다. 그림 3.11은 개선된 하드웨어를 보여준다.

부호있는 나눗셈

지금까지 부호있는 나눗셈은 고려하지 않았다. 간단한 방법은 제수와 피제수의 부호를 기억하고 이 부호들이 다른 경우에는 몫을 음수화하는 것이다.

더 빠른 나눗셈

곱셈과 같이 나눗셈에도 Moore의 법칙이 적용될 수 있으므로 하드웨어를 추가해서 나눗셈 성능을 향상시킬 수 있다. 곱셈의 속도를 빠르게 하기 위해 많은 수의 덧셈기를 사용하였지만 같은 방법을 나눗셈에는 적용할 수 없다.
알고리즘의 다음 단계를 수행하기 전에 뺄셈한 결과의 부호를 알아야 하기 떄문이다. 반면에 곱셈의 경우 32개 부분곱을 바로 계산할 수 있다.
한꺼번에 몫을 두 비트 이상 만들 수 있는 기술이 있다. SRT 나눗셈은 각 단계에서 여러 개의 몫 비트를 예측하는 기법을 사용한다.
피제수와 나머지의 상위 비트들을 이용하여 표를 찾아서 몫을 추측하고, 틀린 추측은 그 후의 단계에서 바로 잡는다.
오늘날 널리 사용되는 방식은 한 번에 4비트를 추측하는 법이다. 핵심은 뺄 값을 추측하는 것인데, 이진 나눗셈에는 한 가지 선택 밖에 없었다. 이 알고리즘은 각 단계별 추측에 필요한 표를 참조하기 위해 나머지의 6비트와 제수의 4비트를 인덱스로 이용한다.
이 빠른 방법의 정확도는 참조표가 얼마나 적합한 값들을 갖고 있는지에 의존한다. 3.9절의 오류는 표가 부정확할 때 어떤 문제가 생길 수 있는가를 보여준다.

MIPS에서의 나눗셈

그림 3.5와 3.11에서 똑같은 순차 하드웨어가 곱셈과 나눗셈 모두에 이용될 수 있음을 보았을 것이다.
왼쪽이나 오른쪽으로 자리이동 될 수 있는 64비트 레지스터와 뎃섬과 뺼심을 할 수 있는 32비트 ALU만 있으면 된다.
MIPS는 곱셈과 나눗셈용 64비트 레지스터로 32비트 Hi와 Lo 레지스터를 사용한다.
위의 알고리즘에서 예측할 수 있듯이 나눗셈 명령이 완료된 뒤 Hi는 나머지를, Lo는 몫을 갖는다.
부호있는 정수와 부호없는 정수 모두를 다루기 위해, MIPS에는 두 개의 명령어 div(divide)와 divu(divide unsigned)가 있다.
MIPS 어셈블러는 범용 레지스터 세 개를 사용하는 나누기 의사명령어를 제공한다. 이 의사명령어는 mflo와 mfhi 명령어를 사용하여 원하는 결과를 범용 레지스터에 넣는다.

요약

MIPS는 같은 하드웨어가 곱셈과 나눗셈을 지원하므로 32비트 레지스터 한 쌍을 두어서 곱셈과 나눗셈 양쪽에 모두 사용하고 있다.
여러 몫 비트를 동시에 예측하고 예측이 틀렸으면 나중에 바로잡는 방법으로 나눗셈을 빠르게 할 수 있다.
그림 3.12는 이전 2개의 절에서 추가된 MIPS 구조를 보여주고 있다.
MIPS 나눗셈 명령은 오버플로를 무시하므로 몫이 너무 커서 오버플로가 발생하는가는 소프트웨어로 검사해야 한다.
나눗셈은 오버플로를 발생시킬 뿐만 아니라 0으로 나누기 같은 부적절한 연산을 초래하기도 한다. 이런 두 가지 비정상 상황을 구별하는 컴퓨터들도 있다. 하지만 MIPS에서는 오버플로나 0으로 나누기를 소프트웨어로 검사해야 한다.

부동소수점

프로그래밍 언어는 부호있는 정수와 부호없는 정수 뿐만 아니라 소수 부분을 갖는 수도 다룰 수 있어야 한다. 수학에서는 이러한 수를 실수(reals)라고 부른다.
3.14159265...(π)3.14159265... (\pi)
2.71828...(e)2.71828... (e)
0.000000001(1.0×109)0.000000001 (1.0 \times 10^{-9}) (seconds in a nanosecond)
3,155,760,000(3.15576×109)3,155,760,000 (3.15576 \times 10^{9}) (second in a typical century)
1세기를 초로 표시한 것은 작은 소수값이 아니고 32비트 부호 있는 정수로 표현할 수 없는 너무 큰 값임에 주목하라. 마지막 두 수의 표현 방식은 과학적 표기법(scientific notation)이라고 불린다.
이 방식에서 소수점 왼쪽에는 한 자리 수만 있다. 과학적 표기법으로 표현된 숫자 중에서 맨 앞에 0이 나오지 않는 것을 정규화된 수(normalized number)라고 부른다. 이것이 일반적으로 사용되는 방법이다. 0.1×108,10.0×10100.1 \times 10^{-8}, 10.0 \times 10^{-10} 은 정규화된 과학적 표기법이 아니다.
십진수를 과학적 표기법으로 표시하듯이, 이진수도 과학적 표기법으로 표시할 수 있다.
1.0×211.0 \times 2^{-1}
이진수를 정규화된 형태로 표현하기 위해서는 소수점 왼쪽에 0이 아닌 숫자가 한 자리만 나타나게 숫자를 자리이동한 후 자리이동 횟수만큼 증가시키거나 감소시킬 수 있는 기수(base)가 필요하다.
이 조건을 만족시킬 수 있는 기수는 2밖에 없다. 이제부터는 기수가 10이 아니므로 십진 소수점(decimal point)을 대신할 새로운 이름이 필요하다. 앞으로는 이진 소수점(binary point)이라는 용어를 사용하기로 한다.
이런 수를 지원하는 컴퓨터 연산은 부동소수점(floating point) 연산이라고 불린다. 왜냐면 이러한 수에서는 정수와 달리 이진 소수점이 고정되어 있지 않기 때문이다. C 언어는 이러한 수를 나타내기 위해 float라는 변수형을 사용한다.
과학적 표기법에서와 같이 이진 소수점 왼쪽에 한 자리 숫자만 나오는 형태로 표현한다. 이진수로 나타내면 다음과 같다.
1.xxxxxx×2yyyy1.xxxxxx \times 2^{yyyy}
컴퓨터는 지수도 나머지 부분처럼 이진법을 사용하여 나타낸다. 그러나 표현의 단순화를 위해서 지수는 십진수로 표현하겠다.
실수를 정규화된 형태의 표준 과학적 표기법으로 나타내면 세 가지 좋은 점이 있다.
첫째, 부동소수점 숫자를 포함한 자료의 교환을 간단하게 한다.
둘째, 숫자가 항상 이런 형태로 표현된다는 것을 알고 있으므로 부동소수점 산술 알고리즘이 간단해진다.
셋째, 불필요하게 선행되는 0을 소수점 오른쪽에 있는 실제의 숫자로 바꾸기 때문에 한 워드 내에 저장할 수 있는 수의 정밀도를 증가시킨다.

부동소수점 표현

부동소수점 표현 방식을 설계하는 사람은 소수부분(fraction)의 크기와 지수(exponent)의 크기 사이에서 타협점을 찾아야 한다. 고정된 워드 크기를 사용하므로 하나를 증가시키면 다른 하나를 감소시켜야 하기 때문이다.
이 문제는 정밀도와 표현 범위 사이의 선택이다. 소수부분의 크기를 증가시키면 소수부분으로 표현할 수 있는 수의 정밀도가 높아지고, 지수의 크기를 증가시키면 표현할 수 있는 수의 범위가 늘어난다.
부동소수점 수의 크기는 보통 워드 크기의 배수이다. MIPS의 부동소수점 표현은 아래 그림과 같다.
여기서 s는 부동소수점 수의 부호이고(1이면 음수), 지수는 8비트 지수필드의 값이며(지수의 부호 포함), 소수부분은 23비트 수이다.
이와 같은 표현 방식은 부호의 크기(sign and magnitude) 표현 방식이라고 불리는데, 부호가 수의 나머지 부분과 떨어져 독립된 비트로 표현되기 때문이다.
일반적으로 부동소수점 수는 다음과 같은 형태를 갖는다.
(1)s×F×2E(-1)^{s} \times F \times 2^{E}
F는 소수부분의 값과 관련이 있고, E는 지수부분의 값과 연관되어 있다. 이런 부분들과의 정확한 관계는 곧 설명될 것이다. (MIPS가 실제로는 더 정교하게 다룬다는 것을 곧 알게 될 것이다.)
이렇게 선택된 지수와 소수부분의 크기는 MIPS 컴퓨터의 산술이 매우 큰 수의 표현 범위를 갖게 한다. 2×10382 \times 10^{-38}만큼 작은 소수로부터 2×10382 \times 10^{38}만큼 큰 수가 컴퓨터에서 표현될 수 있다.
그러나 불행하게도 이것이 무한대는 아니기 때문에 이보다 더 큰 계산 결과가 나올 수도 있다. 그러므로 오버플로 인터럽트는 정수 연산에서 뿐만 아니라 부동소수점 연산에서도 발생할 수 있다.
여기서 오버플로라는 것은 지수가 너무 커서 지수 필드에 들어갈 수 없는 경우라는 점을 주목하라.
부동소수점은 새로운 종류의 비정상적인 상황을 초래한다. 너무 커서 표현할 수 없는 계산 결과가 나왔을 때 프로그래머가 이 사실을 알고 싶어 하듯이, 계산 결과가 너무 작아서 표현할 수 없는 경우에도 알고 싶어 할 것이다. 둘 중 어떤 경우라도 프로그램은 틀린 결과를 내게 된다.
오버플로와 구별하기 위해 후자의 사건을 언더플로(underflow)라고 부른다. 이런 상황은 음수 지수의 절댓값이 너무 커서 지수 부분에 표현될 수 없는 경우에 발생한다.
언더플로와 오버플로의 발생 가능성을 줄이는 방법으로 지수 부분이 더 큰 다른 표현 형식을 사용할 수 있다. C 언어에서는 이것을 double이라고 부른다. 그리고 double 형식을 갖는 수의 연산을 2배 정밀도(double precision) 부동소수점 연산이라고 부른다.
앞서 설명한 표현 방식은 단일 정밀도(single precision) 부동소수점이라고 한다.
2배 정밀도 부동소수점 수를 표현하려면 아래와 같이 MIPS 워드 두 개가 필요하다. 여기에서 s는 여전히 수의 부호를 나타내고, 지수(exponent)는 11비트의 지수 필드 값을 나타낸다. 소수부분의 크기는 52비트이다.
MIPS 2배 정밀도 표현법은 2×103082 \times 10^{-308} 만큼 작고 2×103082 \times 10^{308} 만큼 큰 수를 표현할 수 있다. 이와 같은 2배 정밀도 연산은 지수의 범위를 크게 하여 주기도 하지만, 주된 장점은 훨씬 더 큰 유효자리를 제공하여 정밀도를 높인다는 것이다.
이런 형식은 MIPS만의 것이 아니다. 이것들은 IEEE 754 부동소수점 표준으로서 1980년 이후에 만들어진 컴퓨터는 거의 모두 이 표준을 사용하고 있다.
이 표준은 부동소수점 프로그램의 이식을 매우 쉽게 하였고 컴퓨터 연산의 질을 크게 향상시켰다.
유효자리 부분에 더 많은 수를 담기 위해 IEEE 754 표준은 정규화된 이진수의 가장 앞쪽 1비트를 생략하고 표현하지 않는다. 따라서 실제적으로 유효자리는 단일 정밀도 표현에서는 24비트의 길이를 갖고 (숨겨진 1과 23비트 소수부분), 2배 정밀도에서는 53비트(1+52)의 길이를 갖는다.
정확하게 표현하기 위해서 유효자리(significand)라는 용어는 숨겨진 1을 포함하는 24 또는 53비트를 나타내는데 사용하고, 23 또는 52비트를 나타낼 때는 소수부분이라는 용어를 사용한다.
숫자 0.0은 선행하는 1이 없기 때문에, 예약된 지수 값 0을 가지며 이 경우에 하드웨어는 선행하는 1을 붙이지 않는다.
따라서 00…00은 0을 나타낸다. 0을 제외한 나머지 수는 앞서 설명한 형식을 사용하므로 숨겨진 1을 덧붙이면 아래와 같은 값이 된다.
(1)s×(1+Fraction)×2E(-1)^{s} \times (1 + \text{Fraction}) \times 2^{E}
여기서 소수부분의 비트들은 0과 1 사이의 수를 나타내고 E는 곧 자세히 설명하겠지만 지수 부분의 값을 표시한다. 유효자리의 비트를 왼쪽에서 오른쪽으로 s1, s2, s3… 와 같이 번호를 매기면 다음과 같은 값이 된다.
(1)s×(1+(s1×21)+(s2×22)+(s3×23)+(s4×24)+...)×2E(-1)^{s} \times (1 + (s1 \times 2^{-1}) + (s2 \times 2^{-2}) + (s3 \times 2^{-3}) + (s4 \times 2^{-4}) + ... ) \times 2^{E}
그림 3.13은 IEEE 754 부동소수점 수의 인코딩 방법을 보여준다. IEEE 754의 또 다른 특징은 비정상적인 사건을 표현하는 특수 심벌이 있다는 점이다.
예컨대 0으로 나누기에 대해 인터럽트를 거는 대신에 소프트웨어가 +∞나 -∞를 표시하는 비트열을 결과 값으로 정할 수 있다.
표현 가능한 가장 큰 지수 값을 이런 특수 심벌용으로 예약해 놓았다. 프로그래머가 이 결과를 프린트하면 프로그램은 무한대 기호를 프린트할 것이다. (수학적으로 훈련된 사람들을 위해 설명을 추가하면, 무한대의 목적은 실수의 위상적 닫힘(topological closure)을 형성하기 위함이다.)
IEEE 754는 0 나누기 0이나 무한대 빼기 무한대와 같은 유효하지 않은 연산의 결과를 위한 심벌도 가지고 있다. 이 심벌은 숫자가 아님(Not a Number)을 표현하는 NAN이다. NaN을 사용하면 프로그래머가 테스트나 결정을 나중에 편한 때로 미룰 수 있다.
IEEE 754 설계자들은 정수 비교에 의해 쉽게 정렬(sorting) 될 수 있는 부동소수점 표현을 원했다. 이런 이유로 less than, greater than 또는 equal to 0 테스트를 빠르게 할 수 있도록 부호 비트가 최상위 비트에 놓이게 되었다. (이 표현 방식은 기본적으로 2의 보수법 표현 방식이 아닌 부호와 크기 표현 방식이므로 단순한 정수 정렬보다는 조금 더 복잡하다)
지수를 유효자리 앞에 두면 부호가 같은 수를 비교할 때 지수가 큰 수가 지수가 작은 수보다 더 큰 정수처럼 보인다. 이것 또한 부동소수점 수를 정수 비교 명령어로 정렬하는 일을 쉽게 해준다.
하지만 음수 지수는 숫자 숫자 정렬을 어렵게 만든다. 만약 음수를 나타내기 위해 2의 보수법이나 지수의 최상위 비트를 1로 만드는 다른 표현법을 사용한다면 지수가 음수이면 매우 큰 수처럼 보일 것이다. 예컨대 1.0×211.0 \times 2^{-1}은 다음과 같이 표현될 것이다.
선행하는 1은 소수부분에 나타나지 않고 숨겨져 있다는 사실을 기억하라.
1.0×211.0 \times 2^{1}은 이 숫자보다 작은 이진수처럼 보일 것이다.
이상적인 표기법은 가장 음수인 지수를 00…00로 가장 양수인 지수를 11…11로 표현하는 것이다. 이와 같은 방식을 바이어스된 표현법(biased notation)이라고 부른다. 바이어스는 실제 값을 구하기 위해 부호없이 표현된 수에서 빼야 하는 상수를 말한다.
IEEE 754의 단일 정밀도 표현 방식에서는 바이어스 값 127을 사용한다. 따라서 -1은 -1+127 또는 126=0111 1110 이라는 비트 패턴으로 표현된다. 그리고 +1은 1+127 또는 128 = 1000 0000로 표현된다. 2배 정밀도를 위한 지수의 바이어스는 1023이다. 이것은 부동소수점으로 표현된 값이 실제로는 다음과 같음을 의미한다.
(1)s×(1+Fraction)×2(ExponentBias)(-1)^{s} \times (1 + \text{Fraction}) \times 2^{(\text{Exponent} - \text{Bias})}
단일 정밀도에서 표현할 수 있는 가장 작은 수는 다음과 같고
±1.00000000000000000000000×2126\pm 1.00000000000000000000000 \times 2^{-126}
가장 큰 수는 다음과 같다.
±1.11111111111111111111111×2+127\pm 1.11111111111111111111111 \times 2^{+127}

예제) 부동소수점 표현 방식

0.75를 IEEE 754 이진 표현법에 따라 단일 정밀도 및 2배 정밀도 표현 방식으로 나타내라.
0.75는 십진수로
34(322)-{3 \over 4}(-{3 \over 2^{2}})
이다 이것은 이진수로
11two2ten2(0.11two){-11_{two} \over 2_{ten}^{2}} (-0.11_{two})
으로 표현될 수 있다. 과학적 표기법에서 값은
0.11two×20-0.11_{two} \times 2^{0}
이고 정규화된 과학적 표기법에서는
1.1two×21-1.1_{two} \times 2^{-1}
이다. 단일 정밀도 수의 일반적인 표현 방식은 다음과 같다.
(1)s×(1+Fraction)×2(Exponent127)(-1)^{s} \times (1 + \text{Fraction}) \times 2^{(\text{Exponent} - 127)}
따라서 -1.1 x 2-1의 지수에서 바이어스 127을 빼면 결과는
(1)1×(1+.100000000000000000000000two)×2(126127)(-1)^{1} \times (1 + .1000 0000 0000 0000 0000 0000_{two}) \times 2^{(126-127)}
이다. 단일 정밀도 표현에 따른 -0.75의 표현은 다음과 같다.
2배 정밀도 표현은 다음과 같다.
(1)1×(1+.1000000000000000000000000000000000000000000000000000two)×2(10221023)(-1)^{1} \times (1 + .1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000_{two}) \times 2^{(1022-1023)}

예제) 이진 부동소수점 수를 십진 부동소수점 수로 변환하기

아래의 단일 정밀도 부동소수점 수가 나타내는 값을 십진수로 표시하라.
부호 비트는 1, 지수 부분은 129, 소수부분은 1×22=141 \times 2^{-2} = {1 \over 4} 또는 0.250.25이다. 기본 공식을 이용하면 다음과 같다.
(1)s×(1+Fraction)×2(ExponentBias)(-1)^{s} \times (1 + \text{Fraction}) \times 2^{(\text{Exponent} - \text{Bias})}
=(1)1×(1+0.25)×2(129127)= (-1)^{1} \times (1 + 0.25) \times 2^{(129 - 127)}
=(1)1×(1.25)×22= (-1)^{1} \times (1.25) \times 2^{2}
=1.25×4=5.0= -1.25 \times 4 = -5.0

부동소수점 덧셈

부동소수점으로 표현된 수들의 덧셈을 이해하기 위해 먼저 과학적 표기법으로 표시된 두 수 9.999×1019.999 \times 10^{1}1.610×1011.610 \times 10^{-1}을 손으로 더해보자. 유효자리에 네 자리 그리고 지수에 두 자리의 십진수를 저장할 수 있다고 가정한다.
단계 1)
이러한 수를 더하기 위해서는 먼저 작은 지수를 갖는 수의 소수점을 정렬시켜야만 한다. 따라서 작은 수 1.610×1011.610 \times 10^{-1}을 큰 지수와 일치하는 형태로 바꾸어야 한다.
과학적 표기법에서 정규화 되지 않은 부동소수점 수는 다양한 표현이 존재하기 떄문에 이러한 변환이 가능하다.
아래의 오른쪽 수가 우리가 원하는 형태이다. 지수가 큰 수 9.999×1019.999 \times 10^{1}의 지수와 일치하기 때문이다. 따라서 첫 번째 단계는 작은 수의 유효자리를 지수가 큰 수의 것과 일치할 때까지 오른쪽으로 자리이동 시키는 것이다.
1.610×101=0.1610×100=0.01610×1011.610 \times 10^{-1} = 0.1610 \times 10^{0} = 0.01610 \times 10^{1}
단계 2)
아래와 같이 유효자리를 서로 더한다. 합은 10.015×10110.015 \times 10^{1}이 된다.
9.999 + 0.016 ------ 10.015
Plain Text
단계 3)
이 합은 정규화된 과학적 표기법이 아니므로 아래와 같이 정돈할 필요가 있다.
따라서 덧셈을 한 후에 이것을 정규화된 형태로 만들기 위해 지수를 적절하게 조정하고 합을 자리이동시켜야만한다.
이 예는 오른쪽 자리이동을 보여주지만, 한 수가 양수이고 다른 수가 음수이면 결과가 선행하는 0을 가질 수 있고, 이때는 왼쪽 자리이동이 필요하다.
지수가 증가하거나 감소할 때마다 오버플로와 언더플로를 검사하여야 한다. 다시 말해 지수가 지수 필드에 맞는지를 확인해야 한다.
10.015×101=1.0015×10210.015 \times 10^{1} = 1.0015 \times 10^{2}
단계 4)
유효자리가 네 자리라고 가정했기 때문에 수를 자리 맞춤해야 한다. 초등학교에서 배운 것과 같이 원하는 자리 오른쪽에 있는 수가 0과 4사이의 값이면 버리고 5와 9사이의 값이면 1을 더해준다. (반올림)
1.0015×1021.002×1021.0015 \times 10^{2} \to 1.002 \times 10^{2}
그림 3.14는 위의 예와 같이 이진수 부동소수점 덧셈 알고리즘을 보여준다.
단계 1, 2는 앞의 예제와 같다. 작은 지수를 갖는 수의 유효자리를 조정한 후 두 유효자리 수를 더한다.
단계 3은 오버플로와 언더플로를 검사하면서 결과를 정규화한다. 단계 3의 오버플로와 언더플로 검사는 피연산자가 단일 정밀도인지 2배 정밀도인지에 따라 달라진다.
지수 필드가 모두 0인 패턴은 예약되어 있으며 0의 부동소수점 표현을 위해 사용됨을 기억하자. 또한 지수 필드가 모두 1인 패턴은 일반적인 부동소수점 수의 표현을 벗어나는 값들과 상황을 표현하기 위해 예약되어 있다. 따라서 단일 정밀도 표현 방식의 최대 지수는 127이고 최소 지수는 -126이다.
(예제 생략)
많은 컴퓨터는 부동소수점 계산을 가능한 한 빠르게 수행하는 하드웨어를 요구한다. 그림 3.15는 부동소수점 덧셈을 위한 하드웨어의 기본 구조를 보여준다.

부동소수점 곱셈

이제 부동소수점 곱셈을 알아보자. 우선 과학적 표기법의 십진수 곱셈 1.110×1010×9.200×1051.110 \times 10^{10} \times 9.200 \times 10^{-5} 을 직접 손으로 해보자. 유효자리에는 네 자리, 지수에는 두 자리까지 저장할 수 있다고 가정한다.
단계 1)
덧셈과는 달리 피연산자들의 지수를 더하기만 하면 곱의 지수를 구할 수 있다.
새로운 지수 = 10 + (-5) = 5
똑같은 결과를 얻을 것이라는 확신을 가지고 바이어스된 지수에 대해 계산해 보자. 10+127=137이고 -5+127=122. 따라서
새로운 지수 = 137 + 122 = 259
결과는 8비트 지수 부분에 들어가기에 너무 크다. 무엇이 잘못되었을까? 문제는 바이어스이다. 왜냐하면 지수 뿐만 아니라 바이어스도 더했기 때문이다.
새로운 지수 = (10+127) + (-5+127) = (5+2×127) = 259
따라서 바이어스된 수를 더할 때 정확히 바이어스된 결과를 얻기 위해서는 결과에서 바이어스 값을 빼야만 한다.
새 지수 = 137 + 122 – 127 = 259 – 127 = 132 = (5+127)
단계 2)
다음 단계는 유효자리 곱셈이다.
아래의 계산 결과에 대해 각 피연산자의 소수점 오른쪽에 세 자리의 수가 있다. 따라서 결과의 오른쪽으로부터 여섯 번째 자리에 소수점을 찍는다. 10.212000
그리고 소수점 오른쪽 아래에 단지 3개의 수만 갖는다고 가정하면 곱은 10.212×10510.212 \times 10^{5} 가 된다.
1.110 x 9.200 ------ 0000 0000 2220 9990 -------- 10212000
Plain Text
단계 3)
이 곱은 비정규화되어 있으므로 아래와 같이 알맞게 고칠 필요가 있다.
따라서 정규화된 형태로 맞추기 위해 곱셈 후의 결과를 오른쪽으로 한자리 자리이동하고 지수에는 1을 더한다. 이 시점에서 오버플로와 언더플로를 검사할 수 있다. 언더플로는 두 피연산자가 작을 때 발생할 수 있다. 즉 두 수가 매우 큰 음의 지수를 갖는 경우이다.
10.212×105=1.0212×10610.212 \times 10^{5} = 1.0212 \times 10^{6}
단계 4)
부호 부분을 제외하고 유효자리가 단지 네 자리라고 가정했기 때문에 계산된 결과를 자리맞춤 해야 한다.
10.212×1061.021×10610.212 \times 10^{6} \to 1.021 \times 10^{6}
단계 5)
결과의 부호는 원래 피연산자의 부호에 의존한다. 부호가 같으면 결과의 부호는 양수이고, 그렇지 않으면 음수이다 .따라서 최종결과는 다음과 같다.
덧셈 알고리즘에서는 결과의 부호가 유효자리의 덧셈에 의해 결정되었다. 그러나 곱셈에서 결과의 부호는 피연산자들의 부호에 의해 결정된다.
+1.021×106+1.021 \times 10^{6}
그림 3.16에서와 같이 이진수 부동소수점의 곱셈은 방금 살펴본 십진 부동소수점 수의 곱셈과 매우 유사하다. 먼저 바이어스된 지수를 서로 더한 후 바이어스 하나를 빼서 곱의 새 지수를 구한다.
다음 단계는 유효자리들끼리의 곱셈이고, 이어서 필요에 따라 정규화 과정이 수행된다.
계산된 지수의 크기에 대하여 오버플로나 언더플로가 발생했는지 검사한다. 그리고 결과는 자리맞춤된다.
만약 자리맞춤 과정이 추가 정규화를 필요로 하면, 다시 한 번 지수의 크기를 검사한다. 마지막으로 피연산자들의 부호가 다르면 (곱이 음수인 경우) 부호 비트를 1로 결정하고 피연산자의 부호가 같으면 (곱이 양수인 경우) 0으로 결정한다.
(예제 생략)

MIPS의 부동소수점 명령어

MIPS는 다음과 같은 명령어를 사용하여 IEEE 754 단일 정밀도 및 2배 정밀도 표현 형식을 지원한다.
부동소수점 단일 정밀도 덧셈(add.s), 2배 정밀도 덧셈(add.d)
부동소수점 단일 정밀도 뺄셈(sub.s), 2배 정밀도 뺄셈(sub.d)
부동소수점 단일 정밀도 곱셈(mul.s), 2배 정밀도 곱셈(mul.d)
부동소수점 단일 정밀도 나눗셈(div.s), 2배 정밀도 나눗셈(div.d)
부동소수점 단일 정밀도 비교(c.x.s), 2배 정밀도 비교(c.x.d)
여기서 x는 equal(eq), not equal(neq), less than(lt), less than or equal(le), greater than(gt) 또는 greater than or equal(ge) 등이 될 수 있다.
부동소수점 분기, 참일 때 분기(bc1t), 거짓일 때 분기(bc1f)
부동소수점 비교는 비교 조건에 따라서 cc(condition code) 비트를 참(true) 또는 거짓(false)으로 정한다. 분기 명령은 cc 값에 따라서 분기할 것인지 아닌지를 결정한다.
MIPS의 설계자들은 $f0, $f1, $f2, … 라고 불리는 부동소수점 레지스터를 별도로 두기로 하였다. 이들은 단일 정밀도와 2배 정밀도에 모두 사용된다.
이에 따라 설계자들은 부동소수점 레지스터를 위한 적재 및 저장 명령어 lwc1과 swc1을 포함시켰다.
부동소수점 수의 적재와 저장 명령어의 주소 계산에 사용되는 베이스 레지스터로는 정수 레지스터가 사용된다. 두 개의 단일 정밀도 수를 메모리에서 읽어 와서 더한 다음에 합을 다시 저장하는 MIPS 코드는 다음과 같을 것이다.
lwcl $f4,c($sp)  # Load 32-bit F.P. number into F4 lwcl $f6,a($sp) # Load 32-bit F.P. number into F6 add.s $f2, $f4, $f6 # F2 = F4 + F6 single precision swcl $f2,b($sp) # Store 32-bit F.P. number from F2
Plain Text
2배 정밀도 레지스터 하나가 사실은 단일 정밀도 레지스터 한 쌍을 합쳐서 사용하는 것이고, 이때 짝수 번 레지스터 번호를 레지스터 이름으로 사용한다. 따라서 단일 정밀도 레지스터 $f2와 $f3 한 쌍은 2배 정밀도 레지스터 $f2가 된다.
그림 3.17은 3장에서 살펴본 부동소수점 관련 MIPS 아키텍쳐를 보여준다. 2장의 그림 2.17과 같이 그림 3.18은 이 명령어들의 인코딩을 보여준다.
컴퓨터 설계자가 부동소수점 연산 지원과 관련하여 부딪히는 문제 중 하나는 정수 명령어가 사용하는 레지스터를 같이 사용할 것인지 아니면 부동소수점 전용의 특별 레지스터를 추가할 것인지 하는 것이다.
프로그램이 정수 연산을 할 때와 부동소수점 연산을 할 때는 보통 다른 데이터를 사용하므로, 레지스터를 분리하면 프로그램 수행에 필요한 명령어의 수가 약간 증가할 뿐이다.
정작 크게 영향 받는 부분은 부동소수점 레지스터와 메모리 사이의 데이터 이동에 사용할 별도의 데이터 전송 명령어들을 만들어야 한다는 것이다.
별도의 부동소수점 레지스터를 사용하는 것의 장점은 다음과 같다.
첫째, 명령어 형식의 비트 수를 늘리지 않고도 두 배나 많은 레지스터를 사용할 수 있다.
둘째, 정수 레지스터와 부동소수점 레지스터가 따로 있으니 레지스터 대역폭이 두 배로 늘어난다.
셋째, 부동소수점에 맞게 레지스터를 특화시킬 수 있다는 것이다. 예컨대 어떤 컴퓨터는 레지스터에 있는 다양한 크기의 피연산자들을 한 가지 내부 형식으로 변환한다.

예제) 부동소수점 C 프로그램의 MIPS 어셈블리 코드로의 컴파일

화씨 온도를 섭씨로 변환하는 프로그램이 있다.
float f2c (float fahr) { return ((5.0/9.0) * (fahr - 32.0)); }
C
부동소수점 변수 fahr은 $f12로 전달되고 결과는 $f0에 저장된다고 가정하자. (정수 레지스터와 달리, 부동소수점 레지스터 $f0는 임의의 수를 가질 수 있다) MIPS 어셈블리 코드를 작성하라.
컴파일러가 전역 포인터 $gp로 쉽게 접근할 수 있는 거리에 부동소수점 상수 3개를 넣는다고 가정하자. 처음 두 명령어는 상수 5.0과 9.0을 부동소수점 레지스터에 적재한다.
f2c: lwcl $f16, const5($gp) # $f16 = 5.0 (5.0 in memory) lwcl $f18, const9($gp) # $f18 = 9.0 (9.0 in memory)
Plain Text
이들은 5.0/9.0 값을 얻기 위해 나누어진다.
div.s $f16, $f16, $f18  # f16 = 5.0/ 9.0
Plain Text
(많은 컴파일러들은 컴파일 시간에 5.0을 9.0으로 나누고, 상수 5.0/9.0 하나만을 메모리에 저장한다. 이렇게 함으로써 실행 시 나눗셈을 피할 수 있다.)
다음은 상수 32.0을 적재하고 이것을 fahr($12)로부터 뺀다.
lwcl $f18, const32($gp)  # f18 = 32.0 sub.s $f18, $f12, $f18 # f18 = fahr - 32.0
Plain Text
마지막으로 두 개의 중간 결과를 곱해서 $f0에 넣고 그 값을 복귀 값으로 해서 복귀한다.
mul.s $f0, $f16, $f18  # $f0 = (5/9) * (fahr - 32.0) jr $ra # return
Plain Text
(예제 생략)

정확한 산술

가장 작은 수와 가장 큰 수 사이의 모든 수를 정확하게 나타낼 수 있는 정수와는 달리 부동소수점 숫자는 실제로 나타낼 수 없는 수의 근삿값인 것이 보통이다.
그 이유는 0과 1사이에만도 무수히 많은 실수들이 존재하지만, 2배 정밀도 부동소수점 표현방법을 사용하더라도 나타낼 수 있는 값은 2532^{53}개에 불과하기 때문이다.
우리가 할 수 있는 최선의 방법은 실제의 수에 가장 근접한 부동소수점 표현을 구하는 것이다. 이를 위해 IEEE 754는 프로그래머가 원하는 근사 방법을 선택할 수 있도록 여러 가지 자리 맞춤 방법을 제공한다.
자리맞춤(rounding)은 매우 쉬워보인다. 그러나 정확하게 자리맞춤하기 위해서는 계산할 때 하드웨어가 추가 비트를 포함하여야 한다.
앞의 예제들에서는 중간 결과가 몇 비트까지 저장할 수 있는지를 명확하게 정하지 않았다. 만약 모든 중간 결과가 원래 데이터 비트 수만큼만 보존되고 나머지는 다 잘려나간다면 자리맞춤할 기회는 아예 오지 않을 것이다.
그러므로 IEEE 754는 계산하는 동안 오른편에 항상 두 개의 추가 비트를 유지한다. 이를 각각 보호 비트(guard bit)와 자리맞춤 비트(round bit)라고 부른다.

예제) 보호 자리 수를 사용한 자리맞춤

유효자리가 십진수 세 자리라고 가정하고 2.34 x 102에 2.56 x 100을 더하라. 유효자리 숫자 세 자리를 사용하여 가장 가까운 십진수로 자리맞춤하라. 처음에는 보호 자리와 자리맞춤을 사용하여 계산하고 다음에는 이를 사용하지 말고 계산하라.
먼저 지수를 맞추기 위해 작은 수를 오른쪽으로 자리이동하여야 한다. 따라서 2.56 x 100은 0.0256 x 102가 된다. 보호 자리 및 자리맞춤 자리가 존재해야 하기 떄문에 지수를 정렬할 때 두 개의 최하위 자리를 표현할 수 있다. 보호 자리는 5가 되고, 자리맞춤 자리는 6이 된다. 합은 다음과 같다.
2.3400 + 0.0256 ------- 2.3656
Plain Text
따라서 결과는 2.3656 x 102이 된다. 자리맞춤할 두 자리를 갖고 있기 때문에 0부터 49까지는 버리고 51부터 99까지는 반올림한다. 50은 타이브레이커(tie breaker)가 된다. 세 자리 유효숫자로 자리맞춤하면 결과는 2.37 x 102가 된다.
보호 자리와 자리맞춤 자리가 없으면 두 자리를 버리고 덧셈을 해야 한다. 계산한 새로운 결과는 다음과 같다.
2.34 + 0.02 ----- 2.36
Plain Text
따라서 합은 2.36×1022.36 \times 10^{2}이 된다.
자리맞춤 시 최악의 경우는 실제 값이 두 부동소수점 표현의 중간이 될 때이다. 부동소수점에서 정확도는 유효자리의 최하위 비트 중 오류가 발생한 것이 몇 비트인가에 따라 결정된다. 이 척도를 ulp(units in the last place)라고 부른다.
어떤 수가 최하위 유효자리 비트 중 두 개에 오류가 있을 때 2ulp라고 한다. 오버플로, 언더플로, 또는 유효하지 않은 연산의 예외가 없는 한 IEEE 754는 컴퓨터가 1/2 ulp 이내의 수를 사용함을 보장한다.

요약

정보의 의미는 비트를 보는 것만으로는 확인될 수 없다. 왜냐면 같은 비트 패턴이 다양한 개체를 나타낼 수 없기 때문이다. 이 절은 컴퓨터 연산은 유한하고 따라서 실제 연산과 일치하지 않을 수 있다는 것을 보여준다. 예컨대 IEEE 754 표준 부동소수점 표현
(1)5×(1+Fraction)×2(ExponentBias)(-1)^{5} \times (1 + \text{Fraction}) \times 2^{(\text{Exponent} - \text{Bias})}
은 언제나 실수의 근삿값이다. 컴퓨터 시스템은 컴퓨터 연산과 실제 연산 사이의 이러한 차이를 최소화하는데 주의해야 하며, 프로그래머도 이러한 근사화에 관련되는 내용을 알고 있어야만 한다.

병렬성과 산술연산: 서브워드 병렬성

그래픽 디스플레이가 없는 데스크탑 컴퓨터는 없으므로 칩당 트랜지스터가 점점 많아지면서 그래픽 지원 기능이 추가되는 것은 당연한 일이다.
원래 많은 그래픽 시스템이 화소 하나당 32비트(빛의 3원색 각각에 8비트+추가8비트)를 사용하였다.
원격 회의나 비디오 게임을 위해 스피커와 마이크가 추가되면서 소리에 대한 지원도 필요하게 되었다. 소리 샘플은 8비트 이상의 정밀도가 필요한데 16비트 정도면 충분하다.
모든 마이크로프로세서는 바이트나 하프워드가 메모리에 저장될 때 공간을 덜 차지하도록 지원한다. 그러나 일반적인 정수 프로그램에서 이런 크기의 데이터에 대한 산술 연산이 많이 쓰이지 않기 때문에 데이터 전송 이상의 지원은 거의 없었다.
그러다가 많은 그래픽과 오디오 응용 프로그램이 이런 데이터의 벡터에 같은 연산을 반복 수행한다는 것을 컴퓨터 설계자들이 알게 되었다.
128비트 덧셈기 내부의 올림수 체인을 분할하면 프로세서가 병렬성을 활용해서, 8비트 연산자 16개나 16비트 피연산자 8개, 32비트 피연산자 4개, 또는 64비트 피연산자 2개를 동시에 연산할 수 있다. 이렇게 분리되는 덧셈기의 구현 비용은 크지 않다.
큰 워드 내부에 병렬성이 있다고 할 떄, 이것의 확장을 서브워드 병렬성이라고 한다. 더 일반적인 이름으로 데이터 수준 병렬성이라고도 한다.
벡터 또는 SIMD(single instruction multiple data)라고도 한다. 멀티미디어 응용이 많아지면서 작은 데이터에 대한 병렬 연산을 지원하는 산술 명령어가 등장하게 되었다.
예컨대 ARM은 NEON 멀티미디어 명령어 확장에 서브워드 병렬성 지원을 위해 100개 이상의 명령어를 추가하였다. 이것은 ARMv7이나 ARMv8에서 사용할 수 있다.
NEON을 위해 256바이트 레지스터를 새로 추가하였는데, 이것은 8바이트 레지스터 32개로 볼 수도 있고, 16바이트 레지스터 16개로 볼 수도 있다. NEON은 64비트 부동소수점을 제외하고는 모든 서브워드 데이터형을 지원한다.
그림 3.19는 기본 NEON 명령어의 요약이다.

실례: x86의 SSE와 AVX

x86은 원래 MMX(Multi Media eXtension)와 SSE(Streaming SIMD Extension) 명령어는 ARM NEON과 같은 연산들을 포함하고 있었다.
2장에서 살펴본 바와 같이 2001년 Intel은 SSE2의 일부로 2배 정밀도 부동소수점 레지스터와 연산을 포함해서 144개 명령어를 추가하였다. 부동소수점 피연산자를 저장하는 8개의 64비트 레지스터를 두었다.
AMD는 AMD64의 한 부분으로 레지스터 개수를 16개로 늘리고 이를 XMM이라고 하였다. Intel을 이를 사용하면서 EM64T라고 이름을 바꾸었다. 그림 3.20은 SSE와 SSE2 명령어의 요약이다.
단일 정밀도나 2배 정밀도 수를 레지스터에 저장하는 것 외에 Intel은 여러 개의 부동소수점 수 –단일 정밀도 수 4배 또는 2배 정밀도 수 2개– 를 128비트 SSE2 레지스터 하나에 채워 넣을 수 있게 하였다.
그러므로 SSE2를 위한 부동소수점 레지스터 16개의 실제 크기는 128비트이다. 만약 피연산자가 128비트의 정렬된(aligned) 데이터 형태로 메모리에 넣을 수 있으면, 128비트 데이터 전송 명령어 하나로 여러 개의 피연산자를 적재 또는 저장할 수 있다.
이렇게 묶여진(packed) 부동소수점 형식은 동시에 단일 정밀도 수 4개(PS) 또는 2배 정밀도 수 2개(PD)를 연산하는 명령어로 처리한다.
2011년 Intel은 레지스터의 폭을 다시 두 배로 키웠다. 이는 고급 벡터 확장(AVX)과 함께 YMM이라고 불린다. 따라서 단일 연산으로 8개의 32비트 부동소수점 연산 또는 4개의 64비트 부동소수점 연산을 명시할 수 있다.
기존의 SSE와 SSE2 명령어는 YMM 레지스터의 하위 128비트 상에서 연산을 수행한다.
128비트 연산에서 256비트 연산으로 확장되면서 SSE2 어셈블리어 연산 앞에 접두사 v(vector)를 사용하고 XMM 레지스터 이름 대신에 YMM 레지스터 이름을 사용한다.
예컨대 64비트 부동소수점 덧셈 두 개를 수행하는 SSE 명령어는 다음과 같다.
addpd %xmm0, %xmm4
Plain Text
이것이 AVX에서는
vaddpd %ymm0, %ymm4
Plain Text
이 되어 64비트 부동소수점 곱셈 4개를 한 번에 한다.

더 빠르게: 서브워드 병렬성과 행렬 곱셈

서브워드 병렬처리의 성능을 보이기 위해 Intel Core i7에서 같은 프로그램을 AVX 없이 수행하고 다시 AVX를 사용하여 수행한다. 아래 코드는 C 언어로 쓰여진 행렬 곱셈의 최적화되지 않은 버전을 보여준다. 3.5절에서 살펴본 바와 같이 이 프로그램은 DGEMM(Double precision GEneral Matrix Multiply)이라고 불린다.
void dgemm(int n, double* A, double* B, double* C) { for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { double cij = C[i+j*n]; /* cij = C[i][j] */ for (int k = 0; k < n; ++k) { cij += A[i+k*n] * B[k+j*n]; /* cij += A[i][k] * B[k][j] */ } C[i+j*n] = cij; /* C[i][j] = cij */ } } }
C
이번 5판부터 하드웨어상에서 돌아가는 소프트ㅜ에어의 개선을 통하여 얻어지는 성능 향상을 보여주기 위해 ‘더 빠르게’라는 절을 추가하였다. 사용된 마이크로프로세서는 Intel Core i7의 Sandy Bridge 버전이다. 이 새로운 절은 3, 4, 5, 6장에서 제시되며 각 장에 소개하는 아이디어를 사용하여 DGEMM의 점진적인 성능향상을 보여준다.
아래 코드는 위의 C 코드의 내부 루프에 대한 x86 어셈블리어 출력을 보여준다. 5개의 부동소수점 명령어는 AVX 명령어와 같이 v로 시작하지만 YMM 대신에 XMM 레지스터를 사용하고 스칼라 2배 정밀도를 나타내는 sd를 포함하고 있다. 조금 후에 서브워드 병렬 명령어를 소개할 것이다.
movsd (%r10), %xmm0  # Load 1 element of C into %xmm0 mov $rsi, %rcx # register %rcx = %rsi xor %eax, %eax # register %eax = 0 vmovsd (%rcs), %xmml # Load 1 element of B into %xmml add %r9, %rcx # register %rcx = %rcx + %r9 vmulsd (%r8, %rax, 8), %xmml, %xmml # Multiply %xmml, element of A add $0x1, %rax # register %rax = %rax + 1 cmp %eax, %edi # compare %eax to %edi vaddsd %xmml, %xmm0, %xmm0 # Add %xmml, $xmm0 jg 30<dgemm+0x30> # jump if %eax > %edi add $0x1, %r11 # register %r11 = %r11 + 1 vmovsd %xmm0, (%r10) # Store %xmm0 into C element
Plain Text
컴파일러 작성자는 결국에는 x86의 AVX 명령어를 사용하여 일관되게 양질의 코드를 생성할 수 있을 것이다. 현재는 우리가 C instrinsic을 사용하여 컴파일러가 좋은 코드를 생성할 수 있도록 도와주어야 한다. 아래 코드는 위의 C 코드의 개선된 버전이고 Gnu C 컴파일러가 AVX 코드를 생성한다.
#include <x86intrin.h> void dgemm(int n, double* A, double* B, double* C) { for (int i = 0; i < n; i+=4) { for (int j = 0; j < n; ++j) { __m256d c0 = _mm256_load_pd(C+i+j*n); /* c0 = C[i][j] */ for (int k = 0; k < n; ++k) { c0 = _mm256_add_pd(c0, /* c0 += A[i][k] * B[k][j] */ _mm256_mul_pd(_mm256_load_pd(A+i+k*n), _mm256_broadcast_sd(B+k+j*n))); } _mm256_sotre_pd(C+i+j*n, c); /* C[i][j] = c0 */ } } }
C
개선된 C 코드의 6번째 줄에 선언은 _m256d 데이터형을 사용한다. 이는 컴파일러에게 변수가 4개의 2배 정밀도 부동소수점 수를 갖고 있음을 알려준다.
또한 6번째 줄의 _mm256_load_pd()는 4개의 2배 정밀도 부동소수점 수를 행렬 C에서 c0로 병렬(_pd)로 적재하기 위해 AVX 명령어를 사용한다.
6번째 줄의 주소 계산 C+i+j*n은 C[i+j*n]을 나타낸다. 대칭적으로 11번째 줄의 마지막 단계는 c0부터 행렬 C로 4개의 2배 정밀도 부동소수점 수를 저장하기 위해 _mm256_sotre_pd()를 사용한다.
각 반복에서 4개의 원소를 처리하기 때문에 4번째 줄의 외부 for 순환문은 i를 처음 코드의 3번째 줄과 같이 1을 증가시키는 대신에 4만큼 증가시킨다.
순환문 내부에서는 9번째 줄에서 먼저 A의 4개의 원소를 _mm256_load_pd()를 사용하여 적재한다. B의 하나의 원소와 이 원소들을 곱하기 위하여 10번째 줄에서 _mm256_broadcast_sd()를 사용한다. 이것은 스칼라 2배 정밀도 수(이 경우 B의 원소)의 4개의 같은 복사본을 하나의 YMM 레지스터에 만든다.
그리고 9번째 줄에서 _mm256_load_pd()를 이용하여 4개의 2배 정밀도 곱셈을 병렬로 처리한다.
마지막으로 8번째 줄의 _mm256_add_pd()는 4개의 곱을 c0의 4개의 합에 더한다.
아래 코드는 위의 개선된 코드를 gcc에 -O3 수준의 최적화를 사용한 컴파일러의 출력을 주석과 함께 보여준다.
vmovapd (%r11), %ymm0  # Load 4 element of C into %ymm0 mov $rbx, %rcx # register %rcx = %rbx xor %eax, %eax # register %eax = 0 vbroadcastsd (%rax, %r8, 1), %ymml # Make 4 copies of B element add $0x8, %rax # register %rax = %rax + 8 vmulpd (%rcx), %ymml, %ymml # Parallel mul %ymml, 4 A elements add %r9, %rcx # register %rcx = %rcx + %r9 cmp %r10, %rax # compare %r10 to %rax vaddpd %ymml, %ymm0, %ymm0 # Parallel add %ymml, $ymm0 jne 50<dgemm+0x50> # jump if not %r10 != %rax add $0x1, %esi # register %esi = %esi + 1 vmovapd %ymm0, (%r11) # Store %ymm0 into 4 C elements
Plain Text
위 코드는 컴파일러가 생성한 내부 순환문에 대한 x86 코드를 보여준다. 5개의 AVX 명령어가 사용되었음을 알 수 있다. 이들은 모두 v로 시작되며 5개 중 4개는 2배 정밀도를 위해 pd를 사용한다. 이는 위 C 코드의 instrinsic에 상응된다.
이 코드는 처음 코드의 어셈블리 코드와 유사하다. 둘 다 12개의 명령어를 사용하고 정수 명령어는 거의 동일하다 (그러나 다른 레지스터를 사용한다)
부동소수점 명령어의 차이는 XMM 레지스터를 사용하는 스칼라 2배 정밀도(sd) 대신에 YMM 레지스터를 사용하는 병렬 2배 정밀도(pd)를 사용하는 것이다.
한 가지 예외는 4번째 줄이다. A의 모든 원소는 B의 한 개의 원소와 곱해져야만 된다. 한 가지 해결책은 256비트의 YMM 레지스터에 4개의 동일한 복사본인 64비트 B 원소를 채우는 것이다. 이것은 명령어 vbroadcastsd가 수행한다.
32×32 행렬에 대해 최적화되지 않은 DGEMM은 2.6GHz Intel Core i7(Sandy Bridge)의 하나의 코어에서 1.7 GigaFLOPS(FLoating point Operations Per Second)로 수행되었다. AVX 버전은 3.85배 빠르게 수행된다. 이는 서브워드 병렬성을 사용하여 4배의 연산을 수행함으로써 기대할 수 있는 4배의 성능 향상과 거의 같다.

오류 및 함정

연산의 오류와 함정은 일반적으로 컴퓨터 연산의 제한된 정밀도와 실제 연산의 무제한적인 정밀도의 차이에서 유래한다.
오류: 한 비트 왼쪽 자리이동 명령어가 2를 곱해준 것과 같은 결과를 보이듯이 오른쪽 자리이동 명령어는 2로 나누어준 것과 같은 결과를 나타낸다.
이진수 x에서 xi는 x의 i번째 비트를 나타낸다는 사실을 상기하라
...+(x3×23)+(x2×22)+(x1×21)+(x0×20)... + (x_{3} \times 2^{3}) + (x_{2} \times 2^{2}) + (x_{1} \times 2^{1}) + (x_{0} \times 2^{0})
x의 비트를 오른쪽으로 n비트 자리이동한 것은 2n2^{n}으로 나누어 준 것과 같게 되는 것처럼 보인다. 이것은 부호없는 정수(unsigned integer)에서 그러하다. 문제는 부호있는 정수(signed integer)를 사용하는 경우이다.
예컨대 -5를 4로 나눈다고 가정하자. 몫은 -1이어야 한다. -5의 2의 보수법 표현은 다음과 같다.
1111 1111 1111 1111 1111 1111 1111 1011
Plain Text
이러한 오류에 따르면 2비트 오른쪽으로 자리이동한 것은 4(22)4(2^{2})로 나눈 것과 같아야 한다.
0011 1111 1111 1111 1111 1111 1111 1110
Plain Text
부호 비트가 0이 되므로 결과는 확실히 잘못되었음을 알 수 있다. 오른쪽 자리이동으로 만들어지는 수는 실제로 -1 대신에 1,073,741,822이 되었다.
아마도 해결책은 0을 채우는 대신에 부호비트를 채우는 산술적(arithmetic) 우측이동일 것이다. 실제로 -5의 2비트 산출적 우측이동은 다음 값을 갖는다.
1111 1111 1111 1111 1111 1111 1111 1110
Plain Text
그러나 결과는 -1 대신에 -2가 되었다. 근접하지만 맞는 값이 아니다.
함정: 부동소수점의 덧셈은 결합법칙이 성립하지 않는다.
2의 보수 정수 덧셈을 연속적으로 할 때 그 과정에서 오버플로가 발생하더라도 결합 법칙은 성립한다. 그러나 부동소수점 수는 실수의 근사치이고 컴퓨터 연산도 제한된 정밀도를 갖기 때문에 부동소수점 수에서는 성립하지 않는다.
부동소수점으로 표시될 수 있는 수 범위가 매우 크다고 가정하자. 문제는 부호가 다른 두 개의 매우 큰 수의 덧셈에 어떤 작은 수를 더할 때 발생한다.
예컨대 c+(a+b)=(c+a)+bc + (a + b) = (c + a) + b 를 살펴보자. c=1.5×1038,a=1.5×1038,b=1.0c = -1.5 \times 10^{38}, a = 1.5 \times 10^{38}, b = 1.0이고 모두 단일 정밀도를 갖는 실수라 가정하자. 결과는 다음과 같다.
c+(a+b)=1.5×1038+(1.5×1038+1.0)=1.5×1038+(1.5×1038)=0.0c + (a + b) = -1.5 \times 10^{38} + (1.5 \times 10^{38} + 1.0) = -1.5 \times 10^{38} + (1.5 \times 10^{38}) = 0.0
(c+a)+b=(1.5×1038+1.5×1038)+1.0=0.0+1.0=1.0(c + a) + b = (-1.5 \times 10^{38} + 1.5 \times 10^{38}) + 1.0 = 0.0 + 1.0 = 1.0
부동소수점 수는 정밀도에 한계가 있고, 실제 결과의 근삿값을 결과로 취한다. 따라서 1.5×10381.5 \times 10^{38}1.01.0보다 매우커서 1.5×1038+1.01.5 \times 10^{38} + 1.0은 여전히 1.5×10381.5 \times 10^{38}이 된다. 이것이 부동소수점 덧셈의 순서에 따라 a, b, c의 합이 0.0 또는 1.0이 나오는 이유이다.
따라서 c+(a+b)(c+a)+bc + (a + b) \neq (c + a) + b 이다. 부동소수점 덧셈은 결합법칙이 성립하지 않는다.
오류: 정수 데이터형에서 사용되는 병렬 수행 방식은 부동소수점 데이터형에도 똑같이 적용된다.
프로그램은 일반적으로 병렬로 수행되는 것을 작성하기 전에 순차적 수행 버전이 작성된다. 따라서 두 버전이 같은 결과를 얻는가가 중요하다. 만약 같지 않으면, 병렬 버전이 문제가 있다고 생각할 수 있다.
이 방식은 컴퓨터 연산이 순차적 수행에서 병렬 수행으로 바뀌어도 결과에 영향을 미치지 않음을 가정한다. 즉 100만개의 수를 더한다면 한 개의 프로세서를 사용한 것과 1000개의 프로세서를 사용한 것은 같은 결과를 얻게 될 것이다.
2의 보수법을 사용하는 정수이면 정수 덧셈은 결합법칙이 유효하므로 이 가정은 항상 옳다. 그러나 부동소수점 덧셈은 결합법칙이 성립하지 않으므로 이 가정은 옳지 않게 된다.
어떤 프로그램들이 같이 실행되느냐에 따라 병렬 컴퓨터의 운영체제 스케줄러가 프로세서 수를 다르게 할당하면 더 심각한 문제가 발생한다. 실행할 때마다 프로세서 수가 달라지면 매번 부동소수점 합 계산 순서가 달라져서 같은 입력에 같은 프로그램이 수행되더라도 약간씩 다른 결과가 나오므로 이를 잘 모르는 병렬 프로그래머는 당황스러울 것이다.
이런 문제가 있으므로 부동소수점 수를 다루는 병렬 코드를 작성하는 프로그래머는 병렬 프로그램의 결과가 순차 프로그램과 똑같지 않을 때 이 값이 과연 믿을만한 것인지 검증할 필요가 있다.
이런 문제를 다루는 분야를 수치해석이라고 한다. LAPACK이나 SCALAPAK과 같이 순차형과 병렬형이 모두 검증된 수치 라이브러리가 널리 쓰이는 이유 중 하나가 이것 때문이다.
함정: MIPS 명령어 addiu(add immediate unsigned)는 16비트의 수치 필드(immediate field)를 부호확장해서 사용한다.
이름에도 불구하고 addiu는 오버플로에 대해 신경 쓰지 않을 때 상수를 부호 있는 정수에 더하는데 사용된다. MIPS는 수치를 사용하는 뺄셈 명령어를 지원하지 않으므로 음수는 부호확장되어야 한다. 따라서 MIPS 구조는 수치 필드를 부호확장하여 사용한다.
오류: 이론 수학자만이 부동소수점 연산의 정확성에 신경을 쓴다.
그림 3.25가 이 주장이 오류임을 보여준다. 다음은 이 헤드라인의 실제 뒷 이야기들이다.
Pentium은 매 단계마다 몫 비트 여러 개를 찾아내는 표준 부동소수점 나눗셈 알고리즘을 사용한다. 제수와 피제수의 최상위 비트들을 사용하여 몫의 다음 두 비트를 추측하는데, 이 추측은 -2, -1, 0, +1, +2를 갖고 있는 참조(lookup) 표에서 구한다.
이 추측 값에 제수를 구하고 이 값을 나머지에서 빼면 새 나머지가 구해진다. 비복원 나눗셈과 같이 이전의 추측이 너무 큰 나머지를 생성하면 다음 단계에서 부분 나머지(partial remainder)를 조정한다.
80486의 표에는 Intel 엔지니어들이 절대 사용되지 않을 것으로 생각한 원소가 5개 있었다. Intel이 Pentium을 만들면서 이것을 2대신 0이 되게 PLA를 최적화하였다. Intel이 오류를 범한 것이다. 결과 중 앞부분 11비트는 항상 맞지만, 12번쨰와 52번째 비트 사이(십진수로 따지면 4-15번째 자리 사이)에서 가끔씩 에러가 발생했다.
버지니아 주 Lynchburg Colleage 수학교수 Thomas Nicely가 94년 9월에 버그를 발견하고 Intel의 기술지원부에 전화를 했으나 공식적인 답변을 듣지 못하게 되었고 그는 이 사실을 인터넷에 올렸다.
Intel은 이 버그를 이론 수학자에게만 영향을 미칠 사소한 결함이라고 했다. 일반 스프레드시트 사용자는 27,000년에 한 번 꼴로 에러가 발생할 것이라고 하였다.
IBM 연구자들은 곧 일반 스프레드시트 사용자가 24일마다 한 번씩 에러를 보게 될 것이라고 주장했고, Intel은 12월 21일 사과하고 리콜을 발표 했다. 분석가들은 이 리콜이 Intel에 5억 달러의 부담을 안겨 줄 것이라고 예측했다.

결론

수 세기 동안 컴퓨터 연산은 표준화가 많이 진행되었으며 프로그램의 이식성 향상에 크게 기여하였다. 2의 보수법 이진 정수 연산은 오늘날 팔리는 모든 컴퓨터에서 사용되고 있다. 부동소수점 연산을 지원하는 컴퓨터에서는 IEEE 754 이진 부동소수점 연산이 사용된다.
컴퓨터 연산은 제한된 정밀도 때문에 종이와 연필을 사용하는 연산과는 다르다. 이 제한은 미리 정의된 수의 한계보다 더 큰 수나 더 작은 수가 발생할 경우에 잘못된 연산 결과를 갖게 된다. 오버플로나 언더플로라고 불리는 이 문제는 계획되지 않은 서브루틴 호출과 같이 위험한 상황에 해당되므로 예외나 인터럽트를 발생시키게 된다.
부동소수점 연산은 실제 수의 근사치를 사용하게 되므로 선택된 값이 실제 값에 가장 가까운 표현임을 보장하기 위해 주의가 필요하다. 부동소수점 수의 부정확하고 제한된 표현 때문에 수치해석 분야가 생기게 되었다.
최근 병렬처리로의 변화가 수치해석 분야를 새롭게 주목받게 하였다. 순차 컴퓨터에서 안전하다고 오랜 기간 동안 여겨진 해답이 병렬 컴퓨터에서 수행되는 빠른 알고리즘에서도 정확한 결과를 제공해야 하기 떄문이다.
데이터 수준의 병렬성, 특히 서브워드 병렬성은 정수 또는 부동소수점 데이터에 산술연산이 집중적으로 사용되는 프로그램에서 더 높은 성능을 얻을 수 있도록 해준다. 행렬 곱셈을 수행함에 있어 한 번에 4개의 부동소수점 연산을 수행하는 명령어를 사용하여 거의 4배의 성능 향상이 생기는 것을 보여주었다.
이 장에서 컴퓨터 연산에 대한 설명과 함께 많은 MIPS 명령어 집합에 대한 설명도 추가하였다. 한 가지 혼란스러운 점은 이 장에서 사용된 명령어 MIPS 칩에서 수행되는 명령어, 그리고 MIPS 어셈블러가 받아들이는 명령어의 차이점이다. 두 개의 그림이 이를 명확하게 할 것이다.
그림 3.26은 2장과 3장에서 사용된 MIPS 명령어를 보여준다. 좌측은 MIPS 코어(MIPS Core)라고 칭하는 명령어를, 우측은 MIPS 산술 코어 (MIPS arithmetic core) 명령어를 보여준다.
그림 3.27의 좌측에는 그림 3.26에는 없는 MIPS 프로세서가 수행하는 명령어가 나와 있는데 이를 하드웨어 명령어의 전체 집합을 의미하는 MIPS-32라고 부른다. 그림 3.27의 우측은 MIPS-32의 일부가 아닌 어셈블러가 받아들이는 명령어 집합이다. 이는 유사 MIPS(Pseudo MIPS) 명령어 집합이라고 부른다.
그림 3.28은 SPEC CPU 2006 정수와 부동소수점 벤치마크 프로그램에 사용된 MIPS 명령어의 사용 빈도를 보여준다. 수행된 명령어의 최소 0.2%를 넘는 모든 명령어가 표시되어 있다.
프로그래머와 컴파일러는 MIPS-32의 더 다양한 선택 사항을 사용할 수 있음에도 불구하고 MIPS 코어 명령어가 정수 SPEC CPU 2006 수행에서 주로 사용되었고, SPEC CPU 2006 부동소수점 벤치마크에서도 MIPS 코어 및 산술 코어 명령어가 주로 사용되었음을 아래의 표에서 알 수 있다.
Search
Instruction subset
Integer
Fl. pt.
MIPS arithmetic core
Open
2%
66%
Remaining MIPS-32
Open
0%
3%
이 책의 나머지 부분에서는 컴퓨터 설계를 더 쉽게 설명하기 위해 곱셈과 나눗셈을 제외하고 정수 명령어 집합만을 고려하는 MIPS 코어 명령어에만 집중하기로 한다. 보는 바와 같이 MIPS 코어는 자주 사용되는 대부분의 MIPS 명령어를 포함하고 있다.
MIPS 코어를 수행하는 컴퓨터를 이해하는 것만으로도 훨씬 더 복잡한 컴퓨터를 이해하는데 있어서 충분한 기본을 제공할 수 있다. MIPS, ARM, x86 등 어떤 명령어 집합과 명령어 크기를 사용한다고 하더라도 비트 패턴만으로는 어떤 의미가 없음을 주목하라.
같은 비트 패턴이 부호 있는 정수, 부호 없는 정수, 부동소수점 수, 문자열, 명령어 등을 나타낼 수 있다.
내장 프로그램 컴퓨터에서는 비트 패턴에 의미를 부여하는 것은 연산이다.