| <bgcolor=#96834a> 명령어 집합 | |
| CISC | x86 (AMD64● · IA-32● · IA-16) · M68K · MC68xx · MOS 65xx · Z80 · 8080 · 8051 System/360 · z/Architecture · PDP-11 · VAX |
| RISC | Arm (A64 · A32 · T32) · RISC-V● · MIPS● · DEC Alpha● · Power ISA IBM Power · PowerPC · CELL-BE LoongArch · OpenRISC · PA-RISC · SPARC SuperH · Xtensa · AVR AVR32 |
| VLIW EPIC | E2K · IA-64 · Crusoe |
| <rowcolor=#fff> x86 · AMD64 확장 명령어 집합 | |
| 인텔 주도 확장 명령어 | |
| 범용 | |
| SIMD | AVX-512: F · CD · DQ · BW · VL · IFMA · VBMI · VBMI2 · VNNI · VAES · GFNI · BITALG AVX[3]: AVX-VNNI · AVX-IFMA AVX10 AVX10.1 · AVX10.2 |
| 비트 조작 | BMI1 · BMI2 · ADX |
| 보안 및 암호 | AES-NI · CLMUL · RDRAND · RDSEED · SHA · MPX · SGX · TME · MKTME |
| 가상화 및 기타 | VT-x(VMX) · SMX · TSX-NI[TSXLDTRK] · TSXLDTRK |
| 시스템 및 인터럽트 | IDT[FRED] · FRED · CPUID |
| AMD 주도 확장 명령어 | |
| SIMD 및 비트 연산 | |
| 비트 조작 | ABM · POPCNT · LZCNT |
| 보안 및 암호 | SME |
| 가상화 및 기타 | AMD-V · |
| 시스템 및 인터럽트 | |
| 공동 표준 (x86 Ecosystem Advisory Group) | |
| SIMD | ACE |
| 보안 및 암호 | ChkTag |
[SSE2] SSE2로 대체 권장[SSE2] [3] 512-bit EVEX 인코딩된 AVX-512 명령어의 256-bit VEX 인코딩 버전[TSXLDTRK] 대체권장[FRED] 대체권장[SSE2] [TSXLDTRK] 대체권장[FRED] | |
1. 개요
x87은 인텔이 x86 아키텍처용으로 설계한 부동소수점 연산 명령어 집합이자, 이를 처리하던 보조 프로세서(코프로세서) 계열의 명칭이다.1980년 출시된 인텔 8087을 시초로 하며, UC 버클리의 수치해석학자 윌리엄 카한(William Kahan)이 설계 자문으로 참여하여 데이터 타입과 예외 처리 체계를 정의하였다. 이 설계는 IEEE 754 부동소수점 표준의 근간이 되었다. 내부 연산 형식으로 독자(비표준) 형식인 80비트 확장 정밀도(64비트 가수부, 15비트 지수부, 1비트 부호)를 사용하며, 8단 깊이의 레지스터 스택으로 데이터를 관리한다.
80486DX(1989)부터 CPU 다이에 통합되었고, 이후 SSE, AVX가 등장하면서 사실상 대체되었으나 AMD64에서도 하위 호환용으로 유지되고 있다.
2. 역사
x87의 개발은 1976년 인텔이 차세대 프로세서인 8086과 iAPX 432에 공통으로 적용할 부동소수점 연산 체계를 마련하려는 계획에서 출발했다.1970년대 당시에는 컴퓨터마다 부동소수점 형식과 반올림, 예외 처리 방식이 달랐고, 수학 라이브러리의 품질도 제각각이었다. 프로그램을 다른 기종으로 옮기려면 해당 기계의 특이한 산술 동작을 파악하고 우회해야 했으므로, 신뢰할 만한 수치 소프트웨어를 개발하고 유지하는 데 큰 비용이 들었다. x87의 목표는 이러한 문제가 발생하지 않도록 일관되고 예측 가능한 산술을 설계하고, 기본 연산뿐 아니라 수학 라이브러리의 주요 기능까지 하나의 칩에서 제공하는 것이었다. 이 작업은 이후 IEEE 754 표준화의 중요한 기반이 되었다.
2.1. 8087
1980년 인텔이 인텔 8086/8088용 부동소수점 보조 프로세서로 출시한 제품이다. 당시 x86 본체에는 정수 ALU만 탑재되어 있었기 때문에, 부동소수점 연산이 필요한 환경에서는 별도의 칩을 메인보드 소켓에 장착해야 했다.부동소수점 연산은 과학 계산, 공학 시뮬레이션, 3D, 그래픽 등 한정된 분야에서만 사용되었고 칩 단가도 비쌌기 때문에 일반 사용자는 코프로세서 없이 x86 시스템을 운용하는 경우가 대부분이었다. 코프로세서가 없어도 소프트웨어 에뮬레이션을 통해 부동소수점 연산은 가능했으나 속도가 크게 떨어졌다.
2.2. 80287, 80387
이후 출시된 x86 프로세서에 맞추어 인텔 80286용 80287, 인텔 80386용 80387이 차례로 출시되었다. 이 시기에는 서드파티 제조사들도 x87 호환 명령어 집합을 구현한 자체 코프로세서를 제작하였다.80287은 286 보호 모드와 호환을 위해 기존 8087과 통신 방식이 달라져 8086/8088과는 하드웨어적으로 호환되지 않았다. 반면 80386과는 호환되어 386 시스템에 80287을 장착하는 식으로 하위 버전 코프로세서를 혼용하는 구성도 가능했다.
80387은 x87 계열에서 IEEE 754-1985를 완전히 준수한 최초의 제품이다. FSIN, FCOS, FSINCOS 등 삼각 함수 명령어가 이 세대에서 추가되어, 기존의 FPTAN과 FPATAN만으로 삼각 함수를 합성해야 했던 불편이 해소되었다. 또한 NaN을 고려한 비교 명령인 FUCOM 등도 도입되었다.
2.3. 80486 통합과 487SX
1989년 출시된 인텔 80486 DX부터 x87 유닛이 CPU 내부에 집적되었다. 공정 미세화로 FPU를 같은 다이에 올릴 수 있게 되었으나 다이 면적 증가로 가격이 크게 상승하였고, 인텔은 FPU를 비활성화한 보급형 486SX와 그 짝이 되는 487SX를 함께 출시하였다.487SX는 형식상 코프로세서였으나 실제로는 FPU가 포함된 486DX의 재포장 버전으로, 장착 시 486SX의 CPU를 정지시키고 모든 명령을 자신이 처리하는 구조였다. 시장에서는 같은 클럭의 486SX와 486DX의 성능 차이가 크지 않았음에도 소비자 다수가 486DX를 선택하였고, 487SX는 큰 호응을 얻지 못하였다.
2.4. 펜티엄 시대
1993년 출시된 펜티엄(P5)에서는 FPU가 파이프라인화되었다. 80486의 FPU가 한 명령의 실행이 끝나야 다음 명령을 실행할 수 있던 것과 달리 펜티엄은 덧셈(FADD)과 곱셈(FMUL)을 파이프라인 구조로 실행했고, 나눗셈은 Radix-4 처리를 도입해 레이턴시를 80486의 절반으로 줄였다.또한 펜티엄에서는 스택 최상단을 바꾸는 FXCH를 다른 부동소수점 명령과 같은 클럭에 동시 실행할 수 있게 하여 사실상 공짜로 만들었고, 이때부터 컴파일러는 FXCH를 적극적으로 활용해 스택을 레지스터처럼 쓰는 코드를 생성하게 되었다.
1994년 펜티엄 FPU의 나눗셈 유닛 설계 오류로 발생한 FDIV 버그가 발견되었고, 인텔은 거액을 들여 리콜을 실시해야 했다.
1995년 출시된 펜티엄 프로는 비교 결과를 EFLAGS에 직접 기록하는 FCOMI/FCOMIP/FUCOMI/FUCOMIP와 조건부 이동 FCMOVcc를 추가했다.
이후 1997년 출시된 펜티엄 II에서는 x87 상태 저장/복구용 FXSAVE/FXRSTOR 명령어가 추가되었다.
2.5. SSE로의 대체
1999년 발표된 SSE가 128비트 전용 SIMD 레지스터(XMM)를 도입했으나 단정밀도(32비트) 연산만 지원하여 x87을 곧바로 대체하지는 못했다.2000년의 SSE2는 배정밀도(64비트) 부동소수점까지 SSE 레지스터에서 처리할 수 있도록 확장되어 x87을 사실상 대체하였고, 이후 AVX가 그 뒤를 이었다.
AMD64에서는 x87을 하위 호환용으로만 유지하고 추가 확장을 하지 않는 방향으로 정리되었다. 64비트 호출 규약에서도 부동소수점 인자 전달과 반환은 XMM 레지스터를 사용하며, x87 스택은 레거시 코드나 확장 정밀도가 필요한 경우 외에는 거의 사용되지 않는다.
단, 2004년 발표된 SSE3에서 마지막 x87 명령어인 FISTTP가 추가되었다.
그러나 64비트와 달리 32비트 바이너리에서는 2020년대에도 여전히 SSE2로 완전히 대체되지 못하고 아직까지 x87이 사용되고 있다.
3. 상세
3.1. 프로그래밍 모델
x87은 기본적으로 코프로세서 구조로 설계되었기 때문에 x86 실행과는 비동기로 동작하나, 486 이후 프로세서에 통합되면서 대체로 명시적인 동기화의 생략이 가능해졌다.데이터 처리를 위해 8개의 80비트 물리 레지스터(R0~R7)를 스택 구조로 운용한다. 프로그래머는 물리 레지스터 번호가 아닌 스택 최상단(TOS)으로부터의 상대 위치인 ST(0)~ST(7)로 레지스터를 참조한다.
스택 모델이 채택된 주된 이유는 8086의 구조 및 ESC용으로 배정된 opcode 수에 따라 명령어 인코딩 공간이 부족했기 때문이다. 8087은 x86의 ESC 명령어 메커니즘을 통해 동작하는데, 첫 바이트에서 ESC 접두사(11011₂)가 5비트를 차지하여 3비트(=8개) opcode만 할당되었으며, ModRM 공간을 추가로 사용하더라도 모드 지정에 2비트가 고정되었기 때문에[1] 2바이트 길이 내에서 남은 인코딩 공간이 극히 제한적이었다. 따라서 ST(0)을 암시적 피연산자로 두는 스택 모델은 피연산자 하나를 3비트로 지정할 수 있어 불가피한 선택이었다. 그러나 이 결정은 이후 컴파일러 구현 난이도를 높이게 되었고, 사후 평가에서 설계상 실수로 여겨지게 되었다.
코프로세서 시절의 영향으로 i686 이전 사양에서는 조건 분기 등의 처리가 번거로웠다. FCOM 계열 비교 명령의 결과는 EFLAGS가 아닌 FPU 상태 워드의 조건 코드(C0~C3)에 기록되므로, 비교 결과에 따라 분기하려면 FSTSW AX로 상태 워드를 범용 레지스터에 복사한 뒤 SAHF로 플래그 레지스터에 옮기는 복잡한 절차가 필요했다. EFLAGS에 직접 기록하는 FCOMI/FUCOMI와 조건부 이동 FCMOVcc는 1995년 출시된 펜티엄 프로(P6)에 와서야 추가되었다.
[1] 이는 8086의 메모리 처리 특성상 모드 종류에 따라 주소 계산 및 버스 요청을 수행하는 마이크로코드가 자동으로 호출되었기 때문이다.
3.2. 예외 처리
x87은 잘못된 연산(invalid operation), 비정상수 피연산자, 0으로 나눔, 오버플로, 언더플로, 부정확한 결과(inexact result)에 대응하는 예외 상태를 제공한다. 이 가운데 부정확한 결과는 계산 결과를 표현 가능한 값으로 반올림하면서 정보가 손실되었음을 뜻하며, 프로그램의 오류를 의미하는 것은 아니다. 스택 넘침과 비어 있는 항목에 대한 접근 또한 invalid operation으로 처리되었다.예외가 발생하면 대응하는 상태 플래그가 설정되며, CPU에 예외를 전달하거나 예외를 마스크한 경우 연산을 계속한 다음 예외 발생 여부를 검사하는 방식으로 처리할 수 있다. 이때 플래그는 정상적인 명령을 실행할 때마다 자동으로 지워지지 않으므로, 프로그램은 여러 연산을 수행한 뒤 예외 발생 여부를 검사할 수 있다.
3.3. 동기화
8086/8088은 ESC 명령어 실행 시 8087의 연산 완료를 자동으로 기다리지 않았다. 일반적인 수치 명령을 연속해서 실행하려면 앞선 연산이 끝났음을 보장해야 했으며, 어셈블러는 대부분의 수치 명령 앞에WAIT를 자동으로 삽입했다.80286부터는 CPU가 대부분의 ESC를 시작하기 전에 BUSY를 확인하여 명령 동기화를 수행하도록 바뀌었다. 다만 CPU와 코프로세서 사이에서 메모리 값을 주고받을 때의 완료 대기는 여전히 따로 고려해야 했다.
3.4. 80비트 확장 정밀도
x87의 80비트 확장 정밀도 형식은 1비트 부호, 15비트 지수, 64비트 가수부로 구성된다. 단정밀도(32비트)와 배정밀도(64비트)가 정규화된 수의 가수부 최상위 1을 생략하는 숨김 비트(hidden bit) 방식을 사용하는 것과 달리, 확장 정밀도는 정수 비트(J-bit)를 명시적으로 저장한다. 이 설계 덕분에 숨김 비트를 복원하는 연산이 불필요하고, 언더플로로 인한 정밀도 손실을 후속 연산에서 추적할 수 있다.3.5. 명령어 목록
#!if (문단 == null) == (앵커 == null)
를#!if 문단 != null & 앵커 == null
의 [[x87/명령어 목록#s-|]]번 문단을#!if 문단 == null & 앵커 != null
{{{#!if 문서명 = 문서명 != null ? 문서명 : calleeTitle
의 [[x87/명령어 목록#|]] 부분을}}} 참고하십시오.4. 문제점 및 한계
4.1. 결과의 비결정성
x87의 가장 근본적인 문제는 동일한 소스 코드에서도 조건에 따라 다른 수치 결과가 나올 수 있다는 점이다. x87은 모든 연산을 80비트 확장 정밀도로 수행하지만 메모리에 저장할 때는 32비트 또는 64비트로 반올림한다. 컴파일러의 레지스터 할당에 따라 어떤 변수가 레지스터(80비트)에 머무르고 어떤 변수가 메모리(64비트)에 임시 저장되느냐가 달라지므로, 최적화 수준을 바꾸는 것만으로 연산 결과가 달라질 수 있다. GCC 버그 #323은 이 현상의 대표적 사례로, 같은 변수를 비교하는a == a가 거짓을 반환하는 상황이 보고되었다.운영 체제 간 기본 설정 차이도 재현성을 떨어뜨렸다. Windows의 MSVC는 프로그램 시작 시 정밀도 제어(PC)를 53비트로 설정하는 반면 Linux의 glibc는 64비트를 유지하여, 동일 프로그램이 플랫폼에 따라 다른 결과를 낼 수 있었다. 32비트(x87)와 64비트(SSE2) 빌드 간 수치 불일치도 같은 맥락이다.
이러한 비결정성은 SSE2가 선언된 타입 (32비트/64비트) 그대로 연산을 수행하게 되면서 근본적으로 해소되었다. AMD64가 SSE2를 필수 사양으로 포함한 이후 64비트 환경의 컴파일러는 x87 대신 SSE2를 기본으로 사용하므로, 이 문제는 원천적으로 발생하지 않는다.
4.2. 표준 준수의 한계
x87은 IEEE 754 표준의 모태가 된 설계이지만, 정작 x87 자체는 이 표준의 배정밀도(binary64) 연산을 엄밀히 재현하기는 어렵다.IEEE 754 배정밀도(binary64)는 가수부 53비트, 지수부 11비트(범위 ±1023)로 구성된다. x87 제어 워드의 정밀도 제어(PC) 필드를 53비트로 설정하면 가수부는 배정밀도와 같은 폭으로 반올림되지만, 지수부는 PC와 무관하게 항상 15비트(범위 ±16383)를 유지한다. 따라서 PC 설정만으로는 binary64 연산을 정확히 재현할 수 없으며, 다음과 같은 불일치가 발생한다.
- 오버플로: binary64에서 약 1.8 × 10308을 넘는 결과는 무한대([math(∞)])로 처리되지만, x87 레지스터에서는 약 1.2 × 104932까지 정상 값으로 유지된다. 64비트 메모리에 저장하는 시점에야 비로소 무한대로 변환된다.
- 언더플로: binary64에서 약 2.2 × 10−308 미만의 값은 비정규수가 되거나 0으로 밀리지만, x87 내부에서는 약 3.4 × 10−4932까지 정규수로 유지된다. 저장 시점에야 비정규수나 0으로 변환된다.
이러한 "지연된 오버플로/언더플로"로 인해 x87 레지스터에 머무르는 중간 결과와 메모리에 저장된 결과가 서로 다른 경로를 밟게 되고, 처음부터 binary64로 계산한 결과와 달라질 수 있다. 한편 SSE2에서는 배정밀도(64비트) 또는 단정밀도(32비트) 단위로 직접 연산하여 지수 범위 불일치가 존재하지 않으므로, 별도의 우회 없이 IEEE 754를 준수할 수 있다.
Java의 HotSpot JVM은 strictfp(엄격한 IEEE 754 표준 준수) 구현을 위해 연산마다 메모리 왕복 또는 스케일링 우회를 삽입해야 했으며, 이 비용이 JDK 1.2에서 엄격한 부동소수점 의미론을 완화하는 직접적 계기가 되었다.
4.3. 초월함수의 정확도 문제
FSIN과 FCOS, FSINCOS의 다항 근사에 사용되는 내부 원주율 상수가 66비트에 불과하여, 원주율 [math(π)]의 배수에 가까운 입력에서 최악 약 1.3 × 1018 ULP의 오차가 발생할 수 있다는 점도 널리 알려진 한계이다. 인텔은 오랫동안 문서에 최대 1 ULP 오차를 명시하고 있었으나 2014년 이를 공식 인정하고 문서를 개정하였다.SSE 이후의 SIMD 확장은 삼각 함수나 로그 같은 초월 함수를 하드웨어 명령어로 제공하지 않으며, 현대 수학 라이브러리(glibc libm, Intel SVML 등)는 x87 초월 함수 명령어를 사용하지 않고 소프트웨어로 CORDIC 알고리즘을 직접 구현한다.
5. 여담
- 8087 이전에도 인텔은 실수 연산 칩인 8231/8232를 판매하였으나, 이 칩은 자사 설계가 아닌 AMD의 Am9511 및 Am9512의 설계를 라이선스받아 만든 호환 칩이었다.
- IBM PC 호환기종에서 I/O 주소 00F0h~00FFh가 코프로세서용으로 예약되어 있는 것은 80286이 80287과 통신하던 포트의 흔적이다. IBM PC AT는 00F0h에 쓰면 코프로세서 BUSY 래치를 해제하고, 00F1h에 쓰면 코프로세서를 리셋하도록 했다.
6. 관련 항목
- x86
- MMX
- 스트리밍 SIMD 확장(SSE)
- 고급 벡터 확장(AVX)