Skip to content

언리얼 렌더링 시스템 분석(19) - 컴퓨터 하드웨어 시스템

🌐 원문 링크: 剖析虚幻渲染体系(19)- 计算机硬件体系 (cnblogs.com/timlly)
📅 원문 발행일: 2022-12-11 | ✍️ 저자: Timlly (00 / )

💡 시리즈 분류: Hardware & Architecture국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료


이전 기사에서는 다양한 하드웨어와 기술을 광범위하게 다루었습니다. 본 논문에서는 컴퓨터의 하드웨어 구성과 아키텍처를 보다 종합적이고 체계적이며 심층적으로 설명함으로써 하향식 컴퓨터 신체 지식 시스템을 형성할 것이다. 본 글에서는 주로 다음 내용을 설명합니다.

  • 컴퓨터 기초.

  • 전자회로의 기초.

  • 컴퓨터 하드웨어.

  • 컴퓨터 아키텍처 및 조직.

  • 컴퓨터 하드웨어의 작동 메커니즘.

  • 컴퓨터 하드웨어의 UE 패키징 및 구현.

19.1.1 컴퓨터란 무엇입니까

컴퓨터란 무엇입니까? 컴퓨터는 정보를 처리하고 의미 있는 결과를 생성하도록 프로그래밍할 수 있는 범용 장치입니다. 널리 사용되며 작업을 더 쉽게 해주는 장치입니다. 필요한 결과를 얻기 위해 수행할 지침이나 작업을 입력해야 하는 수동적인 기계입니다.

기본 컴퓨터.

아래에는 전통적인 단일 프로세서 컴퓨터의 내부 구조에 대한 계층화된 보기를 제공하는 간단한 단일 프로세서 컴퓨터가 나와 있습니다. CPU, 메인 메모리, I/O 및 시스템 링크의 네 가지 주요 구조 구성 요소가 있습니다.

아래 그림과 같이 사용자에게 애플리케이션을 전달하는 데 사용되는 하드웨어와 소프트웨어를 계층적 또는 계층적으로 볼 수 있습니다. 이러한 애플리케이션의 사용자인 최종 사용자는 일반적으로 컴퓨터의 아키텍처에 관심이 없으므로 최종 사용자는 컴퓨터 시스템을 애플리케이션 측면에서 봅니다. 애플리케이션은 프로그래밍 언어로 표현될 수 있으며, 애플리케이션 프로그래머가 개발할 수 있습니다. 컴퓨터 하드웨어 제어만을 담당하는 일련의 프로세서 명령으로 애플리케이션을 개발하는 것은 매우 복잡한 작업입니다. 이 작업을 단순화하기 위해 일련의 시스템 프로그램이 제공됩니다.

이러한 프로그램 중 일부를 유틸리티라고 합니다. 이는 프로그램 생성, 파일 관리 및 I/O 장치 제어를 용이하게 하는 일반적으로 사용되는 기능을 구현합니다. 프로그래머는 런타임에 특정 기능을 수행하기 위해 유틸리티를 호출하는 애플리케이션을 개발할 때 이러한 도구를 사용합니다. 가장 중요한 시스템 프로그램은 운영 체제입니다. 운영 체제는 프로그래머에게 하드웨어의 세부 사항을 숨기고 프로그래머에게 시스템 사용에 편리한 인터페이스를 제공합니다. 프로그래머와 애플리케이션이 이러한 시설과 서비스에 더 쉽게 액세스하고 사용할 수 있도록 중개자 역할을 합니다.

하드웨어 중에서 가장 중요한 구성요소는 칩이며, 그 제조과정은 다음과 같다. 실리콘 잉곳을 슬라이싱한 후, 블랭크 웨이퍼를 20~40단계를 거쳐 패턴 웨이퍼를 형성합니다. 이러한 패턴이 있는 웨이퍼는 웨이퍼 테스터로 테스트되고 양호한 부품의 맵이 생성됩니다. 그런 다음 웨이퍼를 작은 조각으로 절단합니다. 이 사진에서는 하나의 웨이퍼에서 20개의 다이가 제작되었으며, 그 중 17개의 다이가 테스트를 통과했습니다(X는 불량 금형을 나타냄). 이 경우 좋은 금형의 수율은 17/20, 즉 85%입니다. 그런 다음 이러한 우수한 금형을 패키지에 접착하고 다시 테스트한 후 포장된 부품을 고객에게 배송합니다. 최종 테스트 중에 잘못 포장된 부품이 발견되었습니다.

19.1.2 컴퓨터 아키텍처

일반적인 데스크탑 컴퓨터에는 CPU(중앙 처리 장치), 메인 메모리, 하드 드라이브라는 세 가지 주요 물리적 구성 요소가 있습니다. 일반적으로 프로세서 또는 간단히 기계라고도 알려진 CPU는 컴퓨터의 두뇌이며 프로그램을 입력으로 받아 실행하는 컴퓨터의 주요 부분입니다. 메인 메모리는 프로그램을 실행하는 동안 필요할 수 있는 데이터(정보 저장 장치)를 저장하는 데 사용됩니다. 프로세서 자체에는 저장 공간이 제한되어 있습니다. 전원이 꺼지면 프로세서와 메인 메모리는 모든 데이터를 잃게 되지만, 하드 드라이브는 프로그램, 데이터, 사진, 비디오, 문서 등의 데이터가 저장되는 영구 저장소입니다.

아래 그림은 세 가지 구성 요소의 단순화된 블록 다이어그램을 보여줍니다. 이러한 주요 구성 요소 외에도 컴퓨터에 연결된 키보드 및 마우스와 같이 컴퓨터에 연결된 일련의 주변 구성 요소도 있습니다. 사용자로부터 입력을 받아 프로세서에서 실행되는 프로그램에 전달합니다.

마찬가지로, 프로그램의 출력을 표시하기 위해 프로세서는 일반적으로 출력 데이터를 모니터로 보냅니다. 모니터는 결과를 그래픽으로 표시하거나 프린터를 사용하여 결과를 인쇄할 수 있습니다. 마지막으로 컴퓨터는 네트워크를 통해 다른 컴퓨터에 연결할 수 있습니다. 모든 주변 장치의 블록 다이어그램은 다음과 같습니다.

19.1.3 컴퓨터 작동 메커니즘

기본 기술에 관계없이 우리가 이해해야 할 기본 개념은 컴퓨터가 근본적으로 어리석은 기계라는 것입니다. 우리의 뇌와는 달리 추상적 사고, 이성, 양심이 부여되지 않았습니다. 적어도 현재로서는 컴퓨터가 스스로 매우 복잡한 결정을 내릴 수는 없습니다. 그들이 할 수 있는 일은 프로그램을 실행하는 것 뿐이다. 그럼에도 불구하고 컴퓨터는 초당 수십억 개의 기본 명령을 실행할 수 있는 프로그램 실행 능력이 뛰어나기 때문에 매우 강력합니다. 아래 표는 컴퓨터와 인간의 두뇌를 비교한 것입니다.

특징 컴퓨터 인간의 뇌

지능 바보 스마트

컴퓨팅 파워 엄청 빠르다 천천히

피곤해요? 절대로 얼마 후

당신은 피곤합니까? 절대로 거의 항상

컴퓨터는 인간의 언어를 이해할 수 없으며 이진 데이터, 즉 0과 1, True와 False, On과 Off만 이해할 수 있습니다. 이러한 상태는 트랜지스터를 통해 구현됩니다. 트랜지스터는 2개의 값(1과 0 또는 켜짐과 꺼짐)을 저장하는 데 사용되는 작은 장치로, 트랜지스터가 켜져 있으면 1의 값을 갖고, 꺼져 있으면 값이 0이 됩니다.

예를 들어, 메모리 칩에는 수억 또는 수십억 개의 트랜지스터가 포함되어 있으며 각 트랜지스터는 개별적으로 켜거나 끌 수 있습니다. 트랜지스터에 아주 적은 양의 전류가 흐르면 상태 1로 유지되고, 전류가 흐르지 않으면 트랜지스터의 상태는 0이 됩니다. 구체적인 예:

cpp
1 : 1
2 : 10
3 : 11
a : 01100001
A : 01000001
U : 01010101 

Hello        : 01001000 01100101 01101100 01101100 01101111 
Hello World! : 01001000 01100101 01101100 01101100 01101111 00100000 01010111 01101111 01110010 01101100 01100100 00100001

문제는 위의 바이너리 코드가 인간이 이해하기에는 너무 어렵고, 번역 격차를 해소하기 위해서는 다양한 컴퓨터 소프트웨어가 필요하다는 점이다. 소프트웨어는 컴퓨터에 무엇을 해야 하는지, 언제 해야 하는지, 어떻게 해야 하는지 알려주는 일련의 지침입니다. 아래 그림은 작업 흐름을 보여줍니다. 첫 번째 단계는 고급 언어(C 또는 C++)로 프로그램을 작성하는 것이고, 두 번째 단계는 프로그램을 컴파일하는 것입니다. 컴파일러는 고급 프로그램을 입력으로 사용하여 종종 실행 파일 또는 이진 파일이라고 하는 기계 명령어가 포함된 프로그램을 생성합니다. 컴파일러 자체는 기본 기계 명령어로 구성된 프로그램입니다.

2+2 명령어를 실행하고 싶다고 가정하면 컴퓨터에 다음과 같은 명령어를 주어야 합니다.

  • 1단계: 2개의 값을 취합니다.

  • 2단계: 2의 값을 저장합니다.

  • 3단계: + 연산자를 사용하여 추가합니다.

  • 4단계: 결과를 저장합니다.

컴퓨터가 + 기호를 만났을 때 추가하는 방법을 알 수 있도록 + 연산자에 대한 별도의 지침이 제공됩니다. 그러면 누가 이 코드를 변환할 것인가? 그 답은 우리의 언어 코드를 컴퓨터가 이해할 수 있는 기계어로 변환하는 인터프리터입니다. 마찬가지로 입력 및 출력 데이터도 특정 소프트웨어 및 해석기에 의존합니다.

모든 언어에 단어 수가 제한되어 있는 것처럼 프로세서가 지원할 수 있는 기본 명령어/기본 명령의 수도 제한되어야 합니다. 이 명령어 세트를 흔히 명령어 세트라고 합니다. 기본 명령어의 예로는 덧셈, 뺄셈, 곱셈, 논리 OR, 논리 NOT 등이 있습니다. 각 명령어는 일련의 변수와 상수를 처리하고 마지막으로 결과를 변수에 저장해야 한다는 점에 유의하세요. 이러한 변수는 프로그래머가 정의한 변수가 아니지만 컴퓨터 내의 내부 위치입니다.

19.1.4 컴퓨터 설계 지침

컴퓨터 디자인은 상호 연결된 구성 요소의 구조입니다. 디자이너는 한 번에 특정 수준의 시스템에서 작업하며 다양한 수준에는 다양한 유형의 문제가 있습니다. 각 계층에서 디자이너는 서로 소통하는 다양한 구성 요소의 뼈대인 구조와 시스템과 관련된 활동인 기능에 관심을 갖습니다. 컴퓨터 설계에는 다음과 같은 문제가 있습니다.

  • 무한 속도의 가정: 컴퓨터의 무한 속도는 가정할 수 없습니다. 왜냐하면 무한 속도를 가정하는 것은 비현실적이며 디자이너의 사고에도 문제를 일으키기 때문입니다.

  • 무한한 메모리의 가정: 컴퓨터의 속도와 마찬가지로 메모리도 무한하다고 가정할 수 없으며 항상 제한되어 있습니다.

  • 메모리와 프로세서 사이의 속도 불일치: 때로는 메모리와 프로세서의 속도가 일치하지 않을 수 있으며, 메모리가 더 빠르거나 프로세서가 더 빠를 수도 있습니다. 메모리와 프로세서가 일치하지 않으면 설계에 문제가 발생할 수 있습니다.

  • 버그 및 오류 처리: 결함 및 오류를 처리하는 것은 모든 컴퓨터 설계자의 큰 책임입니다. 결함 및 오류로 인해 컴퓨터 시스템이 오작동할 수 있으며 때로는 이러한 오류가 더 위험할 수 있습니다.

  • 다중 프로세서: 다중 프로세서로 컴퓨터 시스템을 설계하면 관리 및 프로그래밍의 막대한 작업이 발생하며 컴퓨터 설계의 주요 문제입니다.

  • 멀티스레딩: 다중 스레드가 있는 컴퓨터 시스템은 항상 설계자에게 위협이 됩니다. 다중 스레드가 있는 컴퓨터는 멀티 태스킹 및 다중 처리를 수행할 수 있어야 합니다.

  • 공유 메모리: 한 번에 여러 프로세스가 실행되는 경우 모든 프로세스가 동일한 메모리 공간을 공유합니다. 충돌을 방지하려면 구체적인 방법으로 관리해야 합니다.

  • 디스크 액세스: 디스크 관리는 컴퓨터 설계의 핵심입니다. 디스크 액세스에는 몇 가지 문제가 있습니다. 시스템이 다중 디스크 액세스를 지원하지 않을 수 있습니다.

  • 더 나은 성능: 항상 관심사입니다. 설계자는 항상 전력 소비를 줄이고 비용을 절감하기 위해 더 나은 성능을 얻기 위해 시스템을 단순화하려고 노력합니다.

19.1.5 실제 기계 아키텍처

다양한 유형의 실제 기계의 설계 및 아키텍처가 아래에 설명되어 있습니다.

  • 하버드 아키텍처

아래 그림에 표시된 Harvard 아키텍처는 명령어 목록과 메모리를 유지하기 위해 별도의 구조를 가지고 있습니다. 명령어 테이블은 명령어만 보관하도록 설계된 전용 메모리로 생각할 수 있으므로 명령어 메모리라고도 합니다. 메모리는 프로그램에서 요구하는 데이터 값을 저장하므로 데이터 메모리라고 합니다. 명령을 처리하는 엔진은 제어와 ALU라는 두 부분으로 나뉩니다. 제어 장치의 임무는 명령을 획득하고, 명령을 처리하고, 명령 실행을 조정하는 것입니다. ALU는 산술 논리 유닛(Arithmetic Logic Unit)의 약자로 산술식이나 논리식(and/or/NOT 등)을 계산할 수 있는 특수 회로를 가지고 있습니다.

모든 컴퓨터는 사용자/프로그래머로부터 입력을 받아 궁극적으로 그 결과를 프로그래머에게 다시 전달해야 하며, 이는 오늘날 우리가 사용하는 키보드 및 모니터와 같은 다양한 방법을 통해 수행될 수 있습니다. 초기 컴퓨터는 일련의 스위치를 사용했으며 최종 결과는 종이에 인쇄되었습니다.

  • 폰 노이만 아키텍처

존 폰 노이만(John von Neumann)은 범용 튜링 완전 컴퓨터의 폰 노이만 아키텍처를 제안했습니다. 실제로 Eckert와 Mauchly는 1946년에 이 아키텍처를 기반으로 ENIAC(Electronic Numerical Integrator and Calculator)라고 불리는 최초의 범용 Turing-complete 컴퓨터(작은 제한 있음)를 설계했습니다. 이 컴퓨터는 미 육군 탄도 연구소의 포병 링 테이블을 계산하는 데 사용되었습니다. 나중에 1949년에 미 육군의 탄도 연구소에서도 사용되었던 EDVAC 컴퓨터로 대체되었습니다.

ENIAC 및 EDVAC의 기반이 되는 기본 von Neumann 아키텍처는 다음과 같습니다. 명령 목록은 메모리에 보관되며, CPU(중앙 처리 장치)라고 불리는 Turing 기계의 처리 엔진에는 새로운 명령을 가져와서 실행하는 작업을 수행하는 프로그램 카운터가 포함되어 있습니다. 산술 함수의 결과를 계산하고, 메모리 위치에 값을 로드 및 저장하고, 분기 명령의 결과를 계산하는 전용 기능 단위가 있습니다. 마지막으로 Harvard 아키텍처와 마찬가지로 CPU는 I/O 하위 시스템에 연결됩니다.

이 기계의 혁신은 명령어 목록이 메모리에 저장되어 일반적으로 메모리에 저장된 동일한 기호 세트를 사용하여 각 명령어를 인코딩한다는 것입니다. 예를 들어, 메모리가 십진수 값을 저장하는 경우 각 명령어는 십진수 문자열로 인코딩되어야 합니다. 폰 노이만 CPU는 모든 명령어를 디코딩해야 하며, 아이디어의 핵심은 명령어가 일반 데이터(메모리 값)로 처리된다는 것입니다. 이 간단한 아이디어는 실제로 우아한 컴퓨팅 시스템을 설계하기 위한 매우 강력한 도구이며 저장된 프로그램 개념으로 알려져 있습니다.

저장 프로그램 개념: 프로그램은 메모리에 저장되고 명령어는 일반 메모리 값으로 처리됩니다.

저장된 프로그램 개념은 컴퓨터 설계를 크게 단순화합니다. 메모리 데이터와 명령어는 개념적으로 동일하게 처리되므로 명령어와 데이터를 동일하게 처리하는 통일된 처리 시스템과 메모리 시스템을 가질 수 있다. CPU의 관점에서 보면 프로그램 카운터는 그 내용이 인코딩된 명령어의 내용으로 해석되는 범용 메모리 위치를 가리킵니다. 프로그램은 쉽게 저장, 수정 및 전송될 수 있습니다. 프로그램은 자체 수정이나 다른 프로그램 수정을 통해 런타임 시 동작을 동적으로 변경할 수도 있습니다. 이는 고급 C 프로그램을 기계 명령어로 변환하는 오늘날의 정교한 컴파일러의 기초를 형성합니다. 또한 Java Virtual Machine과 같은 많은 최신 시스템에서는 효율성을 달성하기 위해 지침을 동적으로 수정합니다.

폰 노이만 머신이나 하버드 머신은 튜링 머신처럼 무한한 메모리를 갖고 있지 않습니다. 엄밀히 말하면 튜링 기계와 완전히 동일하지는 않습니다. 이는 모든 실제 기계에 해당됩니다. 충분한 자원이 필요합니다. 그러나 과학계는 이러한 근사치를 받아들이는 법을 배웠습니다.

19.2 컴퓨터 하드웨어 기본

19.2.1 어셈블리 언어

어셈블리 언어는 기계 명령어의 텍스트 표현으로 광범위하게 정의될 수 있습니다. 프로세서를 구축하기 전에 다양한 기계 명령어의 의미를 이해해야 하며, 이와 관련하여 어셈블리 언어에 대한 엄격한 연구가 도움이 될 것입니다. 어셈블리 언어는 ISA 및 컴파일러 프레임워크에 특화되어 있으므로 어셈블리 언어에는 많은 장점이 있습니다. 이 섹션에서는 다양한 어셈블리 언어 변형의 기본 원칙, 몇 가지 일반적인 개념 및 용어를 설명합니다. 이어서 ARM 기반 프로세서용 ARM 어셈블리 언어와 Intel/AMD 프로세서용 x86 어셈블리 언어에 대해 설명한다.

19.2.1.1 어셈블리 언어를 사용하는 이유는 무엇입니까?

먼저 소프트웨어 개발자의 관점에서 설명하겠습니다.

인간은 중국어, 영어, 스페인어 등 자연어를 이해합니다. 약간의 추가 교육을 통해 인간은 C나 Java와 같은 컴퓨터 프로그래밍 언어도 이해할 수 있습니다. 그러나 앞서 언급했듯이 컴퓨터는 멍청한 기계입니다. 인간 언어(예: 영어)나 프로그래밍 언어(예: C)의 명령을 이해할 만큼 똑똑하지 않고 0과 1만 이해할 수 있습니다. 따라서 컴퓨터를 프로그래밍하려면 0과 1의 시퀀스를 제공해야 합니다. 실제로 일부 초기 프로그래머는 스위치 세트를 켜거나 끄는 방식으로 컴퓨터를 프로그래밍하곤 했습니다. 스위치 하나를 켜면 1에 해당하고 켜면 0을 의미합니다. 이는 오늘날의 대규모 수백만 라인 프로그램에는 실현 가능한 솔루션이 아니며 더 나은 접근 방식을 찾아야 합니다.

따라서 C나 자바 등 고급 언어로 작성된 프로그램을 기계어라고 불리는 일련의 0과 1로 변환할 수 있는 자동 변환기가 필요하다. 기계어 코드는 기계 명령어라고 불리는 일련의 명령어로 구성되며, 각 명령어는 0과 1의 시퀀스로 구성되며 프로세서에 특정 작업을 수행하도록 지시합니다. 고급 언어로 작성된 프로그램을 기계어 코드로 변환할 수 있는 프로그램을 컴파일러라고 합니다(아래 그림 참조).

컴파일 과정.

컴파일러는 일반적으로 기계어 코드가 생성될 기계에서 실행되는 실행 가능한 프로그램입니다. 발생할 수 있는 자연스러운 질문은 - 누가 최초의 컴파일러를 작성했습니까?

첫째, 거의 모든 프로그램은 기계 코드로 변환하는 데 사용되는 컴파일러의 편재성을 고려하여 고급 언어로 작성되지만 이 규칙에는 중요한 예외가 있습니다. 컴파일러의 역할은 두 가지입니다. 첫째, 고급 언어로 된 프로그램을 기계 명령어로 올바르게 번역해야 합니다. 둘째, 공간을 많이 차지하지 않고 속도가 빠른 효율적인 기계어 코드를 생성해야 합니다. 따라서 컴파일러의 알고리즘은 수년에 걸쳐 점점 더 복잡해졌지만 이러한 요구 사항이 항상 충족되는 것은 아닙니다. 예를 들어, 어떤 경우에는 컴파일러가 충분히 빠른 코드를 생성하지 않거나 프로그래머가 기대하는 특정 기능을 제공하지 못할 수 있습니다.

  • 첫째, 컴파일러의 알고리즘은 프로그램에서 수행하는 분석의 양에 따라 제한됩니다. 예를 들어, 우리는 컴파일 프로세스가 극도로 느려지는 것을 원하지 않습니다. 컴파일러 세계의 많은 문제는 계산적으로 해결하기 어렵고 따라서 시간이 많이 걸립니다.

  • 둘째, 컴파일러는 코드의 광범위한 패턴을 인식하지 못합니다. 예를 들어, 변수는 제한된 값 집합만 가질 수 있으며, 이를 기반으로 기계어 코드가 더욱 최적화될 수 있으며 이는 컴파일러가 이해하기 어렵습니다. 똑똑한 프로그래머는 컴파일러보다 성능이 뛰어난 몇 가지 광범위한 실행 패턴을 알고 있기 때문에 때때로 컴파일러보다 더 최적화된 기계어 코드를 생성할 수 있습니다.

  • 프로세서 공급업체는 ISA에 새로운 지침을 추가할 수도 있습니다. 이 경우 이전 프로세서용 컴파일러는 새 명령어를 활용하지 못하여 프로그램에 수동으로 추가해야 할 수 있습니다. 널리 사용되는 컴파일러(예: gcc, GNU Compiler Collection)는 매우 일반적이므로 기계 코드를 생성할 때 프로세서에서 제공하는 가능한 모든 기계 명령어를 사용하지 않습니다. 일반적으로 운영 체제 및 장치 드라이버(프린터 및 스캐너와 같은 장치와 인터페이스하는 프로그램)에는 하드웨어에 대한 낮은 수준의 액세스가 필요하기 때문에 누락된 명령어가 많이 필요하므로 시스템 프로그래머는 때때로 컴파일러를 우회하려는 강력한 인센티브를 갖습니다.

이 모든 경우에 프로그래머는 일련의 기계 명령어를 프로그램에 수동으로 삽입해야 합니다. 위에서 언급했듯이 이를 수행하는 두 가지 주요 이유는 효율성과 추가 기능입니다. 따라서 시스템 소프트웨어 개발자의 입장에서는 기계 명령어를 이해하여 작업을 보다 효율적으로 수행할 수 있도록 하는 것이 필요합니다.

이제 우리의 목표는 현대 프로그래머가 0과 1의 복잡한 세부 사항에 접근하지 못하도록 하는 것입니다. 이상적으로는 어셈블리 언어라는 저수준 언어가 개발되었던 50년 전처럼 프로그래머가 수동으로 스위치를 켜고 끄며 프로그래밍하는 것을 원하지 않습니다. 어셈블리 언어는 사람이 읽을 수 있는 형태의 기계 코드이며, 각 어셈블리 언어 명령문은 일반적으로 기계 명령어에 해당합니다. 또한 프로그래머가 명령어를 인코딩하는 데 필요한 0/1의 정확한 시퀀스를 기억하도록 강요하지 않으므로 프로그래머의 부담이 크게 줄어듭니다.

  • 저수준 프로그래밍 언어는 일반적으로 하나의 기계 명령에만 해당하는 간단한 명령문을 사용합니다. 이러한 언어는 ISA 고유의 언어입니다.

  • 어셈블리 언어는 각 ISA에 특정한 일련의 저수준 프로그래밍 언어를 말하며, 일련의 어셈블리 문으로 구성된 일반적인 구조를 갖습니다. 일반적으로 각 어셈블리 문은 두 부분으로 구성됩니다.

명령어 코드는 기본 기계 명령어에 대한 니모닉입니다.

  • 피연산자 목록입니다.

실용적인 관점에서 볼 때, 독립형 어셈블리 프로그램을 작성하고 이를 어셈블러라는 프로그램을 사용하여 실행 가능한 프로그램으로 변환하는 것이 가능하거나, C나 C++와 같은 고급 언어에 어셈블리 코드 조각을 삽입하는 것이 더 일반적입니다.

**어셈블러(어셈블러)**는 어셈블러를 기계어 코드로 변환하는 실행 가능한 프로그램입니다.

컴파일러는 결합된 프로그램이 기계어 코드로 컴파일될 수 있도록 보장합니다. 어셈블리 언어의 장점은 많습니다:

  • 가독성. 어셈블리 코드의 각 줄은 기계 명령어에 해당하므로 기계 코드만큼 표현력이 풍부합니다.

  • 효율성. 이러한 일대일 매핑 덕분에 효율성을 얻기 위해 프로그램을 어셈블리어로 작성할 필요가 없습니다.

  • 단순화. 기계 코드를 사용하여 프로그램을 작성하는 과정을 크게 단순화하는 읽기 쉽고 우아한 형태의 기계 코드 텍스트 표현입니다. 또한 C와 같은 고급 언어로 작성된 소프트웨어에 어셈블리 코드 조각을 명확하게 포함할 수 있습니다. 이는 실제 기계어 코드보다 높은 추상화 수준을 정의합니다. 두 프로세서는 동일한 어셈블리 언어와 호환될 수 있지만 실제로는 동일한 명령어에 대해 서로 다른 기계 인코딩을 가질 수 있습니다. 이 경우 어셈블러는 두 프로세서 모두에서 호환됩니다.

하드웨어 디자이너의 관점에서 설명해보자.

하드웨어 설계자의 임무는 ISA의 모든 명령을 구현할 수 있는 프로세서를 설계하는 것입니다. 그들의 주요 목표는 면적, 전력 효율성 및 설계 복잡성 측면에서 최적인 고효율 프로세서를 설계하는 것입니다. 그들의 관점에서 보면 ISA는 소프트웨어와 하드웨어 사이의 핵심 연결고리입니다. 이것이 그들의 기본적인 질문인 무엇을 구축할 것인가에 대한 답이 되었습니다. 따라서 다양한 명령어 세트의 정확한 의미를 이해하여 그에 맞는 프로세서를 설계하는 것이 매우 중요합니다. 명령어를 단순히 0과 1의 시퀀스로 생각하는 것은 번거로운 일입니다. 기계 명령어의 텍스트 표현을 보고 그것이 어떤 종류의 조립 명령어인지 명확하게 알면 많은 이점을 얻을 수 있습니다.

어셈블리 언어는 명령어 세트와 어셈블러에 특화되어 있습니다. 이 섹션에서는 널리 사용되는 GNU 어셈블러의 어셈블리 언어 형식을 사용하여 일반적인 어셈블리 언어 파일의 구문을 설명합니다. 다른 시스템도 비슷한 형식을 갖고 있으며 개념도 거의 동일합니다.

19.2.1.2 어셈블리 언어 기본

기계 모델

어셈블리 언어는 레지스터가 추가된 추상적 폰 노이만 기계를 가정하여 명령어 메모리와 데이터 메모리를 별도의 개체로 처리하지 않습니다.

기계 모델의 그림은 아래 이미지를 참조하십시오. 프로그램은 주 메모리의 일부에 저장됩니다. 중앙처리장치(CPU)는 프로그램 명령어를 명령어별로 읽고 그에 맞게 명령어를 실행한다. 프로그램 카운터(PC)는 CPU가 실행 중인 명령어의 메모리 주소를 추적합니다. 대부분의 명령어는 입력 피연산자가 레지스터에서 얻어질 것으로 예상합니다. 모든 CPU에는 고정된 수의 레지스터(보통 64개 미만)가 있지만 많은 명령어도 메모리에서 직접 피연산자를 가져올 수 있다는 점을 기억하세요. CPU의 임무는 주 메모리와 레지스터 간의 전송을 조정하는 것입니다. 또한 CPU는 모든 산술/논리 계산을 수행하고 외부 입출력 장치와의 통신을 유지해야 합니다.

대부분의 어셈블리 언어 유형은 대부분의 명령문에서 이 추상 기계 모델을 사용합니다. 그러나 어셈블리 언어를 사용하는 또 다른 목적은 하드웨어를 보다 세밀하고 침해적으로 제어하는 ​​것이기 때문에 프로세서 내부를 식별하는 어셈블리 명령이 꽤 많이 있습니다.

이러한 명령어는 일반적으로 일부 주요 내부 알고리즘의 동작을 변경하여 프로세서의 동작을 수정하고, 전원 관리 설정과 같은 내장 매개변수를 수정하거나 일부 내부 데이터를 읽고 씁니다. 마지막으로, 어셈블리 언어는 기계 독립 명령어와 기계 종속 명령어를 구분하지 않습니다.

모든 기계에는 어셈블리 프로그래머가 볼 수 있는 레지스터 세트가 있습니다. ARM에는 16개의 레지스터가 있고, x86(32비트)에는 8개의 레지스터가 있고, x86_64(64비트)에는 16개가 있습니다. 레지스터에는 이름이 있으며, ARM에서는 r0, r1, ..., r14, r15, x86에 eax, ebx, ecx, edx, esi, edi, ebp 및 esp라는 이름을 지정합니다. 이 이름을 사용하여 레지스터에 액세스할 수 있습니다.

대부분의 ISA에서는 반환 주소 레지스터가 함수 호출에 사용됩니다. 프로그램이 함수 실행을 시작하고 함수 실행 후 반환해야 하는 메모리 주소를 기억해야 한다고 가정해 보겠습니다. 이 주소를 반송 주소라고 합니다. 함수의 시작 주소로 점프하기 전에 이 레지스터에 반환 주소 값을 저장할 수 있습니다. 반환문은 반환 주소 레지스터에 있는 값을 PC에 복사하는 것만으로 간단하게 구현할 수 있습니다. ARM, MIPS 등의 어셈블리 언어에서는 프로그래머가 복귀 주소 레지스터를 볼 수 있지만 x86은 복귀 주소 레지스터를 사용하지 않고 스택을 사용한다.

ARM 프로세서에서 프로그래머는 마지막 레지스터(r15)인 PC를 볼 수 있다. PC의 값을 읽거나 값을 설정할 수 있습니다. PC 값을 설정한다는 것은 프로그램의 새 위치로 분기한다는 의미입니다. 그러나 x86 PC는 암시적이며 프로그래머에게 표시되지 않습니다.

메모리 뷰

메모리는 큰 바이트 배열로 생각할 수 있으며 각 바이트에는 기본적으로 배열에서의 위치인 고유한 주소가 있습니다. 첫 번째 바이트의 주소는 0이고, 두 번째 바이트의 주소는 1입니다. 주어진 비트를 고유하게 주소 지정할 수 있는 방법이 없습니다. 주소는 32비트 시스템에서 32비트 부호 없는 정수이거나 64비트 시스템에서 64비트 부호 없는 정수입니다.

폰 노이만 기계에서는 프로그램이 바이트 시퀀스로 메모리에 저장되고 프로그램 카운터가 실행될 다음 명령을 가리킨다고 가정합니다.

메모리가 큰 바이트 배열이라고 가정하면 모든 데이터 항목의 길이가 1바이트만 되어도 괜찮을 것입니다. C 및 Java와 같은 언어에는 char(1바이트), short(2바이트), 정수(4바이트), long 정수(8바이트) 등 다양한 크기의 데이터 유형이 있습니다. 다중 바이트 데이터 유형의 경우 메모리에 표현을 생성해야 합니다. 메모리에서 멀티바이트 데이터 유형을 표현하는 방법에는 리틀 엔디안과 빅 엔디안이라는 두 가지 방법이 있습니다. 둘째, 메모리에 있는 데이터의 배열이나 목록을 표현하는 방법도 찾아야 합니다.

  • 리틀 엔디안 및 빅 엔디안 표현

0-3 위치에 정수를 저장하는 문제를 고려해 보겠습니다. 정수는 87, 65, 43, 21의 4바이트로 나눌 수 있는 0x87654321입니다. 한 가지 옵션은 가장 중요한 바이트 87을 가장 낮은 메모리 주소 0에 저장하고 다음 위치는 65, 43, 21을 저장할 수 있는 것입니다. 이를 빅 엔디안 표현이라고 합니다. 가장 큰 바이트 위치에서 시작하기 때문입니다. 대조적으로, 가장 작은 바이트를 위치 0에 먼저 저장한 다음 가장 큰 바이트를 위치 3에 계속 저장할 수 있습니다. 이 표현을 리틀 엔디안이라고 합니다. 아래 이미지는 차이점을 보여줍니다.

빅엔디안과 리틀엔디안 표현.

따라서 다른 표현보다 하나의 표현을 선택할 이유가 없습니다. 예를 들어 x86 프로세서는 리틀 엔디안 형식을 사용합니다. 이전 버전의 ARM 프로세서는 리틀엔디안이었지만 이제는 더블엔디안입니다. 즉, ARM 프로세서는 사용자 설정에 따라 리틀엔디안과 빅엔디안 시스템으로 모두 작동할 수 있습니다. 전통적으로 IBM POWER 프로세서와 Sun SPARC 프로세서는 빅엔디안입니다.

  • 배열을 의미

배열은 선형으로 정렬된 객체 집합으로, 객체는 단순 데이터 유형(예: 정수 또는 문자)이거나 복합 데이터 유형일 수 있습니다.

cpp
int a[100];
char c[100];

간단한 정수 배열 a를 고려해 보겠습니다. 배열에 100개의 항목이 있는 경우 메모리에 있는 배열의 전체 크기는 100 4 = 400바이트와 같습니다. 배열의 시작 메모리 위치가 loc이면 [0]은 (loc + 0), (loc + 1), (loc + 2), (loc + 3) 위치에 저장됩니다. 데이터를 저장하는 방법에는 빅 엔디안(Big Endian)과 리틀 엔디안(Little Endian)이라는 두 가지 방법이 있다는 점을 참고하시기 바랍니다. 다음 배열 항목 a[1]은 (loc + 4) ... (loc + 7) 위치에 저장되며, 계속해서 항목 a[i]는 (loc + 4 x i) ... (loc + 4 x i + 3)에 저장됩니다.

대부분의 프로그래밍 언어는 다음 형식의 다차원 배열을 정의합니다.

cpp
int a[100][100];
char c[100][100];

이들은 일반적으로 메모리에 일반 1차원 배열로 표시됩니다. 다차원 배열의 위치와 이에 상응하는 1차원 배열 사이의 매핑 기능이 있습니다. 이 체계를 확장하여 2보다 큰 차원을 가진 다차원 배열을 고려할 수 있습니다.

우리는 2D 배열을 행 메이저로 저장함으로써 이를 1D 배열로 저장할 수 있다는 것을 관찰했습니다. 즉, 데이터가 행에 저장된다는 의미입니다. 첫 번째 행을 저장한 다음 두 번째 행 등을 저장합니다. 마찬가지로, 다차원 배열은 첫 번째 열이 저장되고 두 번째 열이 저장되는 주요 열에 저장될 수 있습니다.

행 주요: 배열은 행별로 메모리에 저장됩니다.

열 주요: 배열은 메모리에 열로 저장됩니다.

19.2.1.3 어셈블리 언어 구문

어셈블리 파일의 정확한 구문은 어셈블러에 따라 다르며 기본 명령어와 피연산자 형식은 동일하더라도 어셈블러마다 다른 구문을 사용할 수 있습니다. 이 섹션에서는 GNU 어셈블러용으로 설계되었으며 GNU 컴파일러 컬렉션(gcc)의 일부인 GNU 어셈블리 언어 제품군의 구문을 설명합니다. 모든 GNU 소프트웨어와 마찬가지로 어셈블러 및 관련 컴파일러는 대부분의 플랫폼에서 무료이며 어셈블러는 gnu.org에서 찾을 수 있습니다. 다른 어셈블러(예: NASM, MASM)에는 자체 형식이 있지만 전체 구조는 개념적으로 이 섹션에 설명된 것과 크게 다르지 않습니다.

어셈블리 파일 구조

어셈블리 파일은 접미사가 .s인 일반 텍스트 파일입니다. GNU 컴파일러 gcc가 설치되어 있는 경우 다음 명령을 실행하여 C 프로그램용 어셈블리 파일을 빠르게 생성하거나 온라인 GCC를 사용할 수 있습니다.

cpp
gcc -S test.c

생성된 어셈블리 파일의 이름은 test.s입니다. GNU 어셈블리의 구조는 아래 그림과 같이 매우 간단합니다. 다양한 부분의 예로는 텍스트(실제 프로그램), 데이터(초기화된 값이 있는 데이터) 및 bss(0으로 초기화된 일반 데이터)가 있습니다. 각 섹션은 "."으로 시작하는 섹션의 이름 기호인 섹션 제목으로 시작합니다. 예를 들어 텍스트 섹션은 ".text" 줄로 시작합니다. 그 다음에는 어셈블리 언어 명령문 목록이 오고, 각 명령문은 일반적으로 개행 문자로 끝나며, 데이터 섹션에는 데이터 값 목록이 포함됩니다.

어셈블리 파일은 ".file <filename>" 형식의 행을 포함하는 파일 세그먼트로 시작됩니다. C 프로그램에서 어셈블리 파일을 생성하기 위해 gcc 컴파일러를 사용할 때 .file 섹션의 파일 이름은 일반적으로 원래 C 프로그램(test.C)과 동일합니다. 텍스트 세그먼트는 필수이고 나머지 세그먼트는 선택 사항입니다. 하나 이상의 데이터 세그먼트가 있을 수 있습니다. 또는 .section 지시문을 사용하여 새 섹션을 정의할 수 있습니다. 이 섹션에서는 학습 지침 세트의 성격에 관심이 있기 때문에 주로 텍스트 부분에 중점을 둡니다.

어셈블리 언어 파일 구조.

기본 어셈블리 문

기본 어셈블리 언어 명령문은 아래 그림과 같이 명령어와 피연산자 목록이라는 두 부분으로 구성된 어셈블리 명령어를 지정합니다. 명령어는 실제 기계 명령어의 텍스트 식별자이며 피연산자 목록에는 각 피연산자의 값이나 위치가 포함됩니다. 피연산자의 값은 즉치값이라고도 하는 숫자 상수이며, 피연산자 위치는 레지스터 위치 또는 메모리 위치일 수 있습니다.

어셈블리 언어 설명.

컴퓨터 아키텍처에서는 명령어에 지정된 상수 값을 즉시값이라고도 합니다.

다음과 같은 명령문이 있다고 가정해 보겠습니다.

cpp
add r3, r1, r2

이 ARM 어셈블리 명령문에서 add 명령어는 두 숫자를 더하고 그 결과를 미리 지정된 위치에 저장한다는 사실을 지정합니다. 이 경우 덧셈 명령어의 형식은 <명령어><대상 레지스터><피연산자 레지스터 1><연산자 레지스터 2>와 같다. 명령어 이름은 add, 대상 레지스터는 r3, 피연산자 레지스터는 r1과 r2입니다. 자세한 지시 단계는 다음과 같습니다.

  1. 레지스터 r1의 값을 읽습니다. 이 값을 v1이라고 부르겠습니다.

  2. 레지스터 r2의 값을 읽습니다. 이 값을 v2라고 부르겠습니다.

  3. v3=v1+v2를 계산합니다.

  4. v3을 레지스터 r3에 저장합니다.

이제 비슷한 방식으로 작동하는 두 명령의 또 다른 예를 살펴보겠습니다.

cpp
sub r3, r1, r2
mul r3, r1, 3

sub 명령어는 레지스터에 저장된 두 숫자를 빼고, mul 명령어는 레지스터 r1에 저장된 숫자에 숫자 상수 3을 곱합니다. 두 명령어 모두 결과를 레지스터 r3에 저장하며 해당 동작 모드는 덧셈 명령어와 유사합니다. 또한 add, sub, mul 등의 산술 명령어를 데이터 처리 명령어라고도 합니다. 메모리에서 값을 로드하거나 저장하는 데이터 전송 명령어, 분기를 구현하는 제어 명령어 등 여러 다른 카테고리의 명령어가 있습니다.

일반적인 명령문 구조

어셈블리 문의 일반적인 구조는 아래 그림에 나와 있습니다. 이는 라벨(명령어 식별자), 키(어셈블리 명령어 또는 어셈블러 명령어) 및 주석의 세 가지 필드로 구성됩니다. 세 필드는 모두 선택 사항이지만 모든 어셈블리 문에는 그 중 하나 이상이 있어야 합니다.

어셈블리 문의 일반적인 구조.

명령문은 선택적으로 명령문의 텍스트 식별자인 레이블로 시작할 수 있습니다. 즉, 레이블은 어셈블리의 어셈블리 문을 고유하게 식별합니다. 동일한 어셈블리 파일에서 레이블 중복을 허용하지 않습니다. 레이블은 분기 명령을 실행할 때 매우 유용합니다.

아래 예제 코드는 레이블 이름이 "label1"이고 뒤에 콜론이 오는 레이블의 예를 보여줍니다. 라벨 뒤에는 어셈블리 명령어를 작성하고 피연산자 목록을 제공합니다. 라벨은 유효한 영숫자 문자 [a-z] [A-Z] [0-9]와 기호 ".", "_" 및 ""로 구성될 수 있습니다. 일반적으로 숫자로 레이블을 시작할 수 없습니다. 레이블을 지정한 후 줄을 비워두거나 키(어셈블리 문의 일부)를 지정할 수 있습니다. 키가 "."로 시작하는 경우 그런 다음 어셈블러에게 새 섹션을 시작하거나 상수를 선언하는 등 특정 작업을 수행하도록 지시하는 것은 모든 컴퓨터에서 유효한 어셈블러 명령입니다. 명령어는 매개변수 목록도 사용할 수 있으며, 키가 문자로 시작하면 일반 어셈블리 명령어입니다.

cpp
label1: add r1, r2, r3

선택적으로 레이블, 어셈블리 지침 및 피연산자 목록 뒤에 설명을 삽입할 수 있습니다. GNU 어셈블러는 두 가지 유형의 주석을 지원하며 C 또는 Java 스타일과 유사한 주석을 삽입할 수 있습니다. ARM 어셈블리에서는 주석 앞에 "@" 문자를 붙여서 작은 한 줄 주석을 가질 수도 있습니다.

cpp
label1: add r1, r2, r3 @ Add the values in r2 and r3
label2: add r3, r4, r5 @ Add the values in r4 and r5
add r5, r6, r7 /* Add the values in r6 and r7 */

어셈블리 문에는 키가 아닌 레이블만 포함될 수 있습니다. 이 경우 레이블은 본질적으로 빈 명령문을 가리키므로 그다지 유용하지 않습니다. 따라서 어셈블러는 이 경우 레이블이 키를 포함하는 가장 가까운 후속 어셈블리 문을 가리키는 것으로 가정합니다.

19.2.1.4 명령 유형

기능별 분류

기능에 따라 크게 4가지 유형으로 나눌 수 있으며, 그 설명은 다음과 같습니다.

  • 데이터 처리 명령어: 데이터 처리 명령어는 일반적으로 덧셈, 뺄셈, 곱셈과 같은 산술 명령어이거나 비트별 OR 및 XOR 계산을 위한 논리 명령어입니다. 비교 명령도 이 유형에 속합니다.

  • 데이터 전송 명령어: 이 명령어는 레지스터나 메모리 주소 등 두 위치 간에 값을 전송합니다.

  • 분기 명령어: 분기 명령어는 프로세서의 제어 장치가 피연산자의 값을 기반으로 프로그램의 다른 부분으로 점프하는 데 도움이 되며, 이는 for 루프 및 if-then-else 문을 구현할 때 유용합니다.

-예외 생성 명령어: 이러한 특수 명령어는 사용자 수준 프로그램에서 운영 체제로 제어를 전송하는 데 도움이 됩니다.

데이터 처리, 데이터 전송 및 제어 지침을 다룹니다.

피연산자에 따른 분류

GNU 어셈블러의 모든 어셈블리 언어 문은 동일한 구조를 가지며 명령어 이름으로 시작하고 그 뒤에 피연산자 목록이 옵니다. 필요한 피연산자에 따라 명령어를 분류할 수 있습니다. 명령어에 n개의 피연산자가 필요한 경우 이를 일반적으로 n-주소 형식이라고 합니다. 예를 들어, 피연산자가 필요하지 않은 명령어는 0 주소 형식 명령어입니다. 3개의 피연산자가 필요한 경우 3주소 형식 명령어입니다.

명령어에 n개의 피연산자(소스 및 대상 포함)가 필요한 경우 이를 n 주소 형식 명령어라고 합니다.

ARM에서는 대부분의 데이터 처리 명령어가 3주소 형식을 사용하고, 데이터 전송 명령어는 2주소 형식을 사용합니다. 그러나 x86에서는 대부분의 명령어가 2주소 형식입니다. 가장 먼저 떠오르는 질문은 3번지 형식 명령어와 2번지 형식 명령어의 논리는 무엇인가? 여기에는 약간의 절충안이 있어야 합니다.

몇 가지 일반적인 경험 법칙을 제시해 보겠습니다. 명령어에 피연산자가 더 많으면 명령어를 표현하는 데 더 많은 비트가 필요하므로 명령어를 저장하고 처리하는 데 더 많은 리소스가 필요합니다. 그러나 이 주장에는 반대 측면이 있습니다. 피연산자가 많을수록 명령어가 더 다양하고 유연해지며, 피연산자가 더 많은 명령어가 더 많은 작업을 수행할 수 있으므로 컴파일러 작성자와 어셈블리 프로그래머의 작업이 더 쉬워집니다. 역방향 논리는 더 적은 수의 피연산자를 차지하고 저장 공간을 덜 차지하며 유연성이 떨어지는 명령어에 적합합니다.

예를 생각해 봅시다. 두 개의 숫자 3과 5를 더하고 결과 8을 얻으려고 한다고 가정합니다. 더하기 위해 사용되는 ARM 명령어는 다음과 같습니다.

cpp
add r3, r1, r2

이 명령어는 레지스터 r1(3) 및 r2(5)의 내용을 추가하고 이를 r3(8)에 저장합니다. 그러나 x86 지침은 다음과 같습니다.

cpp
add edx, eax

여기서는 edx에 3이 포함되고 eax에 5가 포함되어 덧셈이 수행되고 결과 8이 다시 edx에 저장된다고 가정합니다. 따라서 이 경우 x86 명령어는 대상 레지스터가 첫 번째 소스 레지스터와 동일하므로 2주소 형식입니다.

19.2.1.5 피연산자 유형

이제 다양한 유형의 피연산자를 살펴보겠습니다. 어셈블리 문에서 피연산자를 지정하고 액세스하는 방법을 주소 지정 모드라고 합니다.

어셈블리 문에서 피연산자를 지정하고 액세스하는 방법을 주소 지정 모드라고 합니다.

피연산자를 지정하는 가장 간단한 방법은 해당 값을 명령어에 삽입하는 것입니다. 대부분의 어셈블리 언어에서는 사용자가 정수 상수 값을 피연산자로 지정할 수 있습니다. 이 주소 지정 모드를 즉시 주소 지정 모드라고 합니다. 이 방법은 레지스터나 메모리 위치를 초기화하거나 산술 연산을 수행하는 데 유용합니다.

필요한 상수 집합이 레지스터와 메모리 위치에 로드되면 프로그램은 레지스터와 메모리에서 계속 작동해야 합니다. 이 공간에는 여러 가지 주소 지정 모드가 있습니다. 이를 소개하기 전에 레지스터 전송 표기법의 형태로 몇 가지 추가 용어를 소개하겠습니다.

이체 기호 등록

이 표기법을 사용하면 명령어와 피연산자의 의미를 지정할 수 있습니다. 명령어의 기본 동작을 표현하는 다양한 방법을 살펴보겠습니다.

r1\왼r2

이 표현식에는 두 개의 레지스터 피연산자 r1과 r2가 있습니다. r1은 대상 레지스터이고 r2는 소스 레지스터입니다. 우리는 레지스터 r2의 내용을 레지스터 r1로 전송합니다. 다음과 같이 상수를 사용하여 덧셈 연산을 지정할 수 있습니다.

r1\왼r2+4

또한 이 표기법을 사용하여 레지스터에 대한 작업을 지정하고 r2와 r3의 내용을 추가하고 결과를 r1에 저장할 수 있습니다.

r1\왼r2+r3

이 표기법을 사용하여 메모리 액세스를 나타낼 수도 있습니다.

r1\왼[r2+4]

위의 명령문에서 메모리 주소는 레지스터 r2의 내용에 4를 더한 값과 같고, 이 메모리 주소의 내용부터 시작하여 정수가 추출되어 레지스터 r1에 저장됩니다.

피연산자를 위한 범용 주소 지정 모드

피연산자의 값을 V로 표시하겠습니다. 다음 논의에서는 Vr1과 같은 표현식을 사용합니다. 이는 V라는 새 저장 위치가 있다는 의미가 아니라 피연산자의 숫자 값이 RHS(오른쪽)에 의해 지정된다는 의미입니다. 예를 들어 가장 일반적으로 사용되는 주소 지정 모드 중 일부를 간략하게 살펴보겠습니다.

  • 즉시: Vimm. 피연산자의 값으로 상수 imm을 사용합니다.

  • 등록: Vr1. 이 주소 지정 모드에서 프로세서는 레지스터에 포함된 값을 피연산자로 사용합니다.

  • 간접 등록: V[r1]. 레지스터는 값을 포함하는 메모리 위치의 주소를 보유합니다.

  • 베이스 오프셋: V[r1+]. offset이 상수이면 프로세서는 r1에서 기본 메모리 주소를 가져오고 해당 주소에 상수 오프셋을 추가한 다음 새 메모리 위치에 액세스하여 피연산자의 값을 가져옵니다. 오프셋은 변위라고도 합니다.

  • 기본 인덱스: V[r1+r2]. r1은 기본 주소 레지스터이고, r2는 인덱스 레지스터이며, 메모리 주소는 (r1+r2)와 같습니다.

  • 기본 인덱스 오프셋: V[r1+r2+]. 이 값을 포함하는 메모리 주소는 (r1+r2+offset)이며, 여기서 오프셋은 상수입니다.

  • 메모리 다이렉트: Vaddr. 값은 상수인 주소 addr에서 시작하는 메모리에 포함됩니다. 이 경우 메모리 주소는 명령어에 직접 포함됩니다.

  • 메모리 간접 참조: V[[r1]]. 이 값은 주소가 메모리 위치 M에 포함되어 있고 주소가 레지스터 r1에 포함되어 있는 메모리 위치에 존재합니다.

  • PC 관련: V[PC+]. 오프셋 양은 상수이며, 메모리 주소는 PC+오프셋으로 계산됩니다. 여기서 PC는 PC에 포함된 값을 나타냅니다. 이 주소 지정 모드는 분기 명령에 유용합니다. 여기서 PC는 프로그램 카운터를 의미합니다.

베이스 오프셋 주소 지정 모드를 고려하여 유효 메모리 주소라는 새로운 용어를 소개하겠습니다. 메모리 주소는 기본 레지스터의 내용에 오프셋을 더한 것과 같습니다. 계산된 메모리 주소를 유효 메모리 주소라고 합니다. 메모리 피연산자의 경우 다른 주소 지정 모드에 대한 유효 주소를 유사하게 정의할 수 있습니다.

여러 주소 지정 모드.

x86 주소 지정 모드 계산.

19.2.1.6 공통 명령어 설명

이 섹션에서는 ARM을 벤치마크로 사용하여 일반적인 RISC 명령어의 사용법을 설명합니다.

데이터 전송 명령

데이터 전송 지침에는 다음 범주가 포함됩니다.

  • LDR 및 STR

32비트 워드, 8비트 부호 없는 바이트, 하프 워드, 부호 없는 바이트, 더블 워드 등을 포함한 로드 가능한 레지스터 및 저장 레지스터

LDR과 STR에는 모두 제로 오프셋, 사전 인덱스 오프셋, 프로그램 종속 및 사후 인덱스 오프셋이라는 네 가지 형태가 있습니다. 네 가지 형태의 문법적 순서는 동일하며 다음과 같습니다.

cpp
op{cond}{B}{T} Rd, [Rn]
op{cond}{B} Rd, [Rn, FlexOffset]{!}
op{cond}{B} Rd, label
op{cond}{B}{T} Rd, [Rn], FlexOffset

위의 내용은 32비트 단어 또는 8비트 부호 없는 바이트에 대한 것입니다. 이중 단어가 필요한 경우 B를 D로 변경합니다.

cpp
op{cond}D Rd, [Rn]
op{cond}D Rd, [Rn, Offset]{!}
op{cond}D Rd, label
op{cond}D Rd, [Rn], Offset

하프워드 및 부호 있는 바이트 구문은 다음과 같습니다.

cpp
op{cond}        type Rd, [Rn]
op{cond}        type Rd, [Rn, Offset]{!}
op{cond}        type Rd, label
op{cond}        type Rd, [Rn], Offset

예:

cpp
; 示例1
SUB R1, PC, #4 ; R1 = address of following STR instruction
STR PC, [R0]   ; Store address of STR instruction + offset,
LDR R0, [R0]   ; then reload it
SUB R0, R0, R1 ; Calculate the offset as the difference

; 示例2
LDRD    r6,[r11]
LDRMID  r4,[r7],r2
STRD    r4,[r9,#24]
STRD    r0,[r9,-r2]!
LDREQD  r8,abc4

; 示例3
LDREQSH r11,[r6]        ; (conditionally) loads r11 with a 16-bit halfword from the address in r6. Sign extends to 32 bits.
LDRH    r1,[r0,#22]     ; load r1 with a 16 bit halfword from 22 bytes above the address in r0. Zero extend to 32 bits.
STRH    r4,[r0,r1]!     ; store the least significant halfword from r4 to two bytes at an address equal to contents(r0) plus contents(r1). Write address back into r0.
LDRSB   r6,constf       ; load a byte located at label constf. Sign extend.
  • LDM 및 STM

LDM 및 STM은 여러 레지스터를 로드 및 저장하며, 레지스터 r0~r15의 모든 조합을 전송할 수 있습니다. 구문은 다음과 같습니다.

cpp
op{cond}mode Rn{!}, reglist{^}

예:

cpp
LDMIA   r8,{r0,r2,r9}
STMDB   r1!,{r3-r6,r11,r12}
STMFD   r13!,{r0,r4-r7,LR}  ; Push registers including the stack pointer
LDMFD   r13!,{r0,r4-r7,PC}  ; Pop the same registers and return from subroutine
  • PLD

PLD 캐시 사전 로딩. 문법:

cpp
PLD [Rn{, FlexOffset}]

예:

cpp
PLD [r2]
PLD [r15,#280]
PLD [r9,#-2481]
PLD [r0,#av*4]  ; av * 4 must evaluate, at assembly time, to an integer in the range -4095 to +4095
PLD [r0,r2]
PLD [r5,r8,LSL 2]
  • SWP

레지스터와 메모리 간에 데이터를 교환하려면 SWP를 사용하여 세마포어를 구현합니다. 문법:

cpp
SWP{cond}{B} Rd, Rm, [Rn]

일반 데이터 처리 지침

이러한 지침에는 다음이 포함됩니다.

  • 유연한 두 번째 피연산자

대부분의 ARM 범용 데이터 처리 명령어에는 각 명령어의 구문 설명에 Operand2로 표시된 유연한 두 번째 피연산자가 있습니다. 구문은 두 가지 형식으로 제공됩니다.

cpp
#immed_8r
Rm{, shift}

예:

cpp
ADD     r3,r7,#1020         ; immed_8r. 1020 is 0xFF rotated right by 30 bits.
AND     r0,r5,r2            ; r2 contains the data for Operand2.
SUB     r11,r12,r3,ASR #5   ; Operand2 is the contents of r3 divided by 32.
MOVS    r4,r4, LSR #32      ; Updates the C flag to r4 bit 31. Clears r4 to 0.
  • 추가, 하위, RSB, ADC, SBC 및 RSC

이러한 명령어의 구문은 다음과 같습니다.

cpp
op{cond}{S} Rd, Rn, Operand2

예:

cpp
ADD     r2,r1,r3
SUBS    r8,r6,#240      ; sets the flags on the result
RSB     r4,r4,#1280     ; subtracts contents of r4 from 1280
ADCHI   r11,r0,r3       ; only executed if C flag set and Z flag clear
RSCLES  r0,r5,r0,LSL r4 ; conditional, flags set
  • AND, ORR, EOR 및 BIC

논리 연산의 구문은 다음과 같습니다.

cpp
op{cond}{S} Rd, Rn, Operand2

예:

cpp
AND     r9,r2,#0xFF00
ORREQ   r2,r0,r5
EORS    r0,r0,r3,ROR r6
BICNES  r8,r10,r0,RRX
  • MOV, MVN, CMP, CMN, TST, TEQ, CLZ

이동, 비교, 테스트, 선행 0 계산 명령의 구문은 다음과 같습니다.

cpp
MOV{cond}{S} Rd, Operand2
MVN{cond}{S} Rd, Operand2

CMP{cond} Rn, Operand2
CMN{cond} Rn, Operand2

TST{cond} Rn, Operand2
TEQ{cond} Rn, Operand2

CLZ{cond} Rd, Rm

예:

cpp
MOV     r5,r2
MVNNE   r11,#0xF000000B
MOVS    r0,r0,ASR r3

CMP     r2,r9
CMN     r0,#6400
CMPGT   r13,r7,LSL #2

TST     r0,#0x3F8
TEQEQ   r10,r9
TSTNE   r1,r5,ASR r1

CLZ     r4,r9
CLZNE   r2,r3

산술 지침

산술 명령어에는 많은 수의 곱셈 명령어가 포함되며 곱셈 명령어는 더 복잡합니다. 일반적인 산술 명령어는 다음 표에 나와 있습니다.

지침 문법 설명

MUL, MLA MUL{cond}{S} Rd, Rm, Rs MLA{cond}{S} Rd, Rm, Rs, Rn 곱셈 및 곱셈-누산(32비트에 32비트를 곱하고 결과의 하위 32비트 가져옴)

UMULL, UMLAL, SMULL, SMLAL Op{cond}{S} RdLo, RdHi, Rm, Rs 부호 없는 및 부호 있는 긴 곱셈과 곱셈-누산(32비트 x 32비트, 64비트 누산 또는 결과).

SMULxy, SMLAxy, SMULWy, SMLAWy, SMLALxy SMLA{조건} Rd, Rm, Rs, Rn SMULW{조건} Rd, Rm, Rs SMLAW{조건} Rd, Rm, Rs, Rn 부호 있는 곱셈(16 또는 32비트에 16 또는 32비트를 곱하면 결과는 32 또는 64비트, 일부 명령에는 누적이 있음)

MIA, MIAPH, MIAxy MIA{조건} Acc, Rm, Rs MIA{조건} Acc, Rm, Rs XScale 보조 프로세서 0 지시어.

예:

cpp
MUL     r10,r2,r5
MLA     r10,r2,r1,r5
MULS    r0,r2,r2
MULLT   r2,r3,r2
MLAVCS  r8,r6,r3,r8

UMULL       r0,r4,r5,r6
UMLALS      r4,r5,r3,r8
SMLALLES    r8,r9,r7,r6
SMULLNE     r0,r1,r9,r0 ; Rs can be the same as other registers

SMLAWB      r2,r4,r7,r1
SMLAWTVS    r0,r0,r9,r2

MIA     acc0,r5,r0
MIALE   acc0,r1,r9
MIAPH   acc0,r0,r7
MIAPHNE acc0,r11,r10
MIABB   acc0,r8,r9
MIABT   acc0,r8,r8
MIATB   acc0,r5,r3
MIATT   acc0,r0,r6
MIABTGT acc0,r2,r5

지점 지시

분기 문은 다음 표에 설명되어 있습니다.

지침 문법 설명

비엘 B/BL {cond} 라벨 지점 및 연결된 지점

BX BX{cond}RM 분기 및 스왑 명령어 세트

BLX BLX{cond}Rm BLX 라벨 링크 분기를 사용하고 선택적으로 명령어 세트를 교환합니다. 이 설명에는 두 가지 선택적 형식이 있습니다.

  1. 프로그램 상대 주소에 연결된 무조건 분기
  2. 레지스터에 저장된 절대 주소에 연결된 조건 분기.

예:

cpp
B       loopA
BLE     ng+8
BL      subC
BLLT    rtX

BX      r7
BXVS    r0

BLX     r2
BLXNE   r0
BLX     thumbsub

조건부 실행

거의 모든 ARM 명령어에는 선택적 조건 코드가 포함될 수 있습니다. 이는 구문 설명에서 {cond}로 표시됩니다. 조건 코드가 있는 명령은 CPSR의 조건 코드 플래그가 지정된 조건을 충족하는 경우에만 실행됩니다. 사용할 수 있는 조건 코드는 아래 표와 같습니다(일부).

접미사 마크 의미

EQ Z 설정

북동쪽 Z지우기 !=

CS,HS C 설정

= (부호 없음)

CC,LO Cclear = (부호 없음)

MI N 설정 음수

PL N지우기 음수가 아닌 숫자

VS V 설정 넘침

VC Vclear 오버플로 없음

안녕하세요 C가 설정되고 Z가 지워집니다.

(서명되지 않음)

엘에스 C 클리어 또는 Z 세트 <= (부호 없음)

GE N은 V와 같다

= (서명됨)

LT N과 V는 다르다 < (서명됨)

GT Z는 지워지고 N은 V와 동일합니다.

(서명)

르 Z 설정 또는 N과 V가 다릅니다. <= (서명됨)

알 임의의 항상 (보통 생략됨)

거의 모든 ARM 데이터 처리 명령어는 결과에 따라 조건부 코드 플래그를 선택적으로 업데이트할 수 있습니다. 지시문 업데이트 플래그를 만들려면 지시문의 구문 설명에 표시된 대로 S 접미사를 포함합니다.

일부 명령어(CMP, CMN, TST 및 TEQ)에는 S 접미사가 필요하지 않습니다. 유일한 기능은 항상 업데이트되는 플래그를 업데이트하는 것입니다.

플래그는 업데이트될 때까지 유지되며, 실행되지 않은 조건부 명령어는 플래그에 영향을 주지 않으며, 일부 명령어는 플래그의 하위 집합을 업데이트하고, 다른 플래그는 이러한 명령어의 영향을 받지 않습니다. 자세한 내용은 지침에 명시되어 있습니다. 명령어는 다른 명령어에 설정된 플래그를 기반으로 조건부로 실행될 수 있습니다.

  • 플래그를 업데이트하라는 지시 직후.

  • 플래그를 업데이트하지 않고 개입 명령을 여러 번 수행한 후.

예:

cpp
ADD     r0, r1, r2    ; r0 = r1 + r2, don't update flags
ADDS    r0, r1, r2    ; r0 = r1 + r2, and update flags
ADDCSS  r0, r1, r2    ; If C flag set then r0 = r1 + r2, and update flags
CMP     r0, r1        ; update flags based on r0-r1.

기타 지침

ARM에는 조정 프로세서, 의사 명령어, 기타 명령어 등과 같은 다른 명령어도 있습니다. 이 기사에서는 공간 제한으로 인해 해당 명령어를 허용하지 않습니다. 자세한 내용은 다음을 참조하세요.

  • ARM: 어셈블리 언어 소개

  • ARM 명령어 참조

  • ARM 및 Thumb 명령어 요약

19.2.2 바이너리 비트

컴퓨터는 인간처럼 단어나 문장을 이해하지 못하고 0과 1의 시퀀스만 이해하며 수십억 개의 0과 1을 저장, 검색 및 처리하는 것이 매우 쉽습니다. 둘째, 실리콘 트랜지스터를 이용해 컴퓨터를 구현하는 기존 기술은 0과 1을 처리하는 개념과 매우 호환된다. 기본적인 실리콘 트랜지스터는 입력에 따라 출력을 논리 0 또는 논리 1로 설정할 수 있는 스위치이다. 실리콘 트랜지스터는 전화기의 프로세서부터 슈퍼컴퓨터의 프로세서에 이르기까지 오늘날 우리가 사용하는 모든 전자 컴퓨터의 기초입니다. 19세기 후반에 제작된 일부 초기 컴퓨터는 십진수를 처리했으며 대부분 기계식이었습니다. 먼저 몇 가지 간단한 용어를 명확하게 정의해 보겠습니다.

비트: 0 또는 1의 두 가지 값을 가질 수 있는 변수입니다.

바이트: 8비트 시퀀스입니다.

19.2.2.1 논리 연산

이진 변수(0 또는 1)는 1854년 George Boole에 의해 처음 설명되었습니다. 그는 이러한 변수와 관련 연산을 사용하여 논리를 수학적 의미로 설명했습니다. 그는 간단한 이진 변수, 새로운 연산자 세트 및 기본 연산으로 구성된 완전한 대수학을 설계했습니다. George Boole을 기념하여 이진 변수를 부울 변수라고도 하며 부울 변수의 대수 시스템을 부울 대수라고 합니다. 논리 비트 연산은 다음과 같습니다.

  • 아닙니다

논리 보수를 NOT 연산자라고 합니다. 모든 부울 연산자는 진리표로 표현될 수 있습니다. 진리표에는 가능한 모든 입력 조합에 대한 연산자 출력이 나열되어 있습니다. NOT 연산자의 진리표는 다음 표에 나와 있습니다.

원래 값 수술 후 아님

0 1

1 0

  • 또는

OR 연산자는 피연산자 중 하나가 1이라는 사실을 나타냅니다. 예를 들어, A=1 또는 B=1이면 A 또는 B는 1입니다. OR 연산자의 진리표는 아래와 같습니다.

에이 비 A 또는 B

0 0 0

0 1 1

1 0 1

1 1 1

  • 그리고

AND 연산자의 연산은 모든 피연산자가 1이면 결과는 1이 되고 나머지는 0이 되는 것입니다. 예를 들어 A와 B가 모두 1일 때 A와 B는 1입니다. AND 연산자의 진리표는 아래와 같습니다.

에이 비 A와 B

0 0 0

0 1 0

1 0 0

1 1 1

  • NAND, NOR

다른 두 가지 간단한 연산자인 NAND와 NOR도 매우 유용합니다. NAND는 AND의 논리적 보완이고 NOR은 OR의 논리적 보완입니다. 그들의 진리표는 아래와 같습니다.

에이 비 낸드 B A도 B도 아니다

0 0 1 1

0 1 1 0

1 0 1 0

1 1 0 0

NAND와 NOR은 범용 연산자라고 불리기 때문에 매우 중요한 연산자이며, 이들만 사용하여 다른 연산자를 구성할 수 있습니다.

  • XOR

XOR은 배타적 OR 연산자로, A와 B가 같으면 값이 0이고, 다르면 1입니다. 진리표는 아래와 같습니다.

에이 비 A XOR B

0 0 0

0 1 1

1 0 1

1 1 0

19.2.2.2 부울 대수

NOT 연산자의 몇 가지 규칙을 살펴보겠습니다.

  • 정의: 0=1, 및 1=0, 이는 NOT 연산자의 정의입니다.

  • 이중 부정: \overline{\overline{A}=A, A 이외의 모든 것은 A 자체와 동일하지 않습니다.

OR 및 AND 연산자:

  • 항등식: A+0=A, A.1=A, 즉 부울 변수 A와 0의 OR 또는 1의 AND를 계산하면 결과는 A와 같습니다.

  • 소멸: A+1=1, A.0=0, 즉 A OR 1이 계산되면 결과는 항상 1과 같습니다. 마찬가지로 두 번째 피연산자의 값이 최종 결과를 결정하므로 A AND 0은 항상 0과 같습니다.

  • 멱등성: A+A=A, A.A=A, 즉 A와 자신이 A인 OR 또는 AND를 계산한 결과.

  • 상보성: A+A=1, A.A=0, 즉 A=1 또는 A=1. 두 경우 모두 A+A에는 하나의 항(1)이 있으므로 결과는 1입니다. 마찬가지로 A.A의 한 항은 0이므로 결과는 0입니다.

  • 교환성: A.B=B.A, A+B=B+A, 즉 불리언 변수의 순서는 중요하지 않습니다.

  • 상관관계: A+(B+C) = (A+B)+C, A.(B.C)=(A.B).C.

  • 분배법칙: A.(B+C) = A.B+A.C, A+B.C=(A+B).(A+C), 즉 이 법칙을 사용하여 괄호를 열고 표현식을 단순화할 수 있습니다.

이러한 규칙을 사용하여 부울 변수가 포함된 표현식을 다양한 방식으로 조작할 수 있습니다. 부울 대수의 기본 정리 세트를 살펴보겠습니다.

  • 드 모건의 법칙

LHS와 RHS에 대한 진리표를 구성하여 확인할 수 있는 두 가지 Morgan의 법칙이 있습니다.

A+B=A.BAB=A+B
  • 논리 게이트

이제 복잡한 부울 공식을 구현하는 회로를 구현해 보겠습니다. "논리 게이트"는 부울 함수를 구현하는 장치로 정의되며 실리콘, 진공관 또는 기타 재료로 만들어질 수 있습니다.

논리 게이트는 부울 함수를 구현하는 장치입니다.

일련의 논리 게이트가 주어지면 모든 부울 함수를 구현하는 회로를 설계할 수 있습니다. 다양한 논리 게이트의 기호가 아래 그림에 표시되어 있습니다.

논리 게이트 목록.

19.2.3 트랜지스터

실리콘은 주기율표의 14번째 원소이며 4개의 원자가 전자를 가지고 있습니다. 탄소, 게르마늄과 같은 족에 속하지만 화학적 반응성은 게르마늄만큼 좋지 않습니다.

지각의 90% 이상이 규소 기반 광물로 구성되어 있으며, 모래와 석영의 주성분인 실리카는 공급량이 풍부하고 제조 비용이 상당히 저렴합니다. 실리콘은 회로 및 프로세서 설계에 이상적인 기판이 되는 몇 가지 흥미로운 특성을 가지고 있습니다. 실리콘의 분자 구조를 고려해 봅시다. 촘촘한 구조를 가지고 있습니다. 각 실리콘 원자는 4개의 다른 실리콘 원자와 연결되어 있습니다. 단단하게 연결된 실리콘 원자 그룹이 서로 결합하여 강한 격자를 형성합니다. 다른 물질(특히 다이아몬드)도 유사한 결정 구조를 가지고 있습니다. 결과적으로 실리콘 원자는 대부분의 금속보다 서로 더 가깝습니다.

실리콘은 자유전자가 부족하여 전기 전도성이 좋지 않고 양호한 도체와 절연체 사이에 있어서 반도체라고 합니다. 통제된 방식으로 불순물을 추가하면 그 특성이 약간 바뀔 수 있습니다. 이 과정을 도핑이라고 합니다.

19.2.3.1 도핑

일반적으로 실리콘의 특성을 변경하기 위해 n형과 p형이라는 두 가지 불순물이 실리콘에 추가됩니다. N형 불순물은 일반적으로 주기율표의 V족 원소로 구성되며, 인이 가장 일반적인 n형 도펀트이고 때때로 비소가 사용됩니다. 원자가 전자를 갖는 V족 도펀트를 추가하는 효과는 여분의 전자가 결정 격자에서 분리되어 전류를 전도하는 데 사용될 수 있다는 것입니다. 이 도핑 공정은 실리콘의 전도성을 효과적으로 증가시킵니다.

마찬가지로, 붕소나 갈륨과 같은 III족 원소를 실리콘에 첨가하여 p형 도핑된 실리콘을 생성할 수 있는데, 이는 반대 효과를 가지며 결정 격자에 정공이라고도 알려진 공극을 생성합니다. 구멍은 전자가 없음을 나타냅니다. 전자와 마찬가지로 정공도 자유롭게 움직일 수 있으며 전류 전도에도 도움이 됩니다. 전자는 음전하를 띠고, 정공은 개념적으로 양전하와 관련이 있습니다.

이제 n형과 p형이라는 두 가지 반도체 재료를 만들었으니, 이들을 연결하여 p-n 접합을 형성하면 어떤 일이 일어나는지 살펴보겠습니다.

19.2.3.2 P-N 접합

아래 그림과 같이 pn 접합을 생각해 봅시다. p형 영역은 정공이 과잉이고, n형 영역은 전자가 과잉입니다. 접합부에서 일부 정공은 전자에 이끌려 교차하여 n 영역으로 이동합니다. 마찬가지로, 일부 전자는 p-영역 쪽에 교차하여 축적됩니다. 이러한 정공과 전자의 이동을 확산이라고 하며, 이러한 이동을 목격하는 접합 주변 영역을 공핍 영역이라고 합니다. 그러나 전자와 정공의 이동으로 인해 공핍 영역에서는 이동 방향과 반대되는 전기장이 생성됩니다. 이 전기장은 드리프트 전류라고 불리는 전류를 유도합니다. 정상 상태에서는 드리프트 전류와 확산 전류가 서로 균형을 이루므로 실제로 접합을 통해 전류가 흐르지 않습니다.

p측을 양극 단자에 연결하고 n측을 음극 단자에 연결하는 경우 이 구성을 순방향 바이어스라고 합니다. 이 경우 정공은 접합의 p측에서 n측으로 흐르고, 전자는 반대 방향으로 흐릅니다. 따라서 접합에는 전류가 흐릅니다.

p측을 음극 단자에 연결하고 n측을 양극 단자에 연결하는 경우 이 구성을 역바이어스라고 합니다. 이 경우 정공과 전자는 접합부에서 멀어집니다. 따라서 접합을 통해 전류가 흐르지 않으며 이 경우 p-n 접합은 전기를 전도하지 않습니다. 설명된 간단한 p-n 접합을 다이오드라고 하며 한 방향, 즉 순방향 바이어스된 방향으로만 전류를 전도합니다.

다이오드는 일반적으로 한 방향으로만 전류를 전도하는 단일 p-n 접합으로 만들어진 전자 장치입니다.

19.2.3.3 NMOS 트랜지스터

이제 아래 그림 (a)와 같이 두 p-n 접합을 서로 연결해 보겠습니다. 이 구조를 NMOS(Negative Metal Oxide Semiconductor) 트랜지스터라고 합니다. 이 사진에는 p형 도핑된 실리콘의 중앙 기판이 있습니다. n형 도핑된 실리콘을 포함하는 두 개의 작은 영역이 양쪽에 있습니다. 이러한 영역을 각각 드레인 및 소스라고 합니다. 구조가 완전히 대칭이므로 이 두 영역 중 하나를 소스 또는 드레인으로 지정할 수 있으며 소스와 드레인 사이의 영역을 채널이라고 합니다. 채널 상단에는 일반적으로 이산화규소(SiO2)로 만들어진 얇은 절연층이 있으며, 이 층은 소위 게이트라고 불리는 금속 또는 폴리실리콘 기반 전도성 층으로 덮여 있습니다.

따라서 일반적인 NMOS 트랜지스터에는 소스, 드레인, 게이트의 세 가지 단자가 있으며 각 단자는 전압 소스에 연결될 수 있습니다. 이제 논리 1(Vdd 볼트) 또는 논리 0(0 볼트)의 두 가지 게이트 전압 옵션이 있습니다. 게이트의 전압이 논리 1(Vdd 볼트)이면 채널의 전자가 게이트로 끌어당겨집니다. 실제로 게이트의 전압이 특정 임계 전압(현재 기술에서는 일반적으로 0.15V)보다 높으면 전자의 축적으로 인해 드레인과 소스 사이에 낮은 저항의 전도성 경로가 형성됩니다. 따라서 드레인과 소스 사이에 전류가 흐를 수 있습니다. 채널의 유효 저항이 R 채널이면 Vdrain=IRchannel+Vsource가 됩니다. 트랜지스터를 통해 흐르는 전류량이 낮을 경우, 채널 저항(R-채널)이 낮기 때문에 VdrainVsource와 대략 같습니다. 따라서 NMOS 트랜지스터를 스위치로 생각할 수 있습니다(위 그림 b 참조). 게이트 전압이 1이 되면 ON된다.

이제 게이트 전압을 0으로 설정하면 전자로 구성된 전도 경로가 채널에 형성될 수 없습니다. 따라서 트랜지스터는 전류를 전도할 수 없고 o 상태가 됩니다. 이 경우 스위치가 닫힙니다.

NMOS 트랜지스터의 회로 기호는 위의 (c)에 나와 있습니다.

19.2.3.4 PMOS 트랜지스터

NMOS 트랜지스터와 마찬가지로 PMOS 트랜지스터도 사용할 수 있습니다. 아래 (a)와 같이 소스와 드레인은 p형 실리콘으로 이루어진 영역이며 트랜지스터 작동 논리는 NMOS 트랜지스터의 논리와 완전히 반대입니다. 이 경우 게이트가 논리 0이면 정공이 채널로 끌어당겨 전도성 경로를 형성합니다. 그러나 게이트가 논리 1이면 정공이 채널에 의해 반발되어 전도성 경로를 형성하지 않습니다.

PMOS 트랜지스터는 스위치로도 볼 수 있습니다(그림 b). 게이트의 전압이 0이면 켜지고 게이트의 전압이 논리 1이면 꺼집니다. PMOS 트랜지스터의 회로 기호는 그림 (c)에 나와 있습니다.

19.2.3.5 NAND 및 NOR 게이트

아래 이미지는 CMOS 기술로 NAND 게이트를 구축하는 방법을 보여줍니다. 두 개의 입력 A와 B는 각 NMOS-PMOS 쌍의 게이트에 연결됩니다. A와 B가 모두 1이면 PMOS 트랜지스터는 꺼지고 NMOS 트랜지스터는 켜져 출력을 논리 0으로 설정합니다. 그러나 입력 중 하나가 0이면 NMOS 트랜지스터 중 하나는 꺼지고 PMOS 트랜지스터 중 하나는 켜집니다. 따라서 출력은 논리 1로 설정됩니다.

AND 연산자 "."를 사용한다는 점에 유의하세요. 이 기호는 부울 수식을 나타낼 때 널리 사용됩니다. 마찬가지로 OR 연산의 경우 "+" 기호를 사용합니다.

아래 이미지는 NOR 게이트를 만드는 방법을 보여줍니다. 이 경우 두 개의 입력 A와 B도 각 NMOS-PMOS 쌍의 게이트에 연결됩니다. 그러나 토폴로지는 NAND 게이트와 다릅니다. 입력 중 하나가 로직 1이면 NMOS 트랜지스터 중 하나가 켜지고 PMOS 트랜지스터 중 하나가 꺼지며 출력은 0으로 설정됩니다. 두 입력이 모두 0이면 두 개의 NMOS 크리스탈이 꺼지고 두 개의 PMOS 크리스탈이 켜지며 출력은 로직 1과 같습니다.

NAND 게이트의 일부 용도는 다음과 같습니다.

NOR 게이트의 일부 용도는 다음과 같습니다.

19.2.4 조합 논리 회로

19.2.4.1 XOR 게이트

XOR 연산을 수행하는 연산자를 사용하여 배타적 OR(XOR)의 논리 함수를 구현해 보겠습니다. 두 입력이 같지 않으면 XOR 연산은 1을 반환하고, 그렇지 않으면 0을 반환합니다. AB=AB+AB라고 알려져 있으므로 XOR 게이트를 구현하는 진리표와 회로는 다음과 같습니다.

기본 논리 게이트는 다음과 같습니다.

19.2.4.2 멀티플렉서 및 디멀티플렉서

멀티플렉서(Multiplexer)의 블록 다이어그램은 아래 그림의 왼쪽에 나와 있습니다. n개의 입력 비트와 log(n) 선택 비트를 사용하며 선택 비트의 값에 따라 입력을 출력으로 선택합니다(그림의 화살표 참조). 멀티플렉서는 입력 세트에서 출력을 선택해야 하는 프로세서 설계에 많이 사용됩니다. 멀티플렉서는 멀티플렉서라고도 합니다.

신호 분배기는 log(n) 비트 이진수, 1비트를 입력으로 사용하고 입력을 n 출력 라인 중 하나로 전송합니다(아래 오른쪽 그림 참조). 디멀티플렉서는 입력이 하나의 출력 라인에 정확하게 반영되어야 하는 메모리 셀 설계에 사용됩니다.

왼쪽: 단일 멀티플렉서 블록 다이어그램. 중앙: 4입력 멀티플렉서. 오른쪽: 신호 분배기.

프로그램 카운터에 대한 멀티플렉서 입력.

19.2.4.3 인코더 및 디코더

디코더는 log(n) 비트 이진수를 입력으로 취하고 n개의 출력을 갖습니다. 입력에 따라 출력 중 하나를 1로 설정합니다.

디코더의 설계는 아래 그림의 왼쪽에 표시되어 있으며 입력 2개와 출력 4개가 있는 2x4 디코더의 설계입니다. 입력이 A와 B라고 가정합니다. 가능한 모든 조합을 생성합니다: AB,AB,AB,AB. 이러한 부울 조합은 A와 B의 논리적 NOT을 계산한 다음 이 값을 AND 게이트 세트로 라우팅하여 생성됩니다.

이제 디코더의 반대 논리를 가진 회로를 고려해 보겠습니다. 블록 다이어그램은 아래 그림의 오른쪽에 표시됩니다. 회로에는 n개의 입력과 log(n) 출력이 있으며, n개의 입력 중 하나는 1로 가정되고 나머지는 0으로 가정되며, 출력 비트는 1과 동일한 입력 이진 인코딩을 제공합니다. 예를 들어, 8입력, 3출력 인코더에서 행 f가 1이면 출력은 100과 같습니다(계산은 0에서 시작).

왼쪽: 2x4 디코더의 디자인. 오른쪽: n비트 인코더 블록 다이어그램.

이제 입력 행 하나만 1이 될 수 있다는 제한이 없고 1이 될 수 있는 입력이 여러 개 있다고 가정해 보겠습니다. 이 경우 인덱스(우선 순위)가 가장 높은 입력 행의 이진 인코딩을 보고해야 합니다. 예를 들어 행 3과 5인 경우 위 오른쪽 그림과 같이 행 5의 이진 인코딩을 보고해야 합니다. 또한, 4-2비트 인코더의 회로도는 아래 그림과 같습니다.

디코더를 사용하여 디멀티플렉서를 구현합니다.

클록 SR 래치(아래 그림 왼쪽) 및 D 래치(아래 그림 오른쪽):

J–K 래치:

기본 래치 비교:

19.2.5 순차 논리 회로

우리는 이전에 비트에 대한 다양한 기능을 계산하는 조합 논리 회로를 살펴보았습니다. 이 섹션에서는 나중에 사용하기 위해 비트를 저장하는 방법에 대해 설명합니다. 출력은 이벤트 시퀀스의 초기에 발생한 과거 입력에 따라 달라지기 때문에 이러한 구조를 순차 논리 요소라고 합니다. 논리 게이트의 기본 아이디어는 원하는 출력을 얻기 위해 입력 값을 수정하는 것입니다. 조합 논리 회로에서는 입력이 0으로 설정되면 출력도 재설정됩니다. 회로가 값을 저장하고 프로세서 전원을 켤 때 해당 값을 유지하려면 일종의 "내장 메모리"를 사용하여 다른 유형의 회로를 설계해야 합니다. 일련의 요구사항을 개발하는 것부터 시작해 보겠습니다.

  1. 회로는 자립적이어야 하며 외부 입력 재설정 후에도 해당 값을 유지해야 합니다. 저장을 유지하기 위해 외부 신호에 의존해서는 안 되는 요소입니다.

  2. 저장된 값을 파괴하지 않고 읽을 수 있는 방법이 있어야 합니다.

  3. 저장된 값을 0 또는 1로 설정할 수 있는 방법이 있어야 합니다.

회로의 가치를 유지하는 가장 좋은 방법은 피드백 경로를 만들고 출력을 다시 입력에 연결하는 것입니다. 가장 간단한 논리 회로인 SR 래치부터 시작해 보겠습니다.

19.2.5.1 SR 래치

아래 그림은 SR 래치를 보여줍니다. 2개의 입력 S(설정)와 R(리셋)이 있고, 2개의 출력 Q와 그 보수 Q가 있으며, 교차 결합된 2개의 NAND 게이트 회로를 포함합니다. NAND 게이트의 한 입력이 0이면 출력은 1이 보장됩니다. 그러나 입력 중 하나가 1이고 다른 입력이 a이면 출력은 a입니다.

NOR 게이트로 구현된 SR 래치:

19.2.5.2 클록 및 신호

일반적인 프로세서에는 수백만 또는 수십억 개의 논리 게이트와 수천 개의 래치가 포함되어 있으며 다양한 회로에는 서로 다른 시간이 필요합니다. 예를 들어 멀티플렉서는 1ns가 걸리고 디코더는 0.5ns가 걸릴 수 있습니다. 회로가 계산을 완료하면 출력을 전달할 수 있습니다. 글로벌 시간 개념이 없으면 서로 다른 장치, 특히 지연 시간이 가변적인 장치 간의 통신을 동기화하기가 어렵기 때문에 프로세서를 설계, 작동 및 검증하기가 어렵습니다. 여기에서 시간 개념이 필요합니다. 예를 들어 덧셈기가 2개의 시간 단위를 사용하고, 2개의 단위가 끝나면 예상 데이터가 래치 X에서 발견되고, 다른 단위는 2개의 시간 단위 후에 래치에서 값을 가져와 계산을 계속할 수 있다고 말할 수 있습니다.

일부 데이터를 프린터로 전송해야 하는 프로세서의 예를 생각해 보세요. 데이터를 전송하기 위해 프로세서는 일련의 구리선을 통해 일련의 비트를 보내고, 프린터는 비트를 읽은 다음 데이터를 인쇄합니다. 문제는 프로세서가 언제 데이터를 보내는가입니다. 계산이 완료되면 데이터를 전송해야 합니다. 우리가 물어볼 수 있는 다음 질문은 계산이 언제 끝났는지 프로세서가 어떻게 알 수 있느냐는 것입니다. 서로 다른 셀의 정확한 지연을 알아야 하며, 계산된 총 지속 시간이 경과되면 출력 데이터를 래치에 기록하고 통신에 사용되는 구리선의 전압을 설정할 수 있습니다. 따라서 프로세서에는 시간 개념이 필요합니다. 둘째, 설계자는 다양한 하위 장치에 필요한 시간을 프로세서에 알려야 합니다. 2.34ns 및 1.92ns와 같은 숫자를 처리하는 것보다 1, 2, 3과 같은 정수를 처리하는 것이 훨씬 간단합니다. 여기서 1, 2, 3은 시간 단위를 나타내며 시간 단위는 0.9333ns와 같이 임의의 숫자일 수 있습니다.

클럭 신호: 대형 회로나 프로세서의 각 부분으로 전송되는 주기적인 구형파입니다.

클럭 주기: 클럭 신호의 주기입니다.

클럭 주파수: 클록 주기의 역수입니다.

따라서 대부분의 디지털 회로는 정확히 동시에 프로세서의 모든 부분에 주기적인 펄스를 보내는 클록 신호에 동기화됩니다. 클럭 신호는 아래 그림과 같이 구형파이며, 대부분의 경우 마더보드의 전용 장치에 의해 외부에서 클럭 신호가 생성됩니다. 클럭 신호가 1에서 0(하향/네거티브 에지)으로 전환되는 지점을 클럭 사이클의 시작으로 생각해 보겠습니다. 클록 사이클은 클록의 한 하향 에지에서 다음 하향 에지까지 측정됩니다. 클록 주기의 지속 시간을 클록 주기라고도 하며, 클록 주기의 역수를 클록 주파수라고 합니다.

클럭 신호.

컴퓨터, 노트북, 태블릿 또는 휴대폰은 일반적으로 사양에 주파수를 나열합니다. 예를 들어 사양에는 프로세서가 3GHz에서 실행된다고 나와 있으며 이 숫자는 클록 주파수를 나타냅니다.

일반적인 컴퓨팅 모델은 다음과 같습니다. 회로에서 모든 기본 작업을 수행하는 데 필요한 시간은 클록 주기로 측정됩니다. 생산자 장치가 n 클록 사이클을 사용하는 경우 n 클록 사이클이 끝나면 해당 값을 래치에 씁니다. 다른 사용자 유닛은 이 지연을 인식하고 (n+1)번째 클럭 사이클이 시작될 때 래치에서 값을 읽습니다. 모든 장치는 클록에 명시적으로 동기화되고 프로세서는 각 장치의 대기 시간을 알고 있으므로 계산 순서를 지정하고, I/O 장치와 통신하고, 경쟁 조건을 방지하고, 회로를 디버그 및 검증하는 것이 쉽습니다. 데이터를 프린터로 전송하려는 간단한 예는 시계를 사용하여 쉽게 해결할 수 있습니다.

19.2.5.3 클록 SR 래치

아래 그림은 클록을 입력 중 하나로 사용하고 나머지 두 입력은 S 비트와 R 비트인 두 개의 NAND 게이트가 추가된 SR 래치를 보여줍니다. 클록이 0이면 교차 결합된 NAND 게이트의 두 입력은 모두 1이고 이전 값을 유지합니다. 클록이 1이면 교차 결합된 NAND 게이트의 입력은 각각 S와 R이며, 이러한 입력은 기본 SR 래치와 동일합니다. 클럭 래치를 종종 플립플롭(flip-flop)이라고 부릅니다.

시계 SR 래치 범례.

플립플롭은 1비트(0 또는 1)를 보유할 수 있는 클록 래치입니다.

클럭을 사용하여 입력 및 출력 동기화 문제를 부분적으로 해결합니다. 이 경우 클럭이 0이면 출력은 입력의 영향을 받지 않습니다. 클럭이 1이면 출력이 입력의 영향을 받습니다. 이러한 유형의 래치를 레벨 감지 래치라고도 합니다.

레벨 감지 래치 클럭 신호 값에 따라 다릅니다: 0 또는 1. 일반적으로 클럭이 1일 때만 새 값을 읽을 수 있습니다.

레벨 감지 래치에서 회로는 올바른 출력을 계산하기 위해(클럭이 0일 때) 반 클럭 사이클을 갖습니다. 클럭이 1이면 출력이 표시됩니다. 출력을 계산하기 위해 전체 클록 사이클을 갖는 것이 더 낫습니다. 이를 위해서는 클록의 하향 에지에서 출력의 입력만 반영하는 에지 감지 래치가 필요합니다.

에지 감지 래치는 고정된 클록 에지(예: 하향 에지, 1에서 0으로의 전환)에서만 출력 입력을 반영합니다.

19.2.5.4 에지 감지 SR 플립플롭

아래 그림은 에지 감지 SR 플립플롭의 블록 다이어그램을 보여줍니다. 두 개의 에지 감지 SR 플립플롭이 연결됩니다. 유일한 차이점은 두 번째 플립플롭이 클록 신호의 조합을 사용한다는 것입니다. 첫 번째 트리거는 마스터이고 두 번째 트리거는 슬레이브입니다. 이 플립플롭은 마스터-슬레이브 SR 플립플롭이라고도 합니다. 이것이 이 회로가 작동하는 방식입니다.

마스터-슬레이브 SR 플립플롭 외에도 JK 플립플롭, D 플립플롭, 마스터-슬레이브 D 플립플롭 등 다양한 유형의 플립플롭이 있습니다.

위에서 아래로: JK 플립플롭, D 플립플롭, 마스터-슬레이브 D 플립플롭.

19.2.5.5 등록

n개의 마스터-슬레이브 D 플립플롭 세트를 사용하여 n비트 데이터를 저장할 수 있습니다. 각 D 플립플롭은 입력 라인에 연결되고 출력은 출력 라인에 연결됩니다. 이 n비트 구조를 n비트 레지스터라고 합니다. n 비트를 병렬로 로드할 수 있으며 모든 음수 클록 에지에서 n 비트를 병렬로 읽을 수 있습니다. 따라서 이 구조를 병렬 입출력 레지스터라 부른다. 그 구조는 아래 그림과 같습니다.

이제 아래 그림과 같이 직렬 입력-병렬 출력 레지스터를 고려해 보겠습니다. 가장 왼쪽의 D 플립플롭에 입력이 공급됩니다. 매 사이클마다 입력은 오른쪽의 인접한 플립플롭으로 이동합니다. 따라서 n 비트를 로드하는 데 n 사이클이 걸립니다. 첫 번째 비트는 첫 번째 사이클에서 가장 왼쪽 플립플롭에 로드되며 마지막 플립플롭에 도달하는 데 n 사이클이 걸립니다. 그 시점에서 나머지 n - 1개 플립플롭은 나머지 n - 1비트를 로드한 다음 n 비트를 모두 병렬로 읽을 수 있습니다(병렬 병렬 출력 레지스터와 유사). 이 레지스터는 시프트 레지스터라고도 하며 고속 I/O 버스에 사용되는 회로를 구현하는 데 사용됩니다.

8비트 병렬 레지스터의 구조 다이어그램은 다음과 같습니다.

5비트 시프트 레지스터:

진행파 카운터:

19.2.6 메모리

19.2.6.1 정적 메모리(SRAM)

SRAM은 정적 랜덤 액세스 메모리를 나타냅니다. 기본 SRAM 셀에는 아래 그림과 같이 교차 결합된 인버터 2개가 포함되어 있습니다. 대조적으로, 기본 SR 플립플롭 또는 D 플립플롭에는 교차 결합된 NAND 게이트가 포함되어 있습니다. 디자인은 아래와 같습니다.

SRAM 셀의 코어에는 4개의 트랜지스터(각 인버터에 2개)가 포함되어 있으며 이러한 교차 결합 배열은 단일 비트(0 또는 1)를 절약하기에 충분합니다. 그러나 값을 읽고 쓰려면 추가 회로가 필요합니다. 이 시점에서 래치에 교차 결합 인버터를 사용하는 것은 나쁜 생각입니까? 결국에는 더 적은 수의 트랜지스터가 필요합니다. 우리는 SRAM 셀을 읽고 쓰기 위한 회로를 구현하는 오버헤드가 매우 중요하며 SRAM 셀을 코어로 하는 래치를 만드는 것을 정당화하기에는 오버헤드가 충분하지 않다는 것을 알게 될 것입니다.

교차 연결된 인버터는 각 측면(W1, W2)의 트랜지스터에 연결됩니다. W1과 W2의 게이트는 워드라인이라는 동일한 신호에 연결됩니다. 2개의 인버터 W1, W2에 있는 4개의 트랜지스터가 SRAM 셀을 구성하며, 총 6개의 트랜지스터로 구성됩니다. 이제 워드 라인의 전압이 낮으면 W1과 W2가 꺼지고 SRAM 셀을 읽거나 쓸 수 없습니다. 그러나 워드 라인의 신호가 높으면 W1과 W2가 전도되고 SRAM 셀에 액세스할 수 있습니다.

아래 그림은 SRAM 셀이 2차원 매트릭스로 배열된 일반적인 SRAM 어레이를 보여줍니다. 행의 모든 ​​SRAM 셀은 워드 라인을 공유하고, 열의 모든 SRAM 셀은 한 쌍의 비트 라인을 공유합니다. 특정 SRAM 셀을 활성화하려면 관련 워드라인을 켜야 하며, 이는 디코더에 의해 수행되며, 디코더는 주소 비트의 하위 집합을 가져와 적절한 워드라인을 켭니다. SRAM 셀의 행에는 100개 이상의 SRAM 셀이 포함될 수 있으며 일반적으로 32비트 시스템에서 32개 SRAM 셀의 값에 관심이 있습니다. 이 경우, 열 멀티플렉서/디멀티플렉서는 주소의 비트 하위 집합을 열 선택 비트로 사용하여 관심 있는 SRAM 셀에 속하는 비트 라인을 선택합니다. 이 설계 접근 방식은 2.5D 메모리 구성이라고도 알려져 있습니다.

SRAM 셀 어레이.

19.2.6.2 콘텐츠 주소 지정 가능 메모리(CAM)

아래 그림은 10-트랜지스터 CAM 셀입니다. SRAM 셀에 저장된 V 값이 입력 비트 Ai와 같지 않으면 매치 라인의 값을 0으로 설정하려고 합니다. CAM 셀에서 위쪽 절반은 6개의 트랜지스터가 있는 일반 SRAM 셀이고 아래쪽 절반에는 4개의 추가 트랜지스터가 있습니다. 이제 글로벌 매치 라인인 트랜지스터 T2에 연결된 트랜지스터 T1을 고려해 보자. T1은 SRAM 셀에 저장된 V 값에 의해 제어되고, T2는 Ai에 의해 제어됩니다. V=Ai라고 가정하고 둘 다 1이면 트랜지스터 T1과 T2는 온 상태에 있고 매칭 라인과 접지 사이에 직접적인 전도성 경로가 있습니다. 따라서 일치하는 선의 값은 0으로 설정됩니다. 그러나 V와 Ai가 모두 0이면 T1과 T2를 통한 경로는 전도되지 않습니다. 그러나 이 경우 T3 및 T4를 통과하는 경로는 이들 트랜지스터의 게이트가 각각 VAi에 연결되어 있기 때문에 전도성이 됩니다. 두 게이트에 대한 입력은 로직 1이므로 매치 라인은 0으로 풀다운됩니다. 그러면 리더는 V=Ai인 경우 전도성 경로가 형성되지 않음을 확인할 수 있습니다. 따라서 저장된 값이 입력 비트 Ai와 일치하지 않으면 CAM 셀은 매치 라인을 논리 0으로 구동합니다.

10 트랜지스터 CAM 유닛.

아래 그림은 CAM 셀 어레이를 보여줍니다. 구조는 주로 SRAM 어레이와 유사합니다. 인덱스별로 행의 주소를 지정하고 읽기/쓰기 액세스를 수행할 수 있으며, 또한 CAM 셀의 각 행을 입력 A와 비교할 수 있습니다. 입력과 일치하는 행이 있으면 해당 일치 행의 값은 1이 됩니다. 모든 일치 행의 논리적 OR을 계산하여 CAM 배열에 일치 항목이 있는지 확인할 수 있습니다. 또한 CAM 어레이의 모든 일치 라인을 우선순위 인코더에 연결하여 데이터와 일치하는 라인의 인덱스를 찾을 수 있습니다.

CAM 셀 어레이.

19.2.6.3 동적 메모리(DRAM)

이제 약간의 시간을 절약하기 위해 하나의 트랜지스터만 사용하는 메모리 기술을 살펴보겠습니다. 밀도가 매우 높고 면적이 넓으며 에너지 효율적이지만 SRAM 및 래치보다 훨씬 느리고 대형 오프칩 메모리에 적합합니다.

기본적인 DRAM(Dynamic Memory) 유닛은 아래 그림과 같습니다. 단일 트랜지스터의 게이트는 워드 라인에 연결되어 이를 활성화 또는 비활성화하고, 그 단자 중 하나는 전하를 저장하는 커패시터에 연결됩니다. 저장된 비트가 논리 1이면 커패시터가 충전되고, 그렇지 않으면 충전되지 않습니다.

동적 메모리의 단위.

DRAM과 SRAM 셀의 비교 다이어그램.

따라서 값을 읽고 쓰는 것이 매우 쉽습니다. 커패시터에 접근할 수 있도록 먼저 워드 라인을 설정해야 합니다. 이 값을 읽으려면 비트라인의 전압을 감지해야 합니다. 셀은 접지 ​​전위에 있으면 0을 저장하고, 그렇지 않으면 공급 전압에 가까우면 1을 저장합니다. 마찬가지로, 값을 쓰려면 비트 라인(BL)을 적절한 전압으로 설정하고 워드 라인을 설정해야 하며 그에 따라 커패시터가 충전 또는 방전됩니다.

그러나 DRAM의 경우 모든 것이 무료는 아닙니다. 커패시터가 공급 전압과 동일한 전압으로 충전된다고 가정해 봅시다. 실제로 커패시터는 유전체와 트랜지스터를 통해 점차적으로 일부 전하를 누출할 것입니다. 이 전류는 작지만 장기간에 걸친 총 전하 손실이 상당하여 결국 커패시터가 방전될 수 있습니다. 이를 방지하기 위해서는 주기적으로 DRAM 셀의 값을 리프레시(Refresh)해야 하는데, 즉 데이터 값을 읽고 다시 써야 합니다. 이는 또한 읽기 작업 후에 수행되어야 합니다. 왜냐하면 비트 라인을 충전할 때 커패시터가 일부 전하를 잃기 때문입니다. 이제 DRAM 셀 배열을 만들어 보겠습니다.

SRAM 셀 어레이와 동일한 방식으로 DRAM 셀 어레이(아래 그림)를 구축할 수 있지만 세 가지 차이점이 있습니다.

  • 2개의 비트라인이 아닌 1개의 비트라인이 존재한다.

  • 비트 라인에 연결된 전용 리프레시 회로가 있습니다. 이는 읽기 작업 후에 사용되며 주기적으로 호출되기도 합니다.

  • 이 경우 감지 증폭기는 열 다중화기/역다중화기 앞에 옵니다. 또한 감지 증폭기는 전체 DRAM 행(DRAM 페이지라고도 함)에 대한 데이터를 캐시합니다. 감지 증폭기에서 직접 서비스를 받을 수 있으므로 동일한 DRAM 행에 대한 후속 액세스가 빠르게 이루어지도록 보장합니다.

DRAM 셀 어레이.

다음으로 현대 DRAM의 타이밍 측면을 간략하게 설명합니다. 옛날에는 DRAM 메모리가 비동기식으로 액세스되었습니다. 즉, DRAM 모듈이 타이밍을 보장하지 않았습니다. 그러나 이제 모든 DRAM 작업은 시스템 클럭과 동기화되므로 오늘날의 DRAM 칩은 동기식 DRAM 칩(SDRAM 칩)입니다.

지금까지 동기식 DRAM 메모리는 일반적으로 DDR4 또는 DDR5 표준을 사용했습니다. DDR은 Double Data Rate의 약자입니다. 최초의 표준 DDR1을 사용하는 장치는 클록의 상승 및 하강 에지에서 프로세서에 8바이트 데이터 패킷을 보냅니다. DDR은 이중 펌프 작동이라고도 합니다. DDR1의 최대 데이터 속도는 1.6GB/s였습니다. 후속 DDR 세대는 더 높은 주파수에서 데이터를 전송하여 DDR1을 확장했습니다. 예를 들어, DDR2는 DDR1 장치(3.2GB/s)보다 두 배 빠른 데이터 속도를 갖습니다. DDR3은 더 높은 버스 주파수를 사용하여 최대 전송 속도를 두 배로 늘리며 2007년부터 사용되었습니다(최대 속도 6.4GB/s).

19.2.6.4 읽기 전용 메모리(ROM)

읽기 전용 메모리는 일반 ROM과 PROM(Programmable ROM)으로 구분됩니다. 다음은 해당 유닛의 전설입니다.

(a) 로직 0을 저장하는 ROM 셀; (b) ROM 셀 저장 로직 1.

프롬 유닛.

19.2.6.5 프로그래밍 가능 논리 어레이

프로그래밍 가능 논리 어레이(PLA)라고 불리는 장치인 PROM 셀과 유사한 메모리 셀에서 조합 논리 회로를 쉽게 만들 수 있다는 것이 밝혀졌습니다. PLA는 실제로 수십 또는 수백 개의 최소항으로 구성된 복잡한 논리 기능을 구현하는 데 사용됩니다. 논리 게이트로 구성된 유선 회로에 비해 장점은 유연하고 런타임에 PLA에 의해 구현된 부울 논리를 변경할 수 있다는 것입니다. 대조적으로, 실리콘으로 만들어진 회로는 논리를 결코 바꾸지 않습니다. 둘째, PLA는 설계 및 프로그래밍이 더 간단하며 PLA를 설계하고 사용하는 데 필요한 소프트웨어 도구가 많이 있습니다. 마지막으로 PLA는 여러 출력을 가질 수 있으므로 여러 부울 함수를 쉽게 구현할 수 있습니다. 이러한 추가적인 유연성은 성능 측면에서 비용이 발생합니다.

아래 그림 (a)에 표시된 PLA 셀은 원칙적으로 기본 PROM 셀과 유사합니다. 게이트의 값(E)이 1과 같으면 NMOS 트랜지스터는 전도 상태에 있습니다. 따라서 NMOS 트랜지스터의 소스와 드레인 단자 사이의 전압 차이는 매우 작습니다. 즉, 결과 라인의 전압이 신호의 전압 X와 동일하다고 간단하게 가정할 수 있습니다. (E = 0)이면 NMOS 트랜지스터는 오프 상태입니다. 결과 라인은 플로팅 상태이며 사전 충전 전압을 유지합니다. 이 경우 논리 1을 추론하는 것이 좋습니다.

이제 그림 (b)와 같이 각 PLA 셀이 소스 터미널의 입력 라인에 연결되는 PLA 셀 행을 구축해 보겠습니다. 입력 번호는 X1입니다. 활성화 신호 중 하나라도 0과 같으면 해당 특정 트랜지스터가 비활성화되고 PLA 어레이에서 논리적으로 제거된 것으로 간주할 수 있습니다.

PLA 장치 1개.

이제 아래 이미지와 같이 각 행이 최소항에 해당하는 PLA 셀 배열을 만들어 보겠습니다. 변수가 3개인 예의 경우 각 행은 변수당 2개의 열(원본 및 보충)을 포함하여 6개의 열로 구성됩니다. 처음 두 열은 각각 A와 A에 해당합니다. A와 A가 동시에 참일 수 없기 때문에 모든 행에서 이 두 열 중 하나만 PLA 셀을 포함합니다. 첫 번째 행에서는 최소항 ABC의 값이 계산되므로 첫 번째 행에는 A, BC에 해당하는 열의 PLA 셀이 포함됩니다. 나머지 최소항에 대해 나머지 행에서도 유사한 연결을 만듭니다. PLA 배열의 이 부분은 변수 값(원시 또는 보완)의 논리적 AND를 계산하기 때문에 AND 평면이라고 합니다. PLA 배열의 AND 평면은 입력이 주어지면 계산하려는 부울 함수와 독립적이며 가능한 모든 최소항의 값을 계산합니다.

PLA 유닛 어레이.

일반적인 메모리 패키지 핀 및 신호.

256KB 메모리 구성.

1MB 메모리 구성.

DDR 세대 발전 차트.

비휘발성 RAM 기술.

위: 단순화된 DRAM 읽기 타이밍; 하단: Signetics 7489 SRAM 펄스 트레인.

19.2.7 컴퓨터 연산

이 섹션에서는 산술 연산을 위한 하드웨어 알고리즘을 설계합니다. 먼저 두 개의 이진수를 더하는 기본 알고리즘과 같은 정수 연산에 대한 알고리즘을 설명합니다. 이러한 기본 작업을 완료하는 방법에는 여러 가지가 있으며 각 방법에는 고유한 장점과 단점이 있습니다. 이진 뺄셈 문제는 개념적으로 2의 보수 시스템의 이진 덧셈과 동일합니다. 그러므로 따로 다룰 필요는 없습니다. 나중에 우리는 n개의 숫자를 더하는 문제가 곱셈의 문제와 밀접하게 관련되어 있고 하드웨어에서 빠른 연산이라는 것을 알게 될 것입니다. 불행하게도 정수 나누기에 대한 매우 효율적인 방법은 없습니다. 그러나 양의 이진수를 나누는 데 널리 사용되는 두 가지 알고리즘을 고려해 보겠습니다.

정수 연산 후에는 부동 소수점(소수점이 있는 숫자) 연산 방법을 살펴보겠습니다. 대부분의 정수 연산은 약간의 수정을 통해 부동 소수점 영역으로 이식될 수 있습니다. 부동소수점 나눗셈은 정수 나눗셈에 비해 매우 효율적으로 수행할 수 있습니다.

19.2.7.1 추가

두 개의 한 자리 숫자 a와 b를 더하는 문제를 살펴보겠습니다. a와 b는 모두 0 또는 1이라는 두 가지 값을 가질 수 있습니다. 따라서 a와 b의 가능한 조합은 4가지가 있으며 이진 합은 00, 01 또는 10이 될 수 있습니다. a와 b가 모두 1인 경우 합은 10이 되고 두 개의 한 자리 숫자의 합은 두 자리 길이가 될 수 있습니다. 결과의 LSB를 합, MSB를 캐리라고 부르겠습니다. 예를 들어 8과 9를 더하면 합은 7이 되고 캐리는 1이 됩니다.

합과 캐리의 개념은 세 개의 한 자리 숫자를 더하는 것으로 확장될 수 있습니다. 한 자리 숫자 세 개를 더하면 결과 범위는 이진수로 00에서 11 사이가 됩니다.

합계: 합은 2개 또는 3개의 한 자리 숫자를 합한 LSB입니다.

캐리: 캐리는 한자리 숫자 2~3개를 더한 결과의 MSB입니다.

두 개의 1비트 숫자를 더할 수 있는 가산기의 경우 두 개의 출력 비트, 즉 합계 s와 캐리 c가 있습니다. 2비트를 더하는 가산기를 반가산기(half adder)라고 한다. 반덧셈에 대한 진리표는 아래 표에 나와 있습니다.

에 비 초 ㄷ

0 0 0 0

0 1 1 0

1 0 1 0

1 1 0 1

반가산기.

3비트를 더할 수 있는 가산기를 전가산기(full adder)라고 한다. 전자 구조는 다음과 같습니다.

n비트 가산기를 리플 캐리 가산기라고 하며 그 설계는 다음과 같습니다.

두 숫자 A와 B를 더하는 문제를 생각해 보세요. 먼저 아래 그림과 같이 비트 세트를 4비트 블록으로 나눕니다. 각 블록에는 A 단편과 B 단편이 포함됩니다. 두 조각은 블록의 입력 캐리를 고려하여 추가되고 합계 비트 집합과 후속 블록의 입력 캐리인 캐리를 생성합니다.

또한 Carry Lookahead Adder가 있는데, 이는 2단계로 나누어져 있는데, 각 단계는 복잡한 전자적 구조를 가지고 있다.

19.2.7.2 곱셈

이제 덧셈과 비슷하게 두 개의 십진수를 곱하는 가장 간단한 방법을 살펴보겠습니다. 13에 9를 곱해 보세요. 이 경우 13을 피승수, 9를 승수, 117을 곱이라고 합니다.

아래 그림 (a)는 10진수로 곱셈을 나타내고 (b)는 2진수로 곱셈을 나타냅니다. 두 개의 이진수는 십진수와 똑같은 방식으로 곱해집니다. 가장 중요한 위치부터 가장 중요한 위치까지 승수의 모든 자릿수를 고려해야 합니다. 비트가 1이면 피승수 값을 줄 아래에 쓰고 그렇지 않으면 0을 씁니다. 각 승수 위치에 대해 피승수를 한 위치 왼쪽으로 이동합니다. 그 이유는 각 승수 비트가 더 높은 2의 거듭제곱을 나타내기 때문입니다. 우리는 이러한 각 값을 부분합이라고 부릅니다(그림 7.10(b) 참조). 승수에 m 비트가 있는 경우 곱을 얻으려면 m 부분합을 더해야 합니다. 이 경우 곱은 10진수 117, 2진수 1110101입니다. 독자는 두 숫자가 실제로 동일한 숫자를 나타내는지 확인할 수 있습니다. 향후 표기를 용이하게 하기 위해 부분곱이라는 또 다른 용어를 정의해 보겠습니다. 부분합의 연속적인 수열의 합입니다.

(a) 십진수 곱셈; (b) 이진 곱셈.

기존 승수는 O(n2)이며, 향상된 버전의 부스 승수 또는 월리스 트리 승수(아래)는 O(log(n)) 알고리즘 복잡성을 달성할 수 있습니다.

월레스 트리 승수.

19.2.7.3 분할

이제 정수 나누기를 살펴보겠습니다. 불행하게도 덧셈, 뺄셈, 곱셈과 달리 나눗셈은 상당히 느린 과정입니다. 모든 나눗셈 연산은 다음과 같이 표현될 수 있습니다.

N=DQ+R

N은 피제수, D는 제수, Q는 몫, R은 나머지입니다. 제수와 피제수가 양수라고 가정하면 나누기 과정은 다음 속성을 충족해야 합니다.

  • R &lt; DR0.

  • Q는 위의 식을 만족하는 가장 큰 양의 정수이다.

음수를 제거하려면 먼저 양수로 변환하고 나눗셈을 수행한 다음 몫과 나머지의 부호를 조정합니다. ISA의 일부는 나머지가 항상 양수인지 확인하려고 시도합니다. 이 경우 몫에서 1을 빼고 제수와 나머지를 더하여 양수로 만들어야 합니다.

분할을 구현하는 방법에는 반복 분할, 복원 분할, 비복원 분할 등이 있습니다. 이 기사에서는 이러한 알고리즘에 대한 구체적인 설명을 무시합니다. 관심 있는 어린이들이 스스로 정보를 확인할 수 있습니다.

19.2.7.4 부동 소수점 연산

부동 소수점 덧셈과 뺄셈의 문제는 실제로 동일한 문제의 다른 측면입니다. A-B는 두 가지 의미로 해석할 수 있는데, A에서 B를 빼는 것이라고 할 수도 있고, A에 -B를 더한다고 말할 수도 있습니다. 그러므로 뺄셈을 단독으로 보기보다는 덧셈의 특별한 경우로 생각하는 것이 좋습니다. 부동 소수점 숫자의 이진 표현, 속성 및 특별한 의미는 17.2.2 부동 소수점 숫자에서 찾을 수 있습니다.

아래 이미지는 유효 비트의 압축을 풀고 이를 일반 부동 소수점 숫자에 대한 레지스터에 넣는 방법의 예를 보여줍니다. 32비트 IEEE 754 형식에는 가수에 23비트가 있고 소수점 앞에는 0 또는 1이 있습니다. 따라서 유효한 비트에는 24비트가 필요하며 선행 부호 비트(0)를 추가하려면 25비트의 저장 공간이 필요합니다. 이 번호를 레지스터에 저장하고 W라고 부르자.

유효한 비트를 확장하여 레지스터에 넣습니다.

IEEE 754 형식.

부동 소수점 숫자의 연산에는 반올림과 같은 고려 사항이 포함됩니다. 아래 그림은 0의 값을 고려하여 두 개의 부동소수점 수를 더하는 알고리즘을 보여줍니다.

두 개의 부동 소수점 값을 누적하는 순서도입니다.

부동 소수점 곱셈 알고리즘은 몇 단계만 거치면 일반 덧셈 알고리즘과 완전히 동일한 형식을 갖습니다. A x B를 곱하여 곱 C를 얻습니다. 곱셈의 흐름도는 아래 그림에 나와 있습니다. 곱셈의 경우 지수를 정렬할 필요가 없습니다. 다음 초기화 알고리즘은 B의 부호 있는 합을 곱을 수용하기 위해 피연산자 크기의 두 배와 동일한 너비를 가진 레지스터 W에 로드합니다. 곱셈의 경우 중복 계산을 피하기 위해 지수를 더하고 바이어스를 빼기 때문에 E 레지스터는 EA+EB로 초기화되며, 계산된 결과의 부호가 단순합니다.

두 개의 부동 소수점 값을 곱하는 순서도입니다.

또한 Goldschmidt 부문과 Newton-Raphson 부문이 있습니다.

뉴턴-랩슨 방법.

19.3 컴퓨터 아키텍처 및 조직

19.3.1 컴퓨터 수준

컴퓨터 시스템 수준 계층은 컴퓨터와 사용자를 연결하고 컴퓨터를 사용하는 다양한 수준의 조합입니다. 또한 컴퓨터에서 컴퓨팅 활동이 수행되는 방법을 설명하고 시스템에서 사용되는 모든 요소를 ​​다양한 수준으로 보여줍니다. 일반적인 컴퓨터 시스템 수준 계층 구조는 7개 수준으로 구성됩니다.

계층 구조 기능 예 분석하다

레이어 6 사용자 실행 가능한 프로그램 사용자와 실행 가능한 프로그램이 포함되어 있습니다.

레이어 5 고급 언어 C++, 자바 C++, Java, FORTRAN 등을 포함한 고급 언어는 사용자가 명령을 실행하는 데 사용되는 언어입니다.

레이어 4 어셈블리 언어 어셈블리 코드 어셈블리 언어는 컴퓨터 시스템의 다음 단계입니다. 기계는 어셈블리 언어만 이해하므로 모든 고급 언어가 어셈블리 언어로 변경되고 이에 대한 어셈블리 코드가 작성됩니다.

레이어 3 시스템 소프트웨어 운영 체제 주로 프로세스를 작동하고 하드웨어와 운영 체제, 라이브러리 코드 등을 포함할 수 있는 사용자 인터페이스 간의 연결을 설정하는 데 도움이 되는 다양한 유형의 시스템 소프트웨어가 있습니다.

레이어 2 기계 명령어 세트 아키텍처(ISA) 컴퓨터 시스템에서는 명령어 세트 아키텍처를 포함하여 다양한 유형의 활동을 수행하기 위해 다양한 유형의 하드웨어가 사용됩니다.

레이어 1 제어 레이어 마이크로코드 제어는 마이크로코드가 사용되는 시스템의 수준이며 제어 장치는 이 수준의 컴퓨터 시스템에 포함됩니다.

레이어 0 디지털 로직 회로, 게이트 디지털 로직은 디지털 컴퓨팅의 기초이며 컴퓨터 내에서 회로와 하드웨어가 통신하는 방식에 대한 기본적인 이해를 제공합니다. 다양한 회로와 게이트로 구성되어 있습니다.

물론, 위에서 아래로 게임 응용 프로그램, 게임 엔진, 그래픽 API, 운영 체제, 장치 드라이버 및 하드웨어 장치로 구성된 또 다른 수준 구분도 있습니다.

아래 그림은 좀 더 상세한 계층적 모듈입니다. 운영 체제(OS)는 타사 SDK와 그래픽 API와 같은 드라이버 사이에 있습니다. 이는 이전과 다음 사이의 중요한 역할과 의사소통 다리 역할을 합니다. 이는 전체 컴퓨터 계층 구조에서 매우 중요한 구성 요소입니다.

imgimg

내부적으로 컴퓨터 하드웨어는 프로세서, 메모리, I/O 구성 요소와 각 유형별로 하나 이상의 모듈로 구성됩니다. 이러한 구성 요소는 프로그램을 실행하는 컴퓨터의 주요 기능을 달성하는 방식으로 상호 연결됩니다. 네 가지 주요 구조 요소가 있습니다.

  • 프로세서: 컴퓨터의 동작을 제어하고 데이터 처리 기능을 수행합니다. 프로세서가 하나만 있는 경우 이를 중앙 처리 장치(CPU)라고 합니다.

  • 메인 메모리: 데이터와 프로그램을 저장합니다. 쉽게 잃어버릴 수 있으므로 컴퓨터를 끄면 메모리의 내용이 사라집니다. 대신, 컴퓨터 시스템이 종료되더라도 디스크 메모리의 내용은 유지됩니다. 주 메모리는 실제 메모리 또는 주 메모리라고도 합니다.

  • I/O 모듈: 컴퓨터와 외부 환경 간에 데이터를 이동합니다. 외부환경은 2차 저장장치(디스크 등), 통신장치, 단말기 등 다양한 장치로 구성된다.

  • 시스템 버스: 프로세서, 메인 메모리 및 I/O 모듈 간의 통신을 제공합니다.

imgimg

19.3.2 아키텍처와 조직

컴퓨터 아키텍처는 컴퓨터 각 부분의 요구 사항과 설계 구현을 기능적으로 설명하고 컴퓨터 시스템의 기능적 동작을 다룹니다. 컴퓨터가 설계될 때 컴퓨터 조직보다 앞서 있었습니다.

컴퓨터 조직은 컴퓨터 아키텍처 다음에 나타나며 운영 속성이 서로 연결되어 아키텍처 사양 구현에 기여하는 방식입니다. 구조적 관계를 다루고 있습니다.

간단히 말해서, 아키텍처는 소프트웨어 디자이너에게 제시된 컴퓨터의 관점이고 조직은 하드웨어에서 컴퓨터를 실제로 구현하는 것입니다.

컴퓨터 계층적 설계, 하드웨어, 소프트웨어 및 아키텍처, 조직도.

디지털 컴퓨터 블록 다이어그램.

컴퓨터 아키텍처와 컴퓨터 구성의 자세한 차이점은 아래 표에 나와 있습니다.

컴퓨터 아키텍처 컴퓨터 조직

1 컴퓨터의 기능을 설명하세요. 컴퓨터가 이를 어떻게 수행하는지 설명하세요.

2 컴퓨터의 기능적 동작을 다룹니다. 컴퓨터의 구조적 관계를 다룬다.

3 위 그림을 보면 높은 수준의 디자인 문제를 다루고 있음이 분명합니다. 위 그림에서는 낮은 수준의 디자인 문제를 다루고 있다는 것도 분명합니다.

4 하드웨어를 나타냅니다. 성과를 나타냅니다.

5 프로그래머는 아키텍처를 일련의 명령, 주소 지정 모드 및 레지스터로 생각할 수 있습니다. 아키텍처의 구현을 조직이라고 합니다.

6 컴퓨터를 설계하려면 아키텍처가 고정되어 있습니다. 컴퓨터를 설계하기 위해서는 아키텍처에 따라 구성됩니다.

7 ISA(명령어 세트 아키텍처)라고도 합니다. 흔히 마이크로아키텍처라고 불립니다.

8 명령어 세트, 레지스터, 데이터 유형 및 주소 지정 모드와 같은 논리 기능을 포함합니다. 회로설계, 주변기기, 가산기 등 물리적 단위로 구성된다.

9 아키텍처 카테고리: Von Neumann, Harvard, ISA, 시스템 디자인. CPU 조직은 주소 필드 수에 따라 단일 누산기 조직, 일반 레지스터 조직, 스택 조직의 세 가지 범주로 구분됩니다.

10 컴퓨터의 하드웨어를 표시합니다. 컴퓨터 성능에 대한 자세한 정보를 제공합니다.

11 시스템 하드웨어와 소프트웨어를 조정합니다. 시스템의 네트워크 세그먼트를 처리합니다.

12 소프트웨어 개발자들은 그것을 깨닫습니다. 소프트웨어 프로그래머의 감지를 피할 수 있습니다.

13 예: Intel과 AMD는 x86 프로세서를 만들었고 Sun Microsystems 및 기타 업체는 SPARC 프로세서를 만들었으며 Apple, IBM 및 Motorola는 PowerPC를 만들었습니다. 조직의 품질에는 컴퓨터 및 주변 장치 인터페이스, 메모리 기술 및 제어 신호와 같이 프로그래머에게 보이지 않는 하드웨어 요소가 포함됩니다.

19.3.3 폰 노이만 아키텍처

역사적으로 컴퓨터에는 두 가지 유형이 있었습니다.

  • 고정 프로그램 컴퓨터: 해당 기능은 매우 구체적이며 계산기와 같이 다시 프로그래밍할 수 없습니다.

  • 스토어 프로그램 컴퓨터: 다양한 작업을 수행하도록 프로그래밍할 수 있으며 응용 프로그램이 컴퓨터에 저장되므로 이름이 붙여졌습니다.

현대 컴퓨터는 John Von Neumann이 도입한 저장 프로그램 개념을 기반으로 합니다. 이 저장된 프로그램 개념에서 프로그램과 데이터는 메모리라고 하는 별도의 저장 장치에 저장되고 동일하게 취급됩니다. 즉, 이 아키텍처로 구축된 컴퓨터는 다시 프로그래밍하기가 더 쉽습니다. 기본 구조는 다음과 같습니다.

입력, 처리, 출력과 같은 개념과 구성 요소를 갖춘 컴퓨터 모델을 폰 노이만 아키텍처라고 합니다. 프로그램 명령 메모리와 데이터 메모리를 통합한 컴퓨터 설계 개념 구조입니다. 범용 튜링 머신(Universal Turing Machine)을 구현한 컴퓨팅 장치이며, 병렬 컴퓨팅에 상대적인 순차 구조 참조 모델(Reference Model)입니다. 폰 노이만은 ISA(Instruction Set Architecture) 컴퓨터라고도 알려진 중앙 처리 장치에서 저장 장치를 분리하는 개념을 암묵적으로 안내했습니다.

1947년에 출판된 von Neumann의 "전자 컴퓨팅 기기의 계획 및 코딩 문제"의 순서도.

폰 노이만 구조의 추상적인 구성은 다음과 같습니다.

또한 계산과 저장을 위한 바이너리 사용을 규정하고 컴퓨터의 기본 구조를 중앙처리장치(CPU), 메모리, 입력장치, 출력장치, 버스의 5가지 부분으로 정의하고 있습니다.

위의 그림을 결합하여 각 부분의 구조를 자세히 설명하면 다음과 같다.

  • 메모리: 코드와 데이터는 RAM과 ROM에 선형적으로 저장됩니다. 데이터 저장 단위는 바이너리 비트이며, 최소 저장 단위는 바이트이다.

  • 버스: 버스는 CPU와 메모리, 기타 장치 간의 통신에 사용됩니다. 버스에는 세 가지 주요 유형이 있습니다.

주소 버스: CPU가 작동할 메모리 주소를 지정하는 데 사용됩니다.

  • 데이터 버스: 메모리 데이터를 읽고 쓰는 데 사용됩니다.

  • 제어 버스: 인터럽트, 장치 재설정 등과 같은 신호를 보내고 받는 데 사용됩니다. CPU는 신호를 받은 후 응답합니다. 이때 제어 버스도 필요합니다.

  • 입/출력 장치: 입력 장치는 컴퓨터에 데이터를 입력합니다. 계산 후 컴퓨터는 데이터를 출력 장치로 출력합니다. 예를 들어 키보드의 키를 누르면 CPU와 상호 작용해야 합니다. 이 경우 제어 버스를 사용해야 합니다.

  • CPU: 인간의 두뇌와 유사한 중앙 처리 장치로 컴퓨터 시스템의 컴퓨팅 및 제어 코어이자 정보 처리 및 프로그램 실행을 위한 최종 실행 장치입니다. 그 구조는 다음과 같습니다:

제어 장치(CU): 모든 프로세서 제어 신호를 처리하고, 모든 입력 및 출력 스트림을 지시하고, 명령 코드를 얻고, 데이터가 시스템을 통해 이동하는 방식을 제어합니다.

  • 산술 논리 장치(ALU): CPU에 필요할 수 있는 모든 계산(예: 덧셈, 뺄셈, 비교)을 처리하고 논리 연산, 변위 연산 및 산술 연산을 수행합니다.

  • 주 메모리 장치(레지스터): CPU는 레지스터를 사용하여 계산에 필요한 데이터를 저장합니다. 레지스터에는 일반적으로 다음 유형이 포함됩니다.

Accumulator: ALU의 계산 결과를 저장합니다.

  • 프로그램 카운터(PC): 처리할 다음 명령의 메모리 위치를 추적한 다음 PC는 다음 주소를 MAR(메모리 주소 레지스터)에 전달합니다.

  • MAR(메모리 주소 레지스터): 메모리에서 가져오거나 메모리에 저장해야 하는 명령을 저장하는 메모리 위치입니다.

  • 메모리 데이터 레지스터(MDR): 메모리에서 가져온 명령이나 메모리로 전송되어 메모리에 저장될 데이터를 저장합니다.

  • 명령 레지스터(IR): 두 가지 유형으로 나눌 수 있습니다.

CIR(현재 명령어 레지스터): 인코딩 및 실행을 기다리는 동안 가장 최근에 가져온 명령어를 저장합니다.

  • 명령어 버퍼 레지스터(IBR): 즉시 실행되지 않는 명령은 명령 버퍼 레지스터 IBR에 저장됩니다.

  • 범용 레지스터(GPR): 추가해야 하는 두 개의 데이터 등 연산이 필요한 데이터를 저장합니다.

폰 노이만 시스템에서 컴퓨터 명령어 실행의 간략한 과정은 다음과 같습니다.

  • CPU는 프로그램 카운터를 읽어 명령어 메모리 주소를 얻습니다. CPU 제어 장치는 메모리 주소에서 데이터를 얻기 위해 주소 버스를 작동합니다. 데이터는 데이터 버스를 통해 CPU에 도달하고 명령어 레지스터에 저장됩니다.

  • CPU는 명령어 레지스터의 명령어를 분석합니다. 계산형 명령인 경우에는 논리연산부로 전달됩니다. 저장 유형 명령어인 경우 실행을 위해 제어 장치로 전달됩니다.

  • CPU가 명령어 실행을 마친 후 프로그램 카운터 값은 증가하여 다음 명령어를 가리킵니다. 예를 들어 32비트 CPU는 4씩 증가합니다.

  • 증가 후에는 다음 명령이 순차적으로 실행되며 프로그램이 끝날 때까지 루프에서 실행이 계속됩니다.

폰 노이만 병목 현상은 성능을 향상시키기 위해 무엇을 하든 한 번에 하나의 명령만, 순서대로만 실행될 수 있다는 사실을 제거할 수 없다는 사실입니다. 두 명령 모두 CPU의 성능을 방해합니다. von Neumann 프로세서에 더 많은 캐시, 더 많은 RAM 또는 더 빠른 구성 요소를 제공할 수 있지만 CPU 성능을 대폭 향상하려면 CPU 구성을 효과적으로 검토해야 합니다. 이 아키텍처는 매우 중요하며 PC는 물론 슈퍼컴퓨터에도 사용됩니다.

19.3.4 멀티 코어 구조

다음 그림은 일반적인 멀티코어 컴퓨터의 주요 구성 요소를 단순화한 것입니다. PC, 노트북, 워크스테이션은 물론 스마트폰, 태블릿에 내장된 컴퓨터를 포함한 대부분의 컴퓨터는 마더보드에 장착됩니다. 인쇄 회로 기판(PCB)은 칩과 기타 전자 부품을 고정하고 상호 연결하는 데 사용되는 단단하고 평평한 판입니다. 보드는 일반적으로 보드에 에칭된 구리 경로를 통해 구성 요소를 상호 연결하는 2~10개의 레이어로 구성됩니다. 컴퓨터의 기본 인쇄 회로 기판을 시스템 보드 또는 마더보드라고 하며, 마더보드 슬롯에 연결되는 더 작은 인쇄 회로 기판을 확장 보드라고 합니다. 마더보드에서 가장 눈에 띄는 요소는 전자 회로와 논리 게이트가 제조되는 반도체 재료(보통 실리콘)인 칩입니다. 그 결과 나온 제품을 집적 회로라고 합니다.

멀티코어 컴퓨터의 주요 구성 요소를 단순화한 보기입니다.

아래 왼쪽 사진은 IBM zEnterprise EC12 메인프레임 컴퓨터 프로세서 칩의 사진입니다. 27억 5천만 개의 트랜지스터, 6개의 코어(프로세서), 6개의 프로세서 모두가 공유하는 L3 캐시라고 표시된 2개의 큰 영역이 있습니다. L3 제어 로직은 L3 캐시와 코어 사이, L3 캐시와 외부 환경 사이의 트래픽을 제어합니다. 또한 코어와 L3 캐시 사이에는 스토리지 제어(SC) 로직이 있고, 메모리 컨트롤러(MC) 기능은 칩 외부 메모리에 대한 액세스를 제어하며, GX I/O 버스는 I/O에 액세스하는 채널 어댑터에 대한 인터페이스를 제어합니다. 아래 오른쪽 이미지는 단일 프로세서 칩을 구성하는 실리콘 표면적의 일부인 단일 코어의 내부 구조를 보여줍니다.

웨이퍼, 칩, 게이트의 관계는 다음과 같습니다.

QPI(QuickPath Interconnect)는 Intel이 2008년에 출시한 지점 간 상호 연결 방식입니다. QPI 및 기타 지점 간 상호 연결 솔루션의 중요한 기능은 다중 직접 연결, 계층화된 프로토콜 아키텍처 및 패킷 데이터 전송입니다.

아래 그림은 멀티 코어 컴퓨터에서 QPI의 일반적인 사용을 보여줍니다. QPI 링크(다이어그램에서 녹색 화살표 쌍으로 표시)는 데이터가 네트워크 전체에서 이동할 수 있도록 하는 스위칭 패브릭을 형성합니다. 각 코어 프로세서 쌍 사이에 직접 QPI 연결을 설정할 수 있습니다. 다이어그램의 코어 A가 코어 D의 메모리 컨트롤러에 액세스해야 하는 경우 코어 B 또는 C를 통해 요청을 보내며, 코어 B 또는 C는 해당 요청을 코어 D의 메모리 컨트롤러로 전달해야 합니다. 마찬가지로 8개 이상의 프로세서가 있는 대규모 시스템은 3개의 링크가 있는 프로세서를 사용하여 구축할 수 있으며 트래픽은 중간 프로세서를 통해 라우팅됩니다.

QPI를 사용한 멀티 코어 구성.

QPI 레이어 다이어그램.

다른 칩 조직.

다중 코어 조직 대안.

19.3.5 임베디드 시스템

임베디드 시스템이라는 용어는 태블릿이나 데스크탑 시스템과 같은 범용 컴퓨터 이외의 제품에 전자 장치 및 소프트웨어를 사용하는 것을 의미합니다. 매년 생산되는 대규모 장치에 내장된 컴퓨터 시스템의 수는 수십억 대에 비해 랩탑, PC, 워크스테이션, 서버, 메인프레임, 슈퍼컴퓨터를 포함한 수백만 대의 컴퓨터가 매년 판매됩니다. 오늘날 전기를 사용하는 많은(아마도 대부분) 장치에는 컴퓨팅 시스템이 내장되어 있으며, 가까운 미래에는 거의 모든 장치에 컴퓨팅 시스템이 내장될 것입니다.

내장형 시스템을 갖춘 장치 유형은 나열하기 어려울 만큼 많습니다. 예로는 휴대폰, 디지털 카메라, 캠코더, 계산기, 전자레인지, 주택 보안 시스템, 세탁기, 조명 시스템, 온도 조절 장치, 프린터, 다양한 자동차 시스템(예: 변속기 제어, 순항 제어, 연료 분사, 잠김 방지 브레이크 및 서스펜션 시스템), 테니스 라켓, 칫솔, 자동화 시스템의 다양한 유형의 센서 및 액추에이터 등이 있습니다.

일반적으로 임베디드 시스템은 환경과 긴밀하게 결합되어 있으므로 환경과 상호 작용해야 하는 필요성으로 인해 실시간 제약이 발생합니다. 필요한 동작 속도, 필요한 측정 정확도, 필요한 기간 등의 제약 조건에 따라 소프트웨어 작업의 타이밍이 결정되며, 여러 활동을 동시에 관리해야 하는 경우 더욱 복잡한 실시간 제약 조건이 발생합니다.

다음 다이어그램은 임베디드 시스템 구성의 개요를 보여줍니다. 프로세서와 메모리 외에도 일반적인 데스크탑이나 노트북 컴퓨터와 다른 많은 요소가 있습니다.

임베디드 시스템의 구성이 가능합니다.

  • 시스템이 외부 환경을 측정하고 작동하며 상호 작용할 수 있도록 하는 다양한 인터페이스가 존재할 수 있습니다. 임베디드 시스템은 일반적으로 센서 및 액추에이터를 통해 외부 세계와 상호 작용(감지, 조작 및 통신)하므로 일반적으로 환경과 지속적으로 상호 작용하고 해당 환경에 의해 결정된 속도로 실행되는 반응형 시스템입니다.

  • 인간-기계 인터페이스는 손전등만큼 간단할 수도 있고 실시간 로봇 비전만큼 복잡할 수도 있습니다. 많은 경우 인간-기계 인터페이스가 없습니다.

  • 진단 포트는 컴퓨터뿐만 아니라 제어 중인 시스템을 진단하는 데에도 사용할 수 있습니다.

  • 전용 현장 프로그래밍 가능(FPGA), 특정 애플리케이션(ASIC) 또는 심지어 디지털이 아닌 하드웨어를 사용하여 성능이나 신뢰성을 향상시킬 수 있습니다.

  • 소프트웨어는 일반적으로 고정된 기능을 가지며 응용 프로그램별로 다릅니다.

  • 임베디드 시스템에서는 효율성이 매우 중요합니다. 에너지, 코드 크기, 실행 시간, 무게, 부피 및 비용에 맞게 최적화해야 합니다.

또한 범용 컴퓨터 시스템과 몇 가지 주목할 만한 유사점이 있습니다.

  • 명목상 고정 기능 소프트웨어를 사용하더라도 버그 수정, 보안 개선, 기능 추가를 위한 현장 업그레이드 기능은 소비자 장치뿐만 아니라 임베디드 시스템에서도 매우 중요해지고 있습니다.

  • 상대적으로 새로운 개발은 다양한 애플리케이션을 지원하는 임베디드 시스템 플랫폼이며, 스마트폰과 오디오/비디오 장치(예: 스마트 TV)가 좋은 예입니다.

19.3.5.1 마이크로프로세서 및 마이크로컨트롤러

초기 마이크로프로세서 칩에는 레지스터, ALU 및 일종의 제어 장치 또는 명령 처리 논리가 포함되었습니다. 트랜지스터 밀도가 증가함에 따라 명령어 세트 아키텍처와 궁극적으로 메모리 및 다중 프로세서의 복잡성이 증가할 수 있습니다. 최신 마이크로프로세서 칩에는 다중 코어와 대규모 캐시가 포함되어 있습니다.

마이크로컨트롤러 칩은 사용 가능한 논리 공간을 상당히 다르게 활용하며 아래 다이어그램은 마이크로컨트롤러 칩의 일반적인 구성 요소에 대한 개요를 보여줍니다. 마이크로 컨트롤러는 프로세서, 프로그램용 비휘발성 메모리(ROM), 입력 및 출력용 휘발성 메모리(RAM), 클럭 및 I/O 제어 장치를 포함하는 단일 칩입니다. 마이크로컨트롤러의 프로세서 부분은 다른 마이크로프로세서보다 실리콘 면적이 훨씬 낮고 에너지 효율성이 훨씬 높습니다.

"컴퓨터 온 칩(Computer on a Chip)"이라고도 알려진 수십억 개의 마이크로컨트롤러 장치는 매년 장난감에서 가전제품, 자동차에 이르기까지 다양한 제품에 내장되며, 단일 차량에는 70개 이상의 마이크로컨트롤러가 사용됩니다. 특히 더 작고 저렴한 마이크로 컨트롤러의 경우 자동화 프로세스에서 많이 사용되는 마이크로 컨트롤러와 같은 특정 작업을 위한 전용 프로세서로 사용되는 경우가 많습니다. 입력에 대한 간단한 반응을 제공함으로써 기계를 제어하고, 팬을 켜고 끄고, 밸브를 열고 닫는 등의 작업을 수행할 수 있으며, 이는 현대 산업 기술의 필수적인 부분이자 매우 복잡한 기능을 처리할 수 있는 기계를 생산하는 가장 저렴한 방법 중 하나입니다.

마이크로컨트롤러는 4비트에서 32비트 아키텍처 범위의 프로세서를 포함하여 다양한 물리적 크기와 처리 기능으로 제공됩니다. 마이크로컨트롤러는 마이크로프로세서보다 훨씬 느린 경향이 있으며, 마이크로프로세서의 GHz 속도와 달리 MHz 범위에서 작동하는 경우가 많습니다. 마이크로 컨트롤러의 또 다른 일반적인 특징은 인간-컴퓨터 상호 작용을 제공하지 않고, 특정 작업을 위해 프로그래밍되고, 장치에 내장되어 필요할 때 수행된다는 것입니다.

19.3.5.2 임베디드 및 심층 임베디드 시스템

임베디드 시스템의 하위 집합과 상당수의 하위 집합을 심층 임베디드 시스템이라고 합니다. 이 용어는 기술 및 비즈니스 문헌에서 널리 사용되지만 인터넷에서는 직접적인 정의를 찾을 수 없습니다. 일반적으로 심층 임베디드 시스템에는 프로그래머와 사용자가 동작을 관찰하기 어려운 프로세서가 있다고 말할 수 있습니다. 심층 임베디드 시스템은 마이크로프로세서 대신 마이크로컨트롤러를 사용합니다. 장치의 프로그램 로직이 ROM(읽기 전용 메모리)에 구워지면 프로그래밍할 수 없으며 사용자와 상호 작용할 수 없습니다.

심층 임베디드 시스템은 환경에서 무언가를 감지하고 기본 수준 처리를 수행한 다음 결과에 따라 조치를 취하는 특수한 단일 목적 장치입니다. 사물 인터넷은 무선 기능을 갖추고 넓은 지역(예: 공장, 농업 분야)에 배포된 센서 네트워크와 같은 네트워크 구성으로 나타나는 심층 내장형 시스템에 크게 의존합니다. 일반적으로 심층 임베디드 시스템은 메모리, 프로세서 크기, 시간 및 전력 소비 측면에서 리소스 제약이 매우 심합니다.

19.3.6 ARM 아키텍처

ARM 아키텍처는 RISC 설계 원리에서 발전된 프로세서 아키텍처를 말하며 임베디드 시스템에 사용됩니다. 이 섹션에서는 이에 대해 간략하게 설명합니다.

ARM 명령어 세트는 매우 규칙적이며 효율적인 프로세서 구현과 효율적인 실행을 위해 설계되었습니다. 모든 명령어는 길이가 32비트이고 기존 형식을 따르므로 ARM ISA는 다양한 제품에 대한 구현에 적합합니다.

기본 ARM ISA를 향상시키는 것은 ARM 명령어 세트의 다시 코딩된 하위 세트인 Thumb 명령어 세트입니다. Thumb은 16비트 이하의 메모리 데이터 버스를 사용하여 ARM 구현의 성능을 향상시키고 ARM 명령어 세트에서 제공하는 것보다 더 나은 코드 밀도를 허용하도록 설계되었습니다. Thumb 명령어 세트에는 16비트 명령어로 기록된 ARM 32비트 명령어 세트의 하위 세트가 포함되어 있습니다. 몇 년 전에 정의된 버전은 Thumb-2였습니다.

ARM Holdings는 다양한 특수 마이크로프로세서 및 관련 기술에 대한 라이선스를 부여하고 있지만 해당 제품 라인의 대부분은 Cortex 마이크로프로세서 아키텍처 제품군입니다. 세 가지 Cortex 아키텍처가 있으며 편리하게 약어 A, R, M으로 표시되어 있습니다.

  • Cortex-A 및 Cortex-A50: 스마트폰, 전자책 리더와 같은 모바일 장치는 물론 디지털 TV, DSL 및 케이블 인터넷 모뎀과 같은 홈 게이트웨이와 같은 소비자 장치에 적합한 애플리케이션 프로세서입니다. 이 프로세서는 더 높은 클럭 주파수(1GHz 이상)에서 실행되며 Linux, Android, MS Windows 및 모바일 운영 체제와 같은 모든 기능을 갖춘 운영 체제에 필요한 MMU(메모리 관리 장치)를 지원합니다. MMU는 가상 주소를 물리적 주소로 변환하여 가상 메모리와 페이징을 지원하는 하드웨어 모듈입니다. 두 아키텍처 모두 ARM 및 Thumb-2 명령 세트를 모두 사용합니다. 주요 차이점은 Cortex-A가 32비트 시스템인 반면 Cortex-A50은 64비트 시스템이라는 것입니다.

  • Cortex-R: 이벤트에 대한 빠른 응답을 통해 이벤트 타이밍을 제어해야 하는 실시간 애플리케이션을 지원하도록 설계되었습니다. 상당히 높은 클록 주파수(예: 200MHz ~ 800MHz)에서 실행될 수 있으며 응답 대기 시간이 매우 낮습니다. Cortex-R에는 깊게 내장된 실시간 장치를 지원하기 위한 명령어 세트 및 프로세서 구성에 대한 향상된 기능이 포함되어 있습니다. 이러한 프로세서의 대부분에는 MMU가 없으며 제한된 데이터 요구 사항과 제한된 수의 동시 프로세스로 인해 가상 메모리에 대한 복잡한 하드웨어 및 소프트웨어 지원이 필요하지 않습니다. Cortex-R에는 산업용 애플리케이션용으로 설계된 메모리 보호 장치(MPU), 캐시 및 기타 메모리 기능이 있습니다. MPU는 메모리의 한 프로그램이 다른 활성 프로그램에 할당된 메모리에 실수로 액세스하는 것을 방지하는 하드웨어 모듈입니다. 다양한 방법을 사용하여 프로그램 주위에 보호 경계를 만들고 프로그램 내의 명령이 경계 외부의 데이터를 참조하는 것을 금지합니다. Cortex-R을 사용하는 임베디드 시스템에는 자동차 제동 시스템, 대용량 스토리지 컨트롤러, 네트워킹 및 인쇄 장비가 포함됩니다.

  • Cortex-M: 빠르고 매우 결정적인 인터럽트 관리에 대한 요구와 극도로 낮은 게이트 수 및 가능한 최저 전력 소비에 대한 요구가 결합된 마이크로 컨트롤러 분야를 위해 주로 개발되었습니다. Cortex-R 시리즈와 마찬가지로 Cortex-M 아키텍처에는 MPU가 있지만 MMU는 없습니다. Cortex-M은 Thumb-2 명령어 세트만 사용하며, 해당 시장에는 IoT 장치, 공장 및 기타 기업에서 사용되는 무선 센서/액추에이터 네트워크, 자동차 차체 전자 장치 등이 포함됩니다. Cortex-M 시리즈에는 Cortex-M0, Cortex-M0+, Cortex-M3, Cortex-M4 및 기타 버전이 포함됩니다. 아래 그림은 Cortex-M3 기반의 일반적인 마이크로 컨트롤러 칩입니다.

19.3.7 클라우드 컴퓨팅

클라우드 컴퓨팅의 일반적인 개념은 1950년대로 거슬러 올라가지만, 클라우드 컴퓨팅 서비스는 특히 대기업을 대상으로 2000년대 초반에 처음 등장했습니다. 그 이후로 클라우드 컴퓨팅은 중소기업, 그리고 최근에는 소비자까지 확대되었습니다. 2012년 출시된 애플의 아이클라우드(iCloud)는 출시 일주일 만에 사용자 2천만 명을 돌파했고, 2008년 출시된 클라우드 기반 메모 작성 및 보관 서비스 에버노트(Evernote)는 6년도 채 안 돼 사용자 1억 명에 육박했다. 이 섹션에서는 간략한 개요를 제공합니다.

많은 조직에서 점점 더 눈에 띄는 추세는 대부분 또는 심지어 모든 정보 기술(IT) 운영을 엔터프라이즈 클라우드 컴퓨팅으로 알려진 인터넷 연결 인프라로 이동하는 것입니다. 동시에 PC 및 모바일 장치의 개인 사용자는 개인 클라우드 컴퓨팅을 사용하여 데이터를 백업하고 장치를 동기화하며 공유하기 위해 클라우드 컴퓨팅 서비스에 점점 더 의존하고 있습니다. NIST는 NIST SP-800-145(NIST 클라우드 컴퓨팅 정의)에서 클라우드 컴퓨팅을 다음과 같이 정의합니다.

클라우드 컴퓨팅: 구성 가능한 컴퓨팅 리소스(예: 네트워크, 서버, 스토리지, 애플리케이션 및 서비스)의 공유 풀에 대한 어디서나 편리하고 주문형 네트워크 액세스를 가능하게 하는 모델로, 최소한의 관리 노력이나 서비스 제공업체 상호 작용으로 신속하게 프로비저닝 및 해제할 수 있습니다.

기본적으로 클라우드 컴퓨팅을 사용하면 규모의 경제, 전문적인 네트워크 관리, 전문적인 보안 관리 등 대기업과 중소기업, 정부 기관, PC 및 모바일 사용자에게 매력적인 기능을 얻을 수 있습니다. 개인이나 회사는 필요한 스토리지 용량과 서비스에 대해서만 비용을 지불하면 됩니다. 기업이든 개인이든 사용자는 데이터베이스 시스템을 구축하고, 필요한 하드웨어를 확보하고, 유지 관리를 수행하고 데이터를 백업할 필요가 없으며 이 모든 것이 클라우드 서비스의 일부입니다.

이론적으로 클라우드 컴퓨팅을 사용하여 데이터를 저장하고 다른 사람과 공유하는 또 다른 큰 이점은 클라우드 공급자가 보안을 담당한다는 것입니다. 고객이 항상 보호되는 것은 아니며, 2013년 초 모든 사용자에게 위반 사항을 발견한 후 비밀번호를 재설정하라고 지시하여 헤드라인을 장식한 Evernote와 같은 클라우드 제공업체 사이에는 수많은 보안 실패가 있었습니다.

클라우드 네트워킹은 클라우드 컴퓨팅을 지원하기 위해 반드시 갖추어야 할 네트워크 및 네트워크 관리 기능을 의미합니다. 대부분의 클라우드 컴퓨팅 솔루션은 인터넷에 의존하지만 이는 네트워크 인프라의 한 부분일 뿐입니다. 클라우드 네트워크의 예로는 제공자와 가입자 간의 고성능 및/또는 고신뢰성 네트워크가 있는데, 이 네트워크에서는 클라우드 서비스 제공자가 소유하거나 임대한 전용 사설 네트워크 시설을 사용하여 기업과 클라우드 간의 트래픽 일부 또는 전부가 인터넷을 우회합니다. 보다 일반적으로 클라우드 네트워킹은 인터넷의 전문 서비스 활용, 기업 데이터 센터를 클라우드에 연결, 주요 지점에서 방화벽 및 기타 네트워크 보안 장치를 사용하여 액세스 보안 정책을 시행하는 등 클라우드에 액세스하는 데 필요한 네트워크 기능 모음을 의미합니다.

클라우드 스토리지는 클라우드 컴퓨팅의 하위 집합으로 생각할 수 있습니다. 기본적으로 클라우드 스토리지는 클라우드 서버에서 원격으로 호스팅되는 데이터베이스 스토리지와 데이터베이스 애플리케이션으로 구성되어 있어 중소기업과 개인 사용자가 필요에 따라 확장 가능한 데이터 스토리지를 활용하고 스토리지 자산을 구매, 유지, 관리할 필요 없이 다양한 데이터베이스 애플리케이션을 활용할 수 있습니다.

클라우드 컴퓨팅의 기본 목적은 컴퓨팅 자원의 편리한 임대를 제공하는 것입니다. 클라우드 서비스 공급자(CSP)는 인터넷이나 사설 네트워크를 통해 사용할 수 있는 컴퓨팅 및 데이터 스토리지 리소스를 유지 관리하며, 고객은 필요에 따라 이러한 리소스의 일부를 임대할 수 있습니다. 거의 모든 클라우드 서비스는 SaaS, PaaS 및 IaaS의 세 가지 모델(아래 이미지) 중 하나를 사용하여 제공됩니다.

대체 정보 기술 아키텍처.

클라우드 컴퓨팅 요소.

클라우드 서비스 모델.

19.4ISA

19.4.1 ISA 정의

모든 언어에 단어 수가 제한되어 있는 것처럼 프로세서가 지원할 수 있는 기본 명령어/기본 명령의 수도 제한되어야 합니다. 이 명령어 세트를 흔히 명령어 세트라고 합니다. 기본 명령어의 예로는 덧셈, 뺄셈, 곱셈, 논리 OR, 논리 NOT 등이 있습니다. 각 명령어는 일련의 변수와 상수를 처리하고 마지막으로 결과를 변수에 저장해야 한다는 점에 유의하세요. 이러한 변수는 프로그래머가 정의한 변수가 아니지만 컴퓨터 내의 내부 위치입니다. 우리는 명령어 세트 아키텍처를 다음과 같이 정의합니다.

**명령어 세트 아키텍처(ISA)**는 명령어 자체와 피연산자, 주변 장치와의 인터페이스의 의미를 포함하여 프로세서가 지원하는 모든 명령어의 의미입니다.

명령어 세트 아키텍처는 소프트웨어가 하드웨어를 인식하는 방식입니다. 하드웨어가 외부 세계로 출력하는 기능의 기본 목록으로 생각할 수 있습니다. Intel 및 AMD CPU는 x86 명령어 세트를 사용하고, IBM 프로세서는 PowerPC R 명령어 세트를 사용하고, HP 프로세서는 PA-RISC 명령어 세트를 사용하고, ARM 프로세서는 ARMR 명령어 세트(또는 Thumb-1 및 Thumb-2와 같은 변형)를 사용합니다. 따라서 명령어 세트 비호환으로 인해 ARM 기반 시스템에서는 Intel 시스템용으로 컴파일된 바이너리를 실행할 수 없지만 대부분의 경우 C/C++ 프로그램을 재사용하는 것은 가능합니다. 특정 아키텍처에서 C/C++ 프로그램을 실행하려면 해당 특정 아키텍처에 대한 컴파일러를 구입한 다음 C/C++ 프로그램을 적절하게 컴파일해야 합니다.

19.4.2 기본 지침

기본 컴퓨터에는 메모리 참조, 레지스터 참조 또는 입출력 명령을 나타낼 수 있는 16비트 명령 레지스터(IR)가 있습니다. 간단한 명령 형식은 다음과 같습니다.

기본 지침은 다음 범주로 나눌 수 있습니다.

  • 메모리 참조

이 명령어에서는 메모리 주소를 피연산자로 참조하고 다른 피연산자는 항상 누산기입니다. 아래 그림에서는 직접 및 간접 주소 지정을 위한 12비트 주소, 3비트 opcode(111 제외) 및 1비트 주소 지정 모드를 지정합니다.

예: IR 레지스터의 내용은 0001XXXXXXXXXXXX입니다. 즉, ADD 명령어를 디코딩한 후 ADD 연산을 위한 메모리 참조 명령어임을 알 수 있습니다. 그러므로:

cpp
DR ← M[AR]
AC ← AC + DR, SC ← 0
  • 참조등록

이러한 명령어는 메모리 주소가 아닌 레지스터에서 작동합니다. 아래 다이어그램에서는 IR(14 – 12)이 111(메모리 참조와 구별됨)이고 IR(15)이 0(입/출력 명령어와 구별됨)이며 나머지 12비트는 레지스터 작업을 지정합니다.

예: IR 레지스터 내용은 0111001000000000입니다. 즉, CMA는 페치 및 디코드 사이클 후에 이것이 보수 누산기에 대한 레지스터 참조 명령임을 찾습니다. 따라서 다음과 같습니다.

cpp
AC ← ~AC
  • 입력/출력

이 지침은 컴퓨터와 외부 환경 간의 통신에 사용됩니다. 아래 다이어그램에서는 IR(14 – 12)이 111(메모리 참조와 구별됨)이고 IR(15)이 1(레지스터 참조 명령어와 구별됨)이며 나머지 12비트는 I/O 작업을 지정합니다.

예: IR 레지스터의 내용은 1111100000000000입니다. 즉, INP가 명령어 가져오기 및 디코딩 루프를 거친 후 문자 입력을 위한 입출력 명령어임을 알 수 있습니다. 따라서 주변 장치의 INPUT 문자입니다.

16비트 IR 레지스터에 포함된 명령어 세트는 다음과 같습니다.

  • 산술, 논리 및 시프트 명령어(AND, 추가, 보완, 왼쪽 루프, 오른쪽 루프 등).

  • 정보를 메모리 안팎으로 이동합니다(누산기 저장, 누산기 로드).

  • 상태 조건이 포함된 프로그램 제어 명령(분기, 건너뛰기).

  • 입력 및 출력 명령(입력 문자, 출력 문자).

지침의 구체적인 설명은 다음과 같습니다.

상징 16진수 코드 설명

그리고 0xxx, 8xxx AC에게 어떤 말이든

추가 1xxx, 9xxx AC에 어떤 단어를 축적

LDA 2xxx、Axxx AC에 메모리 단어 로드

STA 3xxx, Bxxx AC 단어를 메모리에 저장

번 4xxx, Cxxx 무조건 분기

BSA 5xxx, Dxxx 분기하고 반송 주소 저장

ISZ 6xxx,Exxx 0이면 증가하고 건너뜁니다.

CLA 7800 깨끗한 에어컨

CLE 7400 클리어 E(오버플로 비트)

CMA 7200 보충교재

CME 7100 보충 E

CIR 7080 오른쪽 루프 AC 및 E

CIL 7040 왼쪽 루프 AC 및 E

주식회사 7020 AC 증가

스파 7010 AC>0이면 다음 명령어를 건너뜁니다.

SNA 7008 AC<0이면 다음 명령을 건너뜁니다.

SZA 7004 AC=0이면 다음 명령어를 건너뜁니다.

크기 7002 E=0이면 다음 명령어를 건너뜁니다.

HLT 7001 컴퓨터를 중지

INP F800 AC에 문자 입력

아웃 F400 AC로 문자 출력

스키 F200 입력 플래그 건너뛰기

SKO F100 출력 플래그 건너뛰기

이온 F080 인터럽트 활성화됨

IOF F040 중단하다

19.4.3 명령어 세트 설계 지침

이제 프로세서용 명령어 세트를 설계하는 어려운 과정을 시작하겠습니다. 명령어 세트를 소프트웨어와 하드웨어 간의 법적 계약으로 생각하면 두 당사자 모두 각자의 계약을 이행해야 합니다. 소프트웨어 부분은 사용자가 작성한 모든 프로그램이 기본 지침으로 성공적이고 효과적으로 변환될 수 있도록 보장해야 합니다. 마찬가지로 하드웨어는 명령어 세트의 모든 명령어가 효과적으로 구현되도록 보장해야 합니다. 양 당사자는 ISA가 효율성을 위해 필요한 몇 가지 필수 특성과 일부 특성을 가져야 한다는 합리적인 가정을 해야 합니다.

  • 전체. ISA는 모든 사용자 프로그램을 구현할 수 있어야 하며, 우리는 ISA가 사용자가 작성한 모든 프로그램을 나타낼 수 있기를 원합니다. 예를 들어 ADD 명령어가 하나만 있는 ISA가 있는 경우 두 숫자를 뺄 수 없습니다. 루프를 구현하려면 ISA에는 동일한 코드 조각을 계속해서 다시 실행할 수 있는 방법이 있어야 합니다. 이러한 for 및 while 루프 지원이 없으면 C 프로그램의 루프가 작동하지 않습니다.

범용 프로세서의 경우 가능한 모든 프로그램을 살펴봅니다. 그러나 많은 임베디드 장치용 프로세서는 기능이 제한되어 있습니다. 예를 들어, 문자열 처리를 수행하는 간단한 프로세서는 아날로그 포인트 번호(소수점이 있는 숫자)를 지원할 필요가 없습니다. 우리가 주목해야 할 것은 서로 다른 프로세서가 서로 다른 작업을 수행하도록 설계되었으므로 해당 ISA도 다를 수 있다는 것입니다. 그러나 중요한 점은 모든 ISA는 사용자가 작성하려는 모든 프로그램을 기계어 코드로 표현할 수 있어야 한다는 점에서 완전해야 한다는 것입니다.

  • 간결한. 명령어 세트의 크기가 제한되어 있으므로 명령어를 너무 많이 포함하지 않는 것이 가장 좋습니다. 단일 명령어를 구현하려면 상당한 하드웨어가 필요하며, 많은 수의 명령어를 실행하면 프로세서의 트랜지스터 수가 불필요하게 늘어나고 복잡성이 증가합니다. 따라서 대부분의 명령어 세트에는 64~1000개의 명령어가 있습니다. 예를 들어, MIPS 명령어 세트에는 64개의 명령어가 포함되어 있는 반면, 2012년 현재 Intel x86 명령어 세트에는 약 1,000개의 명령어가 포함되어 있습니다. 1000은 ISA의 명령어 수에 비해 상당히 큰 숫자로 간주됩니다.

  • 일반적인. 명령어는 일반적인 경우를 포착해야 하며, 프로그램에서 가장 일반적인 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈과 같은 간단한 산술 명령어입니다. 가장 일반적인 논리 명령어는 논리 AND, OR, XOR 및 NOT입니다. 따라서 이러한 공통 작업 각각에 대한 명령을 지정하는 것이 합리적입니다.

거의 사용되지 않는 계산에 대한 지침은 좋은 생각이 아닙니다. 예를 들어, sin1(x)를 계산하는 명령을 구현하는 것은 의미가 없을 수 있으며, sin1(x)를 계산하기 위해 기존 수학적 기법(테일러 급수 전개 등)을 사용하여 구현된 전용 라이브러리 함수가 제공될 수 있습니다. 대부분의 프로그램은 이 기능을 거의 사용하지 않기 때문에 이 기능을 실행하는 데 상대적으로 오랜 시간이 걸리더라도 부정적인 영향을 받지 않습니다.

  • 단순한. 지침은 가능한 한 간단하게 유지되어야 합니다. 일련의 숫자를 추가하는 프로그램이 많이 있다고 가정해 보겠습니다. 이러한 프로그램에 맞게 특별히 맞춤화된 프로세서를 설계하기 위해 추가 명령에 대한 몇 가지 옵션이 있습니다. 두 숫자를 더하는 명령어를 구현할 수도 있고, 피연산자 목록을 가져와 목록의 합을 생성하는 명령어를 구현할 수도 있습니다. 여기에는 분명히 복잡성의 차이가 있으며 어떤 구현이 더 빠른지 말하기는 불가능합니다. 전자 접근 방식에서는 컴파일러가 더 많은 명령을 생성해야 하지만 각 추가 작업은 빠르게 수행됩니다. 후자의 접근 방식은 더 적은 수의 명령어를 생성하지만 각 명령어를 실행하는 데 더 오랜 시간이 걸립니다. 전자의 종류의 ISA를 Reduced Instruction Set이라고 하고, 후자의 ISA를 **Complex Instruction Set(Complex Instruction Set)**이라고 합니다.

**RISC(Reduced Instruction Set Computer)**는 간단한 정규 구조로 간단한 명령어를 구현하며, 명령어의 개수는 일반적으로 적습니다(64~128개). 예: ARM, IBM PowerPC, HP PA-RISC.

**복합 명령 집합 컴퓨터(CISC)**는 매우 불규칙한 복잡한 명령어를 구현하고, 여러 피연산자를 사용하며, 복잡한 기능을 구현합니다. 둘째, 명령어 수가 많습니다(보통 500개 이상). 예: Intel x86, VAX.

1990년대 후반까지 RISC 대 CISC 논쟁은 매우 논란이 많은 문제였습니다. 그러나 그 이후로 설계자, 프로그래머 및 프로세서 공급업체는 RISC 설계 스타일로 기울어 왔으며, 일반적인 구조와 형식을 갖춘 소수의 비교적 간단한 명령에 합의한 것으로 보입니다. CISC 지침이 때로는 특정 유형의 애플리케이션에 더 적합하기 때문에 이것이 여전히 논란의 여지가 있다는 점은 주목할 가치가 있습니다. 최신 프로세서는 간단한 지침과 일부 복잡한 지침이 혼합된 혼합 접근 방식을 사용하는 경우가 많습니다. 그러나 내부적으로는 CISC 지침이 RISC 지침으로 변환됩니다. 따라서 업계에서는 간단한 명령을 갖는 것이 바람직한 기능이라고 믿고 RISC 명령에 약간 기울고 있다고 생각합니다.

ISA는 완전하고, 간결하고, 일반적이고 단순해야 하며, 완전해야 하며, 나머지 속성은 바람직합니다(그러나 논란의 여지가 있음).

19.4.4 튜링 기계와 명령어 무결성

ISA의 무결성을 확인하는 방법은 무엇입니까? 이것은 매우 흥미롭고 어렵고 이론적으로 심오한 질문입니다. 주어진 어셈블리에 대해 주어진 ISA가 완전한지 여부를 결정하는 문제는 다소 어렵고 일반적으로 훨씬 더 흥미롭습니다. 우리는 이 질문에 대답해야 합니다. 주어진 ISA가 가능한 모든 프로그램을 나타내는가?

기본 덧셈과 곱셈 명령을 포함하는 ISA가 있다고 가정해 보겠습니다. 이 ISA를 사용하여 가능한 모든 프로그램을 실행할 수 있습니까? 대답은 '아니요'입니다. 기존의 기본 지침으로는 두 숫자를 뺄 수 없기 때문입니다. 명령어 라이브러리에 뺄셈 명령어를 추가하면 숫자의 제곱근을 계산할 수 있나요? 설령 가능하더라도 모든 종류의 계산을 할 수 있다고 보장되나요? 이러한 까다로운 질문에 답하려면 먼저 범용 기계를 설계해야 합니다.

유니버설 머신은 모든 프로그램을 실행할 수 있는 머신입니다.

모든 프로그램을 실행할 수 있는 기계이며, 기계의 모든 기본 동작은 명령으로 취급될 수 있습니다. 일반 기계의 일련의 작업은 ISA이며 이 ISA는 완료됩니다. ISA가 완성됐다는 것은 주어진 ISA를 기반으로 구체적으로 범용 기계를 구축할 수 있고, 범용 기계의 설계 문제를 해결함으로써 ISA의 무결성 문제를 해결할 수 있다는 것과 같다. 이는 이중 문제이며 범용 기계 측면에서 추론이 더 쉽습니다.

20세기 초에 컴퓨터 과학자들은 범용 기계의 설계에 대해 생각하기 시작했습니다. 그들은 계산 가능한 것과 그렇지 않은 것, 그리고 다양한 종류의 기계의 기능을 알고 싶었습니다. 둘째, 가능한 모든 프로그램 결과를 계산할 수 있는 이론적 기계의 형태는 무엇입니까? 컴퓨터 과학의 이러한 근본적인 결과는 오늘날 현대 컴퓨터 아키텍처의 기초를 형성합니다.

앨런 튜링(Alan Turing)은 그의 이름을 따서 튜링 기계(Turing machine)라고 알려진 매우 간단하고 강력한 범용 기계를 최초로 제안했습니다. 이는 수학적 추론 도구로 자주 사용되는 이론적 개체일 뿐이며, 튜링 기계의 하드웨어 구현을 만드는 것도 가능하지만 이는 매우 어렵고 불균형한 양의 리소스가 필요합니다. 그럼에도 불구하고 Turing 기계는 오늘날 컴퓨터의 기초를 형성했으며 현대 ISA는 Turing 기계의 기본 동작에서 파생되었습니다. 따라서 디자인을 연구하는 것이 매우 필요합니다.

아래 그림은 튜링 기계의 일반적인 구조를 보여줍니다. 내부 테이프가 포함되어 있고, 테이프는 셀의 배열이며, 각 셀은 제한된 알파벳의 기호를 포함할 수 있고, 특수 마커로 사용되는 특수 기호 $와 테이프의 셀을 가리키는 전용 테이프 헤드가 있습니다. 일련의 상태에는 현재 상태를 저장할 수 있는 작은 메모리 조각이 있습니다. 이 저장 요소를 상태 레지스터라고 합니다.

튜링 기계의 작동은 매우 간단합니다. 각 단계에서 테이프 헤드는 현재 셀의 기호와 상태 레지스터의 현재 상태를 읽고 각 기호와 상태 조합에 대한 작업 집합이 포함된 테이블을 찾습니다. 이 특수 테이블을 전이 함수 테이블 또는 작업 테이블이라고 합니다. 이 테이블의 각 항목은 세 가지 사항, 즉 테이프 헤드를 왼쪽이나 오른쪽으로 한 단계 이동할지 여부, 다음 상태, 현재 셀에 기록되어야 하는 기호를 나타냅니다. 따라서 각 단계에서 테이프 헤드는 셀의 값을 덮어쓰고, 상태 레지스터의 상태를 변경하고, 새 셀로 이동할 수 있습니다. 유일한 제한 사항은 새 셀이 현재 셀의 가장 왼쪽이나 오른쪽에 있어야 한다는 것입니다. 공식적으로는 다음과 같은 형식입니다.

(, )({L,R}, new_state, new_symbol)

그 중 L은 왼쪽을 나타내고 R은 오른쪽을 나타냅니다.

예: 문자열이 aaa...abb...bb 형식인지 확인하는 Turing 기계를 설계합니다. 답변: 두 가지 상태 (Sa,Sb)와 두 가지 특수 상태(종료 및 오류)를 정의하겠습니다. 상태가 종료 또는 오류와 같으면 계산이 중지됩니다. 튜링 기계는 상태 (Sb)에서 시작하여 오른쪽에서 왼쪽으로 입력 스캔을 시작할 수 있습니다. 작업 테이블은 다음과 같습니다.

\begin{배열}{||l||} \hline \hline\left(S_{b}, b\right) \rightarrow\left(L, S_{b}, b\right) \\ \hline\left(S_{b}, a\right) \rightarrow\left(L, S_{a}, a\right) \\ \hline\left(S_{b}, \$\right) \rightarrow(L, \text { 오류 }, \$) \\ \hline\left(S_{a}, b\right) \rightarrow(L, \text { 오류 }, b) \\ \hline\left(S_{a}, a\right) \rightarrow\left(L, S_{a}, a\right) \\ \hline\left(S_{a}, \$\right) \rightarrow(L, \text {exit}, \$) \\ \hline \hline \end{배열} $ 위의 내용은 튜링 머신의 단순한 응용 사례일 뿐이지만, 실제 시나리오에서는 복잡성이 이보다 훨씬 더 큽니다. 우리는 간단한 문제에도 튜링 기계를 설계하는 것이 불가능하다는 결론을 즉시 내릴 수 있습니다. 작업 테이블에는 많은 상태가 포함되어 크기가 빠르게 초과되기 때문에 기본적으로 이 간단한 장치로 복잡한 문제를 해결할 수 있다는 것입니다. 실제로 이 기계는 날씨 모델링, 재무 계산, 미분 방정식 풀이 등 다양한 문제를 해결할 수 있습니다! 이 관찰은 Church-Turing 논문에 담겨 있습니다. 이 논문에서는 물리적 컴퓨팅 장치로 계산할 수 있는 모든 함수가 Turing 기계로 계산될 수 있다고 말합니다. 일반인의 관점에서 말하면, 인간에게 알려진 모든 컴퓨터에서 결정론적 알고리즘으로 계산할 수 있는 모든 프로그램은 튜링 기계로도 계산할 수 있습니다. 이 명제는 지난 반세기 동안 변함없이 유지되어 왔다. 지금까지 연구자들은 튜링 기계보다 더 강력한 기계를 찾을 수 없었습니다. 이는 튜링 기계 이외의 다른 기계에서는 어떤 프로그램도 계산할 수 없다는 것을 의미합니다. Turing 머신에서는 계산하는 데 오랜 시간이 걸릴 수 있는 프로그램이 있지만 다른 모든 컴퓨터에서도 무한한 시간이 걸립니다. 여러 테이프, 여러 테이프 헤드 또는 각 테이프의 여러 트랙을 고려하여 가능한 모든 방법으로 Turing 기계를 확장할 수 있습니다. 보시다시피, 이들 기계 각각은 단순한 튜링 기계만큼 강력합니다. 위에 설명된 튜링 기계는 기계가 계산하는 기능에 특정한 작업 테이블을 포함하고 있기 때문에 범용 기계가 아닙니다. 진정한 범용 기계는 각 기능에 대해 동일한 작업 테이블, 기호 및 동일한 상태 집합을 갖습니다. 다른 튜링 기계를 시뮬레이션하는 튜링 기계를 설계할 수 있다면 계산되는 함수에 특정하지 않고 보편적인 튜링 기계를 만들 수 있습니다. 시뮬레이션된 튜링 기계를 M이라고 하고, 범용 튜링 기계를 U라고 하겠습니다. 먼저 M의 작업 테이블에 대한 공통 형식을 만들고 이를 U 테이프의 지정된 위치에 저장해 보겠습니다. 각 작업에는 이전 상태, 이전 기호, 방향(왼쪽 또는 오른쪽), 새 상태, 새 기호 등 5개의 매개변수가 필요합니다. 십진수 10자리(0-9)가 될 수 있는 공통 기본 기호 세트를 사용할 수 있습니다. 함수에 더 많은 기호가 필요한 경우 특수 구분 기호로 구분된 연속 단위 집합에 기호를 포함하는 것을 고려할 수 있습니다. 이러한 기호를 아날로그 기호라고 부르겠습니다. 마찬가지로 시뮬레이션 작업 테이블의 상태는 십진수로 인코딩될 수 있습니다. 방향은 왼쪽에 0, 오른쪽에 1을 사용할 수 있습니다. 따라서 단일 작업 테이블 항목은 (@1334@34@0@1335@10@)과 같을 수 있습니다. 여기서 "@"는 구분 기호이며 이 항목은 기호 34가 나타나면 상태 1334에서 1335로 이동한다는 의미입니다. 왼쪽(0)으로 이동하여 10의 값을 씁니다. 따라서 우리는 작업 테이블, 기호 세트 및 일부 기능을 계산하는 데 사용되는 Turing 기계의 상태를 인코딩하는 방법을 찾았습니다. 마찬가지로 아날로그 상태 레지스터라고 하는 M의 상태 레지스터를 포함하도록 테이프 영역을 지정할 수 있습니다. M의 테이프가 U의 테이프에 전용 공간을 갖게 해주세요. 우리는 이 공간을 작업 공간이라고 부릅니다. 이 조직은 아래 그림에 나와 있습니다. ![](/assets/16964884/105.png) *범용 튜링 기계의 레이아웃.* 따라서 테이프는 세 부분으로 나누어집니다. 첫 번째 부분에는 아날로그 작업 테이블이 포함되고, 두 번째 부분에는 아날로그 상태 레지스터가 포함되며, 마지막 부분에는 일련의 아날로그 기호가 포함된 작업 영역이 포함됩니다. Universal Turing Machine(U)에는 매우 간단한 작업 테이블과 상태 집합이 있습니다. 아이디어는 아날로그 상태 레지스터의 값과 테이프 헤더 아래의 아날로그 기호와 일치하는 아날로그 작업 테이블에서 올바른 항목을 찾는 것입니다. 그런 다음 범용 Turing 기계는 새로운 시뮬레이션 상태로 이동하고 필요한 경우 작업 공간의 시뮬레이션 기호를 덮어써서 해당 작업을 수행해야 합니다. 각각의 기본 동작을 수행하기 위해 U는 수십 개의 테이프 헤드 동작을 수행해야 합니다. 그러나 결론은 보편적인 튜링 기계를 구축할 수 있다는 것입니다. 다른 Turing 기계를 시뮬레이션할 수 있는 범용 Turing 기계를 구축하는 것이 가능합니다. 1950년대부터 연구자들은 고유한 상태와 규칙 세트를 사용하여 더 많은 유형의 가상 기계를 설계했으며 이러한 각 기계는 기껏해야 튜링 기계만큼 강력하다는 것이 입증되었습니다. 모든 기계와 컴퓨팅 시스템은 공통 이름을 가지며 모두 Turing 기계만큼 표현력이 풍부하고 기능적입니다. 이 시스템은 Turing Complete라고 할 수 있습니다. 따라서 모든 범용 기계와 ISA는 Turing Complete입니다. 튜링 머신과 동등한 모든 컴퓨팅 시스템을 튜링 머신이라고 합니다. 따라서 ISA가 튜링 완전하다면 ISA가 완전하거나 보편적이라는 것을 증명해야 합니다. 이제 다음과 같은 속성을 가지며 실제 구현에 더 적합한 범용 튜링 기계의 변형(아래)을 고려해보세요. 이러한 기계는 튜링 완전성으로 입증되었습니다. ![](/assets/16964884/106.png) *향상된 범용 튜링 기계* 1. 테이프는 반무한적입니다(한 방향으로만 무한대로 확장됩니다). 2. 시뮬레이션 상태는 시뮬레이션 작업 테이블의 항목에 대한 포인터입니다. 3. 각 상태에는 시뮬레이션 작업 테이블에 고유한 항목이 있습니다. 아날로그 액션 시트를 찾을 때 테이프 헤더 아래에 있는 기호는 신경 쓰지 않습니다. 4. 작업은 테이프 헤드에 작업 공간의 일련의 위치에 액세스하고 간단한 산술 함수를 사용하여 해당 값을 기반으로 새 값을 계산하도록 지시합니다. 이 새 값을 작업 공간의 새 위치에 씁니다. 5. 기본 다음 상태는 작업 테이블의 후속 상태입니다. 6. 테이프의 특정 위치에 있는 기호가 특정 값보다 작으면 작업의 상태가 임의로 변경될 수도 있습니다. 이는 아날로그 테이프 헤드가 아날로그 작업 테이블의 새 영역에서 작업을 추출하기 시작한다는 의미입니다. 이 튜링 기계는 다음과 같은 형태의 기계 조직을 제안합니다. 많은 수의 명령(작업 목록)이 있으며 이러한 명령의 배열을 종종 프로그램이라고 합니다. 프로그램 카운터라고 하는 배열의 현재 명령어에 대한 포인터를 유지하는 상태 레지스터가 있으며, 프로그램 카운터는 새 명령어를 가리키도록 변경될 수 있습니다. 기호를 저장, 검색 및 수정할 수 있는 넓은 작업 영역이 있습니다. 이 작업 영역을 데이터 영역이라고도 합니다. 명령 목록(프로그램)과 작업 공간(데이터)은 수정된 Turing 기계의 테이프에 저장됩니다. 실제 머신에서는 유한 테이프를 메모리로 생각할 수 있습니다. 메모리는 기본 기호를 포함하는 대규모 메모리 셀 배열입니다. 메모리의 한 부분에는 프로그램이 포함되어 있고 다른 부분에는 데이터가 포함되어 있습니다. 또한 각 명령어는 메모리의 일련의 위치를 ​​읽고, 해당 위치에서 작은 산술 함수를 계산하고, 결과를 다시 메모리에 쓸 수 있으며, 메모리의 값을 기반으로 다른 명령어로 점프할 수도 있습니다. 이러한 산술 함수를 계산하고 이를 메모리에 기록하며 다른 명령으로 점프하는 CPU(중앙 처리 장치)라는 전용 장치가 있습니다. 아래 다이어그램은 기계의 개념적 구성을 보여줍니다. ![](/assets/16964884/107.png) *기본 명령어 프로세서.* 위에서 우리는 상태 전환, 테이프 헤드의 이동, 기호 다시 쓰기, 테이프 헤드 아래 기호에 따른 결정 등 Turing 기계의 모든 측면을 포착했습니다. 이 기계는 오늘날 컴퓨터의 기초를 이루는 폰 노이만 기계와 매우 유사했습니다. 이제 향상된 Turing 기계를 위한 ISA를 설계해 보겠습니다. 단 하나의 명령만 포함하는 완전한 ISA를 갖는 것이 가능합니다. 개선된 튜링 기계와 호환되고 튜링 완전성이 입증된 명령어를 고려해보세요. ```cpp sbn a, b, c ``` sbn은 빼기, 음수인 경우 분기를 나타냅니다. 이 명령어는 a에서 b를 빼고(a와 b는 메모리 위치임) 결과를 a에 저장합니다. a&lt;0이면 명령어 테이블의 c 위치에 있는 명령어로 점프하고, 그렇지 않으면 제어가 다음 명령어로 전환됩니다. 예를 들어, 이 명령어를 사용하여 위치 a와 b에 저장된 두 개의 숫자를 추가할 수 있습니다. 종료는 프로그램 끝의 특별한 장소라는 점에 유의하십시오. ```cpp 1: sbn temp, b, 2 2: sbn a, temp, exit ``` 이는 메모리 위치 temp에 이미 값 0이 포함되어 있다고 가정합니다. 첫 번째 명령어는 $-b$를 temp에 저장하고 결과 값에 관계없이 다음 명령어로 점프합니다. 식별자(번호:)는 명령어의 시퀀스 번호입니다. 두 번째 명령어에서는 $a = a + b = a - (-b)$가 계산됩니다. 따라서 두 개의 숫자를 성공적으로 추가했으면 이제 이 기본 코드를 사용하여 1부터 10까지의 숫자를 추가할 수 있습니다. 변수 counter는 9로 초기화되고 index는 10으로 초기화되고 하나는 1로 초기화되고 0으로 초기화된다고 가정합니다. ```cpp 1: sbn temp, temp, 2 // temp = 0 2: sbn temp, index, 3 // temp = -1 * index 3: sbn sum, temp, 4 // sum += index 4: sbn index, one, 5 // index -= 1 5: sbn counter, one, exit // loop is finished, exit 6: sbn temp, temp, 7 // temp = 0 7: sbn temp, one, 1 // (0 - 1 < 0), hence goto 1 ``` 우리는 이 작은 일련의 작업이 for 루프를 실행하는 것을 관찰합니다. 종료 조건은 5행에 있고 루프 반환은 7행에서 발생합니다. 각 반복에서 $-sum += index$를 계산합니다. 작거나 같을 경우 빼기 및 분기, 빌릴 경우 역 빼기 및 건너뛰기, 범용 메모리 이동 작업을 수행하는 컴퓨터 등 완전한 것으로 입증된 유사한 단일 명령 ISA가 많이 있습니다. 단일 명령으로 프로그램을 작성하는 것은 매우 어렵고 프로그램이 매우 긴 경우가 많습니다. 명령어 수를 줄일 이유가 없습니다. 많은 수의 명령어를 고려함으로써 복잡한 프로그램의 구현을 더 쉽게 만들 수 있습니다. 기본 sbn 명령어를 여러 명령어로 분해해 보겠습니다. - 산술 지침. 덧셈, 뺄셈, 곱셈, 나눗셈과 같은 일련의 산술 명령이 있을 수 있습니다. - 이동 지침. 서로 다른 메모리 위치 간에 값을 이동하는 이동 명령이 있을 수 있으므로 상수 값을 메모리 위치에 로드할 수 있습니다. - 지점 지침. 분기 명령어는 계산 결과나 메모리에 저장된 값을 기반으로 새 명령어를 가리키도록 프로그램 카운터를 변경해야 합니다. 이러한 기본 원칙을 염두에 두고 다양한 유형의 완전한 ISA를 설계할 수 있습니다. 연산(데이터 처리), 이동(데이터 전송), 분기(제어)의 세 가지 유형의 명령어만 필요하다는 점에 유의하세요. 모든 명령어 세트에는 최소한 세 가지 유형의 명령어가 필요합니다. 1. 덧셈, 뺄셈, 곱셈, 나눗셈 등의 연산을 수행하기 위해서는 산술 명령어가 필요합니다. 대부분의 명령어 세트에는 논리 OR 및 NOT과 같은 논리 연산을 수행하는 특수 명령어도 있습니다. 2. 메모리 위치 간에 값을 전송할 수 있고 메모리 위치에 상수를 로드할 수 있는 데이터 전송 명령이 필요합니다. 3. 명령어 피연산자의 값에 따라 프로그램의 다른 지점에서 명령어의 분기 명령어 실행을 시작할 수 있어야 합니다. 레지스터 머신에는 레지스터라고 불리는 명명된 저장 위치가 무제한 포함되어 있습니다. 레지스터는 무작위로 액세스 가능하며 모든 명령어는 레지스터 이름을 피연산자로 사용합니다. CPU는 레지스터에 액세스하여 피연산자를 가져와서 처리합니다. 레지스터가 있는 표준 Von Neumann 기계의 저장 공간을 늘리는 하이브리드 기계도 있습니다. 레지스터는 기호를 보관할 수 있는 저장 위치입니다. 메모리는 일반적으로 매우 큰 구조입니다. 최신 프로세서에서는 전체 메모리에 수십억 개의 저장 위치가 포함될 수 있으며 이 크기의 메모리를 실제로 구현하는 것은 실제로 매우 느립니다. 하드웨어에는 일반적인 경험 법칙이 있습니다. 클수록 느리고 작을수록 빠릅니다. 따라서 빠른 작업을 달성하기 위해 모든 프로세서에는 빠르게 액세스할 수 있는 레지스터 세트가 있습니다. 레지스터 수는 일반적으로 8~64개입니다. 산술 및 분기 연산의 대부분의 피연산자는 이 레지스터에 있으며 프로그램은 어느 시점에서나 작은 변수 세트를 재사용하는 경향이 있으므로 레지스터를 사용하면 많은 메모리 액세스를 절약할 수 있습니다. 그러나 때로는 메모리 위치를 레지스터로 가져오거나 레지스터의 값을 다시 메모리 위치에 써야 하는 경우도 있습니다. 이러한 경우 메모리와 레지스터 간에 값을 전송하기 위해 전용 로드 및 저장 명령을 사용합니다. 대부분의 프로그램에는 대부분 레지스터 전용 명령어가 있으며 로드 및 저장 명령어의 수는 일반적으로 실행되는 전체 명령어의 약 1/3입니다. 저장 위치 b와 c에 3제곱한 숫자를 더하고 그 결과를 저장 위치 a에 저장한다고 가정해 보겠습니다. 레지스터가 있는 머신에는 r1, r2 및 r3이 레지스터의 이름이고 특정(일반, 개념적) ISA를 사용하지 않는다고 가정하고 다음 지침이 필요합니다. ```cpp 1: r1 = mem[b] // load b 2: r2 = mem[c] // load c 3: r3 = r1 * r1 // compute b^2 4: r4 = r1 * r3 // compute b^3 5: r5 = r2 * r2 // compute c^2 6: r6 = r2 * r5 // compute c^3 7: r7 = r4 + r6 // compute b^3 + c^3 4: mem[a] = r7 // save the result ``` mem은 메모리를 나타내는 배열이며 먼저 값을 레지스터에 로드한 다음 산술 계산을 수행하고 결과를 다시 메모리에 저장해야 합니다. 위의 코드는 레지스터를 사용하여 메모리 액세스를 절약합니다. 계산의 복잡성을 높이면 더 많은 메모리 액세스를 절약할 수 있으므로 레지스터를 사용하면 더 빠르게 실행됩니다. 최종 프로세서 구성은 아래 그림과 같습니다. ![](/assets/16964884/108.png) 분명히 스택에서 작업하도록 계산을 예약하는 것은 바람직하지 않습니다. 중복된 로드와 저장이 많이 있을 것이기 때문입니다. 그럼에도 불구하고 긴 수학적 표현을 평가하기 위한 기계와 프로그램 크기가 문제가 되는 기계의 경우 일반적으로 스택이 선택됩니다. Burroughs Large Systems, UCSD Pascal 및 HP 3000(클래식)과 같은 스택 기반 시스템의 실제 구현은 거의 없습니다. Java 언어는 컴파일 중에 스택 기반 시스템을 가정합니다. 스택 기반 시스템은 간단하므로 Java 프로그램은 거의 모든 하드웨어 플랫폼에서 실행될 수 있습니다. 컴파일된 Java 프로그램을 실행할 때 JVM(Java Virtual Machine)은 Java 프로그램을 레지스터가 있는 시스템에서 실행할 수 있는 다른 프로그램으로 동적으로 변환합니다. 누산기 기반 컴퓨터는 누산기라는 레지스터를 사용합니다. 각 명령어는 단일 메모리 위치를 입력 피연산자로 사용합니다. 예를 들어 덧셈 연산은 누산기의 값을 메모리 주소의 값에 더한 다음 결과를 다시 누산기에 저장합니다. 레지스터를 수용할 수 없는 초기 기계는 누산기를 사용하여 메모리 액세스 횟수를 줄이고 프로그램 속도를 높였습니다. Accumulator의 일부 측면은 2012년 데스크탑과 노트북에서 가장 일반적으로 사용된 프로세서였던 Intel의 x86 프로세서 세트에 적용되었습니다. 큰 수의 곱셈과 나눗셈을 위해 이러한 프로세서는 레지스터 eax를 누산기로 사용합니다. 기타 범용 명령어의 경우 모든 레지스터를 누산기로 지정할 수 있습니다. ## 19.4.5 공통 ISA 현재 시장에 널리 사용되는 명령어 세트에는 ARM 명령어 세트와 x86 명령어 세트가 있습니다. ARM은 영국 케임브리지에 본사를 둔 상징적인 회사인 Advanced RISC Machines의 약자입니다. 2012년 현재 Apple의 iPhone, iPad를 포함한 모바일 장치의 약 90%가 ARM 기반 프로세서에서 실행됩니다. 마찬가지로, 2012년 현재 데스크탑 및 노트북의 90% 이상이 Intel 또는 AMD 기반 x86 프로세서에서 실행됩니다. ARM은 RISC 명령어 세트이고 x86은 CISC 명령어 세트입니다. 다양한 프로세서에 맞춰진 다른 명령어 세트도 많이 있습니다. 모바일 컴퓨터에 널리 사용되는 또 다른 명령어 세트는 MIPS 명령어 세트입니다. MIPS 기반 프로세서는 자동차 및 산업용 전자 장치의 다양한 프로세서에도 사용됩니다. 대규모 서버의 경우 일반적으로 IBM(PowerPC), Sun(현재 Oracle, UltraSparc) 또는 HP(PA-RISC) 프로세서가 사용됩니다. 각 프로세서 제품군에는 고유한 명령어 세트가 있으며 이는 일반적으로 RISC 명령어 세트이며 대부분의 ISA는 더하기, 빼기, 곱하기, 이동 및 로드/저장 명령어와 같은 간단한 명령어를 공유합니다. 이 간단한 세트 외에도 더 많은 특수 지침을 사용합니다. ISA에서 올바른 명령어 세트를 선택하는 것은 프로세서의 목표 시장, 작업 부하의 성격, 다양한 설계 시간 제약 조건에 따라 달라집니다. 다음 표에는 널리 사용되는 명령어 세트 목록이 나와 있습니다. ISA 유형 연도 제조업체 자릿수 바이트 순서 레지스터 수 VAX CISC 1977년 12월 32 조금 16 스팍 RISC 1986년 태양 32 바이 32 스팍 RISC 1993년 태양 64 바이 32 파워PC RISC 1992년 애플, IBM, 모토로라 32 바이 32 파워PC RISC 2002년 애플, IBM 64 바이 32 PA-RISC RISC 1986년 HP 32 큰 32 PA-RISC RISC 1996년 HP 64 큰 32 m68000 CISC 1979년 모토로라 16 큰 16 m68000 CISC 1979년 모토로라 32 큰 16 MIPS RISC 1981년 MIPS 32 바이 32 MIPS RISC 1999년 MIPS 64 바이 32 알파 RISC 1992년 12월 64 바이 32 x86 CISC 1978년 인텔, AMD 16 조금 8 x86 CISC 1985년 인텔, AMD 32 조금 8 **x86** CISC 2003년 인텔, AMD 64 64 작은 16 팔 RISC 1985년 팔 32 bi(작은 기본값) 16 **암** RISC 2011년 팔 64 bi(작은 기본값) 31 ## 19.4.6 명령어 구현 메커니즘 이진 데이터를 저장하고 해당 데이터에 대한 산술 및 논리 연산을 수행하기 위해 다양한 방법으로 결합할 수 있는 작은 기본 논리 구성 요소 집합이 있습니다. 특정 계산을 수행하려는 경우 해당 계산을 위해 특별히 설계된 논리적 구성 요소의 구성을 구성할 수 있습니다. 다양한 구성요소를 원하는 구성으로 연결하는 과정을 프로그래밍의 한 형태로 생각할 수 있습니다. 생성된 "프로그램"은 하드웨어 형식이며 하드와이어 프로그램이라고 합니다. 이제 이 대안을 고려해보세요. 산술 및 논리 기능의 공통 구성을 구성한다고 가정하면 이 하드웨어 세트는 하드웨어에 적용된 제어 신호를 기반으로 데이터에 대해 다양한 기능을 수행합니다. 원래 맞춤형 하드웨어의 경우 시스템은 데이터를 받아들이고 결과를 생성했습니다(아래 그림 a). 범용 하드웨어를 사용하면 시스템이 데이터와 제어 신호를 받아들이고 결과를 생성하므로 프로그래머는 새로운 프로그램마다 하드웨어를 재배선하는 대신 새로운 제어 신호 세트만 제공하면 됩니다. ![](/assets/16964884/109.png) *하드웨어 및 소프트웨어 방법.* 제어 신호를 제공하는 방법은 무엇입니까? 대답은 간단하면서도 미묘합니다. 전체 프로그램은 실제로 일련의 단계이며 각 단계에서 일부 데이터에 대해 산술 또는 논리 연산이 수행됩니다. 각 단계마다 새로운 제어 신호 세트가 필요합니다. 가능한 제어 신호의 각 세트에 고유한 코드를 부여하고 코드를 수용하고 제어 신호를 생성할 수 있는 범용 하드웨어에 세그먼트를 추가해 보겠습니다(위의 b). 이제 프로그래밍이 더 쉬워졌습니다. 우리가 해야 할 일은 새로운 프로그램마다 하드웨어를 재배선하는 대신 새로운 코드 시퀀스를 제공하는 것입니다. 실제로 각 코드는 명령이며 하드웨어의 일부는 각 명령을 해석하고 제어 신호를 생성합니다. 프로그래밍에 대한 이 새로운 접근 방식을 차별화하기 위해 일련의 코드 또는 명령을 소프트웨어라고 합니다. ### 19.4.6.1 저장 지침 로드 명령어인 ld r1, 12[r2]를 고려해 보겠습니다. 여기서 메모리 주소는 r2의 내용과 숫자 12의 합으로 계산됩니다. ld 명령어는 이 메모리 주소에 액세스하고 저장된 정수를 가져와 r1에 저장합니다. 계산된 메모리 주소는 정수의 첫 번째 저장된 바이트(즉, 리틀 엔디안 표현)를 가리키므로 메모리 주소에는 LSB가 포함되어 있다고 가정합니다. 자세한 내용은 아래 그림 (a)에 나와 있습니다. 저장 동작은 반대이며, 아래 (b)와 같이 r1의 값을 메모리 주소(r2+12)에 저장합니다. ![](/assets/16964884/110.png) ### 19.4.6.2 기능 간단한 기능을 구현하기 위한 기본 요구 사항을 검토해 보겠습니다. 주소 A의 명령어가 foo 함수를 호출한다고 가정합니다. foo 함수를 실행한 후 A의 명령어 다음의 명령어를 즉시 반환해야 합니다. 이 명령어의 주소는 A+4입니다(A의 명령어 길이를 4바이트라고 가정할 경우). 이 과정을 함수에서 복귀한다고 하며 주소(a+4)를 복귀 주소라고 합니다. **반환 주소**는 함수 실행 후 프로세스가 분기해야 하는 명령어의 주소입니다. 따라서 함수 구현에는 두 가지 기본 측면이 있습니다. 1. 함수를 호출하는 프로세스. 2. 함수에서 복귀하는 것과 관련됩니다. 함수는 본질적으로 어셈블리 코드의 일부이며, 함수를 호출하면 본질적으로 PC가 이 코드의 시작 부분을 가리킵니다. 각 함수에 레이블을 연결할 수 있으며, 레이블은 함수의 첫 번째 명령과 연결되어야 하며, 함수를 호출하는 것은 함수 시작 부분의 레이블로 분기하는 것만큼 간단합니다. 그러나 이는 이야기의 일부일 뿐이므로 반환 기능도 구현해야 합니다. 따라서 함수 호출을 구현하기 위해 무조건 분기 명령을 사용할 수 없습니다. 그러면 함수가 반환해야 하는 주소(반환 주소라고 함)를 저장하면서 함수의 시작 부분으로 분기하는 전용 함수 호출 명령을 생각해 보겠습니다. 다음 C 코드를 고려하고 각 C 문이 한 줄의 어셈블리 코드에 해당한다고 가정해 보겠습니다. ```cpp a = foo(); /* Line 1 */ c = a + b; /* Line 2 */ ``` 이 작은 코드 조각에서는 함수 호출 명령을 사용하여 foo 함수를 호출하고 반환 주소는 2행에 있는 명령의 주소입니다. 호출 명령은 나중에 검색할 수 있도록 반환 주소를 전용 저장 위치에 저장해야 합니다. 대부분의 RISC 명령어 세트에는 반환 주소를 저장하는 데 사용되는 반환 주소 레지스터(ra라고도 함)라는 전용 레지스터가 있습니다. 반환 주소 레지스터는 함수 호출 명령에 의해 자동으로 채워집니다. 함수에서 반환해야 할 경우 반환 주소 레지스터에 포함된 주소로 분기해야 합니다. foo가 다른 함수를 호출하면 어떻게 되나요? 이 경우 ra의 값을 덮어쓰게 됩니다. 이에 대해서는 나중에 논의하겠습니다. 이제 함수에 매개변수를 전달하고 반환값을 반환하는 문제를 고려해 보겠습니다. foo 함수가 foobar 함수를 호출한다고 가정해 보겠습니다. foo를 호출자라고 하고 foobar를 호출 수신자라고 합니다. 발신자와 수신자 관계는 고정되어 있지 않습니다. foo는 foobar를 호출할 수 있고, foobar는 동일한 프로그램에서 foo를 호출할 수 있습니다. 단일 함수 호출의 호출자와 호출 수신자는 어떤 함수가 다른 함수를 호출하는지에 따라 결정됩니다. 호출자와 호출 수신자 모두 동일한 레지스터 보기를 볼 수 있습니다. 따라서 레지스터를 통해 매개변수를 전달할 수 있고, 레지스터를 통해 반환 값을 전달할 수도 있습니다. 그러나 아래에 열거한 것처럼 이 간단한 아이디어에는 몇 가지 문제가 있습니다(16개의 레지스터가 있다고 가정). 1. 함수는 16개 이상의 매개변수를 수용할 수 있는데 이는 기존 범용 레지스터 수보다 많아 매개변수 저장을 위한 추가 공간이 필요합니다. 2. 함수는 C의 대규모 구조와 같은 대량의 데이터를 반환할 수 있습니다. 이 데이터는 레지스터에 저장되지 않을 수 있습니다. 3. 호출 수신자는 나중에 호출자에게 필요할 수 있는 레지스터를 덮어쓸 수 있습니다. 따라서 레지스터를 통해 매개변수와 반환 값을 전달하는 것은 단순한 경우에만 적합하며 그다지 유연하고 일반적인 솔루션은 아니라는 것을 알 수 있습니다. 그럼에도 불구하고 우리의 논의에서는 두 가지 요구 사항이 제기됩니다. - **공간 문제**. 더 많은 매개변수를 보내고 반환하려면 추가 공간이 필요합니다. - **보장 문제**. 호출 수신자가 호출자의 레지스터를 덮어쓰지 않도록 해야 합니다. 이 두 가지 문제를 해결하려면 기능이 어떻게 작동하는지에 대한 더 깊은 이해가 필요합니다. foo 함수는 일련의 매개변수를 받아들이고 일련의 값을 반환하는 블랙박스로 생각할 수 있습니다. foo가 작업을 수행하는 데는 나노초, 일주일 또는 심지어 1년이 걸릴 수 있습니다. foo는 작업을 수행하고, I/O 장치에 데이터를 보내고, 메모리 위치에 액세스하기 위해 다른 함수를 호출할 수 있습니다. 아래 그림은 foo 함수를 시각화한 것입니다. ![](/assets/16964884/111.png) 요약하면 일반 함수는 인수를 처리하고, 필요에 따라 메모리 및 I/O 장치에서 값을 읽고 쓰고, 결과를 반환합니다. 메모리 및 I/O 장치에 대해서는 현재로서는 특별히 우려하지 않습니다. 사용 가능한 메모리는 많고 공간은 주요 제한 사항이 아니며 I/O 장치를 읽고 쓰는 것은 일반적으로 공간 제한과 관련이 없습니다. 가장 큰 문제는 레지스터가 부족하다는 것입니다. 먼저 공간 문제를 해결해 보겠습니다. 레지스터와 메모리를 통해 값을 전송할 수 있습니다. 단순화를 위해 소량의 데이터를 전송해야 하는 경우 레지스터를 사용할 수 있고, 그렇지 않으면 메모리를 통해 전송할 수 있습니다. 마찬가지로 반환 값의 경우 메모리를 통해 값을 전송할 수 있습니다. 데이터를 전송하기 위해 메모리를 사용하면 공간의 제약이 없습니다. 그러나 이 접근 방식은 사용할 메모리 위치와 관련하여 호출자와 호출 수신자 간에 엄격한 합의에 도달해야 하기 때문에 유연성이 부족합니다. 호출 수신자가 자신을 재귀적으로 호출할 수 있기 때문에 고정된 메모리 위치 집합을 사용할 수 없습니다. ```cpp void foobar() { ... foobar(); ... } ``` 기민한 독자라면 호출 수신자가 메모리에서 매개변수를 읽고 이를 메모리의 다른 임시 영역으로 옮긴 다음 다른 함수를 호출할 수 있다고 생각할 수도 있습니다. 그러나 이 접근 방식은 우아하지도 않고 효율적이지도 않습니다. 더 우아한 해결책은 나중에 조사될 것입니다. 그러므로 우리는 공간 문제를 부분적으로 해결했다고 결론 내릴 수 있습니다. 호출자와 수신자 간에 또는 그 반대로 일부 값을 전송해야 하는 경우 레지스터를 사용할 수 있습니다. 그러나 매개변수/반환 값이 사용 가능한 레지스터 세트에 없으면 메모리를 통해 전송되어야 합니다. 메모리를 통해 데이터를 전송하려면 데이터 전송에 사용되는 메모리 위치에 대한 호출자와 호출 수신자 간의 엄격한 합의가 필요하지 않은 우아한 솔루션이 필요합니다. 레지스터를 메모리에 저장하고 나중에 복원하는 개념을 레지스터 스필링이라고 합니다. 덮어쓰기 문제를 해결하기 위한 두 가지 해결 방법이 있습니다. 1. 호출자는 필요한 레지스터 세트를 메모리의 전용 위치에 저장할 수 있습니다. 해당 레지스터 세트는 호출 수신자가 완료되고 제어권이 호출자에게 반환된 후에 검색할 수 있습니다. 2. 호출 수신자가 필요한 레지스터를 저장하고 복원하도록 합니다. 두 가지 방법 모두 아래 이미지에 나와 있습니다. 레지스터 값을 메모리에 저장한 다음 검색하는 이 방법을 스필링(spilling)이라고 합니다. ![](/assets/16964884/112.png) *발신자 저장 및 수신자 저장 레지스터.* 호출자와 호출 수신자 모두 사용해야 하는 메모리 위치에 대해 엄격한 합의에 도달해야 하는 동일한 문제가 다시 발생합니다. 이제 이 두 가지 문제를 해결하기 위해 함께 노력합시다. 함수 간에 인수를 전달하고 메모리의 전용 위치를 사용하여 레지스터를 저장/복원하는 프로세스를 단순화했습니다. 그러나 이 솔루션은 유연하고 대규모 실제 프로그램에 구현하기가 상당히 복잡할 수 있는 것으로 나타났습니다. 아이디어를 단순화하기 위해 함수 호출에서 패턴을 정의해 보겠습니다. 일반적인 C 또는 Java 프로그램은 main 함수로 시작합니다. 그런 다음 이 함수는 다른 함수를 호출하고, 이는 차례로 다른 함수를 호출할 수도 있습니다. 마지막으로, 메인 함수가 종료되면 실행이 종료됩니다. 각 함수는 지역 변수 세트를 정의하고 이러한 변수 및 함수 매개변수에 대한 계산을 수행합니다. 다른 함수를 호출할 수도 있습니다. 마지막으로 함수는 값을 반환하고 값 배열(C의 구조체)을 반환하는 경우는 거의 없습니다. 함수가 종료된 후에는 지역 변수와 매개변수가 더 이상 필요하지 않습니다. 따라서 이러한 변수나 매개변수 중 일부가 메모리에 유지되는 경우 공간을 회수해야 합니다. 둘째, 함수가 레지스터를 오버플로하는 경우 해당 메모리 위치도 종료 후 해제되어야 합니다. 마지막으로 호출 수신자가 다른 함수를 호출하는 경우 반환 주소 레지스터의 값을 메모리에 저장해야 하며 이 위치는 함수가 종료된 후 해제되어야 합니다. 이 모든 정보를 함수의 활성화 블록이라고 하는 메모리 영역에 연속적으로 보관하는 것이 가장 좋습니다. 다음 그림은 활성화 블록의 메모리 맵을 보여줍니다. ![](/assets/16964884/113.png) 활성화 블록에는 매개변수, 반환 주소, 레지스터 오버플로 영역(발신자 저장 및 호출 수신자 저장 시나리오용) 및 지역 변수가 포함되어 있습니다. 함수가 종료되면 활성화 블록을 완전히 제거할 수 있습니다. 함수가 일부 값을 반환하려는 경우 레지스터를 사용하여 그렇게 할 수 있지만 큰 구조를 반환하려는 경우 이를 호출자의 활성화 블록에 쓸 수 있으며 호출자는 데이터를 쓸 수 있는 활성화 블록의 위치를 ​​제공할 수 있습니다. 나중에 이를 더 우아하게 수행하는 것이 가능하겠지만 이를 수행하는 방법을 설명하기 전에 활성화 블록이 메모리에서 어떻게 배열되는지 이해해야 합니다. 모든 활성화 블록이 인접한 영역에 저장되는 저장 영역을 가질 수 있습니다. 예를 고려하십시오. foo 함수가 foobar 함수를 호출하고, 그 함수가 foobarbar를 호출한다고 가정해 보겠습니다. 다음 그림은 (a) foobar 호출 전, (b) foobarbar 호출 전, (c) foobarbar 호출 후, (d) foobarar 반환 후의 4가지 메모리 상태를 보여줍니다. ![](/assets/16964884/114.png) 이 메모리 영역에는 후입선출 동작이 있으며, 마지막으로 호출된 함수가 가장 먼저 완료되는 함수입니다. 이러한 후입선출 구조를 전통적으로 컴퓨터 과학에서는 스택이라고 부르기 때문에 활성화 블록을 저장하는 전용 저장 영역을 스택이라고 합니다. 전통적으로 스택은 아래쪽(더 작은 메모리 주소 방향)으로 증가하는 것으로 간주되었습니다. 즉, 주 기능에 대한 활성화 블록이 매우 높게 시작되고 새 활성화 블록이 기존 활성화 블록 바로 아래(낮은 주소 방향)에 추가된다는 의미입니다. 스택의 맨 위는 실제로 스택에서 가장 작은 주소이고, 스택의 맨 아래는 가장 큰 주소입니다. 스택의 맨 위는 현재 실행 중인 함수의 활성화 블록을 나타내고, 스택의 맨 아래는 초기 주 함수를 나타냅니다. **스택**은 프로그램의 모든 활성 블록을 저장하는 메모리 영역입니다. 일반적으로 아래쪽으로 자랍니다. 함수를 호출하기 전에 활성화 블록을 스택에 푸시해야 하며, 함수 실행이 완료되면 활성화 블록을 스택에 팝해야 합니다. 스택 포인터 레지스터는 스택의 상단에 대한 포인터를 보유합니다. 대부분의 아키텍처는 스택 포인터(종종 sp라고 함)라는 특수 레지스터에 스택 상단에 대한 포인터를 유지합니다. 많은 아키텍처에서 스택은 순전히 소프트웨어 구성입니다. 그들에게는 하드웨어가 스택에 대해 알지 못합니다. 그러나 일부 아키텍처(예: x86)의 경우 하드웨어는 스택을 인식하고 이를 사용하여 반환 주소나 다른 레지스터의 값을 푸시합니다. 이 경우에도 하드웨어는 각 활성화 블록의 내용을 알지 못하며 구조는 어셈블리 프로그래머나 컴파일러에 의해 결정됩니다. 모든 경우에 컴파일러는 스택을 관리하기 위해 어셈블리 명령을 명시적으로 추가해야 합니다. 호출 수신자를 위한 새로운 활성화 블록을 생성하는 단계는 다음과 같습니다. 1. 활성화 블록의 크기만큼 스택 포인터를 줄입니다. 2. 매개변수 값을 복사합니다. 3. 필요한 경우 해당 메모리 위치에 기록하여 지역 변수를 초기화합니다. 4. 필요한 경우 모든 레지스터를 오버플로합니다(활성 블록에 저장). 함수에서 복귀할 때 활성화 블록을 파괴해야 하며, 이는 활성화 블록의 크기를 스택 포인터에 추가하여 수행할 수 있습니다. 스택을 사용하여 모든 문제를 해결했습니다. 호출자와 호출 수신자는 서로의 지역 변수를 덮어쓸 수 없으며 지역 변수는 활성화 블록에 저장되며 두 활성화 블록은 겹치지 않습니다. 변수 외에도 활성화 블록에 레지스터 저장 명령을 명시적으로 삽입하여 호출 수신자가 호출자의 레지스터를 덮어쓰는 것을 방지할 수 있습니다. 이를 달성하는 방법에는 호출자 저장 구성표와 호출 수신자 저장 구성표의 두 가지가 있습니다. 둘째, 매개변수를 전달하는 데 사용될 메모리 영역에 대해 명시적으로 합의할 필요가 없습니다. 스택은 이 목적으로 사용될 수 있으며 호출자는 간단히 매개변수를 스택에 푸시할 수 있으며 이러한 매개변수는 호출 수신자가 쉽게 사용할 수 있는 호출 수신자의 활성화 블록으로 푸시됩니다. 마찬가지로 함수에서 반환 값을 전달할 때 호출 수신자는 반환 값을 스택에 푸시하기 전에 먼저 스택 포인터를 줄여 활성화 블록을 파괴해야 합니다. 호출자는 호출 수신자의 의미를 알게 되므로 호출 수신자가 반환된 후 반환 값이 추가 공간을 차지하면서 활성화 블록이 호출 수신자에 의해 효과적으로 확대되었다고 가정할 수 있습니다. ARM은 B/BL/BX/BLX와 같은 명령문을 사용하여 함수를 호출하고 함수를 반환하는 반면 x86은 call과 같은 명령을 사용하여 함수를 호출합니다. 또한 x86과 ARM 모두 ret 명령어를 사용하여 주소를 반환할 수 있습니다. 다음은 ARM의 함수 호출 샘플 코드입니다. ```cpp .globl main .extern abs .extern printf .text output_str: .ascii "The answer is %d\n\0" @ returns abs(z)+x+y @ r0 = x, r1 = y, r2 = z .align 4 do_something: push {r4, lr} add r4, r0, r1 mov r0, r2 bl abs ; 调用abs add r0, r4, r0 pop {r4, pc} main: push {ip, lr} mov r0, #1 mov r1, #3 mov r2, #-4 bl do_something ; 调用do_something mov r1, r0 ldr r0, =output_str bl printf mov r0, #0 pop {ip, pc} ``` 흥미로운 명령어는 pushpop과 bl입니다. 이는 단순히 제공된 레지스터 목록을 가져와 스택에 푸시하거나 제공된 레지스터에 팝하는 것입니다. bl은 링크가 있는 분기일 뿐이며 분기 후 다음 명령어의 주소가 링크 레지스터 lr에 로드됩니다. 우리가 호출하는 루틴이 실행되면 lr을 PC에 다시 복사하여 CPU가 bl 명령어 이후의 코드에서 계속 진행할 수 있도록 합니다. do_someting에서는 링크 레지스터를 스택에 푸시하여 다시 팝될 수 있도록 합니다. 비록 ABS 호출이 링크 레지스터의 원래 내용을 덮어쓰더라도 마찬가지입니다. Arm 프로시저 호출 표준에 따르면 함수 호출 사이에 r4-r11이 유지되어야 하고(아래 그림) 호출된 함수가 해당 유지를 담당하므로 프로그램은 r4를 저장합니다. 즉, do_someting은 r0+r1의 결과를 ABS에 의해 손상되지 않는 레지스터에 저장해야 하며 해당 결과를 저장하는 데 사용되는 모든 레지스터의 내용도 저장해야 합니다. 물론 이 특별한 경우에는 r3을 사용할 수도 있지만 이를 고려해야 합니다. 프로시저 호출 표준에 따라 스택이 64비트로 정렬되어야 하기 때문에 레지스터를 유지할 필요는 없지만 레지스터를 푸시하고 팝합니다. 이는 CPU 내의 64비트 데이터 경로를 활용할 수 있으므로 스택 작업을 사용할 때 성능상의 이점을 제공합니다. ![](/assets/16964884/115.png) 상위 주소의 값을 직접 푸시할 수 있습니다. 결국 ABS를 등록해야 하는 경우 이것이 값을 저장하는 방법입니다. 우리가 알고 있는 필요한 값 대신 r4를 푸시하면 작은 성능 문제가 있지만 가장 강력한 주장은 아마도 함수의 시작과 끝에서 필요한 모든 레지스터를 푸시/팝함으로써 오류 발생 가능성을 줄이고 코드 가독성을 향상시킬 수 있다는 것입니다. 또한 "main" 함수는 lr의 내용을 푸시하고 팝하기도 합니다. 왜냐하면 메인 코드가 내 코드에서 가장 먼저 실행될 수 있지만 프로그램이 로드될 때 두 번째로 실행되는 것은 아니기 때문입니다. 컴파일러는 main을 호출하기 전에 몇 가지 기본 설정 함수에 대한 호출을 삽입하고 종료 시 몇 가지 최종 정리 호출을 수행합니다. 이제 각 명령어를 32비트 값으로 인코딩해 보겠습니다. 0, 1, 2, 3 주소 형식의 명령어가 있다고 가정합니다. 둘째, 일부 명령어는 즉시 값을 취하므로 32비트를 필드로 나누어야 합니다. 21개의 명령어가 있다고 가정하면 명령어 유형을 인코딩하는 데 5비트가 필요합니다. 일반 명령어의 각 명령어에 대한 코드는 아래 표와 같습니다. 32비트 필드의 최상위 비트를 사용하여 명령어 유형을 지정할 수 있습니다. 명령어의 코드를 opcode라고도 합니다. 지침 바이너리 코드 추가하다 00000 서브 00001 멀 00010 div 00011 모드 00100 cmp 00101 그리고 00110 또는 00111 아니 01000 이동 01001 lsl 01010 lsr 01011 ASR 01100 안돼 01101 ld 01110 성 01111 베크 10000 bgt 10001 비 10010 전화 10011 리트 10100 이제 0번지 명령어부터 시작하여 각 유형의 명령어를 인코딩해 보겠습니다. 우리가 가지고 있는 두 개의 0 주소 명령어는 ret와 nop입니다. Opcode는 5개의 최상위 비트로 지정됩니다. 이 경우 ret는 10100이고 b는 10010입니다(위 표 참조). 해당 인코딩은 아래 그림에 나와 있습니다. MSB 위치에 5비트 opcode만 지정하면 되며 나머지 27비트는 필요하지 않습니다. ![](/assets/16964884/116.png) *ret 명령어를 인코딩합니다.* 우리가 가지고 있는 1-주소 명령어는 레이블을 인수로 사용하는 call, b, beq 및 bgt입니다. 명령어를 인코딩할 때 라벨의 주소를 매개변수로 지정해야 합니다. 레이블의 주소는 레이블이 가리키는 명령어의 주소와 동일합니다. 레이블 뒤의 줄이 비어 있으면 지침이 포함된 다음 어셈블리 명령문을 고려해야 합니다. 이들 4개의 명령어에 대한 opcode에는 5비트가 필요하며 나머지 27비트는 주소에 사용됩니다. 메모리 주소는 길이가 32비트이고 27비트로 주소 공간을 감당할 수 없지만 두 가지 주요 최적화가 가능합니다. 첫째, PC 기준 주소 지정을 가정할 수 있으며 비트 27이 현재 PC를 기준으로 오프셋(양수 또는 음수)을 지정한다고 가정할 수 있습니다. 최신 프로그램의 분기 문은 for/while 루프 또는 if 문의 결과로 생성되며 이러한 구문의 경우 분기 대상은 일반적으로 수백 개의 명령어 범위에 있습니다. 오프셋을 지정하는 데 27비트가 있고 2의 보수라고 가정하면 모든 방향(양수 또는 음수)의 최대 오프셋은 226이며 이는 거의 모든 프로그램에 충분합니다. 또 다른 중요한 관찰이 있습니다. 명령어에는 4바이트가 필요합니다. 모든 명령어가 4바이트 경계로 정렬되어 있다고 가정하면 명령어의 모든 시작 메모리 주소는 4의 배수이므로 주소의 최소 두 개의 부호 있는 이진수는 00이 됩니다. 이를 지정하려고 비트를 낭비할 이유가 없습니다. 27비트가 명령어를 포함하는 메모리 단어(4바이트 메모리 단어)의 주소 오프셋을 지정한다고 가정할 수 있습니다. 이러한 최적화를 통해 PC의 바이트 오프셋은 29비트가 되며, 이는 가장 큰 프로그램에도 충분한 숫자입니다. 분기 대상이 228바이트 이상 떨어져 있는 극단적인 경우를 대비해 어셈블러는 한 분기가 다른 분기를 호출하도록 분기를 연결해야 합니다. 이러한 명령어의 인코딩은 아래 그림에 나와 있습니다. ![](/assets/16964884/117.png) *1 주소 명령어의 인코딩(분기 형식).* 1-주소 명령어 형식은 0-주소 형식에서 사용되지 않은 비트의 사용을 금지한다는 점에 유의하십시오. ret 명령어의 0-주소 형식은 1-주소 형식의 특별한 경우로 간주될 수 있습니다. 1-주소 형식을 분기 형식이라고 합니다. 이 형식의 필드 이름을 지정하고 op 형식의 opcode 부분과 오프셋 오프셋을 호출합니다. 연산 필드에는 위치 28-32의 비트가 포함되고 오프셋 필드에는 위치 1-27의 비트가 포함됩니다. 다음으로 add, sub, mul, div, mod 및 or, lsl, lsr 및 asr의 3가지 주소 명령어를 고려하세요. 대상 레지스터, 입력 소스 레지스터, 레지스터나 즉치값이 될 수 있는 두 번째 소스 피연산자가 있는 일반적인 3주소 명령어를 생각해 보세요. 두 번째 소스 피연산자가 레지스터이거나 즉치값인 경우 1비트를 입력하고 출력해야 합니다. 이를 I 비트라고 부르고 명령어의 opcode 뒤에 지정합니다. I=1이면 두 번째 소스 피연산자는 즉치값이고, I=0이면 두 번째 소스 피연산자는 레지스터입니다. 이제 두 번째 소스 피연산자를 레지스터(I=0)로 갖는 3주소 레지스터의 경우를 생각해 보십시오. 16개의 레지스터가 있으므로 각 레지스터를 고유하게 지정하려면 4비트가 필요합니다. 레지스터 ri는 i에 해당하는 부호 없는 4비트 이진수로 인코딩될 수 있습니다. 따라서 대상 레지스터와 두 개의 입력 소스 레지스터를 지정하려면 12비트가 필요합니다. 구조는 아래 그림과 같습니다. 이 명령어 형식을 레지스터 형식이라고 합니다. 분기 형식과 마찬가지로 op(opcode, 비트: 28-32), I(즉시 값, 비트: 27), rd(대상 레지스터, 비트: 23-26), rs1(소스 레지스터 1, 비트: 19-22) 및 rs2(소스 레지스터 2, 비트: 15-18) 등 다양한 필드의 이름을 지정할 수도 있습니다. ![](/assets/16964884/118.png) 두 번째 소스 피연산자가 즉치수라고 가정하면 I를 1로 설정한 다음 지정된 즉치수의 나머지 자릿수를 계산해야 합니다. 이제 opcode에 5비트, I 비트에 1비트, 대상 레지스터에 4비트, 첫 번째 소스 레지스터에 4비트를 투자하여 총 14비트가 되었습니다. 따라서 32비트 중 18비트가 남고 즉치값을 지정하는 데 사용할 수 있습니다. 18비트를 2비트(수정자) ​​+ 16비트(즉치값의 상수 부분)의 두 부분으로 나누는 것이 좋습니다. 두 개의 수정 비트는 00(기본값), 01("u") 및 10("h")의 세 가지 값을 가질 수 있습니다. 기본 수정자를 사용하는 경우 나머지 16비트는 16비트 2의 보수를 지정하는 데 사용됩니다. u 및 h 수정자의 경우 직접 필드의 16비트 상수는 부호 없는 것으로 간주됩니다. 즉치 필드의 길이가 18비트이고 수정된 부분과 상수 부분이 있다고 가정하면 프로세서는 내부적으로 수정자를 기반으로 즉시 필드를 32비트 값으로 확장합니다. 이 인코딩은 아래 그림에 나와 있습니다. 이 명령어 형식을 즉시 형식(immediate format)이라고 부를 수 있습니다. 분기 형식과 마찬가지로 op(opcode, 비트: 28-32), I(즉시 값, 비트: 27), rd(대상 레지스터, 비트: 23-26), rs1(소스 레지스터 1, 비트: 19-22) 및 imm(즉시 값: 1-18)과 같이 다양한 필드의 이름을 지정할 수도 있습니다. ![](/assets/16964884/119.png) 비슷한 방식으로 cmp, not 및 mov 명령을 아래와 같이 인코딩할 수 있습니다. ![](/assets/16964884/120.png) 로딩 명령어의 구현은 다음과 같습니다: ![](/assets/16964884/121.png) ## 19.4.7 ARM 명령어 인코딩 ARM에는 데이터 처리(더하기/빼기/곱하기/비교), 로드/저장, 분기 및 기타 정보를 표현하는 데 2비트가 필요한 네 가지 유형의 명령어가 있습니다. 이 비트는 명령어 유형을 결정합니다. 아래 그림은 ARM의 일반적인 명령어 형식을 보여줍니다. ![](/assets/16964884/122.png) 데이터 처리 명령어의 경우 유형 필드는 00이고 나머지 26비트에는 명령어 유형, 특수 조건 및 레지스터가 포함되어야 합니다. 아래 그림은 데이터 처리 명령어의 형식을 보여줍니다. ![](/assets/16964884/123.png) ![](/assets/16964884/124.png) 비트 26은 앞서 언급한 I 비트와 유사하게 I(즉시) 비트라고 합니다. 1로 설정되면 두 번째 피연산자는 즉치값이고, 그렇지 않으면 레지스터입니다. ARM에는 16개의 데이터 처리 명령어가 있고 이를 표현하는 데 4비트가 필요하므로 이 정보는 비트 22-25에 보관됩니다. 비트 21은 S 비트를 보유하며 켜져 있으면 명령어가 CPSR을 설정합니다. 나머지 20비트는 입력 및 출력 피연산자를 보유합니다. ARM에는 16개의 레지스터가 있으므로 레지스터를 인코딩하는 데 4비트가 필요합니다. 비트 17-20은 레지스터여야 하는 첫 번째 입력 피연산자(rs)의 식별자를 보유합니다. 비트 13-16은 대상 레지스터(rd)의 식별자를 보유합니다. 비트 1-12는 즉치값 또는 시프터 피연산자를 보유하는 데 사용됩니다. 이 12비트를 가장 잘 활용하는 방법을 살펴보겠습니다. ARM은 32비트 리터럴을 지원하지만 이를 인코딩하는 데 실제로는 12비트만 사용됩니다. 모든 $2^{32}$ 가능한 값을 인코딩하는 것은 불가능하며, 그 중에서 의미 있는 하위 집합을 선택해야 합니다. 아이디어는 12비트를 사용하여 32비트 값의 하위 집합을 인코딩하는 것입니다. 하드웨어는 명령을 처리할 때 이러한 12비트를 디코딩하고 32비트로 확장할 것으로 예상됩니다. 이제 12비트는 1바이트도 2바이트도 아닌 다소 융통성이 없는 값입니다. 매우 영리한 솔루션을 생각해 내는 것이 필요했습니다. 아이디어는 12비트를 4비트 상수(rot)와 8비트 페이로드(payload)의 두 부분으로 나누는 것입니다. 아래 그림을 참조하세요. ![](/assets/16964884/125.png) 12비트로 인코딩된 실제 숫자가 n이라고 가정하면 다음과 같습니다. $$n = 페이로드 \ \ ror \ \ (2 \cdot rot)

여기서 ror는 오른쪽 회전 작업입니다. 실제 숫자 n은 rot 필드 값의 2배로 페이로드를 오른쪽으로 회전하여 얻습니다. 이제 이 작업의 논리를 이해하려고 노력하십시오.

숫자 n은 32비트 값입니다. 순진한 해결책은 12비트를 사용하여 n의 최소 부호 비트를 지정하는 것입니다. 상위 비트는 0이 될 수 있습니다. 그러나 프로그래머는 데이터와 메모리에 바이트 단위로 액세스하는 경향이 있으므로 1.5바이트는 쓸모가 없습니다. 더 나은 솔루션은 1바이트 페이로드를 사용하고 이를 32비트 필드의 아무 곳에나 배치하는 것입니다. 나머지 4비트를 이 목적으로 사용하면 0에서 15 사이의 숫자를 인코딩할 수 있습니다. ARM 프로세서는 0에서 30 사이의 모든 짝수를 설명하기 위해 이 값을 두 배로 늘려 해당 양만큼 페이로드를 회전시킵니다. 이것의 장점은 더 넓은 숫자 집합을 인코딩할 수 있다는 것입니다. 모든 8비트는 페이로드에 해당하고 나머지 24비트는 모두 0입니다. 로트 비트는 32비트 필드 중 페이로드가 차지하는 8비트만 결정합니다.

마찬가지로 합리적인 사고를 통해 다음과 같은 변위 명령 형식 다이어그램을 얻을 수 있습니다.

또한 로드 및 저장 명령어의 형식은 다음과 같습니다.

분기 지침은 다음과 같습니다.

ARM Endian은 E-Bit를 사용하여 단어를 로드/저장하는 것을 지원합니다.

19.4.8 x86 명령어 인코딩

x86은 진정한 CISC 명령어 세트이며 인코딩 프로세스가 더 규칙적이며 거의 모든 명령어가 표준 형식을 따릅니다. 둘째, x86의 opcode에는 여러 모드와 접두사가 있는 경우가 많습니다. 먼저 인코딩된 기계 명령어의 광범위한 구조를 살펴보면 다음 그림은 이진 인코딩 명령어의 구조를 보여줍니다.

x86 바이너리 명령어 형식.

x86 명령어 형식 세부정보.

1~4바이트의 첫 번째 세트는 명령어의 접두사를 인코딩하는 데 사용되며, rep 접두사는 한 예이며, 1~4바이트의 첫 번째 세트에서 인코딩할 수 있는 다른 유형의 접두사가 많이 있습니다.

다음 1-3바이트는 opcode를 인코딩하는 데 사용되며 x86 ISA에는 수백 개의 명령어가 있으며 opcode는 피연산자의 형식도 인코딩합니다. 예를 들어, 덧셈 명령어는 첫 번째 피연산자를 메모리 피연산자로 갖거나 두 번째 피연산자를 메모리 피연산자로 가질 수 있습니다. 이 정보는 opcode의 일부이기도 합니다.

다음 2바이트는 선택 사항입니다. 첫 번째 바이트는 ModR/M 바이트라고 하며 소스 및 대상 레지스터의 주소를 지정하는 데 사용됩니다. 두 번째 바이트는 SIB(Scale Index Base) 바이트라고 합니다. 이 바이트는 기본 스케일 인덱스 및 기본 스케일 인덱스 오프셋 주소 지정 모드의 매개변수를 기록합니다. 메모리 주소는 선택적으로 32비트 변위(이 책에서는 오프셋이라고도 함)를 가질 수 있습니다. 따라서 변위 값을 기록하기 위해 하나의 명령어에 4바이트를 더 사용하도록 선택할 수 있습니다. 마지막으로 일부 x86 명령어는 최대 32비트일 수 있는 즉시 피연산자를 허용하므로 마지막 필드(선택 사항)를 사용하여 즉시 피연산자를 지정합니다.

ModR/M 바이트에는 아래 그림과 같이 세 개의 필드가 있습니다.

SIB 바이트의 구조는 아래 그림에 나와 있습니다.

x86 디지털 데이터 형식은 다음과 같습니다.

x86 EFLAGS 레지스터:

x86 제어 레지스터:

MMX 레지스터를 부동 소수점 레지스터로 매핑:

19.5 프로세서

19.5.1 명령 처리 개요

아래 그림과 같이 프로세서의 작동을 대략 5단계로 나눌 수 있습니다.

명령 처리의 5단계: 명령 획득, 피연산자 획득, 실행, 메모리 액세스 및 레지스터 쓰기.

명령의 다중 클록 사이클 파이프라인 다이어그램. 다이어그램의 시간은 페이지의 왼쪽에서 오른쪽으로 진행되고 지침은 페이지의 위에서 아래로 진행됩니다. 파이프라인 단계의 표현은 명령 축을 따라 각 섹션에 배치되어 적절한 클록 주기를 차지합니다. 그림은 각 단계 사이의 파이프라인 레지스터를 보여주고 데이터 경로는 파이프라인의 5개 단계를 그래픽으로 나타내지만 각 파이프라인 단계의 이름을 지정하는 직사각형은 동일하게 유효합니다.

1단계는 메모리에서 명령어를 가져오는 것입니다. 기계의 기본 구성은 중요하지 않습니다. 기계는 von Neumann 기계(공유 명령 및 데이터 메모리) 또는 Harvard 기계(전용 명령 메모리)일 수 있습니다. 가져오기 단계에는 다음 명령어의 주소를 계산하는 논리 요소가 있습니다. 현재 명령어가 분기가 아닌 경우 현재 명령어의 크기(4바이트)를 PC에 저장된 주소에 추가해야 합니다. 그러나 현재 명령어가 분기인 경우 다음 명령어의 주소는 분기의 결과와 대상에 따라 달라집니다. 이 정보는 프로세서의 다른 장치에서 얻습니다.

2단계는 "명령어"를 디코딩하고 레지스터에서 피연산자를 가져오는 것입니다. 다양한 명령어 유형에 필요한 처리는 매우 다릅니다. 예를 들어 로드 저장 명령어는 전용 메모리 장치를 사용하지만 산술 명령어는 그렇지 않습니다. 명령을 디코딩하기 위해 프로세서에는 명령의 필드를 기반으로 신호를 생성하는 전용 논리 회로가 있으며, 이 신호는 명령을 올바르게 처리하기 위해 다른 모듈에서 사용됩니다. Intel 프로세서와 같은 상용 프로세서에는 매우 복잡한 디코딩 장치가 있으며 x86 명령어 세트를 디코딩하는 것은 매우 복잡합니다. 디코딩의 복잡성에 관계없이 디코딩 프로세스는 일반적으로 다음 단계로 구성됩니다.

  • 피연산자의 값을 추출하고, 포함된 즉시 항목을 계산 및 32비트 또는 64비트로 확장하며, 명령어 처리에 대한 추가 정보를 생성합니다.

  • 명령어에 대한 추가 정보를 생성하는 과정에는 프로세서별 신호를 생성하는 과정이 포함됩니다. 예를 들어, "메모리 유닛 활성화" 형태의 신호는 로드/저장 명령에 대해 생성될 수 있고, 저장 명령에 대해서는 레지스터 쓰기 기능을 비활성화하는 신호가 생성될 수 있습니다.

3단계는 산술 및 논리 연산을 수행하는 것입니다. 여기에는 모든 산술 및 논리 연산을 수행할 수 있는 ALU(산술 및 논리 장치)가 포함되어 있습니다. ALU는 로드-저장 작업의 유효 주소를 계산하는 데도 필요합니다. 일반적으로 프로세서의 이 부분은 분기 결과도 계산합니다.

**ALU(산술 논리 단위)**에는 데이터 값에 대한 산술 및 논리 계산을 수행하는 요소가 포함되어 있으며 일반적으로 덧셈기, 곱셈기, 나누기가 포함되어 있으며 논리 비트 연산을 계산하는 단위가 있습니다.

4단계에는 로드-저장 명령을 처리하는 데 사용되는 메모리 셀이 포함됩니다. 이 장치는 메모리 시스템과 인터페이스하고 메모리에서 값을 로드하고 저장하는 작업을 조정합니다. 일반적인 프로세서의 메모리 시스템은 상당히 복잡하며, 그 복잡성 중 일부는 프로세서의 이 부분에서 구현됩니다.

5단계는 ALU에서 계산한 값이나 메모리 유닛에서 가져온 값을 레지스터 파일에 쓰는 것이다.

단일 명령어에 필요한 처리를 명령어 주기라고 합니다. 앞에서 설명한 단순화된 2단계 설명을 사용하여 명령 주기가 아래 그림에 표시됩니다. 두 단계를 가져오기 주기와 실행 주기라고 합니다. 프로그램 실행은 컴퓨터가 종료되거나, 복구할 수 없는 오류가 발생하거나, 컴퓨터를 정지시키는 프로그램 명령이 발생한 경우에만 중지됩니다.

기본 명령어 주기입니다.

아래 그림에 나열된 특성을 포함하는 가상 기계를 사용하는 간단한 예를 생각해 보십시오. 프로세서에는 AC(Accumulator)라는 데이터 레지스터가 포함되어 있습니다. 명령어와 데이터 모두 16비트 길이이므로 16비트 워드를 사용하여 메모리를 구성하는 것이 편리합니다. 명령어 형식은 opcode에 4비트를 제공하므로 최대 24=16개의 서로 다른 opcode가 있을 수 있으며 최대 212=4096(4K)개의 메모리 단어를 직접 주소 지정할 수 있습니다.

가상 기계의 특징.

다음 그림은 프로그램 실행의 일부를 보여주며, 메모리 및 프로세서 레지스터의 관련 부분을 보여줍니다. 표시된 프로그램 조각은 주소 940에 있는 메모리 워드의 내용을 주소 941에 있는 메모리 워드의 내용에 추가하고 그 결과를 후자 위치에 저장합니다. 3개의 명령어가 필요하며 이는 3개의 가져오기 및 3개의 실행 주기로 설명할 수 있습니다.

  1. PC에는 첫 번째 명령어의 주소인 300이 포함되어 있습니다. 명령어(16진수 값 1940)가 명령어 레지스터 IR에 로드되고 PC가 증가됩니다. 이 프로세스에는 메모리 주소 레지스터와 메모리 버퍼 레지스터의 사용이 포함됩니다. 단순화를 위해 이러한 중간 레지스터는 무시됩니다.

  2. IR의 처음 4비트(첫 번째 16진수)는 AC가 로드됨을 나타내고 나머지 12비트(3개의 16진수)는 데이터가 로드될 주소(940)를 지정합니다.

  3. 위치 301에서 다음 명령어(5941)를 가져오면 PC가 증가됩니다.

  4. AC의 이전 내용과 위치 941의 내용을 추가하고 결과를 AC에 저장합니다.

  5. 다음 명령어(2941)가 위치 302에서 페치되고 PC가 증가됩니다.

  6. AC의 내용은 위치 941에 저장됩니다.

프로그램 실행 예(16진수로 된 메모리 및 레지스터의 내용)

특정 명령어의 실행 주기에는 메모리에 대한 두 개 이상의 참조가 포함될 수 있습니다. 또한 명령어는 메모리 참조가 아닌 I/O 작업을 지정할 수 있습니다. 이러한 추가 고려 사항을 염두에 두고 아래 다이어그램은 상태 다이어그램 형식으로 기본 명령 주기에 대한 보다 자세한 보기를 제공합니다. 특정 명령 주기에 대해 일부 상태는 비어 있을 수 있지만 다른 상태는 여러 번 액세스될 수 있습니다.

명령주기 상태도.

상태 설명은 다음과 같습니다.

  • 명령어 주소 계산(iac): 실행될 다음 명령어의 주소를 결정하려면 일반적으로 이전 명령어의 주소에 고정된 숫자를 추가해야 합니다. 각 명령어의 길이가 16비트이고 메모리가 16비트 워드로 구성된 경우 이전 주소에 1을 추가합니다. 반대로 메모리가 개별적으로 주소 지정이 가능한 8비트 바이트로 구성되면 이전 주소에 2를 추가합니다.

  • 명령어 가져오기(if): 메모리 위치에서 프로세서로 명령어를 읽습니다.

  • 명령어 연산 디코딩(iod): 명령어를 분석하여 수행할 연산 유형과 사용할 피연산자를 결정합니다.

  • 피연산자 주소 계산(oac): 연산에 메모리의 피연산자에 대한 참조 또는 I/O를 통해 사용 가능한 피연산자가 포함된 경우 피연산자의 주소를 결정합니다.

  • 피연산자 획득(of): 메모리에서 피연산자를 얻거나 I/O에서 피연산자를 읽습니다.

  • 데이터 연산(do) : 명령어에 표시된 연산을 수행합니다.

  • 피연산자 저장(os): 결과를 메모리에 쓰거나 I/O로 출력합니다.

다음 그림은 인터럽트 주기 처리를 포함하여 수정된 명령 주기 상태 다이어그램을 보여줍니다.

아래 그림은 왼쪽의 간접 클럭 사이클과 오른쪽의 인터럽트 클럭 사이클을 보여줍니다.

다음 다이어그램은 각 모듈 유형의 주요 입력 및 출력 형태를 표시하여 필요한 교환 유형을 보여줍니다.

마이크로프로세서 등록 구성 예:

19.5.2 프로세서 구조

19.5.2.1 프로세서 유닛

프로세서에는 수집 장치, 데이터 경로 및 제어 장치, 피연산자 수집 장치, 실행 장치(분기 장치, ALU), 메모리 액세스 장치, 레지스터 쓰기 저장 장치 등과 같은 많은 장치가 포함되어 있습니다.

아래 그림은 획득 장치 회로의 구현을 보여줍니다. 한 사이클에는 두 가지 기본 작업이 수행되어야 합니다. 1. 다음 PC 계산(프로그램 카운터) 2. 지침을 가져오는 중입니다.

회로에는 두 가지 유형의 구성 요소가 있습니다.

  • 첫 번째 유형의 요소는 데이터 값을 처리하는 데 사용되는 레지스터, 메모리, 산술 및 논리 회로입니다.

  • 두 번째 유형의 요소는 데이터 흐름의 방향을 결정하는 제어 장치입니다. 프로세서에 있는 제어 장치는 일반적으로 모든 멀티플렉서를 제어하기 위한 신호를 생성하며, 그 역할이 정보의 흐름을 제어하는 ​​것이기 때문에 제어 신호라고 합니다.

따라서 개념적으로 프로세서는 두 개의 서로 다른 하위 시스템으로 구성되어 있다고 생각할 수 있습니다.

  • 데이터 경로. 여기에는 정보를 저장하고 처리하기 위한 모든 요소가 포함되어 있습니다. 예를 들어 데이터 메모리, 명령 메모리, 레지스터 파일 및 ALU(산술 논리 장치)는 데이터 경로의 일부입니다. 메모리와 레지스터는 정보를 저장하는 반면 ALU는 정보를 처리합니다. 예를 들어 두 숫자를 더해 그 결과로 합계를 생성하거나 두 숫자의 논리 함수를 계산할 수 있습니다.

  • 제어 경로. 이는 분기 대상과 기본 다음 PC 중에서 선택하도록 멀티플렉서에 지시하는 신호를 생성하여 올바른 정보 흐름을 지시합니다. 이 경우 멀티플렉서는 isBranchTaken 신호에 의해 제어됩니다.

제어 경로와 데이터 경로는 도시의 교통 네트워크와 같은 회로의 서로 다른 두 요소로 생각할 수 있습니다. 도로와 신호등은 데이터 경로와 유사합니다. 신호등을 제어하는 ​​회로가 제어 경로를 구성합니다. 제어 경로는 조명 전환 시간을 결정합니다. 현대 스마트시티에서는 도시의 모든 신호등을 제어하는 ​​과정이 통합되는 경우가 많다. 자동차가 교통 정체와 사고 현장을 우회할 수 있도록 지능적으로 교통을 제어하는 ​​것이 가능하다면 어떨까요? 마찬가지로, 프로세서의 제어 장치는 매우 지능적이며 그 임무는 가능한 한 빨리 명령을 실행하는 것입니다. 최신 프로세서의 제어 장치는 이미 매우 복잡합니다.

데이터 경로: 데이터 경로는 레지스터, 메모리, ALU 등 데이터 저장, 검색, 처리 전용 프로세서의 모든 요소로 구성됩니다.

제어 경로: 제어 경로에는 주로 데이터 경로의 명령 및 데이터 이동을 제어하기 위해 적절한 신호를 생성하는 역할을 하는 제어 장치가 포함됩니다.

데이터 경로와 제어 경로 간의 관계.

이제 실행 지침을 살펴보세요. 첫째, 명령어는 분기형(branching)과 비분기형(non-branching)의 두 가지 유형으로 구분됩니다. 분기 명령은 분기 결과와 최종 목표를 계산하는 전용 분기 장치에 의해 처리되고, 비분기 명령은 ALU(산술 논리 장치)에 의해 처리됩니다. 분기 장치의 회로는 아래 그림과 같습니다.

멀티플렉서를 사용하여 반환 주소(op1) 값과 명령어에 포함된 BranchT 대상 중에서 선택합니다. isRet 신호는 멀티플렉서를 제어하여 op1이 1과 같으면 선택하고, 그렇지 않으면 분기 대상을 선택합니다. 멀티플렉서 BranchPC의 출력은 추출 장치로 전송됩니다.

아래 그림은 ALU가 포함된 실행 단위 섹션을 보여줍니다. ALU의 첫 번째 피연산자(A)는 항상 op1(피연산자 획득 장치에서 가져옴)이지만 두 번째 피연산자(B)는 제어 장치에서 생성된 isImmediate 신호에 의해 결정되는 레지스터 또는 부호 확장 즉시값이 될 수 있습니다. isImmediate 신호는 명령어의 즉시 비트 값과 같습니다. 1이면 그림의 멀티플렉서는 immx를 피연산자로 선택합니다. 0이면 op2가 피연산자로 선택됩니다. ALU는 제어 장치에 의해 생성되고 ALU 작동 유형을 지정하는 aluSignals라고 하는 일련의 신호를 입력으로 사용합니다. ALU의 결과를 aluResult라고 합니다.

아래 그림은 ALU의 한 가지 설계를 보여줍니다. ALU는 모듈 세트로 구성되며 각 모듈은 덧셈이나 나눗셈과 같은 별도의 산술 또는 논리 함수를 계산합니다. 둘째, 각 모듈에는 이를 활성화하거나 비활성화하는 전용 신호가 있습니다. 예를 들어 간단한 추가를 수행하려는 경우 분배기를 활성화할 이유가 없습니다. 셀을 활성화하거나 비활성화하는 방법에는 여러 가지가 있으며, 가장 간단한 방법은 각 입력 비트에 대해 전송 게이트(아래 이미지 참조)를 사용하는 것입니다. 신호(S)가 ON되면 입력값이 출력에 반영됩니다. 그렇지 않으면 이전 값을 유지합니다. 따라서 활성화 신호가 꺼지면 모듈에 새 입력이 표시되지 않습니다. 따라서 전력을 소모하지 않으며 효과적으로 비활성화됩니다.

알루.

전송 게이트.

요약하면 아래 그림은 실행 단위(분기 단위 및 ALU)의 전체 설계를 보여줍니다. 출력(aluResult)을 설정하려면 ALU의 모든 모듈에서 올바른 출력을 선택할 수 있는 멀티플렉서가 필요합니다. 이 멀티플렉서는 다이어그램에 표시되지 않습니다.

실행 유닛(브랜치 및 ALU 유닛).

다음 그림은 메모리 액세스 장치를 보여줍니다. 여기에는 두 개의 입력(데이터와 주소가 있습니다. 주소는 ALU에 의해 계산되고 ALU(aluResult)의 결과와 동일합니다. 로드 및 저장 명령어 모두 이 주소를 사용하며 주소는 전통적으로 MAR(메모리 주소 레지스터)이라고 하는 레지스터에 보관됩니다.

메모리 유닛.

전체는 모든 부분이 연결되어 형성됩니다. 지금까지 프로세서는 명령 가져오기 장치(IF), 피연산자 가져오기 장치(OF), 실행 장치(EX), 메모리 액세스 장치(MA) 및 레지스터 쓰기 저장 장치(RW)의 다섯 가지 기본 단위로 나누어졌습니다. 이제 모든 조각을 모아 통일된 그림을 볼 차례입니다(아래에서는 세부 회로를 생략하고 데이터 및 제어 신호의 흐름에만 초점을 맞췄습니다).

기본 프로세서.

19.5.2.2 제어 장치

간단한 프로세서의 하드와이어 제어 장치는 6비트를 입력(5개의 opcode 비트와 1개의 즉시 비트)으로 취하고 22개의 제어 신호를 출력으로 생성하는 블랙 박스로 생각할 수 있습니다. 아래 그림과 같습니다.

하드와이어 제어 장치의 추상화.

컨트롤 유닛의 구조도.

유선 제어 장치는 빠르고 효율적이므로 오늘날 대부분의 상용 프로세서는 유선 제어 장치를 사용하지만 유선 제어 장치는 그다지 유연하지 않습니다. 예를 들어, 프로세서가 공장에서 출고된 후에는 명령어의 동작을 변경하거나 새 명령어를 도입하는 것도 불가능합니다. 때로는 기능 단위에 버그가 있는 경우 명령어가 실행되는 방식을 변경해야 합니다. 예를 들어 승수에 설계 결함이 있는 경우 가산기와 시프트 단위를 사용하여 부스 곱셈 알고리즘을 실행하는 것이 이론적으로 가능합니다. 그러나 명령이 실행되는 방식을 동적으로 재구성하려면 매우 정교한 제어 장치가 필요합니다.

유연한 제어 장치를 지원하는 데에는 다른 보다 실용적인 이유가 있습니다. 일부 명령어 세트(예: x86)에는 명령어를 지정된 횟수만큼 반복하는 표현 명령어가 있고, 대량의 데이터를 처리할 수 있는 복잡한 문자열 명령어도 있으며, 이러한 명령어를 지원하려면 매우 복잡한 데이터 경로가 필요합니다. 원칙적으로 이러한 명령은 이러한 명령을 처리하기 위한 간단한 프로세서가 있는 잘 설계된 제어 장치에 의해 실행될 수 있습니다. 이러한 하위 프로세서는 복잡한 CISC 명령을 구현하는 데 사용되는 제어 신호를 생성할 수 있습니다.

데이터 경로 및 제어 신호.

내부 버스가 있는 CPU.

19.5.2.3 마이크로프로그램 기반 프로세서

하드와이어 제어 장치가 있는 프로세서는 이전에 연구되어 명령을 처리하고 실행하는 데 필요한 모든 요소가 포함된 데이터 경로를 설계했습니다. 입력 피연산자 중에서 선택할 수 있는 경우 멀티플렉서가 추가되며 이는 제어 장치의 신호에 의해 제어됩니다. 제어 장치는 명령 내용을 입력으로 받아 모든 제어 신호를 생성합니다. 최신 고성능 프로세서는 종종 이러한 디자인 스타일을 채택합니다. 효율성에는 비용이 따르며 비용에는 유연성이 따릅니다. 더 많은 멀티플렉서를 추가하고 각각의 새로운 명령어에 대해 더 많은 제어 신호를 생성해야 할 수도 있습니다. 둘째, 고객에게 배송된 후에는 프로세서에 새 명령을 추가할 수 없습니다. 때때로 우리는 이런 유연성을 갈망합니다.

이러한 추가적인 유연성은 ISA의 명령어를 간단한 마이크로 명령어 세트로 변환하는 변환 테이블을 도입함으로써 도입될 수 있습니다. 각 마이크로명령어는 프로세서의 모든 래치와 내부 상태 요소에 액세스할 수 있습니다. 우리는 명령어와 관련된 마이크로 명령어 세트를 실행하여 명령어의 기능을 구현하며, 이러한 마이크로 명령어 또는 마이크로코드는 마이크로코드 테이블에 저장됩니다. 이 표의 내용은 일반적으로 소프트웨어에 의해 수정될 수 있으므로 하드웨어가 명령을 실행하는 방식이 변경됩니다. 이러한 유연성에는 여러 가지 이유가 있습니다. 이를 통해 새로운 명령어를 추가하거나 기존 명령어의 동작을 수정할 수 있습니다. 그 이유 중 일부는 다음과 같습니다.

  • 특정 명령을 실행할 때 프로세서에 오류가 발생하는 경우가 있습니다. 인텔은 설계 과정에서 디자이너의 실수나 제조 결함으로 인해 고객에게 판매한 모든 펜티엄 프로세서를 리콜해야 했습니다. 한 가지 유명한 예는 Intel Pentium 프로세서의 분할 오류였습니다. 분할 명령어의 구현이 동적으로 변경될 수 있다면 모든 프로세서를 호출할 필요가 없습니다. 따라서 프로세서의 재구성 가능성은 설계 및 제조 프로세스의 다양한 단계에서 발생할 수 있는 결함을 해결하는 데 도움이 된다는 결론을 내릴 수 있습니다.

  • Intel Pentium 4와 같은 프로세서와 Intel Core i3 및 Intel Core i7과 같은 최신 프로세서는 메모리에 저장된 일련의 마이크로 명령을 실행하여 일부 복잡한 명령을 구현합니다. 마이크로코드는 일반적으로 데이터 문자열이나 명령어에 대한 복잡한 작업을 구현하는 데 사용됩니다. 이러한 작업으로 인해 일련의 반복 계산이 발생합니다. 즉, Intel 프로세서는 내부적으로 복잡한 명령을 간단한 명령이 포함된 코드 세그먼트로 대체하여 프로세서가 복잡한 명령을 더 쉽게 구현할 수 있게 해줍니다. 복잡한 명령을 구현하기 위해 추가 상태, 멀티플렉서 및 제어 신호를 추가하여 데이터 경로를 불필요하게 변경할 필요가 없습니다.

  • 오늘날의 프로세서는 시스템 온 칩(SOC)이라고 불리는 많은 다른 구성 요소가 포함된 칩의 한 부분일 뿐입니다. 예를 들어 휴대폰의 칩에는 프로세서, 비디오 컨트롤러, 카메라 인터페이스, 사운드 및 네트워크 컨트롤러가 포함될 수 있습니다. 프로세서 공급업체는 일반적으로 간단한 명령 세트를 하드와이어하는 반면, 비디오 및 오디오 컨트롤러와 같은 주변 장치와 인터페이스하는 다른 많은 명령은 마이크로코드로 작성됩니다. 마이크로코드는 애플리케이션 도메인 및 주변 구성요소 세트를 기반으로 사용자 정의할 수 있습니다.

  • 맞춤형 진단 루틴은 특수한 마이크로 명령어 세트를 사용하여 작성되는 경우도 있습니다. 이러한 루틴은 작동 중에 칩의 다양한 부분을 테스트하고 오류를 보고하며 수정 조치를 취합니다. 이러한 BIST(내장 자체 테스트) 루틴은 일반적으로 사용자 정의가 가능하며 마이크로코드로 작성됩니다. 예를 들어, 높은 신뢰성을 원할 경우 CPU에서 신뢰성 검사를 수행하는 명령어의 동작을 수정하여 모든 구성 요소를 검사할 수 있습니다. 그러나 시간을 절약하기 위해 이러한 루틴을 압축하여 더 적은 수의 구성요소를 검사할 수 있습니다.

따라서 안정성을 달성하고 추가 기능을 활성화하며 이식성을 향상시키기 위해 프로세서의 명령 동작을 프로그래밍 방식으로 변경해야 하는 강력한 이유가 있음을 관찰했습니다. 결과적으로 현대 컴퓨팅 시스템, 특히 휴대폰이나 태블릿과 같은 소형 장치에 사용되는 칩은 마이크로코드에 의존합니다. 이러한 마이크로코드 시퀀스를 흔히 펌웨어라고 합니다.

현대 컴퓨팅 시스템, 특히 휴대폰, 모뎀, 프린터, 태블릿과 같은 소형 장치는 마이크로코드에 의존하는 칩을 사용합니다. 이 마이크로코드 시퀀스를 흔히 펌웨어라고 합니다.

따라서 프로세서가 제조되어 고객에게 배송된 후에도 명령어 세트를 사용자 정의할 수 있는 더 큰 유연성을 제공하는 마이크로프로그램 기반 프로세서를 설계해 보겠습니다. 기존의 유선 프로세서와 마이크로 프로그래밍된 프로세서 사이에는 근본적인 균형이 있다는 점에 유의하는 것이 중요합니다. 절충점은 효율성과 유연성이며, 빠르고 전력 효율적이면서도 매우 유연한 프로세서를 기대할 수는 없습니다.

19.5.2.4 마이크로프로그래밍 데이터 경로

마이크로프로그래밍된 프로세서의 데이터 경로를 설계하고 프로세서의 데이터 경로를 수정해 보겠습니다. 프로세서에는 페치 장치, 레지스터 파일, ALU, 분기 장치 및 메모리 장치와 같은 몇 가지 주요 장치가 있습니다. 단위는 와이어로 연결되며 여러 소스 피연산자가 있을 가능성이 있을 때마다 데이터 경로에 멀티플렉서를 추가합니다. 제어 장치의 역할은 멀티플렉서에 대한 모든 제어 신호를 생성하는 것입니다.

문제는 멀티플렉서의 연결이 하드와이어되어 임의의 연결이 이루어질 수 없다는 것입니다. 예를 들어 저장 장치의 출력이 실행 장치의 입력으로 전송될 수 없습니다. 그래서 우리는 구성 요소 간에 고정된 상호 연결이 없는 설계를 원했고, 이론적으로는 모든 장치가 다른 장치로 데이터를 보낼 수 있습니다.

가장 유연한 상호 연결은 버스 기반 구조입니다. 버스는 모든 장치를 연결하는 일반 구리선 세트로, 언제든지 하나의 쓰기와 여러 판독기를 지원합니다. 예를 들어, 장치 A는 특정 시점에 버스에 쓸 수 있고 다른 모든 장치는 장치 A가 쓴 값을 얻을 수 있습니다. 원하는 경우 한 장치에서 다른 장치로 또는 한 장치에서 다른 장치 그룹으로 데이터를 보낼 수 있습니다. 제어 장치는 어느 시점에서나 단 하나의 장치만 버스에 쓰고 기록되는 값을 처리해야 하는 장치가 버스에서 값을 읽도록 보장해야 합니다.

이제 마이크로프로그래밍된 프로세서의 데이터 경로에서 적절하게 사용할 수 있는 하드와이어 프로세서용으로 도입한 모든 장치의 단순화된 버전을 설계하는 작업으로 넘어가겠습니다.

마이크로프로그래밍된 프로세서의 설계 원리를 설명하는 것부터 시작해 보겠습니다. 각 유닛에 레지스터를 추가합니다. 이 레지스터는 특정 유닛의 입력 데이터를 저장하고, 전용 출력 레지스터는 유닛에서 생성된 결과를 저장하며, 두 레지스터 세트 모두 공통 버스에 연결됩니다. 서로 다른 장치 사이에 많은 양의 결합이 있는 하드와이어 프로세서와 달리 마이크로 프로그래밍된 프로세서의 장치는 서로 독립적입니다. 이들의 임무는 일련의 작업을 수행하고 결과를 버스에 반환하는 것입니다. 각 유닛은 프로그래밍 언어의 함수와 같습니다. 데이터를 읽기 위한 레지스터 세트로 구성된 인터페이스가 있습니다. 일반적으로 출력을 계산하는 데 1사이클이 걸리고 유닛은 출력 값을 출력 레지스터에 씁니다.

위의 원리를 바탕으로 아래 그림은 pc(프로그램 카운터)와 ir(명령 레지스터)라는 두 개의 레지스터를 갖는 페치 유닛의 설계를 보여줍니다. PC 레지스터는 버스에서 해당 값을 읽을 수 있고 해당 값을 버스에 쓸 수도 있습니다. 다른 장치는 명령어의 정확한 내용에 관심이 없기 때문에 버스에 연결된 IR이 없으며 다른 장치는 명령어의 다른 필드에만 관심이 있습니다. 따라서 명령어를 디코딩하고 이를 여러 필드 세트로 나누는 것이 필요하며, 이는 디코딩 장치에서 수행됩니다.

마이크로프로그램 프로세서의 페치 장치.

디코딩 장치는 기능적으로 피연산자 가져오기 장치와 유사하지만 이 장치에 레지스터 파일을 포함하지 않고 마이크로프로그래밍된 프로세서에서 별도의 장치로 처리합니다. 아래 그림은 피연산자 획득 장치의 설계를 보여줍니다.

마이크로프로그램 프로세서의 디코딩 장치.

우리는 디코딩 유닛과 레지스터 파일을 하드와이어 프로세서의 피연산자 페치 유닛이라고 하는 단일 유닛으로 결합하지만, 하드 와이어 프로세서에서는 명령어를 디코딩한 후 즉시 액세스되기 때문에 레지스터 파일을 마이크로프로그래밍 프로세서에서 독립적으로 유지하는 것이 더 바람직합니다. 그러나 마이크로프로그래밍된 프로세서의 경우에는 그렇지 않을 수 있습니다. 명령을 실행하는 동안 여러 번 액세스해야 할 수도 있습니다.

마이크로 프로그래밍된 프로세서에 파일을 등록합니다.

ALU의 구조는 아래 그림과 같습니다. A와 B라는 두 개의 입력 레지스터가 있습니다. ALU는 레지스터 A와 B에 포함된 값에 대해 연산을 수행하며, 연산의 성격은 args 값으로 지정됩니다. 예를 들어, 덧셈 연산이 지정되면 ALU는 레지스터 A와 B에 포함된 값을 더합니다. 뺄셈 연산이 지정되면 B의 값은 A에 포함된 값에서 뺍니다. cmp 명령어의 경우 ALU 업데이트 플래그가 지정됩니다. 두 개의 플래그를 사용하여 같음 및 보다 큼 조건을 지정합니다. 이 조건은 각각 플래그 flags.E 및 flags.GT 레지스터에 저장되며 ALU 연산의 결과는 aluResult 레지스터에 저장됩니다. 또한 여기서는 ALU가 버스에 args 값을 지정한 후 실행하는 데 1사이클이 걸린다고 가정합니다.

마이크로 프로그래밍된 프로세서의 ALU.

메모리 유닛은 아래 그림과 같습니다. 하드와이어 프로세서와 마찬가지로 mar(메모리 주소 레지스터)와 mdr(메모리 데이터 레지스터)이라는 두 개의 소스 레지스터가 있으며 mar는 메모리 주소를 버퍼링하고 mdr은 저장해야 하는 값을 버퍼링합니다. 메모리 작업의 특성(로드 또는 저장)을 지정하려면 매개변수 세트도 필요합니다. 로드 작업이 완료된 후 ldResult 레지스터의 데이터를 사용할 수 있습니다.

마이크로프로그램 프로세서의 메모리 장치.

요약하면, 마이크로프로그램 프로세서의 데이터 경로 개요는 다음과 같습니다.

19.5.3 파이프라인 프로세서

19.5.3.1 파이프라인 개요

앞에서 설명한 하드와이어 프로세서가 명령어 결과를 가져오고, 실행하고, 레지스터 파일이나 메모리에 쓰는 데 한 사이클이 필요하다고 가정합니다. 전기적 수준에서 이는 추출 장치에서 다른 장치를 통해 레지스터 쓰기 저장 장치로 흐르는 신호에 의해 수행되며 전기 신호가 한 장치에서 다른 장치로 전파되는 데 시간이 걸립니다.

예를 들어 명령어 메모리에서 명령어를 가져오는 데 시간이 걸립니다. 그런 다음 레지스터 파일에서 값을 읽고 ALU로 결과를 계산하는 데 시간이 걸립니다. 메모리 액세스와 결과를 레지스터 파일에 다시 쓰는 작업 역시 시간이 많이 걸리는 작업입니다. 다음 명령의 처리를 시작하기 전에 이러한 모든 개별 하위 작업이 완료될 때까지 기다려야 한다는 것은 회로에 많은 유휴 상태가 있고 피연산자 가져오기 장치가 작업을 수행하는 동안 다른 모든 장치는 유휴 상태임을 의미합니다. 마찬가지로 ALU가 활성화되면 다른 모든 장치는 비활성화됩니다. 5개 단계(IF, OF, EX, MA, RW) 각각에 동일한 시간이 소요된다고 가정하면 어떤 순간에든 회로의 약 80%가 유휴 상태입니다! 이는 컴퓨팅 성능의 낭비를 나타내며 리소스를 유휴 상태로 두는 것은 확실히 좋은 생각이 아닙니다.

칩의 모든 장치를 작동 상태로 유지하는 방법을 찾을 수 있다면 명령 실행 속도를 높일 수 있습니다.

파이프라인 프로세스

앞에서 설명한 단순한 단일 주기 프로세서의 유휴 문제에 대한 비유를 생각해 보십시오. 명령어가 EX 단계에 있으면 다음 명령어는 OF 단계에 있을 수 있고 후속 명령어는 IF 단계에 있을 수 있습니다. 실제로 프로세서에 5개의 단계가 있는 경우 각 단계가 대략 동일한 시간이 걸린다고 가정하면 5개의 명령이 동시에 처리되고 각 명령은 프로세서의 서로 다른 장치에서 처리된다고 가정할 수 있습니다. 조립 라인의 자동차와 유사하게 명령은 프로세서의 한 단계에서 다른 단계로 이동합니다. 이 정책은 프로세서의 모든 다른 장치가 언제든지 사용 중이므로 프로세서에 유휴 장치가 없도록 보장합니다.

이 시나리오에서 명령어의 수명 주기는 다음과 같습니다. 사이클 n에서 IF 단계, 사이클 n+1에서 OF 단계, 사이클 n+2에서 EX 단계, 사이클 n+3에서 MA 단계로 진입하고, 마지막으로 사이클 n+4에서 RW 단계의 실행을 완료합니다. 이 전략을 파이프라이닝이라고 하며, 파이프라이닝을 구현하는 프로세서를 파이프라인 프로세서라고 합니다. 개념적으로 차례로 배치된 5단계(IF, OF, EX, MA, RW)의 시퀀스를 파이프라인(자동차 조립 라인과 유사)이라고 합니다. 다음 다이어그램은 파이프라인 데이터 경로의 구성을 보여줍니다.

파이프라인 데이터 경로.

위 그림에서 데이터 경로는 5개의 단계로 나누어져 있으며, 각 단계는 별도의 명령을 처리합니다. 다음 주기에서는 그림과 같이 각 명령이 다음 단계로 전달됩니다.

성능 이점

이제 파이프라인 프로세서의 경우를 고려해 보겠습니다. 단계가 균형을 이루고 있다고 가정합니다. 즉, 각 단계를 실행하는 데 동일한 시간이 소요됩니다. 대부분의 경우 프로세서 설계자는 이 목표를 최대한 달성하려고 노력합니다. 따라서 r을 5로 나누고 각 단계를 실행하는 데 r/5나노초가 걸린다고 결론을 내릴 수 있으며 루프 시간을 r/5로 설정할 수 있습니다. 루프가 끝나면 파이프라인의 각 단계에 있는 명령이 다음 단계로 들어갑니다. RW 단계의 명령은 파이프라인 밖으로 이동되어 실행이 완료됩니다. 동시에 새로운 지침이 IF 단계로 들어갑니다. 아래 그림과 같습니다.

파이프라인의 지침.

5단계 파이프라인으로 5배의 이점을 얻을 수 있다면 동일한 논리로 100단계 파이프라인으로 100배의 이점을 얻을 수 있어야 합니다. 실제로 스테이지에 트랜지스터가 하나만 포함될 때까지 스테이지 수를 계속 늘릴 수 있습니다. 그러나 이는 사실이 아니며 파이프라인 프로세서의 성능에는 근본적인 한계가 있으며 파이프라인 단계 수를 늘려 프로세서 성능을 임의로 향상시키는 것은 불가능합니다. 어느 정도 단계를 추가하는 것은 비생산적일 수 있습니다.

아래 그림은 분명히 낭비적인 프로세스인 파이프라인이 없는 명령 시퀀스의 타이밍을 보여줍니다. 아주 간단한 파이프라인이라도 성능을 크게 향상시킬 수 있습니다. 아래 그림 b는 서로 다른 두 명령어의 I 및 E 단계가 동시에 실행되는 2단계 파이프라인 체계를 보여줍니다. 파이프라인의 두 단계는 명령어 가져오기 단계와 레지스터에서 메모리로, 메모리에서 레지스터로의 작업을 포함하여 명령어를 실행하는 실행/메모리 단계이므로 두 번째 명령어의 명령어 가져오기 단계가 실행/저장 단계의 첫 번째 부분과 병렬로 실행될 수 있음을 알 수 있습니다. 그러나 두 번째 명령어의 실행/저장 단계는 첫 번째 명령어가 파이프라인의 두 번째 단계를 지울 때까지 지연되어야 합니다. 이 방식의 실행 속도는 직렬 방식의 두 배에 달할 수 있으며 최대 속도 향상을 방해하는 두 가지 문제가 있습니다. 먼저, 단일 포트 메모리가 사용되고 각 단계가 하나의 메모리에만 액세스할 수 있으므로 일부 명령어에 대기 상태를 삽입해야 한다고 가정합니다. 둘째, 분기 명령어는 순차적 실행 흐름을 중단합니다. 최소한의 회로로 이러한 상황을 수용하기 위해 컴파일러나 어셈블러는 NOOP 명령을 명령 흐름에 삽입할 수 있습니다.

파이프라인은 단계당 2개의 메모리 액세스를 허용함으로써 더욱 향상될 수 있으며, 그 결과 아래 그림 c에 표시된 시퀀스가 ​​생성됩니다. 이제 최대 3개의 명령을 겹칠 수 있어 성능이 3배 향상됩니다. 마찬가지로 분기 명령으로 인해 속도 향상이 가능한 최대값보다 낮아집니다. 데이터 종속성도 영향을 미칩니다. 명령어에 이전 명령어에 의해 변경된 피연산자가 필요한 경우 지연이 필요하며 이는 NOOP로도 달성할 수 있습니다.

RISC 명령어 세트의 단순성과 규칙성으로 인해 3~4단계로 나누어진 설계를 쉽게 완성할 수 있습니다. 아래 그림 d는 4단계 파이프라인의 결과를 보여줍니다. 한 번에 최대 4개의 명령을 실행할 수 있으며 가능한 최대 속도 향상은 4배입니다. NOOP는 데이터 및 분기 지연을 설명하는 데 사용됩니다.

19.5.3.2 파이프라인 단계

,면 :

파이프라인 프로세서의 IF 단계.

파이프라인 프로세서의 OF 단계.

파이프라인 프로세서의 EX 단계.

파이프라인 프로세서의 MA 단계.

파이프라인 프로세서의 RW 단계.

이제 아래 그림에 표시된 파이프라인 레지스터가 있는 데이터 경로를 살펴보며 간단한 파이프라이닝에 대한 논의를 마무리합니다. 우리의 프로세서 설계가 상당히 복잡해졌고 다이어그램 크기가 한 페이지에 도달했으며 더 복잡한 다이어그램을 소개하고 싶지 않습니다.

파이프라인 데이터 경로.

다음 다이어그램은 파이프라인 데이터 경로의 추상화를 보여줍니다. 그림에는 주로 다양한 장치의 블록 다이어그램이 포함되어 있으며 4개의 파이프라인 레지스터가 표시되어 있습니다. 이 다이어그램을 고급 파이프라인을 논의하기 위한 기준으로 사용하겠습니다. 첫 번째 레지스터 피연산자는 명령어의 rs1 필드이거나 ret 명령어의 반환 주소 레지스터일 수 있다는 점을 기억하세요. RA와 RS1 사이의 멀티플렉서를 선택하는 것은 기본 파이프라인 설계의 일부이며 레지스터 파일 단위의 일부라고 가정하여 단순화를 위해 그림에 표시되지 않았습니다. 마찬가지로 두 번째 레지스터 피연산자(rd와 rs2 사이)를 선택하는 멀티플렉서도 레지스터 파일 단위의 일부로 간주되므로 그림에는 표시되지 않고 두 번째 피연산자(레지스터 또는 즉시)를 선택하는 멀티플렉서만 표시됩니다.

아래 그림은 파이프라인을 통과하는 세 가지 명령의 파이프라인 다이어그램을 보여줍니다. 각 행은 각 파이프라인 단계에 해당하고 열은 클록 주기에 해당합니다. 샘플 코드에는 서로 종속되지 않는 세 가지 명령어가 있습니다. 이러한 명령어의 이름은 [1], [2] 및 [3]입니다. 가장 빠른 명령어 [1]은 첫 번째 사이클에서 파이프라인의 IF 단계에 들어가고 다섯 번째 사이클에서 파이프라인을 떠납니다. 마찬가지로, 두 번째 명령어 [2]는 두 번째 사이클에서 파이프라인의 IF 단계에 들어가고 여섯 번째 사이클에서 파이프라인을 떠납니다. 이러한 각 명령은 각 사이클에서 파이프라인의 다음 단계로 진행되며 파이프라인 다이어그램에서 각 명령의 궤적은 오른쪽 하단 모서리를 향한 대각선입니다. 이 시나리오는 명령어 간의 종속성을 고려하면 상당히 복잡해집니다.

파이프라인 다이어그램.

파이프라인 다이어그램을 작성하기 위한 규칙은 다음과 같습니다.

  • 5개의 행과 N개의 열로 구성된 셀 그리드를 구축합니다. 여기서 N은 고려하려는 총 클럭 사이클 수이고 5개 행마다 파이프라인 단계에 해당합니다.

  • 명령어([k])가 사이클 m에서 파이프라인에 입력되면 첫 번째 행의 열 m에 [k]에 해당하는 항목을 추가합니다.

  • (m+1)번째 사이클에서 명령은 동일한 단계에 머물거나(나중에 설명할 파이프라인이 멈출 수 있기 때문에) 다음 라인(OF 단계)으로 이동할 수 있습니다. 그리드 셀에 해당 항목을 추가합니다.

  • 유사한 방식으로 명령은 IF 단계에서 RW 단계로 순차적으로 이동하며 결코 뒤로 이동하지 않지만 연속 주기에서 동일한 단계에 남아 있을 수 있습니다.

  • 한 셀에 두 개의 항목이 있을 수 없습니다.

  • 명령어가 RW 단계를 벗어나면 결국 파이프라인 그래프에서 제거됩니다.

위의 규칙을 기반으로 한 간단한 예로 다음 코드를 가정해 보겠습니다.

cpp
add r1, r2, r3
sub r4, r2, r5
mul r5, r8, r9

위 코드 조각에 대해 구성된 파이프라인 다이어그램은 다음과 같습니다(첫 번째 명령이 주기 1에서 파이프라인에 들어간다고 가정).

명령 파이프라인 작업에 대한 조건부 분기의 영향:

6단계 CPU 명령어 파이프라인:

대체 파이프라인 설명:

아래 그림은 분기 없이 실행되는 명령 수의 함수로 속도 향상 요소를 표시합니다. 예상할 수 있듯이 한계(n이 양의 무한대에 접근)에서는 k배 속도 향상이 있습니다. 그림 b는 명령 파이프라인의 계열 수에 따른 속도 향상 요소를 보여줍니다. 이 경우 속도 향상 요소는 분기 없이 파이프라인에 공급될 수 있는 명령 수에 접근합니다. 따라서 파이프라인 단계의 수가 많을수록 가속 가능성이 커집니다. 그러나 실질적인 문제로 추가 파이프라인 단계의 잠재적 이점은 비용 증가, 단계 간 지연, 파이프라인 플러시가 필요한 분기 발생으로 인해 상쇄됩니다.

19.5.3.3 파이프라인 충돌

다음 코드 조각을 고려해 보겠습니다.

cpp
add r1, r2, r3
sub r3, r1, r4

여기서 덧셈 명령어는 하위 명령어에 의해 소스 피연산자로 사용되는 레지스터 r1의 값을 생성합니다. 이러한 지침은 아래와 같이 파이프라인 다이어그램을 작성합니다.

RAW(쓰기 후 읽기)의 위험성을 보여주는 파이프라인 다이어그램.

문제가 있음을 보여줍니다. 명령어 1은 5번째 사이클에서 r1의 값을 쓰고, 명령어 2는 3번째 사이클에서 r1의 값을 읽어야 합니다. 이는 분명히 불가능하므로 종속성이 존재함을 나타내기 위해 두 명령의 관련 파이프라인 단계 사이에 화살표를 추가했습니다. 화살표가 왼쪽(시간이 거꾸로 흐름)을 가리키므로 파이프라인에서 이 코드 시퀀스를 실행할 수 없습니다. 이를 데이터 해저드라고 합니다. 충돌은 파이프라인에서 명령을 잘못 실행할 가능성으로 정의됩니다. 이 특별한 경우는 데이터 충돌로 분류됩니다. 적절한 조치를 취하지 않으면 명령 2에서 잘못된 데이터가 나올 수 있습니다.

**충돌(위험)**은 파이프라인에서 명령이 잘못 실행될 가능성으로 정의되며, 올바른 데이터를 얻을 수 없어 잘못된 실행이 발생할 가능성을 나타냅니다.

이러한 특정 유형의 데이터 위험을 RAW(읽기 후 쓰기) 위반이라고 합니다. 위 명령문의 뺄셈 명령어는 덧셈 명령어로 써야 하는 r1을 읽으려고 시도합니다. 이 경우 읽기가 쓰기를 따릅니다.

이것이 유일한 종류의 데이터 충돌은 아니며, 두 가지 다른 데이터 위험 유형은 WAW(쓰기 후 쓰기) 및 WAR(쓰기 후 읽기) 충돌입니다. 이러한 충돌은 명령 순서를 절대 변경하지 않고 이전 명령이 항상 다음 명령보다 앞에 있기 때문에 파이프라인에서는 문제가 되지 않습니다. 이와 대조적으로 최신 프로세서에는 명령을 다른 순서로 실행하는 비순차적 파이프라인이 있습니다.

(우리와 같은) 순차 파이프라인에서는 이전 명령이 항상 파이프라인의 다음 명령보다 앞에 옵니다. 최신 프로세서는 비순차적 파이프라인을 사용하여 이 규칙을 깨고 이후 명령이 이전 명령보다 먼저 실행되도록 합니다.

다음 어셈블리 코드 조각을 살펴보겠습니다.

cpp
add r1, r2, r3
sub r1, r4, r3

명령어 1과 2는 레지스터 r1에 쓰고 있습니다. 순서에 따라 파이프라인 r1은 올바른 순서로 작성하므로 WAW 위험이 없습니다. 그러나 순서가 잘못된 파이프라인에서는 명령 1보다 먼저 명령 2를 완료할 위험이 있으므로 r1이 잘못된 값을 갖게 될 수 있습니다. 이것은 WAW 충돌의 예입니다. 독자들은 최신 프로세서가 레지스터 이름 변경이라는 기술을 사용하여 r1이 잘못된 값을 얻지 않도록 보장한다는 점에 유의해야 합니다.

잠재적인 WAR 충돌의 예를 들어보겠습니다.

cpp
add r1, r2, r3
add r2, r5, r6

명령어 2는 r2에 쓰기를 시도하는 반면 명령어 1은 r2를 소스 피연산자로 사용합니다. 명령어 2가 먼저 실행되면 명령어 1이 잘못된 r2 값을 얻을 수 있습니다. 실제로 이것은 레지스터 이름 변경 체계 등으로 인해 최신 프로세서에서는 발생하지 않습니다. 충돌은 오류 발생의 이론적 위험이지만 프로그램이 잘못 실행되지 않도록 충분한 조치가 취해지기 때문에 실제 위험은 아니라는 점을 이해해야 합니다.

WAW 및 WAR 위험은 최신 비순차적 프로세서에만 관련되므로 이 기사에서는 주로 RAW 위험에 중점을 둘 것입니다. 솔루션의 성격을 개략적으로 설명하겠습니다. RAW 위험을 방지하려면 파이프라인에 한 쌍의 명령어가 포함되어 있음을 확인해야 합니다. 그 중 하나는 레지스터에 쓰고 다른 하나는 나중에 프로그램 순서대로 동일한 레지스터에서 읽습니다. 이를 위해서는 소비자 명령어가 생산자 명령어로부터 피연산자(이 경우 레지스터) 값을 올바르게 수신하는지 확인해야 하며 하드웨어와 소프트웨어 솔루션을 모두 살펴보겠습니다.

이제 파이프라인에 분기 명령이 있을 때 발생하는 또 다른 위험을 살펴보겠습니다. 다음 코드 조각을 고려해보세요.

cpp
[1]: beq .foo
[2]: mov r1, 4
[3]: add r2, r4, r3
...
...
.foo:
[100]: add r4, r1, r2

아래 그림은 처음 세 명령의 파이프라인 다이어그램을 보여줍니다.

여기서 분기의 결과는 사이클 3에서 결정되어 페치 유닛으로 전달되며, 이 유닛은 사이클 4에서 시작하여 올바른 명령어를 가져옵니다. 분기가 수행되면 명령어 2와 3이 실행되지 않아야 합니다. 안타깝게도 2주기와 3주기에서는 분기의 결과를 알 수 있는 방법이 없습니다. 따라서 이러한 지침을 가져와서 파이프라인의 일부가 됩니다. 분기가 실행되면 명령 2와 3이 프로그램 상태를 손상시켜 오류를 일으킬 수 있습니다. 명령어 2와 3은 잘못된 경로의 명령어로 알려져 있습니다. 이러한 상황을 통제 위험이라고 합니다. 분기 결과가 실제 결과와 다른 경우 실행된 명령은 잘못된 것으로 간주됩니다. 예를 들어, 분기가 실행되면 프로그램에서 분기 명령 다음에 오는 명령 경로가 잘못됩니다.

제어 위험은 분기의 잘못된 경로에 있는 명령이 실행될 수 있고 결과가 메모리나 레지스터 파일에 저장될 수 있으므로 파이프라인에서 잘못된 실행 가능성을 나타냅니다.

제어 충돌을 방지하려면 잘못된 경로의 명령어를 식별하고 해당 결과가 레지스터 파일과 메모리에 커밋되지 않도록 해야 합니다. 이러한 지침을 무효화하거나 완전히 피할 수 있는 방법이 있어야 합니다.

구조적 충돌은 서로 다른 명령어가 동일한 리소스에 액세스하려고 시도할 때 발생하며, 리소스는 모든 명령어가 동일한 주기에 해당 리소스에 액세스하는 것을 허용하지 않습니다. 예를 들어 보겠습니다. 메모리에서 피연산자를 읽는 덧셈 명령이 있다고 가정해 보겠습니다. 그 형식은 다음과 같습니다.

cpp
add r1, r2, 10[r3]

구조적 위험은 리소스 제약으로 인해 명령이 실행되지 않을 수 있음을 의미합니다. 예를 들어, 여러 명령이 동일한 주기에서 기능 장치에 액세스하려고 시도하고 용량 제약으로 인해 해당 장치가 모든 관심 명령이 진행되도록 허용할 수 없는 경우에 발생할 수 있습니다. 이 경우 충돌이 발생한 일부 명령어의 실행을 일시 중지해야 합니다.

여기에는 레지스터 소스 피연산자 r2와 메모리 소스 피연산자 10[r3]이 있다. 또한 파이프라인은 OF 단계에서 메모리 피연산자의 값을 읽는다고 가정합니다. 이제 잠재적인 충돌 상황을 살펴보겠습니다.

cpp
[1]: st r4, 20[r5]
[2]: sub r8, r9, r10
[3]: add r1, r2, 10[r3]

여기에는 제어 및 데이터 충돌이 없지만 저장 명령이 MA 단계에 있을 때 파이프라인 다이어그램의 한 지점을 고려해 보겠습니다. 이때 명령어 2는 EX 단계이고, 명령어 3은 OF 단계이다. 이 루프에서 명령어 1과 3은 모두 메모리 위치에 액세스해야 합니다. 그러나 메모리 장치가 주기당 하나의 요청만 처리할 수 있다고 가정하면 명령 중 하나가 실행을 일시 중지해야 하는 충돌 상황이 분명히 존재합니다. 이런 상황은 구조적 갈등의 한 예이다.

모범적인 사례이기 때문에 추후에는 RAW를 제거하고 충돌을 제어하기 위한 노력에 집중하겠습니다.

19.5.3.4 파이프라인 충돌 해결

소프트웨어 솔루션

이제 다음 코드를 가정하여 RAW 충돌을 피하는 방법을 찾아보겠습니다.

cpp
[1]: add r1, r2, r3
[2]: sub r3, r1, r4

명령어 2에는 OF 단계에서 r1 값이 필요합니다. 그러나 현재 명령어 1은 EX 단계에 있으므로 r1의 값을 레지스터 파일에 다시 쓰지 않으므로 명령어 2가 파이프라인에서 계속되도록 허용할 수 없습니다. 간단한 소프트웨어 솔루션은 스마트 컴파일러가 코드 시퀀스를 분석하고 RAW 충돌이 있음을 인식할 수 있다는 것입니다. 이러한 명령어 사이에 nop 명령어를 도입하여 RAW 충돌을 제거할 수 있습니다. 다음 코드 시퀀스를 고려하세요.

cpp
[1]: add r1, r2, r3
[2]: nop
[3]: nop
[4]: nop
[5]: sub r3, r1, r4

하위 명령어가 OF 단계에 도달하면 추가 명령어는 해당 값을 쓰고 파이프라인을 떠나므로 하위 명령어가 올바른 값을 얻습니다. nop 명령을 추가하는 것은 본질적으로 컴퓨팅 성능을 낭비하기 때문에 비용이 많이 드는 솔루션입니다. 이 예에서 nop 명령을 추가하면 기본적으로 3사이클이 낭비됩니다. 그러나 더 긴 코드 시퀀스를 고려하면 컴파일러는 nop 명령어 수가 최소화되도록 명령어 순서를 변경할 수 있습니다. 모든 컴파일러 개입의 기본 목표는 생산자 명령어와 소비자 명령어 사이에 최소 3개의 명령어를 포함하는 것입니다.

구체적인 예로, 다음 코드 세그먼트의 순서를 변경하고 충분한 수의 nop 명령어를 추가하여 파이프라인에서 올바르게 실행되도록 하세요.

cpp
add r1, r2, r3    ; 1
add r4, r1, 3     ; 2
add r8, r5, r6    ; 3
add r9, r8, r5    ; 4
add r10, r11, r12 ; 5
add r13, r10, 2   ; 6

대답은 다음과 같습니다.

cpp
add r1, r2, r3    ; 1
add r8, r5, r6    ; 3
add r10, r11, r12 ; 5
nop
add r4, r1, 3     ; 2
add r9, r8, r5    ; 4
add r13, r10, 2   ; 6

여기서 우리는 두 가지 중요한 점을 이해해야 합니다. 첫 번째는 nop 명령어의 능력이고 두 번째는 컴파일러의 능력입니다. 컴파일러는 프로그램의 정확성을 보장하고 성능을 향상시키는 중요한 도구입니다. 이 경우 최소한의 nop 명령어가 도입되도록 코드를 재정렬하려고 합니다.

다음으로 동일한 기술을 사용하여 제어 충돌을 해결해 보십시오.

파이프라인 다이어그램을 다시 보면 분기 명령어와 분기 대상의 명령어 사이에 최소한 두 개의 명령어가 필요한 것을 알 수 있습니다. EX 단계가 끝나면 분기 결과와 분기 대상을 얻기 때문이다. 이 시점에서 파이프라인에는 여전히 두 개의 명령이 있습니다. 분기 명령이 각각 OF 및 EX 단계에 있는 경우 이러한 명령이 페치되어 잘못된 경로를 실행했을 수 있습니다. EX 단계에서 분기 대상과 결과를 결정한 후 IF 단계에서 올바른 지침을 계속 얻을 수 있습니다.

이제 분기 결과가 불확실할 때 가져온 다음 두 가지 명령을 고려해보세요. 지점의 PC가 p1과 같으면 주소는 각각 p1+4와 p1+8입니다. 아무런 조치도 취하지 않으면 잘못된 경로를 실행하지 않습니다. 그러나 분기가 실행되면 이러한 명령은 잘못된 경로에 있으므로 파이프라인에서 삭제해야 합니다.

하드웨어가 분기 명령어 뒤의 두 명령어가 잘못된 경로에 있지 않다고 가정하는 간단한 소프트웨어 솔루션을 살펴보겠습니다. 이 두 명령어의 위치를 ​​지연 슬롯이라고 합니다. 일반적으로 분기 후에 두 개의 nop 명령어를 삽입하여 지연 간격에 있는 명령어가 오류를 발생시키지 않도록 하는 것이 가능하지만 이렇게 하여 추가 성능을 얻지 않고도 분기 명령어 이전에 실행된 두 명령어를 묶고 분기 후 두 개의 지연 슬롯으로 이동할 수 있습니다.

명령을 임의로 지연 슬롯으로 이동할 수 없으며 데이터 종속성 제약 조건을 위반할 수 없으며 RAW 충돌을 피해야 합니다. 또한 비교 명령을 지연 슬롯으로 이동할 수 없습니다. 적절한 지침을 사용할 수 없는 경우 언제든지 일반 솔루션으로 돌아가서 nop 명령을 삽입할 수 있습니다. 재정렬할 수 있는 명령어를 찾은 다음 분기 명령어 뒤에 nop 명령어를 삽입하기만 하면 되는 것도 가능합니다. 지연 분기 접근 방식은 제어 충돌을 피하기 위해 추가해야 하는 nop 명령 수를 줄이는 매우 효과적인 방법입니다.

간단한 파이프라인 데이터 경로에서 분기 후에 가져온 두 명령어의 PC는 각각 p1+4 및 p1+8과 같습니다(p1은 분기 명령어의 PC입니다). 컴파일러는 분기 결과에 관계없이 이러한 명령이 항상 올바른 경로에 있도록 보장하므로 이를 가져와도 버그를 신고하지 않습니다. 분기 결과가 결정된 후, 가져온 다음 명령어의 PC는 분기가 수행되지 않은 경우 p1+12와 같고, 분기가 수행된 경우 PC는 분기 대상과 같습니다. 따라서 두 경우 모두 분기 결과를 확인한 후 올바른 명령을 가져오며, 소프트웨어 솔루션이 프로세서의 파이프라인 버전에서 프로그램을 올바르게 실행한다는 결론을 내릴 수 있습니다.

정리하자면, 소프트웨어 기술의 핵심은 지연 슬롯(Delay Slot)의 개념이다. 다음 두 명령은 확실하지 않고 잘못된 경로에 있을 수 있으므로 분기 후에 두 개의 지연 슬롯이 필요합니다. 그러나 스마트 컴파일러를 사용하면 분기 결과에 관계없이 실행된 명령을 지연 슬롯으로 이동할 수 있습니다. 따라서 지연 슬롯에 nop 명령을 배치하는 것을 피할 수 있으므로 성능이 향상됩니다. 이 분기 명령을 지연 분기 명령이라고 합니다.

결과가 결정되기 전에 가져온 모든 후속 명령어가 올바른 경로에 있다고 프로세서가 가정하는 경우 분기 명령어를 지연 분기라고 합니다. 프로세서가 분기 명령을 가져오고 그 결과가 결정되는 시간 사이에 n개의 명령을 가져오면 n개의 지연 슬롯이 있다고 말합니다. 컴파일러는 올바른 경로의 명령이 지연 슬롯을 차지하고 추가 제어나 RAW 충돌이 발생하지 않도록 해야 합니다. 컴파일러는 지연 슬롯에 nop 명령을 도입할 수도 있습니다.

이제 예를 들어 보겠습니다. 분기 명령어당 두 개의 지연 슬롯이 있다고 가정하고 분기가 지연된 파이프라인 프로세서에서 올바르게 실행되도록 아래 어셈블리 코드를 재정렬하세요.

cpp
add r1, r2, r3  ; 1
add r4, r5, r6  ; 2
b .foo          ; 3
add r8, r9, r10 ; 4

답변:

cpp
b .foo          ; 3
add r1, r2, r3  ; 1
add r4, r5, r6  ; 2
add r8, r9, r10 ; 4

하드웨어 솔루션

위에서 RAW 및 제어 충돌을 제거하는 소프트웨어 솔루션을 검토했지만 컴파일러 접근 방식은 다음과 같은 이유로 그다지 일반적이지 않습니다.

  • 첫째, 프로그래머는 언제든지 수동으로 어셈블리 코드를 작성하고 이를 프로세서에서 실행해 볼 수 있습니다. 이 경우 프로그래머가 충돌을 제거하기 위해 코드를 올바르게 재정렬하지 않았을 수 있으므로 오류 가능성이 높습니다.

  • 둘째, 휴대성의 문제가 있다. 한 파이프라인에 대해 작성된 어셈블리 코드 조각은 지연 슬롯 수가 다르거나 단계 수가 다를 수 있으므로 동일한 ISA를 따르는 다른 파이프라인에서 실행되지 않을 수 있습니다. 동일한 ISA를 사용하는 서로 다른 시스템 간에 어셈블러를 이식할 수 없다면 어셈블러 도입의 주요 목표 중 하나가 무산됩니다.

하드웨어 수준에서 솔루션을 설계해 보겠습니다. 하드웨어는 어셈블러에 관계없이 올바른 작동을 보장해야 하며 출력은 항상 단일 사이클 프로세서에서 생성된 출력과 일치해야 합니다. 이러한 프로세서를 설계하려면 명령이 잘못된 데이터를 수신하지 않고 잘못된 경로 명령이 실행되지 않도록 해야 합니다. 이는 다음 조건이 충족되는지 확인하여 달성할 수 있습니다.

  • 데이터 잠금. 레지스터 파일에서 올바른 데이터를 수신하지 않는 한 명령어가 OF 단계를 벗어나는 것을 허용할 수 없습니다. 즉, IF 및 OF 단계는 효과적으로 일시 중지되어야 하며 OF 단계의 명령어가 피연산자를 안전하게 읽을 수 있을 때까지 나머지 단계가 실행되어야 합니다. 이 기간 동안 OF 단계에서 EX 단계로 전달되는 명령은 nop 명령이어야 합니다.

  • 분기 잠금. 우리는 결과가 알려질 때까지 프로세서를 정지시키거나 잘못된 경로의 명령이 변경 사항을 메모리나 레지스터에 커밋할 수 없도록 하는 기술을 사용하여 잘못된 경로에서 명령을 실행하지 않습니다.

파이프라인의 순수 하드웨어 구현에서는 특정 조건이 유지되지 않을 때까지 새로운 명령이 파이프라인 단계에 진입하는 것을 방지해야 하는 경우가 있습니다. 새로운 데이터를 받아들이고 처리하기 위해 파이프라인 단계를 중지하는 개념을 파이프라인 정지 또는 파이프라인 인터록이라고 합니다. 주요 목적은 프로그램 실행의 정확성을 보장하는 것입니다.

데이터 잠금 및 분기 잠금 조건이 모두 true인지 확인하면 파이프라인은 명령을 올바르게 실행합니다. 두 경우 모두 파이프라인의 특정 단계를 일정 기간 동안 일시 중지해야 할 수도 있습니다. 이러한 일시 중지를 파이프라인 인터록이라고도 합니다. 즉, 일정 기간 동안 파이프라인을 유휴 상태로 유지함으로써 잘못된 실행으로 이어질 수 있는 명령을 방지합니다. 다음 표는 파이프라인의 전체 논리를 구현하기 위한 순수 소프트웨어 및 하드웨어 솔루션의 장단점을 보여줍니다. 소프트웨어 솔루션에서는 충돌의 영향을 제거하기 위해 코드 순서를 변경한 다음 최소한의 nop 명령을 삽입하려고 합니다. 대조적으로, 하드웨어 솔루션에서는 잘못된 경로나 잘못된 피연산자 값으로 명령이 실행되는 것을 방지하기 위해 파이프라인의 일부를 동적으로 중단합니다. 파이프라인을 일시 중지하는 것은 특정 단계를 유휴 상태로 유지하고 다른 단계에 nop 명령을 삽입하는 것과 같습니다.

속성 소프트웨어 하드웨어(인터록)

이식성 특정 프로세서로 제한됨 프로그램은 모든 프로세서에서 실행 가능

가지 지연 슬롯을 사용해도 성능 저하가 없을 수 있습니다. 이 문서에서는 2주기 동안 파이프라인을 일시 중지해야 합니다.

RAW 충돌 코드 스케줄링을 통해 제거할 수 있습니다. 파이프라인을 일시 중지해야 함

성능 프로그램 특성에 크게 의존함 연동 기능이 있는 파이프라인의 기본 버전은 소프트웨어보다 느립니다.

우리는 소프트웨어 솔루션의 효율성이 프로그램의 성격에 크게 좌우된다는 점을 관찰했으며, 일부 프로그램의 지침은 RAW 충돌 및 분기의 유해한 영향을 완전히 숨기기 위해 재정렬될 수 있습니다. 그러나 일부 프로그램에서는 재정렬할 수 있는 명령어가 충분하지 않아 많은 수의 nop 명령어를 삽입해야 하므로 성능이 저하될 수 있습니다. 대조적으로, 데이터 잠금 및 분기 잠금 조건을 존중하는 순수 하드웨어 체계는 잠재적으로 잘못 실행된 명령이 감지될 때마다 파이프라인을 중지합니다. 이는 일반적인 접근 방식이며 순수 소프트웨어 솔루션보다 느립니다.

이제 하드웨어와 소프트웨어 솔루션을 결합하고 코드 순서를 변경하여 가능한 한 파이프라인 친화적으로 만든 다음 인터록이 있는 파이프라인에서 실행할 수 있습니다. 이 접근 방식에서 컴파일러는 정확성을 보장하지 않으며 단지 생산자와 소비자 명령어를 최대한 분리하고 지원되는 지연된 분기를 활용합니다. 이렇게 하면 파이프라인을 일시 중지하는 데 필요한 횟수가 줄어들고 두 가지 측면 모두에서 최상의 결과를 얻을 수 있습니다. 인터로킹이 포함된 조립 라인을 설계하기 전에 조립 라인 다이어그램의 도움으로 인터로킹의 특성을 살펴보겠습니다.

이제 인터록이 포함된 파이프라인 다이어그램을 그리려면 다음 코드 조각을 고려하세요.

cpp
add r1, r2, r3
sub r4, r1, r2

버블이 포함된 조립 라인 다이어그램.

명령어 [1]은 레지스터 r1에 쓰고 명령어 [2]는 r1에서 읽습니다. 분명히 RAW 종속성이 있습니다. 데이터 잠금 조건을 보장하려면 명령 [2]가 명령 [1]에 의해 기록된 r1 값을 읽었을 때만 OF 단계를 벗어나도록 보장해야 합니다. 이는 사이클 6(위)에서만 가능하지만 명령 [2]는 사이클 3에서 OF 단계에 도달합니다. 충돌이 없으면 이상적으로는 사이클 4에서 EX 단계에 진입하는 것이 좋습니다. 인터록이 있으므로 명령 [2]도 사이클 4, 5 및 6에서 OF 단계에 유지되어야 합니다. 문제는 사이클 4, 5 및 6에서 유효한 명령을 처리하지 않을 때 EX 단계가 무엇을 하느냐는 것입니다. 마찬가지로 MA 단계는 사이클 5, 6, 7에서 유효한 명령을 처리하지 않습니다. 중복 작업을 수행하지 않도록 파이프라인 단계를 비활성화하는 방법이 필요하며, 표준 방법은 단계에 nop 명령을 삽입하는 것입니다.

다시 위 이미지를 참고하세요. 사이클 3이 끝나면 인터록을 도입해야 하는 것으로 알려져 있으므로 사이클 4에서는 명령 [2]가 OF 단계에 남아 있고 nop 명령이 EX 단계에 삽입되어 사이클 5에서 MA 레벨로 이동하고 사이클 6에서 RW 레벨로 이동합니다. 이 nop 명령을 파이프라인 버블이라고 합니다. 버블은 연동 하드웨어에 의해 동적으로 삽입되는 nop 명령어로, 일반 명령어와 유사한 파이프라인 단계를 통해 이동합니다. 마찬가지로 루프 5와 6에서는 파이프라인 버블을 삽입해야 합니다. 마지막으로 사이클 7에서는 명령어[2]가 EX 및 후속 단계로 자유롭게 진입할 수 있습니다. 버블은 아무런 효과가 없으므로 스테이지에서 버블을 만나면 제어 신호가 켜지지 않습니다. 주목해야 할 또 다른 미묘한 점은 동일한 사이클에서 동일한 레지스터를 읽고 쓸 수 없다는 것입니다. 쓰기는 이전 명령어이므로 우선 순위가 지정되어야 하며 읽기는 한 사이클 동안 일시 중지되어야 합니다.

버블을 구현하는 방법에는 두 가지가 있습니다.

  • 지침 패키지에 별도의 버블 슬롯이 있을 수 있습니다. 비트가 1일 때마다 명령은 버블로 해석됩니다.

  • 명령어의 opcode를 nop의 opcode로 변경하고 모든 제어 신호를 0으로 바꿀 수 있습니다. 이 방법은 더 침입적이지만 회로에서 중복 작업을 완전히 제거합니다. 전자의 방법에서는 제어 신호가 켜지고 이에 의해 활성화된 장치는 계속 작동됩니다. 하드웨어는 버블이 레지스터나 메모리를 변경할 수 없도록 보장해야 합니다.

파이프라인 버블은 하드웨어를 연동하여 파이프라인 레지스터에 동적으로 삽입되는 nop 명령입니다. 버블은 일반 명령과 동일한 방식으로 파이프라인을 통해 전파됩니다.

요약하자면, 파이프라인에 버블을 동적으로 삽입하면 데이터 충돌을 피할 수 있습니다.

다음으로 div, mod 명령어 등 느린 명령어의 문제점에 대해 설명하겠습니다. 대부분의 파이프라인에서 이러한 명령은 EX 단계에서 실행되는 데 n(n > 1) 주기가 걸릴 가능성이 높습니다. n 사이클 각각에서 ALU는 div 또는 mod 명령어 처리의 일부를 완료합니다. 이러한 각 루프를 T 상태라고 합니다. 일반적으로 한 단계에는 1T 상태가 있지만 느린 명령의 EX 단계에는 T 상태가 많이 있습니다. 따라서 느린 명령어를 올바르게 구현하려면 작업이 완료될 때까지 IF 및 OF 단계를 n-1주기 동안 일시 중지해야 합니다.

단순화를 위해 이 문제를 다시 논의하지 않고 대신 모든 파이프라인 단계가 균형을 이루고 작업을 완료하는 데 1주기가 필요하다는 간단한 가정을 계속하겠습니다. 이제 제어 충돌을 살펴보면 먼저 다음 코드 조각을 고려하십시오.

cpp
[1]: beq .foo
[2]: add r1, r2, r3
[3]: sub r4, r5, r6
....
....
.foo:
[4]: add r8, r9, r10

지연된 분기를 사용하는 대신 분기가 실행되면 파이프라인에 버블을 삽입할 수 있습니다. 그렇지 않으면 아무 작업도 수행할 필요가 없습니다. Branch가 제거되었다고 가정하면, 이 경우의 파이프라인 다이어그램은 아래와 같습니다.

이 경우 명령 [1]의 분기 조건 결과는 루프 3에서 결정됩니다. 이 시점에서 명령 [2]와 [3]은 이미 파이프라인(각각 IF 및 단계)에 있습니다. 분기 조건이 take로 평가되므로 명령 [2]와 [3]을 취소해야 합니다. 그렇지 않으면 명령이 잘못 실행됩니다. 따라서 위 이미지에 표시된 대로 명령 [2]와 [3]이 루프 4에서 버블로 변환됩니다. 둘째, 루프 4에서는 올바른 분기 대상(.foo)이 가져오므로 명령 [4]가 파이프라인에 들어갑니다. 두 거품 모두 모든 파이프라인 단계를 통과하고 최종적으로 각각 루프 6과 7에서 파이프라인을 떠납니다.

따라서 파이프라인에 버블을 동적으로 도입하여 두 가지 조건(데이터 잠금 및 분기 잠금)을 모두 보장할 수 있습니다. 이러한 방법을 더 자세히 살펴보겠습니다.

데이터 잠금 조건을 보장하려면 OF 단계의 명령어와 후속 단계의 명령어 간에 충돌이 없는지 확인해야 합니다. 충돌은 RAW 충돌로 이어질 수 있는 상황으로 정의됩니다. 즉, 다음 단계의 명령어가 OF 단계의 명령어가 읽은 레지스터에 쓰면 충돌이 발생한다. 따라서 데이터 잠금 조건을 구현하려면 두 개의 하드웨어가 필요합니다. 첫 번째 단계는 충돌이 있는지 확인하는 것이고 두 번째 단계는 파이프라인이 중지되었는지 확인하는 것입니다.

먼저 충돌 감지 하드웨어를 살펴보십시오. 충돌 감지 하드웨어는 OF 단계의 명령어 내용을 다른 세 단계(예: EX, MA 및 RW)의 각 명령어 내용과 비교해야 하며, 이러한 명령어 중 하나와 충돌이 있는 경우 충돌이 선언될 수 있습니다. 충돌을 감지하는 논리에 초점을 맞추고 충돌 감지 회로의 의사 코드를 간략하게 소개하겠습니다. OF 단계의 명령어를 [A], 다음 단계의 명령어를 [B]라고 하자. 충돌을 감지하는 알고리즘 의사코드는 다음과 같습니다.

cpp
Data: Instructions: [A] and [B]
Result: Conflict exists (true), no conflict (false)

1 if [A].opcode 2 (nop,b,beq,bgt,call) then
/* Does not read from any register */
2 return false
3 end

4 if [B].opcode 2 (nop, cmp, st, b, beq, bgt, ret) then
/* Does not write to any register */
5 return false
6 end

/* Set the sources */
7 src1   [A]:rs1
8 src2   [A]:rs2
9 if [A].opcode = st then
10 src2   [A]:rd
11 end

12 if [A].opcode = ret then
13 src1   ra
14 end
/* Set the destination */
15 dest   [B]:rd
16 if [B].opcode = call then
17 dest   ra
18 end

/* Check if the first operand exists */
19 hasSrc1   true
20 if [A].opcode 2 (not,mov) then
21 hasSrc1   false
22 end

/* Check the second operand to see if it is a register */
23 hasSrc2   true
24 if [A].opcode =2 (st) then
25 if [A]:I = 1 then
26 hasSrc2   false
27 end
28 end

/* Detect conflicts */
29 if (hasSrc1 = true) and (src1 = dest) then
30 return true
31 end
32 else if (hasSrc2 = true) and (src2 = dest) then
33 return true
34 end
35 return false

위의 알고리즘을 하드웨어로 구현하는 것은 간단하며 논리 게이트와 멀티플렉서 세트만 있으면 됩니다. 대부분의 하드웨어 설계자는 일반적으로 하드웨어 설명 언어(예: Verilog 또는 VHDL)로 위의 알고리즘과 유사한 회로 설명을 작성하고 스마트 컴파일러를 사용하여 설명을 실제 회로로 변환합니다.

세 개의 충돌 감지기가 필요합니다(OFEX,OFMA,OFRW). 충돌이 없으면 명령은 EX 단계로 자유롭게 들어갈 수 있지만, 충돌이 하나라도 있으면 IF 및 OF 단계를 일시 중지해야 합니다. 명령어가 OF 단계를 통과하면 모든 소스 피연산자를 갖는 것이 보장됩니다.

이제 파이프라인 일시 중지를 살펴보겠습니다. 기본적으로 충돌이 발생하기 전에 새로운 명령이 IF 및 OF 단계에 들어가지 않도록 해야 합니다. 이는 PC 및 IF-OF 파이프라인 레지스터의 쓰기 기능을 비활성화함으로써 간단히 보장할 수 있습니다. 따라서 클럭 에지에서 새 데이터를 받아들일 수 없으며 이전 값을 계속 유지합니다.

둘째, 파이프라인에 거품을 삽입해야 합니다. 예를 들어 OF에서 EX 단계로 전달되는 지침은 유효하지 않은 지침 또는 버블이어야 하며 이는 nop 지침을 전달하여 보장할 수 있습니다. 따라서 데이터 잠금 조건을 보장하는 회로는 간단하며 PC에 연결된 충돌 감지기와 IF-OF 레지스터가 필요합니다. 충돌이 발생할 때까지 두 레지스터가 모두 비활성화되고 새 데이터를 받아들일 수 없습니다. OF-EX 레지스터의 명령에 nop가 포함되도록 강제하면 파이프라인의 향상된 회로도가 아래 그림에 표시됩니다.

연동된 파이프라인이 있는 데이터 경로(데이터 잠금 조건 구현)

다음으로 분기 잠금 조건에 대해 설명합니다.

파이프라인에 분기 명령(b, beq, bgt, call, ret)이 있다고 가정합니다. 지연 슬롯이 있는 경우 데이터 경로는 위 그림에 표시된 것과 동일하며 전체 실행 복잡성이 이미 소프트웨어에 로드되어 있으므로 변경할 필요가 없습니다. 그러나 파이프라인을 소프트웨어에 노출시키는 것은 장단점이 있으며, 파이프라인에 더 많은 단계가 추가되면 기존 실행 파일이 작동을 멈출 수 있습니다. 이를 방지하기 위해 대기 시간 슬롯을 소프트웨어에 노출하지 않는 파이프라인을 설계해 보겠습니다.

두 가지 디자인 옵션이 있습니다.

  • 첫째: 결과가 결정될 때까지 분기가 이루어지지 않는다고 가정할 수 있습니다. 우리는 브랜치 이후에도 이 두 명령을 계속 가져와 처리할 수 있으며, EX 단계에서 브랜치의 결과가 결정되면 결과에 따라 적절한 조치를 취할 수 있습니다. 분기가 수행되지 않으면 분기 명령어 이후에 가져온 명령어가 올바른 경로에 있으므로 더 이상 수행할 필요가 없습니다. 그러나 분기가 실행되면 이 두 명령어를 취소하고 파이프라인 버블(nop 명령어)로 대체해야 합니다.

  • 두 번째: 결과에 관계없이 분기 결과가 결정될 때까지 파이프라인을 일시 중지합니다.

분명히 두 번째 디자인의 성능은 가지가 사용되지 않는다고 가정할 때 첫 번째 대안보다 낮습니다. 예를 들어 가지가 30%의 시간 동안 사용되지 않으면 첫 번째 디자인의 경우 30%의 시간이 유용한 작업을 수행합니다. 그러나 두 번째 옵션을 사용하면 분기 명령을 가져온 후 2주기에서 유용한 작업을 수행하지 않습니다.

따라서 성능 관점에서 첫 번째 디자인을 고려해 보겠습니다. 분기가 취해질 때만 분기 이후의 두 명령을 취소합니다. 이 접근 방식은 사용되지 않을 것으로 예상됩니다.

taken), 실제로는 해당 분기가 취해지지 않을 것이라고 예측하기 때문입니다. 나중에 이 예측 오류가 발견되면 잘못된 경로의 명령이 취소될 수 있습니다.

분기 명령어의 PC가 p와 같으면 다음 두 사이클의 p+4 및 p+8에서 가져오도록 명령어가 선택됩니다. 분기가 실행되지 않은 경우 실행이 계속됩니다. 그러나 분기가 수행되면 이 두 명령이 취소되고 파이프라인 버블로 변환됩니다.

데이터 경로를 크게 변경할 필요는 없으며 EX 단계에서 입력을 받는 작은 분기 충돌 단위만 변경할 수 있습니다. 분기가 실행되면 다음 주기에서 If-OF 및 OF-EX 단계의 명령을 파이프라인 버블로 변환합니다. 분기 연동 장치를 사용한 확장 데이터 경로는 아래 그림과 같습니다.

연동된 파이프라인이 있는 데이터 경로(데이터 잠금 및 분기 잠금 조건 구현)

다음으로 Forwarding이 포함된 파이프라인에 대해 설명합니다.

위에서 연동된 파이프라인이 구현되었습니다. 인터록은 명령어 간의 종속성에 관계없이 파이프라인이 올바르게 실행되도록 보장합니다. 데이터 잠금 조건의 경우 올바른 값이 레지스터 파일에 있을 때까지 명령이 피연산자 가져오기 단계를 벗어나는 것을 허용하지 않는 인터록을 파이프라인에 추가하는 것이 좋습니다. 그러나 아래에서 볼 수 있듯이 인터록을 추가하는 것이 항상 필요한 것은 아닙니다. 실제로 레지스터 파일에는 없지만 파이프라인 레지스터에는 올바른 데이터가 이미 존재하는 경우가 많습니다. 내부 파이프라인 레지스터의 데이터를 적절한 기능 단위로 올바르게 전달하는 방법을 고안할 수 있습니다. 다음 코드를 고려해보세요.

cpp
add r1, r2, r3
sub r4, r1, r2

아래 그림에는 이 두 명령어에 대한 파이프라인 다이어그램만 포함되어 있습니다. (a) 인터록이 있는 파이프라인 다이어그램을 보여주고, (b) 인터록과 버블이 없는 파이프라인 다이어그램을 보여줍니다. 이제 명령어 사이에 거품을 삽입할 필요가 없다고 주장해 보세요.

(a) 인터록이 있는 파이프라인 다이어그램, (b) 인터록과 버블이 없는 파이프라인 다이어그램.

위의 이미지 (b)를 자세히 살펴보겠습니다. 명령어 1은 EX 단계가 끝날 때 결과를 생성하거나, 사이클 3이 끝날 때 결과를 생성하고 사이클 5의 레지스터 파일에 기록됩니다. 명령어 2는 사이클 3이 시작될 때 레지스터 파일에 r1 값이 필요하며 이는 당연히 불가능하므로 이 문제를 해결하려면 파이프라인 인터록을 추가하는 것이 좋습니다.

명령이 실행되도록 허용하는 다른 해결 방법을 시도해 보겠습니다. 그러면 사이클 3에서 [2]가 잘못된 값을 얻어 사이클 4에서 EX 단계로 들어갈 수 있습니다. 이 시점에서 명령 [1]은 MA 단계에 있고 해당 명령 패킷에는 올바른 r1 값이 포함되어 있습니다. r1 값은 이전 사이클에서 계산되었으며 사이클 4의 EX-MA 레지스터에 있는 명령어 패킷 [1]의 aluResult 필드에 있습니다. 이제 EX-MA의 aluResult 필드와 ALU의 입력 사이에 연결을 추가하면 올바른 r1 값을 ALU에 성공적으로 전송할 수 있습니다. ALU 피연산자가 정확하고 따라서 ALU 연산의 결과가 올바르게 계산되므로 계산이 잘못되지 않습니다.

아래 그림은 파이프라인 다이어그램에서 명령어 [1]의 MA 단계를 명령어 [2]의 EX 단계에 추가한 작업 결과를 보여줍니다. 화살표는 시간을 거꾸로 이동하지 않으므로 데이터(r1 값)를 한 단계에서 다른 단계로 전달할 수 있습니다.

파이프라인에서의 전달 예.

전달은 단계 간 직접 연결을 통해 서로 다른 파이프라인 단계의 명령어 간에 피연산자 값을 전송하는 방법입니다. 명령어 전체에 걸쳐 피연산자 값을 전송하기 위해 레지스터 파일을 사용하지 않으므로 비용이 많이 드는 파이프라인 인터록을 방지합니다.

우리는 전달이라는 파이프라인의 지연을 방지하는 매우 강력한 기술을 살펴보았습니다. 기본적으로 우리는 명령어 간에 값을 전송하기 위해 레지스터 파일을 사용하지 않고 피연산자 값을 스테이지 간에 직접 전달하여 명령어 간에 흐르도록 허용합니다. 전달 개념을 사용하면 일시 중지 주기를 추가하지 않고도 명령어 [1]과 [2]를 연속적으로(연속 주기로) 실행할 수 있습니다. 따라서 코드 순서를 바꾸거나 nops를 삽입할 필요가 없습니다.

명령 [1]과 [2] 사이에 r1 값을 전달하기 위해 MA 단계와 EX 단계 사이에 연결을 추가합니다. 이 연결은 명령 [1]과 [2]의 해당 단계 사이에 화살표를 그려 위의 그림 9에 표시됩니다. 이 화살표의 방향은 수직 위쪽을 향하고 있으며, 시간적으로 뒤로 가지 않기 때문에 이전에는 불가능했던 값을 앞으로 보내는 것이 가능합니다.

이제 일반적인 질문에 답해 보겠습니다. 모든 명령어 쌍 간에 값을 전달할 수 있습니까? 생산자 ALU 명령어와 소비자 ALU 명령어 사이에 명령어가 있더라도 계속해서 값을 전달해야 하므로 연속 명령어일 필요는 없습니다. 이제 파이프라인의 단계 간에 가능한 모든 전달 경로를 고려해 보십시오.

널리 준수되는 전달의 기본 원칙은 다음과 같습니다.

  • 후기 단계와 초기 단계 사이에 전달 경로가 추가되었습니다.

  • 파이프라인에서 가능한 한 늦게 값을 전달합니다. 예를 들어, 주어진 단계에서 주어진 값이 필요하지 않고 이후 단계의 생산자 명령에서 얻을 수 있는 경우 우리는 나중 단계에서 전달된 값을 얻을 때까지 기다립니다.

이러한 기본 원칙 중 어느 것도 프로그램의 정확성에 영향을 미치지 않으며 중복된 전달 경로만 제거할 수 있습니다. 이제 파이프라인에 필요한 모든 전달 경로를 체계적으로 살펴보겠습니다.

  • RW --> MA: MA 단계에는 RW 단계의 전달 경로가 필요합니다. 아래 그림에 표시된 코드 조각을 살펴보세요. 명령 [2]는 MA 단계(사이클 5)에서 r1 값을 요구하는 반면, 명령 [1]은 사이클 4의 끝에서 메모리에서 r1 값을 얻습니다. 따라서 해당 값을 사이클 5의 명령 [2]에 전달할 수 있습니다.

  • RW --> EX: 아래 표시된 코드 세그먼트는 사이클 4의 끝에서 레지스터 r1의 값을 얻는 로드 명령과 사이클 5에서 r1의 값을 요구하는 후속 ALU 명령을 보여줍니다. 시간을 되돌릴 수 없기 때문에 값이 전달될 수 있습니다.

  • MA --> EX: 아래에 표시된 코드 세그먼트는 사이클 3의 끝에서 레지스터 r1의 값을 계산하는 ALU 명령과 사이클 4에서 r1의 값을 요구하는 후속 ALU 명령을 보여줍니다. 이 경우 MA와 EX 단계 사이에 상호 연결(전달 경로)을 추가하여 데이터를 전달할 수도 있습니다.

  • RW --> OF: 일반적으로 OF 단계는 기능 단위가 없고 값을 즉시 사용할 필요가 없으며 원칙 2에 따라 나중에 값을 전달할 수 있기 때문에 전달 경로가 필요하지 않습니다. 그러나 유일한 예외는 명령이 파이프라인에 없기 때문에 나중에 값을 전달할 수 없는 RW 단계에서 전달되는 것입니다. 따라서 RW에서 OF 레벨로 전달 경로를 추가해야 합니다. RW --> OF 전달이 필요한 코드 세그먼트의 예가 아래 그림에 나와 있습니다. 명령어[1]은 사이클 4의 끝에서 메모리에서 r1 값을 읽어 r1 값을 생성한 다음 사이클 5에서 r1 값을 레지스터 파일에 씁니다. 동시에 명령어 [4]는 사이클 5의 OF 단계에서 r1 값을 읽으려고 시도하는데 불행하게도 여기서 충돌이 발생합니다. 따라서 우리는 RW와 OF 단계 사이에 전달 경로를 추가하여 충돌을 해결할 것을 제안합니다. 따라서 명령 [4]에서는 r1 값의 레지스터 파일을 읽는 것이 금지됩니다. 대신 명령어 [4]]는 RW --> OF 전달 경로를 사용하여 명령어 [1]에서 r1 값을 가져옵니다.

다음 전달 경로(RW-->EX) 및 (MA-->EX)를 사용할 수 있으므로 MA-->OF 및 EX-->OF 전달 경로를 추가할 필요가 없습니다. 원칙 2에 따르면 중복된 전달 경로를 피해야 하므로 MA 및 EX 수준에서 OF 수준으로의 전달 경로는 추가되지 않습니다. 이 단계에서는 명령어가 디코딩되지 않았고 해당 피연산자를 알 수 없기 때문에 IF 단계에 전달 경로를 추가하지 않습니다.

이제 또 다른 질문이 생깁니다. 전달하면 데이터 충돌이 완전히 제거됩니까?

이제 이 질문에 대답해 보세요. EX 단계에서 결과를 생성하고 MA 단계로 진행할 준비가 된 ALU 명령을 먼저 고려하십시오. 모든 후속 사용자 명령어에는 EX 단계의 이전 ALU 명령어에 의해 가장 먼저 생성된 피연산자 값이 필요합니다. MA 단계에서 이미 피연산자 값을 사용할 수 있으므로 이 시점에서 성공적인 전달이 이루어집니다. 생산자 명령어가 파이프라인을 떠난 경우 후속 명령어는 사용 가능한 전달 경로를 사용하거나 레지스터 파일에서 값을 얻을 수 있습니다. 생산자 명령어가 ALU 명령어인 경우 ALU 작업의 결과는 항상 소비자 명령어로 전달될 수 있습니다. 이 사실을 증명하려면 가능한 모든 명령어 조합을 고려하고 입력 피연산자가 사용자 명령어에 전달될 수 있는지 여부를 결정해야 합니다.

레지스터 값을 명시적으로 생성하는 유일한 다른 명령어는 로드 명령어입니다. 저장 명령어는 어떤 레지스터에도 쓰지 않는다는 점을 기억하세요. 로드 명령어를 살펴보겠습니다. 로드 명령어는 MA 단계가 끝날 때 값을 생성하므로 RW 단계에서 해당 값을 전달할 준비가 됩니다. 아래 이미지의 코드 조각과 해당 파이프라인 다이어그램을 살펴보세요.

로드 사용량 충돌.

명령어 [1]은 레지스터 r1에 쓰는 로드 명령어이고 명령어 [2]는 레지스터 r1을 소스 피연산자로 사용하는 ALU 명령어이며 로드 명령어는 사이클 5의 시작 부분에서 전달할 준비가 되어 있습니다. 불행하게도 ALU 명령어는 사이클 4의 시작 부분에 r1 값이 필요하므로 시간이 거꾸로 흐르는 파이프라인 다이어그램에 화살표를 그려야 합니다. 따라서 이 경우에는 전달이 불가능합니다.

로드 사용 위험은 로드 명령이 EX 단계 동안 해당 값을 요구하는 바로 다음 명령에 로드된 값을 제공하는 상황을 나타냅니다. 전달하는 경우에도 파이프라인은 로드 명령 뒤에 일시 중지 주기를 삽입해야 합니다.

이는 파이프라인에 일시 중지 루프를 도입해야 하는 유일한 상황입니다. 이러한 상황을 로드-사용 충돌이라고 하며, 로드 명령어는 EX 단계의 값이 필요한 바로 다음 명령어에 로드된 값을 제공합니다. 로드 사용 충돌을 제거하는 표준 방법은 파이프라인이 버블을 삽입하도록 허용하거나 컴파일러를 사용하여 명령어 순서를 변경하거나 nop 명령어를 삽입하는 것입니다.

요약하자면, 전달 기능이 있는 파이프라인에는 실제로 연동이 필요할 수 있으며, 유일한 특수한 경우는 로드 사용 충돌입니다.

로드 값을 저장하는 로드 명령어 뒤에 저장 명령어가 있는 경우 저장 명령어에는 MA 단계의 값이 필요하므로 일시 중지 루프를 삽입할 필요가 없습니다. 이 시점에서 로드 명령은 RW 단계에 있으므로 값을 전달할 수 있습니다.

파이프라인 전달을 구현하려면 다양한 파이프라인 단계에 따라 구현해야 합니다.

전달을 지원하는 OF 단계는 아래 그림과 같습니다. 전달되지 않은 기본 파이프라인의 멀티플렉서는 더 밝은 색상으로 표시되고, 전달을 활성화하기 위해 추가된 추가 멀티플렉서는 더 어두운 색상으로 표시됩니다.

아래 이미지는 변형된 EX 스테이지를 보여줍니다. EX 스테이지가 OF 스테이지에서 얻는 세 가지 입력은 A(첫 번째 ALU 피연산자), B(두 번째 ALU 피연산자), op2(두 번째 레지스터 피연산자)입니다. A와 B의 경우 OF 단계에서 계산된 값과 MA 및 RW 단계에서 각각 전달된 값 중에서 선택하기 위해 두 개의 멀티플렉서 M3 및 M4를 추가했습니다. 저장된 값이 포함될 수 있는 op2 필드의 경우 MA 단계에서 저장된 값이 필요하기 때문에 MA --> EX 전달이 필요하지 않으므로 RW --> MA 전달을 사용하여 전달 경로를 1개 줄일 수 있습니다. 따라서 멀티플렉서 M5에는 두 개의 입력(기본값과 RW 단계에서 전달된 값)이 있습니다.

아래 그림은 추가 전달 지원이 포함된 MA 단계를 보여줍니다. 메모리 주소는 EX 단계에서 계산되어 명령어 패킷의 aluResult 필드에 저장되며, 메모리 장치는 이 값을 직접 주소로 사용합니다. 그러나 저장의 경우에는 저장해야 할 값(op2)을 RW단에서 전달할 수 있기 때문에 명령어 패킷의 op2 필드와 RW단에서 전달한 값 사이에서 선택하는 멀티플렉서 M6이 추가된다. 나머지 회로는 변경되지 않습니다.

아래 이미지는 RW 단계를 보여줍니다. 마지막 단계이므로 전달 값을 사용하지 않습니다. 그러나 파일을 등록하기 위해 작성한 값을 각각 MA, EX, OF 단계로 보냅니다.

아래 다이어그램은 이를 모두 모아서 전달을 지원하는 파이프라인을 보여줍니다. 요약하자면, 전달된 값을 전달하려면 6개의 멀티플렉서를 추가하고 장치 간에 추가 상호 연결을 만들어야 합니다. 멀티플렉서에 대한 제어 신호를 계산하는 전용 전달 장치를 상상해 보십시오(그림에는 표시되지 않음). 이러한 작은 변경 외에 데이터 경로에 대한 다른 주요 변경은 필요하지 않습니다.

전달이 포함된 파이프라인 데이터 경로(간략화된 다이어그램)

전달에 대해 논의할 때 단순화된 다이어그램(위)을 사용합니다. 이제 실제 회로가 상당히 복잡해졌다는 점에 유의하는 것이 중요합니다. 데이터 경로 확장 외에도 멀티플렉서에 대한 제어 신호를 생성하려면 전용 전달 장치를 추가해야 합니다. 자세한 사진은 아래와 같습니다.

이제 데이터 잠금 및 분기 잠금 조건이 필요한 연동 논리를 파이프라인에 추가합니다. 이제 로드 사용 충돌을 제외한 모든 RAW 충돌이 성공적으로 처리되었습니다. 부하 사용 충돌이 발생하는 경우 한 사이클만 중지하면 되므로 데이터 잠금 회로가 크게 단순화됩니다. EX 단계에 로드 명령이 있는 경우 로드 명령과 OF 단계의 명령 간에 RAW 데이터 종속성이 있는지 확인해야 합니다. 고려할 필요가 없는 유일한 RAW 충돌은 로드-저장 종속성입니다. 즉, 로드는 저장된 값이 포함된 레지스터를 씁니다. 저장할 값을 RW에서 MA 단계로 전달할 수 있기 때문에 일시정지할 필요가 없습니다. 다른 모든 데이터 종속성의 경우, 로드 사용 충돌을 해결하고 회로의 분기 잠금 조건이 변경되지 않도록 보장하는 버블을 도입하여 파이프라인을 1주기 동안 일시 중지해야 합니다. EX 단계의 명령어도 확인해야 하며, 실행된 분기인 경우 if 및 OF 단계의 명령어를 무효화해야 합니다. 마지막으로, 연동은 항상 전달보다 우선한다는 점에 유의해야 합니다.

19.5.3.5 성능 표준 및 측정

이 섹션에서는 파이프라인 프로세서의 성능에 대해 설명합니다.

성능이 무엇을 의미하는지 먼저 프로세서의 맥락에서 정의해야 합니다. 대부분의 경우 노트북이나 스마트폰의 사양을 검색하면 클럭 주파수, RAM, 하드 드라이브 크기와 같은 용어가 넘쳐납니다. 불행하게도 이러한 용어 중 어느 것도 프로세서의 성능을 직접적으로 나타내지는 않습니다. 컴퓨터 레이블에 성능이 명시적으로 언급되지 않는 이유는 "성능"이라는 용어가 다소 모호하고, 프로세서 성능이 프로그램마다 다르게 수행되기 때문에 프로세서 성능이라는 용어는 항상 특정 프로그램이나 어셈블리를 지칭하기 때문입니다.

프로그램 P가 주어지면 특정 프로세서의 성능을 정량화해 보겠습니다. P가 B에서 P를 실행하는 데 걸리는 시간보다 A에서 P를 실행하는 데 걸리는 시간이 더 짧다면 프로세서 A는 프로세서 B보다 더 나은 성능을 발휘합니다. 따라서 주어진 프로그램의 성능을 정량화하는 것은 프로그램을 실행하는 데 걸리는 시간을 측정한 다음 그 역수를 계산하는 것만큼 간단합니다. 이 숫자는 프로그램에 대한 프로세서의 성능에 비례하는 것으로 해석될 수 있습니다.

먼저 프로그램 P를 실행하는 데 필요한 시간을 계산합니다.

\begin{정렬됨} \tau &=\# \text { 초 } \\ \\ &=\frac{\# \text { 초 }{\# \text { 주기 } \times \frac{\# \text { 주기 }{\# \text { 지침 } \times(\# \text { 지침 }) \\ \\ &=\underbrace{\frac{\# \text { 초 }{\# \text { 사이클 &#125;&#125;_{1 / f} \times \underbrace{\frac{\# \text { 사이클 }{\# \text { 지침 &#125;&#125;_{C P I} \times(\# \text { 지침 }) \\ \\ &=\frac{ \text { CPI } \times(\# \text { 지침 })}{f} \\ \end{정렬됨}
  • 초당 사이클 수는 프로세서의 클럭 주파수(f)입니다.

  • 명령어당 평균 사이클 수를 **CPI(명령어당 사이클)**라고 하며, 그 역수(사이클당 명령어 수)를 **IPC(사이클당 명령어)**라고 합니다.

  • 마지막 항목은 명령어 수(#insts로 축약됨)입니다. 이는 프로그램 실행 파일의 명령어 수가 아니라 동적 명령어 수 또는 프로세서에 의해 실제로 실행되는 명령어 수라는 점에 유의하세요.

정적 명령어는 명령어 목록의 각 명령어를 포함하는 프로그램의 바이너리 또는 실행 파일입니다.

동적 명령어는 명령어가 파이프라인에 입력될 때 프로세서에 의해 생성되는 정적 명령어의 인스턴스입니다.

이제 성능 P를 시간에 반비례하는 양 τ으로 정의할 수 있습니다(성능 방정식이라고 함).

P \propto \frac{I PC \times f}{\# \text { insts }

따라서 프로그램 대비 프로세서의 성능은 IPC 및 주파수에 정비례하고 명령어 수에 반비례한다는 결론을 내릴 수 있습니다.

이제 단일 사이클 프로세서 성능을 살펴보십시오. 모든 명령어에 대해 CPI는 1이고 성능은 \cfrac{f}{\text{instsCount}에 비례하는데 이는 매우 사소한 결과입니다. 주파수가 증가함에 따라 단일 사이클 프로세서는 비례적으로 더 빨라집니다. 마찬가지로, 프로그램의 명령 수를 X배만큼 줄일 수 있다면 성능도 X배만큼 증가할 것입니다. 분석이 더 복잡하고 통찰력이 매우 심오할 수 있는 파이프라인 프로세서의 성능을 고려해 보겠습니다.

성능 방정식의 세 가지 용어는 아래에 설명되어 있습니다.

  • 지침의 수. 프로그램의 명령어 수는 컴파일러의 지능에 따라 달라집니다. 진정한 지능형 컴파일러는 ISA에서 올바른 명령어 세트를 선택하고 스마트 코드 변환을 사용하여 명령어를 줄일 수 있습니다. 예를 들어, 프로그래머는 종종 데드 코드로 분류되는 일부 코드를 가지고 있습니다. 이 코드는 최종 출력에 영향을 미치지 않습니다. 똑똑한 컴파일러는 발견한 데드 코드를 모두 제거할 수 있습니다. 추가 명령의 또 다른 소스는 레지스터를 오버플로하고 복원하는 코드입니다. 컴파일러는 매우 작은 함수에 대해 함수 인라인을 수행하는 경우가 많습니다. 이 최적화는 이러한 함수를 동적으로 제거하고 해당 코드를 함수를 호출하는 코드에 붙여넣습니다. 작은 함수의 경우 이는 레지스터를 오버플로하고 복원하는 코드를 제거하는 데 매우 유용한 최적화입니다. 코드 크기를 줄이는 데 도움이 되는 컴파일러 최적화도 많이 있습니다. 이 섹션에서는 명령어 수가 일정하다고 가정하고 하드웨어 측면에만 중점을 둡니다.

  • 총 사이클 수를 계산합니다. 이상적인 파이프라인이 거품이나 정지의 삽입을 필요로 하지 않는다고 가정하면 사이클당 하나의 명령어를 완료할 수 있으므로 CPI는 1입니다. n 명령어가 있는 프로그램을 가정하고 파이프라인에 k 단계가 있다고 가정하고 모든 n 명령어가 파이프라인을 떠나는 데 필요한 총 사이클 수를 계산해 보겠습니다.

첫 번째 명령어는 사이클 1에서 파이프라인에 들어가고 사이클 k에서 파이프라인을 떠납니다. 매 사이클마다 하나의 명령이 파이프라인을 떠납니다. (n-1) 주기에서는 모든 명령이 파이프라인을 떠나며 총 주기 수는 n+k-1입니다. CPI는 다음과 같습니다:

CPI=n+k1n

n이 양의 무한대에 접근하면 CPI는 1에 접근합니다.

  • 빈도와의 관계. 단일 사이클 프로세서에서 명령어 실행을 완료하는 데 필요한 최대 시간은 tmax이며, 이는 총 알고리즘 작업량이라고도 합니다. tmax를 계산할 때 파이프라인 레지스터 지연을 무시합니다. 이제 데이터 경로는 k 파이프라인 단계로 나누어지고 k1 파이프라인 레지스터를 추가해야 합니다. 파이프라인 레지스터의 대기 시간을 l로 둡니다. 모든 파이프라인 단계가 균형을 이루고 있다고 가정하면(동일한 작업을 수행하고 동일한 시간이 소요됨) 한 단계에서 가장 느린 명령이 작업을 완료하는 데 필요한 시간은 \cfrac{t_{max}{k}와 같습니다. 각 단계의 총 시간은 회로 지연 및 파이프라인 레지스터 지연과 같습니다.
t_{단계} = \cfrac{t_{최대}{k} + 1

이제 최소 클록 주기 시간은 파이프라인 단계의 대기 시간과 동일해야 합니다. 파이프라인은 각 단계에 단 하나의 클록 주기만 필요하다는 가정으로 설계되었기 때문입니다. 따라서 최소 클록 사이클 시간(tclk) 또는 최대 주파수(f)는 다음과 같습니다.

t_{c l k}=\frac{1}{f}=\frac{t_{\max }{k}+l

다음으로 파이프라인의 성능을 계산합니다. 명령어 수가 일정(n)하므로 간단히 성능이 (f/CPI)와 같다고 가정합니다.

\begin{배열}{l} P=\frac{f}{C PI}\\ =\frac{\frac{1}{\frac{t_{m a x}{k}+l}{\frac{n+k-1}{n}\\ =\frac{n}{\left(t_{\max } / k+l\right) \times(n+k-1)}\\ =\frac{n}{\left((n-1) t_{\max } / k+\left(t_{\max }+l n-l\right)+l k\right.} \end{배열}

올바른 k 값을 선택하여 성능을 최대화해 보세요. 다음이 있습니다.

\begin{정렬됨} & \frac{\partial\left((n-1) t_{\max } / k+\left(t_{\max }+l n-l\right)+l k\right)}{\partial k}=0 \\ \오른쪽 화살표 &-\frac{(n-1) t_{\max }{k^{2}+l=0 \\ \오른쪽 화살표 & k=\sqrt{\frac{(n-1) t_{\max }{l} \end{정렬됨}

명령어 수(n)가 매우 크다는 가정 하에 지연 효과는 CPI 방정식에 포함되어야 합니다. 이상적인 CPI를 CPIideal라고 하면, 이 경우 CPIideal=1입니다.

CPI=CPIideal+×

성능을 최대화하려면 다음을 제공하여 분모를 최소화해야 합니다.

\begin{정렬됨} & \frac{\partial\left((n-1) t_{\max } / k+\left(r c n t_{\max }+t_{\max }+l n-l\right)+l k(1+r c n)\right)}{\partial k}=0 \\ \오른쪽 화살표 &-\frac{(n-1) t_{\max }{k^{2}+l(1+r c n)=0 \\ \Rightarrow & k=\sqrt{\frac{(n-1) t_{\max }{l(1+r c n)} \about \sqrt{\frac{t_{\max }{l r c} \quad(\text { as } n \rightarrow \infty) \end{정렬됨}

최적의 파이프라인 단계 수의 성능을 결정하기 위해 n이 양의 무한대이므로 (n+k1)/n이 1에 접근한다고 가정하고 다음을 얻습니다.

\begin{정렬됨} P_{\text {이상적 } &=\frac{1}{\left(t_{\max } / k+l\right) \times(1+r c k)} \\ &=\frac{1}{t_{\max } / k+l+r c t_{\max }+l r c k} \\ &=\frac{1}{t_{\max } \times \sqrt{\left(\frac{l r c}{t_{\max }\right)}+l+r c t_{\max }+l r c \times \sqrt{\left(\frac{t_{\max }{l r c}\right)} \\ &=\frac{1}{r c t_{\max }+2 \sqrt{l_{r c t} \max }+l} \\ &=\frac{1}{\left(\sqrt{r c t_{\max }+\sqrt{l}\right)^{2} \end{정렬됨}

대부분의 경우 프로그램에 대한 프로세서 성능을 측정하지 않습니다. 알려진 일련의 벤치마크 프로그램을 고려하고 모든 프로그램을 기준으로 프로세서 성능을 측정하여 통일된 그림을 얻습니다. 대부분의 프로세서 공급업체는 일반적으로 SPEC(Standard Performance Evaluation Corporation)와 관련된 프로세서의 성능 벤치마크를 요약하여 프로세서와 소프트웨어 시스템 성능을 측정, 요약 및 보고하기 위한 벤치마크 제품군을 출시합니다.

컴퓨터 아키텍처는 일반적으로 SPEC CPU 벤치마크 제품군을 사용하여 프로세서 성능을 측정합니다. SPEC CPU 2006 벤치마크는 정수 산술 벤치마크(SPECint)와 부동 소수점 벤치마크(SPECfp)의 두 가지 프로그램 유형으로 제공됩니다. C/C++로 작성된 12개의 SPECint 벤치마크가 있습니다. 벤치마크에는 C 컴파일러, 유전자 시퀀서, AI 엔진, 이산 이벤트 시뮬레이터 및 XML 프로세서에 대한 섹션이 포함되어 있습니다. 비슷한 맥락에서 SPECfp 제품군에는 물리학, 화학, 생물학 분야의 다양한 문제를 해결하는 17개의 프로그램이 포함되어 있습니다.

대부분의 프로세서 공급업체는 일반적으로 프로세서 성능을 나타내는 SPEC 점수를 계산합니다. 권장되는 절차는 벤치마크가 참조 프로세서에서 소비하는 시간과 벤치마크가 특정 프로세서에서 소비하는 시간의 비율을 구하는 것입니다. SPEC 점수는 모든 비율의 기하 평균과 같습니다. 컴퓨터 아키텍처에서는 평균 상대 성능(예: SPEC 점수)을 보고할 때 일반적으로 기하 평균을 사용합니다. 평균 실행 시간(절대 시간)을 보고하려면 산술 평균을 사용할 수 있습니다.

때때로 SPEC 점수 대신 초당 실행되는 평균 명령 수가 보고되거나 과학 프로그램의 경우 초당 평균 부동 소수점 연산 수가 보고됩니다. 이러한 지표는 프로세서 또는 프로세서 시스템의 속도를 나타냅니다. 다음 용어가 일반적으로 사용됩니다.

  • KIPS: 초당 수천(103) 명령.

  • MIPS: 초당 수백만(106) 명령.

  • MFLOPS: 초당 수백만(106) 부동 소수점 연산.

  • GFLOPS: 초당 기가비트(109) 부동 소수점 연산.

  • TFLOPS: 초당 수조(1012) 부동 소수점 연산.

  • PFLOPS: 초당 천조(1015) 부동 소수점 연산.

이제 성능, 컴파일러 설계, 프로세서 아키텍처 및 제조 기술 간의 관계를 살펴봄으로써 논의를 요약합니다. 성능 방정식을 다시 고려하십시오.

P=f×IPCC#insts

최종 목표가 성능을 최대화하는 것이라면 동적 명령어 수(#insts)를 최소화하면서 주파수(f)와 IPC를 최대화해야 합니다. 우리가 통제하는 세 가지 변수는 프로세서 아키텍처, 제조 기술, 컴파일러입니다. 여기서 "아키텍처"라는 용어는 프로세서의 실제 구성 및 설계를 나타내는 데 사용되는 반면, 문헌에서는 일반적으로 아키텍처를 사용하여 ISA 및 프로세서 설계를 나타냅니다. 각 변수에 대해서는 아래에서 자세히 설명합니다.

  • 컴파일러

스마트 컴파일러 기술을 사용하면 동적 명령어 수를 줄일 수 있고, 일시 중지 횟수도 줄여 IPC가 향상됩니다. 다음 예에서는 add 및 ld 명령어를 다시 정렬하여 일시 중지 주기를 제거합니다. 비슷한 맥락에서 컴파일러는 일반적으로 수백 개의 명령어를 분석하고 지연을 최소화하기 위해 최적의 순서를 지정합니다.

cpp
; -----示例1-----
; 在不违反程序正确性的情况下重新排序以下代码,以减少暂停。
add r1, r2, r3
ld r4, 10[r5]
sub r1, r4, r2

;答案
ld r4, 10[r5]
add r1, r2, r3
sub r1, r4, r2
; 没有加载-使用冲突,程序的逻辑保持不变。

; -----示例2-----
; 在不违反程序正确性的情况下重新排序以下代码,以减少暂停。假设有2个延迟时隙的延迟分支.
add r1, r2, r3
ld r4, 10[r5]
sub r1, r4, r2
add r8, r9, r10
b .foo

; 答案
add r1, r2, r3
ld r4, 10[r5]
b .foo
sub r1, r4, r2
add r8, r9, r10
; 消除了加载-使用风险,并最佳地使用了延迟时隙。
  • 건축

우리는 파이프라인을 사용하여 높은 수준의 아키텍처를 설계했습니다. 파이프라이닝 자체는 성능을 향상시키지 않으며 일시 중지로 인해 단일 주기 프로세서에 비해 프로그램의 IPC를 줄입니다. 파이프라이닝의 주요 이점은 프로세서를 더 높은 주파수에서 실행할 수 있고 최소 사이클 시간이 단일 사이클 파이프라인의 tmax에서 k 스테이지 파이프라인 시스템의 tmax/k+l로 감소된다는 것입니다. 매 사이클마다 새로운 명령어의 실행이 완료되므로 지연이 발생하지 않는 한 명령어 세트는 훨씬 더 높은 명령어 실행 처리량으로 파이프라인 머신에서 훨씬 빠르게 실행될 수 있습니다.

파이프라이닝의 주요 이점은 프로세서를 더 높은 빈도로 실행하면 더 높은 명령 처리량(초당 더 많은 명령이 완료됨)이 보장된다는 것입니다. 파이프라인 자체는 프로그램의 IPC를 줄이고 단일 사이클 프로세서에 비해 단일 명령을 처리하는 데 필요한 시간을 늘립니다.

지연된 분기 및 전달과 같은 기술은 파이프라인 기계의 IPC를 개선하는 데 도움이 됩니다. 다양한 기술을 통해 복잡한 파이프라인의 성능을 향상시키는 데 집중해야 합니다. 주목해야 할 중요한 점은 아키텍처 기술이 빈도(파이프라인 단계 수를 통해) 및 IPC(전달 및 지연된 분기와 같은 최적화를 통해)에 영향을 미친다는 것입니다.

  • 제조과정

제조 공정은 트랜지스터의 속도에 영향을 미치며, 이는 다시 조합 논리 블록과 래치의 속도에 영향을 미치며, 트랜지스터가 작을수록 속도는 빨라집니다. 따라서 전체 알고리즘 작업 부하(tmax)와 래치 대기 시간(l)도 꾸준히 감소하여 프로세서가 더 높은 주파수에서 실행될 수 있게 되어 성능이 향상됩니다. 제조 기술은 프로세서를 실행할 수 있는 빈도에만 영향을 미치며 IPC 또는 명령 수에는 영향을 미치지 않습니다.

요컨대, 이 단락의 논의는 다음 그림으로 요약될 수 있다.

성능, 컴파일러, 아키텍처 및 기술 간의 관계.

전체적인 그림은 이 섹션에 설명된 것처럼 단순하지 않으며 고려해야 할 성능 및 복잡성 문제가 있습니다. 20개 이상의 단계로 구성된 파이프라인을 구현하는 것은 복잡성 증가로 인해 매우 어려운 경우가 많습니다. 둘째, 대부분의 최신 프로세서에는 심각한 전력 및 온도 제한이 있습니다. 이는 전력 벽이라고도 알려진 문제입니다(아래 그림). 일반적으로 전력소모 증가를 감당할 수 없기 때문에 주파수를 높이는 것은 불가능하며, 경험적으로 보면 주파수의 3제곱에 따라 전력이 증가하므로 주파수를 10% 올리면 전력소모가 30% 이상 늘어나는 것은 매우 큰 수치입니다. 설계자들은 매우 높은 빈도로 실행되는 심층적인 파이프라인 설계를 점점 더 피하고 있습니다.

인텔 x86 마이크로프로세서의 클럭 주파수와 전력 소비량은 30년이 지나면서 8세대를 초과합니다. 펜티엄 4는 클럭 속도와 성능 면에서 극적인 향상을 이루었지만 성능 면에서는 그렇게 인상적이지는 않았습니다. Prescott의 열 문제로 인해 Pentium 4 시리즈가 폐기되었습니다. Core 2 시리즈는 더 낮은 클럭 속도와 칩당 여러 프로세서를 갖춘 더 간단한 파이프라인으로 되돌아갑니다. Core i5 파이프라인이 바로 그 뒤를 따릅니다.

성능과 관련하여 마지막으로 논의할 사항은 전력과 온도 문제입니다.

전력 및 온도 문제는 수년에 걸쳐 점점 더 중요해지고 있습니다. 고성능 프로세서 칩은 일반적으로 정상 작동 중에 60~120W의 전력을 소비합니다. , 서버급 컴퓨터에 4개의 칩이 있다면 대략 400W의 전력을 소비하게 됩니다. 일반적으로 메인 메모리, 하드 드라이브, 주변 장치, 팬 등 컴퓨터의 다른 구성 요소도 비슷한 전력을 소비하며 총 전력 소비량은 약 800W입니다. 전원 공급 장치 및 디스플레이 하드웨어의 비이상적인 효율성과 같은 추가 오버헤드가 추가되면 전력 요구 사항은 약 1KW에 도달합니다. 100개의 서버가 있는 일반적인 서버 팜에서는 컴퓨터를 실행하는 데 100kW의 전력이 필요합니다. 또한, 에어컨 등의 냉각 장비도 필요합니다. 일반적으로 1W의 열을 제거하려면 0.5W의 냉각 전력이 필요하므로 서버팜의 전체 전력 소모량은 약 150kW 정도이다. 이에 비해 일반적인 가정의 정격은 6~8kW입니다. 즉, 서버 팜 하나가 20~25가구의 전력을 소비한다는 의미입니다. 이는 매우 분명한 사실입니다. 100개의 시스템을 포함하는 서버 팜은 상대적으로 작은 설정입니다. 실제로는 작은 마을에 전력을 공급하기에 충분한 메가와트의 전력이 필요한 수천 대의 시스템을 포함하는 대규모 서버 팜이 있습니다.

이제 휴대폰 프로세서와 같은 매우 작은 장치를 고려하면 제한된 배터리 수명으로 인한 전력 소비도 중요한 문제입니다. 모든 사람은 배터리 수명이 긴 장치, 특히 기능이 풍부한 스마트폰을 높이 평가합니다. 이제 심장 박동기와 같은 장치에 작은 마이크로칩을 사용하는 의료 응용 분야용 신체 내부에 내장된 소형 프로세서와 같은 소형 장치를 고려하십시오. 이런 경우 환자에게 무거운 배터리를 자주 들고 다니거나 자주 충전하도록 하여 환자에게 불편을 끼치고 싶지는 않을 것입니다. 배터리 수명을 연장하려면 가능한 한 적은 전력을 사용하는 것이 중요합니다.

게다가 온도는 매우 밀접하게 관련된 개념이다. 아래 그림에 있는 칩의 일반적인 패키징 다이어그램과 결합하면 일반적으로 200~400제곱밀리미터의 실리콘 다이가 있습니다. 다이는 칩 회로를 포함하는 직사각형 실리콘 블록을 나타냅니다. 이 작은 실리콘 조각은 60~100W의 전력(CFL 전구 6~10개에 해당)을 소비하므로 실리콘 다이를 냉각하기 위한 추가 조치를 취하지 않는 한 온도는 섭씨 200도까지 올라갈 수 있습니다. 먼저 소위 디퓨저라고 불리는 실리콘 다이에 5cm x 5cm 크기의 니켈 도금 구리판을 추가합니다. 디퓨저는 열을 확산시켜 금형에 균일한 온도 분포를 형성하는 데 도움을 주어 핫스팟을 제거합니다. 칩의 모든 부분이 동일한 양의 열을 발산하지 않기 때문에 디퓨저가 필요합니다. 예를 들어 ALU는 일반적으로 많은 열을 발산하는 반면 저장 요소는 상대적으로 차갑습니다. 둘째, 열 방출은 프로그램의 성격에 따라 달라집니다. 정수 벤치마크의 경우 부동 소수점 ALU는 유휴 상태일 때 더 시원합니다. 열이 실리콘 다이에서 디퓨저로 올바르게 흐르도록 하기 위해 열 인터페이스 재료(TIM)라고 하는 열 전도성 젤이 추가되는 경우가 많습니다.

대부분의 칩은 방열판 위에 방열판이 있는 구조를 갖고 있습니다. 위 그림과 같이 일련의 핀이 있는 구리 기반 구조입니다. 표면적을 늘리기 위해 일련의 핀이 추가되어 프로세서에서 발생하는 대부분의 열이 주변 공기로 분산될 수 있습니다. 데스크톱, 노트북, 서버에 사용되는 칩에는 라디에이터에 장착되거나 컴퓨터 케이스에 설치된 팬이 있어 라디에이터를 통해 공기를 불어넣어 뜨거운 공기는 배출되고 외부의 찬 공기는 라디에이터를 통해 흐릅니다. 방열판, 라디에이터 및 팬의 조합은 프로세서에서 발생하는 많은 열을 방출하는 데 도움이 됩니다.

고급 냉각 기술에도 불구하고 프로세서는 여전히 섭씨 60~100도에 도달할 수 있습니다. 대화형 컴퓨터 게임을 하거나 날씨 시뮬레이션과 같은 대규모 데이터 처리 애플리케이션을 실행할 때 칩의 온도는 섭씨 120도까지 올라갈 수 있습니다. 이는 물을 끓이고 야채를 요리하고 심지어 겨울에 작은 방을 따뜻하게 하는 데에도 충분합니다. 히터를 살 필요는 없고 컴퓨터만 돌리면 됩니다! 온도에는 많은 해로운 영향이 있습니다.

  • 온칩 구리 라인과 트랜지스터의 신뢰성은 온도가 증가함에 따라 기하급수적으로 감소합니다. 칩은 NBTI(음의 바이어스 온도 불안정성)라는 효과로 인해 시간이 지남에 따라 노화되는 경향이 있으며, 이는 트랜지스터의 속도를 효과적으로 저하시킵니다. 따라서 올바른 작동을 보장하려면 시간이 지남에 따라 프로세서의 주파수를 줄여야 합니다.

  • 일부 전력 소비 메커니즘(예: 누설 전력)은 온도에 따라 달라집니다. 즉, 온도가 증가하면 총 전력의 누설 구성 요소도 증가하여 온도가 더욱 높아집니다.

요약하면, 전기 요금 절감, 냉각 비용 절감, 배터리 수명 연장, 신뢰성 향상 및 노화 지연을 위해서는 칩의 전력과 온도를 줄이는 것이 중요합니다. 이제 주요 전력 소비 메커니즘을 빠르게 검토해 보겠습니다. 주로 동적 전력과 누설 전력이라는 두 가지 메커니즘에 중점을 둡니다. 누설 전력은 정적 전력이라고도 합니다.

먼저 동적전력에 대해 설명하겠습니다.

칩 패키지는 전력이 유입되고 열이 유출되는 폐쇄형 블랙박스로 생각할 수 있습니다. 오랜 시간 동안 칩에 흐르는 전기 에너지의 양은 에너지 보존 법칙에 따라 열로 소산되는 에너지의 양과 정확히 같습니다. I/O 링크를 따라 전기 신호를 보낼 때 손실되는 에너지는 여기서 무시되지만 전체 칩의 전력 소비에 비하면 무시할 수 있는 수준입니다.

트랜지스터와 구리선으로 구성된 모든 회로는 저항기, 커패시터, 인덕터가 있는 등가 회로로 모델링할 수 있습니다. 커패시터와 인덕터는 열을 방출하지 않지만 저항기는 저항기를 통해 흐르는 전기 에너지의 일부를 열로 변환합니다. 이는 등가 회로에서 전기 에너지가 열로 변환되는 유일한 메커니즘입니다.

이제 아래 그림과 같이 저항과 커패시터가 있는 작은 회로를 생각해 보십시오. 저항은 회로의 전선 저항을 나타내고 커패시터는 회로의 트랜지스터의 등가 커패시턴스를 나타냅니다. 트랜지스터의 게이트와 같은 회로의 다른 부분은 주어진 시점에서 특정 전위를 갖는다는 점에 유의하는 것이 중요합니다. 즉, 트랜지스터의 게이트가 커패시터 역할을 하여 전하를 저장한다는 의미입니다. 마찬가지로, 트랜지스터의 드레인과 소스는 동등한 드레인 및 소스 커패시턴스를 갖습니다. 대부분의 와이어는 일반적으로 짧고 인덕터로 기능하지 않기 때문에 등가 인덕턴스는 일반적으로 간단한 분석에서 고려되지 않습니다.

저항과 커패시터가 있는 회로.

소비되는 전력은 주파수와 공급 전압의 제곱에 비례합니다. 이 전력 소비는 입력 및 출력의 전환으로 인한 저항 손실을 나타내며 이를 동적 전력이라고 합니다. 그래서 다음이 있습니다:

\mathcal{P}_{d y n} \propto \sum_{i=1}^{n^{\prime} \alpha_{i} C_{i} V^{2} f

동적 전력은 회로에 있는 모든 트랜지스터의 입력 및 출력 전환으로 인해 소비되는 누적 전력입니다.

정적 전력(누설 전력)은 다음과 같습니다.

동적 전력은 프로세서의 유일한 전력 소비 메커니즘이 아닙니다. 정적 또는 누설 전력은 고성능 프로세서 전력 소비의 주요 구성 요소로 전체 프로세서 전력 예산의 약 20~40%를 차지합니다.

지금까지 우리는 트랜지스터가 오프 상태에 있을 때 전류 흐름을 허용하지 않고 커패시터 단자 사이 또는 NMOS 트랜지스터의 게이트와 소스 사이에 전류가 전혀 흐르지 않는다고 가정했지만 이러한 가정 중 어느 것도 엄격하게 정확하지 않습니다. 실제로 완벽한 절연체 구조는 없으며, 닫힌 상태에서도 단자를 통해 소량의 전류가 흐릅니다. 다른 인터페이스에는 이상적으로 전류를 통과시키지 않아야 하는 다른 누설 소스가 많이 있습니다. 이러한 전류원을 총칭하여 누설 전류라고 하며, 관련 전력 손실을 누설 전력이라고 합니다.

임계값 미만 누설, 게이트 유도 드레인 누설 등 누설 전력 소실에는 다양한 메커니즘이 있습니다. 연구원들은 일반적으로 BSIM3 모델에서 다음 방정식을 사용하여 누설 전력(주로 하위 임계값 누설 캡처)을 계산합니다.

\mathcal{P}_{l e a k}=A \times \nu_{T}^{2} \times e^{\frac{V_{G S}-V_{t h}-V_{o f f}{n \times \nu_{T&#125;&#125;\left(1-e^{\frac{-V_{D S}{\nu_{T&#125;&#125;\right)

그 중에는:

변수 정의

$A $ 면적 의존 비례 상수

$ \nu_{T}$ 열전압

k 볼츠만 상수

q1.6×1019

온도

$V_{G S} $ 게이트와 소스 사이의 전압

$ V_{t h}$ 임계 전압은 온도에 따라 달라집니다.

$V_{o f f} $ 잔류전압

n 역치 이하 스윙 계수

$ V_{D S}$ 드레인과 소스 사이의 전압

누설 전력은 변수 νT=kT/q를 통해 온도에 따라 달라집니다. 온도 의존성을 보여주기 위해 방정식을 단순화하여 다음 방정식을 얻을 수 있습니다.

PleakT2×eA/T×(1eB/T)

위의 공식에서 A와 B는 상수입니다. 약 20년 전, 트랜지스터 임계 전압이 높았던(약 500mV) 당시에는 누설 전력이 온도에 따라 기하급수적으로 증가했기 때문에 온도가 조금만 올라가도 누설 전력이 크게 증가했습니다. 그러나 오늘날의 임계 전압은 100~150mV 사이이므로 온도와 누출 간의 관계는 대략 선형이 됩니다.

누설 전력은 항상 회로의 모든 트랜지스터에 의해 소산된다는 점에 유의하는 것이 중요합니다. 누설 전류의 양은 작을 수 있지만 수십억 개의 트랜지스터의 누적 효과를 고려하면 소비되는 총 누설 전력의 양은 상당하며 동적 전력의 큰 부분이 될 수도 있습니다. 결과적으로 설계자는 누설 전력을 제어하기 위해 온도를 제어하려고 시도합니다. 총 전력은 다음과 같이 주어진다:

\mathcal{P}_{\text {tot }=\mathcal{P}_{\text {dyn }+\mathcal{P}_{\text {누출 }

다음으로 온도를 모델링해 보겠습니다.

칩의 온도를 모델링하는 것은 열역학과 열 전달에 대한 많은 배경 지식이 필요한 상당히 복잡한 문제입니다. 여기에는 기본적인 결과가 명시되어 있습니다. 실리콘 다이의 면적을 그리드로 나누고, 그리드 포인트에 1...m의 번호를 부여하고, 전력 벡터 Ptot가 각 그리드 포인트에서 소비되는 총 전력을 나타낸다고 가정하겠습니다. 마찬가지로, 각 그리드 점의 온도를 벡터 T로 표시합니다. 그리드 점 수가 많은 경우 전력과 온도는 일반적으로 다음 선형 방정식으로 관련됩니다.

TTamb=ΔT=A×Ptot
  • Tamb는 주변 온도라고 하며 주변 공기의 온도입니다.

  • A는 열 저항 매트릭스라고도 알려진 m*m 매트릭스입니다. 공식에 따르면 온도(T) 변화와 전력 소비량은 선형적으로 연관되어 있습니다.

\mathcal{P}_{\text {tot }=\mathcal{P}_{\text {dyn }+\mathcal{P}_{\text {leak }에서 \mathcal{P}_{\text {leak }는 온도의 함수이며 위 공식으로 피드백 루프를 형성합니다. 따라서 온도에 대한 초기값을 가정하고, 누설 전력을 계산하고, 새로운 온도를 추정하고, 누설 전력을 계산하고, 값이 수렴될 때까지 계속 반복해야 합니다.

19.5.4 첨단 기술

이 섹션에서는 프로세서 구현을 위한 고급 기술을 간략하게 소개합니다. 이 섹션은 결코 독립적이지 않습니다. 주요 목적은 독자에게 추가 학습을 위한 지침을 제공하는 것입니다. 이 섹션에서는 최첨단 프로세서가 채택한 극적으로 향상된 성능 기술의 몇 가지 광범위한 예를 설명합니다.

최신 프로세서는 일반적으로 매우 깊은 파이프라인(12~20단계)을 사용하여 동일한 주기에서 여러 명령을 실행하고 고급 기술을 사용하여 파이프라인의 충돌을 제거합니다. 몇 가지 일반적인 방법을 살펴보겠습니다.

19.5.4.1 분기 예측

IF 단계부터 시작하여 이를 더 잘 수행하는 방법을 살펴보겠습니다. 파이프라인에서 실행 중인 분기가 있는 경우 특히 IF 단계는 파이프라인에서 2주기 동안 일시 중지한 다음 분기 대상에서 가져오기를 시작해야 합니다. 더 많은 라인 단계를 추가하면 분기 페널티가 2사이클에서 20사이클 이상으로 증가하여 분기 명령이 매우 비싸고 성능이 심각하게 제한됩니다. 따라서 이미 사용된 분기에 대해서도 파이프라인 일시 중지를 방지해야 합니다.

가지의 방향을 예측할 수 있고, 가지의 목표도 예측할 수 있다면 어떨까요? 이 경우 가져오기 단위는 예측된 분기 대상에서 즉시 가져오기를 시작할 수 있습니다. 나중에 예측 오류가 발견되면 잘못 예측된 분기 명령 뒤의 모든 명령을 취소하고 파이프라인에서 폐기해야 합니다. 이런 종류의 명령어를 추측적 명령어라고도 합니다.

최신 프로세서는 일반적으로 분기 방향을 예측하고 이에 따라 예측된 분기 대상에서 시작하여 명령을 가져오고 나중에 분기 명령이 실행될 때 예측을 확인하는 등 예측을 기반으로 대규모 명령 세트를 실행합니다. 예측 오류가 발견되면 잘못 가져오거나 실행된 모든 명령이 파이프라인에서 삭제됩니다. 이러한 명령어를 추측적 명령어라고 합니다. 대조적으로, 올바르게 가져와서 실행되거나 예측이 검증된 명령어를 비추측 명령어라고 합니다.

추측성 명령어가 레지스터 파일을 변경하거나 메모리 시스템에 쓰는 것을 비활성화하는 것이 매우 중요하므로 명령어가 영구적인 변경을 허용하기 전에 추측적이지 않게 될 때까지 기다리십시오. 둘째, 비추측적일 때까지 파이프라인을 떠날 수 없지만, 추측적 명령어를 폐기해야 하는 경우 최신 파이프라인은 추측적 명령어를 파이프라인 버블로 선택적으로 변환하는 대신 일반적으로 잘못 예측된 분기 명령어 이후에 가져온 모든 명령어를 제거하는 더 간단한 메커니즘을 사용합니다. 이 간단한 메커니즘은 실제로 매우 잘 작동하며 이를 파이프라인 플러시라고 합니다.

최신 프로세서는 파이프라인의 모든 추측 명령어를 삭제하는 간단한 접근 방식을 채택하는 경우가 많습니다. 잘못 예측된 명령까지 모든 명령의 실행을 완료한 다음 전체 파이프라인을 정리하여 잘못 예측된 명령 이후에 가져온 모든 명령을 효과적으로 제거합니다. 이 메커니즘을 파이프라인 플러시라고 합니다.

이제 분기 예측의 주요 과제를 간략하게 설명합니다.

  • 명령어가 분기인 경우 가져오기 단계에서 먼저 읽어야 합니다. 분기인 경우 분기 대상의 주소를 읽어야 합니다.

  • 다음으로 예상되는 가지의 방향을 예측해야 한다.

  • 예측지시 결과에 대한 모니터링이 필요하다. 잘못된 예측이 있는 경우 나중에 파이프라인 플러시를 수행하여 모든 추론적 명령을 효과적으로 제거해야 합니다.

분기의 경우 잘못된 예측을 감지하는 것은 매우 간단합니다. 예측을 명령 패킷에 추가하고 실제 결과로 예측을 확인합니다. 서로 다른 경우 파이프라인 새로 고침을 예약하세요. 주요 과제는 분기 명령의 목표와 결과를 예측하는 것입니다.

최신 프로세서는 분기 대상 버퍼(BTB)라는 간단한 하드웨어 구조를 사용합니다. 이는 마지막 N(128~8192 범위) 분기 명령과 해당 대상의 프로그램 카운터를 보유하는 간단한 메모리 배열입니다. 프로그램은 일반적으로 어느 정도의 지역성을 나타내기 때문에 일치 항목을 찾을 확률이 높습니다. 즉, 루프와 같이 일정 기간 동안 동일한 코드 조각을 반복적으로 실행하는 경향이 있으므로 BTB의 항목은 짧은 시간 내에 재사용되는 경향이 있습니다. 일치하는 항목이 있으면 명령어가 분기라고 자동으로 추론할 수도 있습니다.

가지의 방향을 효과적으로 예측하는 것이 훨씬 더 어렵지만 나중에 설명하는 패턴을 활용하는 것은 가능합니다. 프로그램의 대부분의 분기는 일반적으로 루프 또는 if 문으로 이루어지며 양방향이 불가능합니다. 실제로 한 방향이 다른 방향보다 훨씬 더 가능성이 높습니다. 예를 들어 루프의 분기는 대부분의 시간을 차지합니다. 때때로 if 문은 특정 예외 조건이 true인 경우에만 평가되며, 대부분의 경우 이러한 if 문과 관련된 분기가 실행되지 않습니다. 마찬가지로, 대부분의 프로그램에서 설계자는 거의 모든 분기 명령이 한 방향으로 강하게 편향되거나 과거 기록을 기반으로 예측 가능하거나 다른 분기의 동작을 기반으로 예측 가능한 특정 패턴을 따른다는 것을 관찰합니다. 물론 이 진술에는 이론적인 근거가 없으며 단지 프로세서 설계자의 관찰일 뿐이므로 프로그램에서 이 패턴을 활용하도록 예측자를 설계했습니다.

이 섹션에서는 간단한 2비트 분기 예측기에 대해 설명합니다. 아래 그림과 같이 테이블의 각 분기에 2비트 값을 할당하는 분기 예측 테이블이 있다고 가정합니다.

값이 00이나 01이면 분기가 실행되지 않을 것으로 예측됩니다. 10 또는 11이면 예측 분기가 사용됩니다. 또한 분기가 수행될 때마다 해당 카운터는 1씩 증가하고 분기가 수행되지 않을 때마다 카운터는 1씩 감소합니다. 오버플로를 방지하기 위해 11은 1씩 증가하여 00을 생성하지 않으며, 00은 감소하여 11을 생성하지 않습니다. 우리는 포화 산술 규칙, 즉 (이진수): 11+1=11을 따릅니다. 001=00. 이 2비트 값을 2비트 포화 카운터라고 하며, 그 상태도는 아래 그림과 같습니다.

예측 분기에는 예측과 훈련이라는 두 가지 기본 작업이 있습니다. 분기를 예측하기 위해 분기 예측 테이블에서 해당 프로그램 카운터의 값을 조회합니다. 특정 경우에는 PC 주소의 마지막 n 비트를 사용하여 2n 항목 분기 예측 테이블에 액세스합니다. 2비트 포화 카운터의 값을 읽고 해당 값을 기준으로 분기를 예측합니다. 분기의 실제 결과가 얻어지면 포화 알고리즘을 사용하여 카운터 값을 증가시키거나 감소시키도록 훈련시킵니다.

이제 이 예측자가 어떻게 작동하는지 보려면 간단한 C 코드 조각과 이에 상응하는 어셈블리 코드를 고려해 보세요.

cpp
void main()
{
    foo();
    ...
    foo();
}

int foo() 
{
    int i, sum = 0
    for(i=0; i < 10; i++) 
    {
        sum = sum + i;
    }
    return sum;
}
cpp
.main:
    call .foo
    ...
    call .foo

.foo:
    mov r0, 0 /* sum = 0 */
    mov r1, 0 /* i = 0 */

.loop:
    add r0, r0, r1 /* sum = sum + i */
    add r1, r1, 1 /* i = i + 1 */
    cmp r1, 10 /* compare i with 10 */
    bgt .loop /* if(r1 > 10) jump to .loop */
    ret

루프 bgt.loop의 분기 문을 살펴보겠습니다. 마지막 반복을 제외한 모든 반복에 대해 분기가 실행됩니다. 상태 10에서 예측기를 시작하면 처음에 분기가 올바르게 예측되고(취해지며) 카운터가 증가하여 11이 되며, 모든 후속 반복에서 분기가 올바르게 예측됩니다. 그러나 마지막 반복에서는 취하지 않은 것으로 예측해야 하며, 잘못된 예측이 있으므로 2비트 카운터가 감소하고 10으로 설정됩니다. 이제 foo 함수가 다시 호출되고 2비트 카운터의 값이 10이고 분기 bgt.loop가 취해지는 것으로 올바르게 예측되는 상황을 고려하십시오.

따라서 2비트 카운터 방식은 예측 방식에 약간의 지연(또는 과거 이력)을 추가합니다. 분기가 역사적으로 한 방향이었다면 이상 현상으로 인해 예측이 변경되지 않습니다. 이 패턴은 이 간단한 예에서 볼 수 있듯이 루프에 매우 유용합니다. 분기 명령의 방향은 루프의 마지막 반복에서 항상 다르지만 다음 번 루프에 들어갈 때 이 예에서와 같이 분기가 올바르게 예측됩니다. 이는 패턴 중 하나일 뿐이며 최신 분기 예측기는 더 많은 유형의 패턴을 활용할 수 있습니다.

프로그램 최적화 팁:

  • 수행 중인 작업에 대해 가능한 한 많은 정보를 컴파일러에 제공합니다.

  • 가능하다면 상수와 지역변수를 사용하세요. 언어에서 허용하는 경우 프로토타입을 정의하고 정적 함수를 선언하세요.

  • 가능하면 포인터 대신 배열을 사용하십시오.

  • 불필요한 유형 변환을 피하고 부동 소수점에서 정수로의 변환을 최소화합니다.

  • 오버플로우와 언더플로우를 피하세요.

  • 적절한 데이터 유형(예: float, double, int)을 사용하십시오.

  • 나눗셈 대신 곱셈을 사용해 보세요.

  • 불필요한 가지를 모두 제거합니다.

  • 가능하면 재귀 대신 반복을 사용하십시오.

  • 가장 가능성이 높은 시나리오를 먼저 사용하여 조건문(예: if, switch, case)을 작성합니다.

  • 크기 순서대로 구조체에 변수를 선언하며, 가장 큰 변수가 먼저 선언됩니다.

  • 프로그램에 성능 문제가 있는 경우 최적화를 시작하기 전에 프로그램을 프로파일링합니다. (프로파일링은 코드를 작은 덩어리로 나누고 각 덩어리의 시간을 측정하여 가장 많은 시간이 걸리는 덩어리를 결정하는 프로세스입니다.)

  • 순수 성능에만 기반한 알고리즘을 포기하지 마십시오. 공정한 비교는 모든 알고리즘이 완전히 최적화된 경우에만 이루어질 수 있습니다.

  • 프로시저 인라인, 함수 호출을 함수 본문으로 대체, 프로시저의 매개변수를 호출자의 매개변수로 대체.

  • 루프 오버헤드를 줄이고, 메모리 액세스를 개선하며, 하드웨어를 보다 효율적으로 활용할 수 있는 루프 변환.

루프 풀기. 루프 풀기의 최적화는 전통적으로 For 문으로 제어되는 것과 같이 여러 반복을 수행하는 루프에서 유용한 경우가 많습니다. 루프 풀기에는 루프를 만들고, 본문을 여러 번 복제하고, 변환된 루프가 실행되는 횟수를 줄이는 작업이 포함됩니다. 루프 풀기는 루프 오버헤드를 줄이고 다른 많은 최적화 기회를 제공합니다.

  • 더 나은 메모리 동작을 위해 중첩 루프 교체 및 블로킹 루프와 같은 복잡한 루프 변환.

  • 로컬 및 글로벌 최적화. 로컬 및 글로벌 최적화 전용 프로세스에서는 다음 최적화가 수행됩니다.

로컬 최적화는 단일 기본 블록 내에서 작동합니다. 로컬 최적화 단계는 전역 최적화 전후에 코드를 "정리"하기 위해 전역 최적화의 전 단계와 이후에 실행되는 경우가 많습니다.

  • 전역 최적화는 여러 기본 블록에서 작동합니다.

  • 전역 레지스터 할당은 코드 영역의 레지스터에 변수를 할당합니다. 레지스터 할당은 최신 프로세서에서 우수한 성능을 얻는 데 매우 중요합니다.

  • 보다 구체적으로 공통 하위식 제거, 강도 감소, 상수 전파, 복사 전파, 데드 스토어 제거 등의 작업이 있습니다.

두 비트가 사용되는 경우 해당 명령어 실행의 마지막 두 인스턴스 결과를 기록하거나 상태를 기록하는 데 사용할 수 있습니다. 아래 그림은 알고리즘이 순서도의 왼쪽 상단에서 시작한다고 가정하는 일반적인 접근 방식을 보여줍니다. 각 후속 조건 분기 명령이 실행되는 한 의사 결정 프로세스는 다음 분기가 실행될 것이라고 예측합니다. 단일 예측이 잘못된 경우 알고리즘은 실행될 다음 분기를 계속해서 예측합니다. 알고리즘은 두 개의 연속 분기가 수행되지 않은 경우에만 순서도의 오른쪽으로 이동합니다. 그러면 알고리즘은 연속된 두 분기가 수행될 때까지 분기가 수행되지 않을 것이라고 예측하므로 알고리즘은 예측 결정을 변경하려면 두 개의 연속 잘못된 예측이 필요합니다.

분기 예측 흐름도.

예측 과정은 아래 그림과 같이 유한 상태 기계로 보다 간결하게 표현될 수 있으며, 많은 문헌에서는 유한 상태 기계를 사용하는 경우가 많습니다.

분기 예측 상태 다이어그램.

아래 차트는 이 시나리오를 전혀 수행되지 않은 예측 전략과 비교합니다. 이전 전략을 사용하면 명령어 가져오기 단계에서는 항상 다음 순차 주소를 가져옵니다. 분기가 수행되면 프로세서의 일부 로직이 이를 감지하고 다음 명령어를 대상 주소에서 가져오도록 지시합니다(파이프라인을 플러시하는 것 외에도). 분기 기록 테이블은 캐시로 처리되며 각 프리페치는 분기 기록 테이블에서 조회를 트리거합니다. 일치하는 항목이 없으면 다음 순차 주소를 페칭에 사용하고, 일치하는 항목이 있으면 명령어 상태를 기반으로 예측이 이루어집니다. 다음 순차 주소 또는 분기 대상 주소가 선택 논리에 공급됩니다.

종속성을 보완하기 위해 분기 명령을 먼저 고려하는 코드 재구성 기술이 개발되었습니다. 지연 분기는 파이프라인 효율성을 향상시키는 방법입니다. 사용하는 분기는 다음 명령이 실행될 때까지 적용되지 않습니다(따라서 이름이 지연됨). 분기 직후의 명령 위치를 지연 슬롯이라고 합니다. 이 이상한 과정은 아래 표에 설명되어 있습니다. "Normal Branch"라는 열에는 일반적인 기호 명령 기계어 프로그램이 있습니다. 102를 실행한 후 다음으로 실행될 명령은 105입니다. 파이프라인을 표준화하려면 이 분기 뒤에 NOOP를 삽입하세요. 그러나 101번과 102번의 명령어를 바꾸면 성능이 향상될 수 있다.

아래 이미지는 결과를 보여줍니다. 그림 A는 전통적인 파이프라인 접근 방식을 보여줍니다. JUMP 명령어는 4번 시점에서 인출되고, 5번 시점에서는 103번 명령어(ADD 명령어)가 인출되는 것과 동시에 JUMP 명령어가 실행된다. JUMP가 발생했기 때문에 프로그램 카운터를 업데이트했기 때문에 파이프라인의 103번 명령어를 클리어해야 하고, 6번 시점에서는 JUMP의 대상이었던 105번 명령어가 로드된다.

그림 b는 일반적인 RISC 조직에서 처리하는 동일한 파이프라인을 보여줍니다. 타이밍은 동일하지만 NOOP 명령이 삽입되기 때문에 파이프라인을 클리어하는 데 특별한 회로가 필요하지 않으며 아무런 효과도 없이 단순히 NOOP가 실행됩니다.

그림 c는 지연 분기의 사용을 보여줍니다. JUMP 명령은 시간 3에서 얻은 ADD 명령보다 먼저 시간 2에서 얻습니다. 그러나 ADD 명령은 JUMP 명령이 프로그램 카운터를 변경할 기회를 갖기 전에 얻어집니다. 따라서 시간 4 동안 ADD 명령은 명령 105를 가져오는 것과 동시에 실행되어 프로그램의 원래 의미를 유지하지만 실행하는 데 두 개의 더 적은 클록 사이클이 필요합니다.

19.5.4.2 지연 로딩

지연된 분기와 같은 전략을 지연된 로드라고 하며 명령을 로드하는 데 사용할 수 있습니다. LOAD 명령어에서는 로드 대상이 될 레지스터가 프로세서에 의해 잠깁니다. 그런 다음 프로세서는 해당 레지스터가 필요한 명령에 도달할 때까지 명령 스트림을 계속 실행하며, 이 시점에서 로드가 완료될 때까지 유휴 상태가 됩니다. 로딩 프로세스 중에 유용한 작업이 수행되도록 컴파일러가 명령을 재배열할 수 있다면 효율성이 높아질 것입니다.

19.5.4.3 루프 확장

명령어 병렬성을 향상시키는 또 다른 컴파일러 기술은 루프 언롤링입니다. 언롤링은 언롤링 요소(u)라고 하는 루프 본문을 여러 번 복사하고 1단계 대신 u단계에서 반복합니다.

  • 루프 오버헤드 감소

  • 파이프라인 성능을 향상하여 명령 병렬성을 향상시킵니다.

  • 레지스터, 데이터 캐시 또는 TLB 위치 개선

아래 이미지는 한 가지 예에서 세 가지 개선 사항을 모두 보여줍니다. 루프 종료 시 테스트 및 분기 전에 두 번의 반복이 수행되므로 루프 오버헤드가 절반으로 줄어듭니다. 첫 번째 할당의 결과가 저장되고 루프 변수가 업데이트되는 동안 두 번째 할당을 수행할 수 있으므로 명령어 병렬성이 향상됩니다. a[i] 및 a[i+1]이 루프 본문에서 두 번 사용되어 반복당 로드 수가 3에서 2로 줄어들기 때문에 배열 요소가 레지스터에 할당되면 레지스터 지역성이 향상됩니다.

명령어 파이프라인의 설계는 시스템에 적용되는 다른 최적화 기술과 분리되어서는 안 됩니다. 예를 들어 파이프라인의 명령어 스케줄링과 레지스터의 동적 할당은 최대 효율성을 달성하기 위해 함께 고려해야 합니다.

19.5.4.4 순차적 파이프라인 문제

간단한 파이프라인에서는 사이클당 하나의 명령만 실행되지만 이것이 반드시 필요한 것은 아닙니다. 우리는 두 개의 병렬 파이프라인을 갖춘 원래 Intel Pentium과 같은 프로세서를 설계할 수 있습니다. 프로세서는 한 주기에 두 개의 명령을 동시에 실행할 수 있습니다. 이러한 파이프라인에는 추가 기능 단위가 있으므로 두 파이프라인의 명령을 큰 구조적 충돌 없이 실행할 수 있습니다. 이 전략은 IPC를 증가시키지만 프로세서를 더욱 복잡하게 만듭니다. 이러한 프로세서는 동일한 주기에서 실행 장치에 여러 명령이 발행될 수 있기 때문에 여러 순차 실행 파이프라인을 포함한다고 합니다. 사이클당 여러 명령을 실행할 수 있는 프로세서를 수퍼스칼라 프로세서라고도 합니다.

둘째, 이러한 유형의 프로세서는 프로그램 순서(명령어의 동적 인스턴스가 프로그램에 나타나는 대로 실행되는 순서)에 따라 명령을 실행하기 때문에 순차 프로세서라고 합니다. 예를 들어, 단일 주기 프로세서 또는 파이프라인 프로세서는 프로그램 순서대로 명령을 실행합니다.

사이클당 여러 명령을 실행할 수 있는 프로세서를 수퍼스칼라 프로세서라고 합니다.

순차 프로세서는 프로그램 순서대로 명령어를 실행합니다. 프로그램 순서는 명령의 동적 인스턴스 순서로 정의되며, 프로그램의 각 명령이 순차적으로 실행될 경우 인식되는 순서와 동일합니다.

수퍼스칼라 조직과 일반 스칼라 조직의 비교.

수퍼스칼라 방법과 슈퍼파이프라인 방법의 비교.

수퍼스칼라 처리의 개념적 설명입니다.

이제 두 파이프라인 간의 종속성과 잠재적인 충돌을 찾아야 합니다. 둘째, 결과가 두 파이프라인 모두에서 전달될 수 있기 때문에 전달 논리도 훨씬 더 복잡합니다. 인텔이 출시한 최초의 펜티엄 프로세서에는 U 파이프라인과 V 파이프라인이라는 두 개의 파이프라인이 있었습니다. U 파이프라인은 모든 명령을 실행할 수 있는 반면, V 파이프라인은 간단한 명령으로 제한됩니다. 명령어는 2개 명령어 번들로 가져오며, 번들의 이전 명령어는 U-파이프라인으로 전송되고 다음 명령어는 V-파이프라인으로 전송됩니다. 이 전략을 사용하면 이러한 명령을 병렬로 실행할 수 있습니다.

원래 펜티엄 프로세서의 두 파이프라인인 U와 V를 사용하는 간단한 프로세서를 개념적으로 설계해 보겠습니다. 우리는 2개의 명령어 번들을 형성하고 동시 실행을 위해 두 파이프라인으로 전송되는 결합된 명령어와 피연산자 가져오기 장치를 구상합니다. 그러나 명령어가 특정 제약 조건을 충족하지 않으면 장치는 1개의 명령어 번들을 구성하여 U 파이프라인으로 보냅니다. 이러한 번들을 생성할 때마다 광범위하게 따를 수 있는 몇 가지 일반 규칙이 있습니다. RAW 종속성이 있는 두 가지 명령은 피해야 하며, 이 경우 파이프라인이 중단됩니다.

둘째, EX 단계가 끝날 때까지 종속성을 발견할 수 없으므로 메모리 명령에 특별한 주의를 기울여야 합니다. 명령어 묶음의 첫 번째 명령어는 저장 명령어이고 두 번째 명령어는 로드 명령어이며 우연히 동일한 메모리 주소에 액세스한다고 가정합니다. 이러한 상황은 EX 단계가 끝날 때 감지되어야 하며, 스토어에서 로드로 값이 전달되어야 합니다. 반대의 경우, 첫 번째 명령어가 로드 명령어이고 두 번째 명령어가 동일한 주소에 대한 저장인 경우 로드가 완료될 때까지 저장 명령어를 일시 중지해야 합니다. 명령어 번들의 두 명령어가 동일한 주소에 저장되면 이전 명령어는 중복되며 nop로 변환될 수 있습니다. 따라서 이러한 규칙을 준수하고 복잡한 연동 및 전달 논리를 갖는 프로세서를 설계해야 합니다.

간단한 예가 아래에 나와 있습니다. 파이프라인에 2가지 문제가 있다고 가정하고 다음 어셈블리 코드에 대한 파이프라인 다이어그램을 그립니다.

cpp
[1]: add r1, r2, r3
[2]: add r4, r5, r6
[3]: add r9, r8, r8
[4]: add r10, r9, r8
[5]: add r3, r1, r2
[6]: ld r6, 10[r1]
[7]: st r6, 10[r1]

여기서 파이프라인 다이어그램에는 두 명령이 동시에 한 단계에 있을 수 있으므로 각 단계에 대해 두 개의 항목이 포함되어 있습니다. 먼저 명령어 [1]과 [2]는 병렬로 실행될 수 있지만 명령어 [3]과 [4]는 병렬로 실행될 수 없다는 점을 관찰합니다. 명령어 [3]은 r9를 쓰는 반면 명령어 [4]는 r9를 소스 피연산자로 갖기 때문입니다. r9 값은 EX 단계에서 생성되고 EX 단계에서 필요하기 때문에 동일한 주기에서 이 두 명령을 실행할 수 없습니다. [4]와 [5]를 계속해서 병렬로 실행하고, 명령 [4]의 경우 전달을 사용하여 r9 값을 얻을 수 있습니다. 마지막으로 명령 [6]과 [7]을 병렬로 실행할 수 없습니다. 두 명령은 동일한 메모리 주소에 액세스하고 저장이 시작되기 전에 로드가 완료되어야 하므로 또 다른 버블이 삽입됩니다. 파이프라인 다이어그램은 다음과 같습니다.

19.5.4.5 EPIC 및 VLIW 프로세서

이제 하드웨어 대신 소프트웨어로 명령어 번들을 준비할 수 있습니다. 컴파일러는 코드에 대한 가시성이 훨씬 뛰어나며 광범위한 분석을 수행하여 다중 명령 번들을 생성할 수 있습니다. Intel과 HP가 설계한 Itanium 프로세서는 유사한 원리를 기반으로 하는 매우 고전적인 프로세서입니다. EPIC과 VLIW라는 용어를 정의하는 것부터 시작하겠습니다.

VLIW(Very Long Instruction Word, Very Long Instruction Word): 컴파일러에서 생성된 명령어 번들 간에는 종속성이 없습니다. 하드웨어는 각 번들의 명령을 병렬로 실행합니다. 정확성에 대한 전적인 책임은 컴파일러에게 있습니다.

EPIC(Explicitly Parallel Instruction Computing, Explicitly Parallel Instruction Computing): 이 패러다임은 VLIW 컴퓨팅을 확장하지만 이 경우 컴파일러가 어떤 코드를 생성하든 하드웨어는 올바른 실행을 보장합니다.

EPIC/VLIW 프로세서는 프로그램을 분석하고 명령어 묶음을 생성하기 위해 매우 똑똑한 컴파일러가 필요합니다. 프로세서에 4개의 파이프라인이 있는 경우 각 번들에는 4개의 명령어가 포함됩니다. 컴파일러는 번들의 명령어 간에 종속성이 없도록 번들을 생성합니다. EPIC/VLIW 프로세서 설계의 더 넓은 목표는 모든 복잡성을 소프트웨어로 전환하는 것입니다. 컴파일러는 프로세서에 필요한 연동, 전달 및 명령 처리 논리를 최소화하는 방식으로 번들을 배열합니다.

그러나 돌이켜보면 이러한 프로세서는 하드웨어가 설계자가 원래 계획한 것만큼 단순할 수 없었기 때문에 약속을 지키지 못했습니다. 고성능 프로세서에는 여전히 상당히 복잡한 하드웨어가 필요하며 일부 복잡한 아키텍처 기능이 필요합니다. 이러한 기능은 하드웨어 복잡성과 전력 소비를 증가시킵니다.

19.5.4.6 순서가 잘못된 파이프라인

지금까지 우리는 프로그램에 나타나는 순서대로 명령어를 실행하는 순차 파이프라인에 대해 주로 생각해 왔지만 꼭 필요한 것은 아닙니다. 다음 코드 조각을 고려해보세요.

cpp
[1]: add r1, r2, r3
[2]: add r4, r1, r1
[3]: add r5, r4, r2
[4]: mul r6, r5, r2
[5]: div r8, r9, r10
[6]: sub r11, r12, r13

위 코드에서는 데이터 종속성으로 인해 명령어 1~4를 순서대로 실행하도록 제한되어 있습니다. 그러나 명령어 5와 6은 명령어 1~4에 종속되지 않으므로 병렬로 실행될 수 있습니다. 명령 5, 6이 순서 없이 실행되더라도 정확성은 희생되지 않습니다. 예를 들어, 한 주기에 두 개의 명령어를 실행할 수 있는 경우 (1, 5)를 함께 커밋할 수 있고, 그 다음 (2, 6), 마지막으로 명령어 3과 4를 커밋할 수 있습니다. 이 경우 처음 두 사이클에서 2개의 명령을 실행함으로써 6개의 명령 시퀀스를 4개의 사이클에서 실행할 수 있습니다. 사이클당 여러 명령을 실행할 수 있는 프로세서는 수퍼스칼라 프로세서입니다.

프로그램 순서와 일치하지 않는 순서로 명령을 실행할 수 있는 프로세서를 OOO(Out-of-order) 프로세서라고 합니다.

수퍼스칼라 명령어 실행 및 완료 전략.

완전한 비순차적 실행 메커니즘을 갖추고 있습니다.

OOO(Out-of-order) 프로세서는 명령어를 순서대로 가져오고, 가져오기 단계 후에 명령어 디코딩을 진행합니다. 대부분의 실제 명령어에는 두 개 이상의 디코드 주기가 필요하며 이러한 명령어는 ROB(재순서 버퍼)라는 대기열에 프로그램 순서대로 동시에 추가됩니다. 명령어를 디코딩한 후 레지스터 이름 바꾸기라는 단계를 수행해야 합니다. 일반적인 개념은 다음과 같습니다. 실행된 명령이 순서에 맞지 않기 때문에 WAR과 WAW 간에 충돌이 발생할 수 있습니다. 다음 코드 조각을 고려해보세요.

cpp
[1]: add r1, r2, r3
[2]: sub r4, r1, r2
[3]: add r1, r5, r6
[4]: add r9, r1, r7

명령 [3]과 [4]가 명령 [1]보다 먼저 실행되면 잠재적인 WAW 충돌이 발생합니다. 명령 [1]이 명령 [3]에 의해 작성된 r1 값을 덮어쓰고 이로 인해 잘못된 실행이 발생할 수 있기 때문입니다. 따라서 우리는 이러한 충돌을 제거하기 위해 레지스터 이름을 바꾸려고 노력합니다. 대부분의 최신 프로세서는 소프트웨어(어셈블러)에 노출된 것과 동일한 레지스터인 아키텍처 레지스터 세트로 설계되었습니다. 또한 내부에서만 볼 수 있는 물리적 레지스터 세트가 있으며 이름 변경 단계에서는 아키텍처 레지스터 이름을 물리적 레지스터 이름으로 변환하여 WAR 및 WAW 충돌을 제거합니다. 위 코드에 남아 있는 유일한 충돌은 RAW 충돌입니다. 이는 실제 데이터 종속성이 있음을 나타냅니다. 따라서 물리적 레지스터가 p1…p128 범위에 있다고 가정하면 이름이 변경된 코드 세그먼트는 다음과 같습니다.

cpp
[1]: add p1, p2, p3 /* p1 contains r1 */
[2]: sub p4, p1, p2
[3]: add p100, p5, p6 /* r1 is now begin saved in p100 */
[4]: add p9, p100, p7

우리는 명령어 3의 r1을 p100에 매핑하여 WAW 충돌을 제거했습니다. 유일한 기존 종속성은 명령 [1] --> [2]와 [3] --> [4] 사이의 RAW 종속성입니다. 이름이 변경된 명령어가 명령어 창에 들어갑니다. 지금까지의 지침은 순차적으로 처리되었습니다.

명령 창 또는 명령 대기열에는 일반적으로 64-128개의 항목이 포함됩니다(아래 그림 참조). 각 명령어는 소스 피연산자를 모니터링합니다. 명령어의 모든 소스 피연산자가 준비되어 있는 한 명령어는 해당 기능 단위에 제출될 수 있습니다. 명령어는 항상 물리적 레지스터 파일에 액세스할 필요는 없지만 전달 경로에서 값을 가져올 수도 있습니다. 명령어 실행이 완료된 후 결과 값이 명령어 창의 대기 명령어로 브로드캐스팅됩니다. 결과를 기다리는 명령어는 해당 소스 피연산자를 준비됨으로 표시합니다. 이 프로세스를 명령어 웨이크업이라고 합니다. 이제 동일한 사이클에 여러 명령을 준비할 수 있으며 구조적 충돌을 피하기 위해 명령 선택 장치는 실행할 명령 세트를 선택합니다.

로드 및 저장 명령을 위해서는 프로그램 순서대로 로드 및 저장 목록을 보유하는 로드-스토어 큐라는 또 다른 구조가 필요하며, 동일한 주소에 이전 스토어가 있는 경우 로드가 내부 전달 메커니즘을 통해 해당 값을 가져올 수 있도록 합니다.

명령어 실행이 완료된 후 해당 항목을 재정렬 버퍼에 표시하고 명령어는 프로그램 순서대로 재정렬 버퍼를 떠납니다. 어떤 이유로 명령어가 빠르게 완료되지 않으면 재정렬 버퍼의 모든 명령어를 일시 중지해야 합니다. 재정렬 버퍼의 명령어 항목은 프로그램 순서대로 정렬되며 명령어는 정확한 예외를 보장할 수 있도록 재정렬 버퍼를 프로그램 순서로 유지해야 한다는 점을 기억하세요.

요약하자면, OOO(비순차적 프로세서)의 주요 장점은 명령 간의 RAW 종속성 없이 명령을 병렬로 실행할 수 있다는 것입니다. 대부분의 프로그램에는 일반적으로 대부분의 시점에 이러한 명령 세트가 있습니다. 이 속성을 ILP(명령 수준 병렬성)라고 하며 최신 OOO 프로세서는 ILP를 최대한 활용하도록 설계되었습니다.

19.5.4.7 마이크로 작업

프로그램을 실행할 때 컴퓨터의 작업은 사이클당 하나의 기계 명령이 있는 일련의 명령 사이클로 구성됩니다. 분기 명령어가 있기 때문에 이 명령어 사이클 시퀀스는 프로그램을 구성하는 명령어 시퀀스와 반드시 동일하지는 않습니다. 여기서 말하는 것은 명령어의 실행 시간 순서이다.

각 명령 주기는 여러 개의 작은 단위로 구성되며, 그 중 편리한 하위 단위는 가져오기, 간접 참조, 실행 및 인터럽트이며 항상 가져오기 및 실행 주기만 발생합니다. 다만, 제어장치를 설계하기 위해서는 기술의 추가적인 세분화가 필요하며, 추가적인 세분화가 가능하다. 실제로 우리는 각각의 작은 사이클이 일련의 단계를 포함하고 각 단계에는 프로세서 레지스터가 포함된다는 것을 알게 될 것입니다. 이러한 단계를 마이크로 작업이라고 합니다.

접두사 micro는 각 단계가 매우 간단하고 완료되는 작업이 거의 없음을 의미합니다. 다음 그림에서는 다양한 개념 간의 관계를 설명합니다. 요약하면, 프로그램 실행은 명령어의 순차적 실행으로 구성되며 각 명령어는 더 짧은 하위 주기(예: 가져오기, 간접, 실행, 인터럽트)로 구성된 명령어 주기 내에서 실행됩니다. 각 하위 주기의 실행에는 마이크로 작업이라고 하는 하나 이상의 짧은 작업이 포함됩니다.

프로그램 실행의 구성요소.

마이크로 연산은 프로세서의 기능적 연산 또는 원자적 연산입니다. 이 섹션에서는 마이크로 연산을 검토하여 명령 주기의 이벤트가 이러한 마이크로 연산의 시퀀스로 어떻게 설명될 수 있는지 확인합니다. 간단한 예를 사용하여 마이크로 연산의 개념이 제어 장치 설계의 가이드 역할을 할 수 있는 방법을 보여줍니다.

먼저 Fetch Cycle에 대해 설명하겠습니다.

페치 사이클은 각 명령어 사이클의 시작 부분에 발생하며 4개의 레지스터를 포함하는 메모리에서 명령어를 페치하게 됩니다.

  • MAR(Memory Address Register): 시스템 버스에 연결된 주소 라인. 읽기 또는 쓰기 작업을 위해 메모리의 주소를 지정합니다.

  • 메모리 버퍼 레지스터(MBR): 시스템 버스에 연결된 데이터 라인. 여기에는 메모리에 저장할 값이나 메모리에서 마지막으로 읽은 값이 포함됩니다.

  • 프로그램 카운터(PC): 가져올 다음 명령어의 주소를 보유합니다.

  • 명령어 레지스터(IR): 마지막으로 가져온 명령어를 저장합니다.

프로세서 레지스터에 미치는 영향의 관점에서 가져오기 주기의 이벤트 순서를 살펴보겠습니다. 아래 그림에 예시가 나와 있습니다. 페치 사이클이 시작될 때 실행될 다음 명령어의 주소는 프로그램 카운터(PC)에 있으며 주소는 1100100입니다.

  • 첫 번째 단계는 이 주소를 메모리 주소 레지스터(MAR)로 이동하는 것입니다. 이는 시스템 버스 주소 라인에 연결된 유일한 레지스터이기 때문입니다.

  • 두 번째 단계는 명령을 도입하는 것입니다. 필요한 주소(MAR에 있음)가 주소 버스에 배치되고, 제어 장치가 제어 버스에서 READ 명령을 발행하고, 결과가 데이터 버스에 나타나고 메모리 버퍼 레지스터(MBR)에 복사됩니다. 또한 다음 명령을 준비할 수 있도록 PC를 명령 길이만큼 늘려야 합니다. 이 두 가지 작업(메모리에서 단어 읽기, PC 증가)은 서로 간섭하지 않으므로 동시에 수행하여 시간을 절약할 수 있습니다.

  • 세 번째 단계는 MBR의 내용을 명령어 레지스터(IR)로 이동하는 것입니다. 그러면 가능한 간접 루프 동안 사용할 수 있도록 MBR이 해제됩니다.

이벤트 순서, 획득 주기.

따라서 간단한 추출 주기는 실제로 3단계와 4개의 마이크로 작업으로 구성됩니다. 각 마이크로 작업에는 레지스터 안팎으로 데이터를 이동하는 작업이 포함됩니다. 이러한 작업이 서로 간섭하지 않는 한 한 단계에서 여러 작업을 수행할 수 있으므로 시간이 절약됩니다. 상징적으로 이 일련의 사건을 다음과 같이 작성할 수 있습니다.

여기서 I는 명령 길이입니다. 클록이 타이밍 목적으로 사용될 수 있고 규칙적인 간격의 클록 펄스를 방출한다는 가정 하에 이 시퀀스에 대해 몇 가지 설명이 필요합니다. 각 클록 펄스는 시간 단위를 정의하므로 모든 시간 단위는 동일한 지속 시간을 갖습니다. 각 마이크로 연산은 단일 시간 단위로 실행될 수 있으며, 기호(t1, t2, t3)는 연속된 시간 단위를 나타냅니다. 즉, 우리는 다음을 갖습니다:

  • 최초 단위 : PC에서 MAR로 콘텐츠를 이동합니다.

  • 초 시간 단위: MAR에서 지정한 메모리 위치의 내용을 MBR로 이동합니다. I를 누르면 PC의 내용이 증가합니다.

  • 세 번째 시간 단위: MBR의 내용을 IR로 이동합니다.

두 번째 및 세 번째 micro-op은 모두 두 번째 시간 단위 동안 발생하며, 세 번째 micro-op은 가져오기 작업에 영향을 주지 않고 네 번째 micro-op과 그룹화될 수 있습니다.

마이크로 작업 그룹화는 두 가지 간단한 규칙을 따라야 합니다.

  • 올바른 이벤트 순서를 따라야 합니다. 따라서 메모리 읽기 작업은 MAR의 주소를 사용하므로 (MAR <--(PC))가 (MBR <-- 메모리)보다 앞에 나와야 합니다.

  • 갈등은 피해야 한다. 결과를 예측할 수 없기 때문에 동일한 레지스터를 한 시간 단위로 읽거나 쓰려고 시도해서는 안 됩니다. 마이크로 연산(MBR dMemory)과 (IR dMBR)은 동일한 시간 단위로 발생해서는 안 됩니다.

마지막으로 주목할 만한 점은 마이크로 작업 중 하나에 추가가 포함된다는 것입니다. 회로 중복을 피하기 위해 이 추가 작업은 ALU에 의해 수행될 수 있습니다. ALU를 사용하려면 ALU의 기능과 프로세서 구성에 따라 추가적인 마이크로 작업이 필요할 수 있습니다.

이 밖에도 간접주기(Indirect Cycle), 인터럽트 주기(Interrupt Cycle), 실행주기(Execute Cycle), 명령주기(Instruction Cycle) 등에도 유사한 메커니즘과 원리가 관련되어 있다.

명령주기 흐름도.

19.5.5 상용 프로세서 사례

이제 지금까지 배운 모든 개념을 실용적인 관점으로 적용할 수 있도록 일부 실제 프로세서의 설계를 살펴보겠습니다. 이후에는 ARM, AMD, Intel 등 3대 프로세서 회사의 임베디드(소형 모바일 기기용) 프로세서와 서버 프로세서에 대해 공부하겠습니다. 이 섹션의 목적은 세 회사의 프로세서 디자인이나 심지어 같은 회사의 다른 모델을 비교하고 대조하는 것이 아닙니다. 각 프로세서는 특정 시장 부문에 최적화되어 있으며 특정 주요 비즈니스 결정을 고려합니다. 따라서 이 섹션에서는 기술적인 관점에서 디자인을 연구하고 그 뉘앙스를 이해하는 데 중점을 둡니다.

RISC 기계에 대한 초기 열정 이후 다음과 같은 인식이 커졌습니다.

  • RISC 설계는 일부 CISC 기능을 포함함으로써 이점을 얻을 수 있습니다.

  • CISC 설계에는 일부 RISC 기능을 포함하면 이점을 얻을 수 있습니다. 결과적으로 최신 RISC 디자인, 특히 PowerPC는 더 이상 "순수한" RISC가 아니며, 최신 CISC 디자인, 특히 Pentium II 및 이후 Pentium 모델에는 일부 RISC 기능이 통합되어 있습니다.

아래 표에서는 일부 프로세서를 나열하고 여러 기능을 비교합니다. 비교를 위해 일반적인 RISC는 다음과 같습니다.

  • 단일 명령 크기.

  • 크기는 일반적으로 4바이트입니다.

  • 일반적으로 5개 미만의 소수의 데이터 주소 지정 모드는 식별하기 어렵습니다. 표에서 레지스터 및 리터럴 모드는 계산되지 않으며 오프셋 크기가 다른 다양한 형식은 별도로 계산됩니다.

  • 메모리에 있는 다른 피연산자의 주소를 얻기 위해 메모리에 액세스할 필요 없이 간접 주소 지정.

  • 산술 연산과 로드/저장을 결합하는 작업이 없습니다(예: 메모리에서 추가, 메모리에 추가).

  • 명령어 당 하나 이상의 메모리 주소 지정 피연산자를 초과할 수 없습니다.

  • 로드/저장 작업에 대한 데이터의 임의 정렬은 지원되지 않습니다.

  • MMU(Memory Management Unit)의 명령어에서 데이터 주소를 사용하는 최대 횟수입니다.

  • 정수 레지스터 지정자의 비트 수는 5보다 크거나 같습니다. 이는 적어도 32개의 정수 레지스터가 한 번에 명시적으로 참조될 수 있음을 의미합니다.

  • 부동 소수점 레지스터 지정자의 비트 수는 4보다 크거나 같습니다. 이는 최소 16개의 부동 소수점 레지스터가 한 번에 명시적으로 참조될 수 있음을 의미합니다.

19.5.5.1 ARM 프로세서

ARM은 원래 Acorn 컴퓨터의 프로세서였으므로 원래 이름은 Acorn RISC Machine이고 Berkeley RISC 문서는 아키텍처에 영향을 미쳤습니다. 가장 중요한 초기 응용 프로그램 중 하나는 Acorn 컴퓨터에 많은 소프트웨어를 제공하도록 설계된 16비트 마이크로프로세서 AM 6502의 에뮬레이션이었습니다. 6502에는 바이트의 배수인 가변 길이 명령어 세트가 있으므로 6502 에뮬레이션은 ARMv7 명령어 세트의 이동 및 마스킹에 대한 강조를 설명하는 데 도움이 됩니다. 저전력 임베디드 컴퓨터로서의 인기는 불운한 Apple Newton 개인용 디지털 단말기의 프로세서로 선택되면서 시작되었습니다.

Newton은 Apple이 기대했던 만큼 인기가 없었지만 Apple의 축복 덕분에 초기 ARM 명령어 세트가 인기를 얻었고 이후 휴대폰을 포함한 여러 시장에서 인기를 얻었습니다. Newton의 경험과는 달리 휴대폰의 놀라운 성공은 2014년에 120억 개의 ARM 프로세서가 출시된 이유를 설명합니다. ARM 역사상 주요 사건은 버전 8이라는 64비트 주소 확장으로, ARM은 이전 ARM 버전이 아닌 MIPS처럼 보이도록 명령어 세트를 재설계할 기회를 얻었습니다.

ARM 프로세서(종종 ARM 코어라고도 함)에서 가장 중요한 점은 ARM이 프로세서를 설계한 다음 해당 설계에 대한 라이센스를 고객에게 제공한다는 것입니다. Intel이나 IBM과 같은 다른 공급업체와 달리 ARM은 실리콘 웨이퍼를 생산하지 않습니다. 대신 Texas Instruments 및 Qualcomm과 같은 공급업체는 ARM 코어 설계를 사용하기 위해 라이선스를 구입하고 추가 구성 요소를 추가했습니다. 그런 다음 반도체 제조 회사와 계약을 맺거나 자체 제조 시설을 사용하여 전체 SOC(시스템 온 칩)를 실리콘에 구축합니다.

ARM의 최신(2012년 기준) ARMv8 아키텍처에는 세 가지 프로세서 라인이 있습니다.

  • 시리즈 1 프로세서를 ARM Cortex-M 시리즈라고 합니다. 이러한 프로세서는 주로 의료 기기, 자동차, 산업 전자 제품과 같은 임베디드 애플리케이션에서 마이크로컨트롤러로 사용하도록 설계되었으며 설계 이면의 주요 관심사는 전력 효율성과 비용입니다. 이 섹션에서는 3단계 파이프라인이 있는 ARM Cortex-M3 프로세서에 대해 설명합니다.

  • 시리즈 2 프로세서를 ARM Cortex-R 시리즈라고 합니다. 이 프로세서는 안정성, 고속 및 실시간 응답에 중점을 두고 실시간 애플리케이션용으로 설계되었습니다. 스마트폰과 같은 소비자 가전 장치용이 아니며 가상 메모리를 사용하는 운영 체제 실행을 지원하지 않습니다.

  • 세 번째 프로세서 시리즈는 ARM Cortex-A 시리즈라고 합니다. 이 프로세서는 스마트폰, 태블릿 및 다양한 고급 임베디드 장치에서 일반 사용자 애플리케이션을 실행하도록 설계되었습니다. 이러한 ARM 코어는 일반적으로 복잡한 파이프라인을 갖고, 벡터 연산을 지원하며, 가상 메모리 하드웨어 지원이 필요한 복잡한 운영 체제를 실행할 수 있습니다.

ARM Cortex-M3

주로 임베디드 프로세서 시장을 위해 설계된 ARM Cortex-M 시리즈 프로세서부터 시작해 보겠습니다. 이러한 임베디드 프로세서의 경우 기본 성능보다 에너지 효율성과 비용이 더 중요합니다. 따라서 ARM 엔지니어들은 매우 복잡한 기능이 없는 3-실행 파이프라인을 설계했습니다.

Cortex-M3는 ARMv7-M 명령 세트의 기본 버전을 지원하며 일반적으로 아래 이미지에 표시된 것처럼 ARM AMBA 버스를 사용하여 다른 구성 요소에 연결됩니다.

ARM Cortex-M3 및 AMBA 버스에 연결된 기타 구성 요소.

ARM Cortex-M3 아키텍처 다이어그램.

AMBA(Advanced Microcontroller Bus Architecture)는 ARM 코어를 SOC 기반 시스템의 다른 구성 요소와 연결하는 데 사용되는 ARM에서 설계한 버스 아키텍처입니다. 예를 들어, 스마트폰 및 모바일 장치의 대부분 프로세서는 AMBA 버스를 사용하여 브리징 장치를 통해 고속 메모리 장치, DMA 엔진 및 기타 외부 버스에 연결합니다. 이러한 외부 버스 중 하나는 키보드, UART 컨트롤러(Universal Asynchronous Receiver Transmitter Protocol), 타이머 및 PIO(병렬 입력 출력) 인터페이스와 같은 주변 장치에 연결하는 데 사용되는 APB 버스(Advanced Peripheral Bus)입니다.

아래 그림은 ARM Cortex-M3의 파이프라인을 보여줍니다. 획득(F), 디코딩(D), 실행(E)의 세 단계로 구성됩니다. 가져오기 단계는 메모리에서 명령을 가져오며 세 단계 중 가장 작은 단계입니다.

ARM Cortex-M3 파이프라인.

디코딩 단계(D 단계)에는 위 그림에 표시된 것처럼 세 가지 하위 단위가 있습니다. D 스테이지에는 명령어를 디코딩하고 명령어 패킷을 형성하는 명령어 디코딩 및 레지스터 판독 장치가 있습니다. 또한 명령어에 포함된 피연산자의 값을 읽고, 레지스터 파일에서도 값을 읽습니다. AGU(Address Generation Unit)는 명령어의 모든 필드를 가져오고 파이프라인의 다음 단계에서 로드 또는 저장 명령어 실행을 예약합니다. 이는 ldm(다중 로드) 및 stm(다중 저장) 명령을 처리하는 데 특별한 역할을 합니다. 이러한 명령어는 여러 레지스터를 동시에 읽거나 쓸 수 있으며 AGU는 파이프라인에서 단일 ldm 또는 stm 명령어를 사용하여 여러 작업을 생성합니다. 분기 단위는 분기 결과와 분기 목표를 예측하는 분기 예측에 사용됩니다.

실행 단계는 기능 측면에서 상당히 무겁습니다. 일부 명령어는 실행하는 데 2사이클이 걸리며, 일반 ALU 및 분기 명령어를 먼저 살펴보세요. ARM 명령어에는 시프터 피연산자가 있을 수 있으며, 두 번째로 12비트 인코딩에서 32비트 즉시 값을 계산하는 것은 기본적으로 시프트 연산입니다(회전은 시프트 유형임). 두 연산 모두 배럴 시프터라는 하드웨어 구조를 갖춘 시프트 유닛에 의해 수행됩니다. 피연산자가 준비되면 ALU 및 분기 유닛으로 전달되어 분기 결과/대상 및 ALU 결과를 계산합니다.

ARM에는 직접 분기와 간접 분기라는 두 가지 유형의 분기가 있습니다. 직접 분기의 경우 현재 PC에서 분기 대상의 오프셋이 명령어에 포함됩니다. 예를 들어, 레이블에 대한 분기가 직접 분기인 경우 직접 분기의 분기 대상은 디코딩 단계에서 계산될 수 있습니다. ARM은 분기 대상이 ALU 또는 메모리 명령어의 결과인 간접 분기도 지원합니다. 예를 들어, ldr-pc, [r1, #10] 명령은 간접 분기의 예입니다. 여기서 분기 대상의 값은 로드 명령에 의해 메모리에서 로드된 값과 같습니다. 간접 분기의 대상을 예측하는 것은 종종 어렵습니다. Cortex-M3 프로세서에서는 잘못된 분기 예측(대상 또는 결과)이 발생할 때마다 분기 이후에 가져온 두 명령이 취소되고 프로세서는 올바른 분기 대상에서 명령을 가져오기 시작합니다.

Cortex-M3에는 기본 ALU 외에도 부호 있는/비부호, 곱셈 및 나눗셈을 수행할 수 있는 곱셈 및 나눗셈 장치도 있습니다. Cortex-M3은 부호 있는 분할과 부호 없는 분할에 대해 각각 sdiv 및 udiv라는 두 가지 명령어를 지원합니다. 이러한 명령어 외에도 곱셈 및 곱셈-누산 연산을 지원합니다.

로드 및 저장 명령어는 일반적으로 두 주기가 걸리며 주소 생성 단계와 메모리 액세스 단계가 있습니다. 로드 명령을 실행하는 데 2사이클이 걸립니다. 두 번째 사이클 동안에는 E 단계에서 다른 명령을 실행할 수 없습니다. 따라서 파이프라인은 한 주기 동안 정지됩니다. 이 특수 기능은 파이프라인의 성능을 저하시킵니다. ARM은 고성능 프로세서에서 이러한 제한을 제거했습니다. 저장 명령어도 실행하는 데 2사이클이 걸리지만 메모리에 액세스하는 두 번째 사이클에서는 파이프라인이 중단되지 않습니다. 프로세서는 저장 버퍼(쓰기 버퍼와 유사)에 값을 쓴 다음 실행을 계속합니다. 로드가 저장소에 의해 기록된 값을 읽는 연속(연속 루프) 저장 및 로드 명령을 발행하는 것도 가능합니다. 파이프라인은 저장소 버퍼에서 저장소가 쓴 값을 읽기 때문에 로드 명령을 위해 일시 ​​중지할 필요가 없습니다.

ARM Cortex-A8

Cortex-M3(임베디드 프로세서)에 비해 Cortex-A8은 복잡한 스마트폰 및 태블릿 프로세서에서 실행할 수 있는 풀엣지 프로세서로 설계되었습니다. A는 애플리케이션을 의미하며 ARM의 의도는 이 프로세서를 사용하여 모바일 장치에서 일반 애플리케이션을 실행하는 것입니다. 둘째, 이러한 프로세서는 가상 메모리를 지원하도록 설계되었으며 전용 부동 소수점 및 SIMD 장치도 포함합니다.

Cortex-A8 코어 파이프라인의 설계 특징은 듀얼 이슈 수퍼스칼라 프로세서이지만 완전히 순서가 잘못된 프로세서는 아니라는 것입니다. 문제의 논리는 Cortex-A8이 복잡한 분기 예측 논리를 갖춘 13단계 정수 파이프라인을 가지고 있다는 것입니다. 깊은 파이프라인을 사용하기 때문에 더 얕은 파이프라인을 사용하는 다른 ARM 프로세서보다 더 높은 주파수에서 클럭킹할 수 있습니다. Cortex-A8 코어는 500MHz에서 1GHz 사이에서 클럭되며 이는 임베디드 세계에서 매우 빠른 클럭 속도입니다.

Cortex-A8에는 정수 파이프라인 외에도 전용 부동 소수점 및 SIMD 장치도 포함되어 있습니다. 부동 소수점 장치는 ARM의 VFP(벡터 부동 소수점) ISA 확장을 구현하고 SIMD 장치는 ARM NEON 명령어 세트를 구현합니다. 이 장치는 또한 10단계로 구성된 파이프라인 스타일입니다. 또한 ARM Cortex-A8 프로세서에는 선택적으로 대규모 공유 L2 캐시에 연결할 수 있는 별도의 명령 및 데이터 캐시가 있습니다.

아래 그림은 ARM Cortex-A8 프로세서의 파이프라인 설계를 보여줍니다. 가져오기 단위는 두 단계 사이의 파이프라인 처리를 수행합니다. 주요 목적은 지침을 가져오고 PC를 업데이트하는 것입니다. 또한 명령어 프리페처, ITLB(명령 TLB) 및 분기 예측기가 내장되어 있습니다. 그런 다음 명령은 디코딩 장치로 전달됩니다.

ARM Cortex-A8 프로세서의 파이프라인.

ARM Cortex-A8 아키텍처 다이어그램.

디코딩 유닛은 5단계 사이에서 파이프라인됩니다. Cortex-A8 프로세서의 디코드 장치는 명령어 간의 종속성을 확인하고 두 명령어를 함께 발행하는 추가 책임이 있기 때문에 Cortex-M3보다 더 복잡합니다. 따라서 전달, 일시 중지 및 연동 논리는 훨씬 더 복잡합니다. 두 개의 명령 실행 슬롯에 0과 1의 번호를 지정하겠습니다. 디코딩 단계에서 종속성이 없는 두 개의 명령을 찾으면 두 실행 슬롯을 명령으로 채우고 이를 실행 단위로 보냅니다. 그렇지 않으면 디코딩 단계에서 슬롯 하나만 방출됩니다.

실행 단위는 6단계에 걸쳐 파이프라인됩니다. 여기에는 2개의 ALU 파이프라인이 있는 4개의 독립적인 파이프라인이 포함되어 있으며 두 명령을 모두 사용할 수 있습니다. 여기에는 슬롯 0에서 발행된 명령어에 의해서만 사용할 수 있는 곱셈 파이프라인이 있습니다. 마지막으로 두 발행 슬롯에서 발행된 명령어에 의해 다시 사용될 수 있는 로드/저장 파이프라인이 있습니다.

NEON 및 VFP 명령어는 NEON/VFP 장치로 전송됩니다. NEON/VFP 명령어의 디코딩 및 스케줄링에는 3주기가 소요됩니다. NEON/VFP 장치는 32개의 64비트 레지스터가 포함된 NEON 레지스터 파일에서 피연산자를 가져옵니다. NEON 명령어는 레지스터 파일을 16개의 128비트 레지스터로 처리할 수도 있습니다. NEON/VFP 장치에는 산술 연산을 위한 6개의 6단계 파이프라인과 로드/저장 작업을 위한 6단계 파이프라인이 있습니다. 벡터 데이터 로딩은 SIMD 프로세서에서 매우 중요한 성능 작업입니다. 따라서 ARM은 L1 캐시에서 데이터를 로드하여 NEON 레지스터 파일을 채우기 위해 NEON 장치에 전용 로드 큐를 가지고 있습니다. 데이터를 저장하기 위해 NEON 셀은 L1 캐시에 직접 데이터를 씁니다.

각 L1 캐시(명령어/데이터)는 블록 크기가 64바이트이고 연관성이 4이며 16KB 또는 32KB일 수 있습니다. 둘째, 각 L1 캐시에는 2개의 포트가 있어 NEON 및 부동 소수점 작업을 위해 주기당 4워드를 제공할 수 있습니다. NEON/VFP 장치와 정수 파이프라인은 L1 데이터 캐시를 공유한다는 점에 유의해야 합니다. L1 캐시는 선택적으로 블록 크기가 64바이트이고 8방향 세트 연관이며 최대 1MB까지 가능한 대형 L2 캐시에 연결되며 L2 캐시는 여러 저장소로 분할됩니다. 두 개의 태그를 동시에 조회할 수 있으며 데이터 배열 액세스는 병렬로 발생합니다.

ARM Cortex-A8 NEON 및 부동 소수점 파이프라인.

ARM Cortex-A15

ARM Cortex-A15는 고성능 애플리케이션을 대상으로 2013년 초에 출시된 최신 ARM 프로세서입니다.

Cortex-A5 프로세서는 Cortex-M3 및 Cortex-A8보다 훨씬 더 복잡하고 강력합니다. 비순차적 커널을 사용하는 대신 3-실행 수퍼스칼라 비순차적 커널을 사용합니다. 또한 더 깊은 파이프라인도 있습니다. 특히 15단계 정수 파이프라인과 17~25단계 부동 소수점 파이프라인이 있습니다. 파이프라인이 깊어지면 더 높은 주파수(1.5~2.5GHz)에서 실행될 수 있습니다. 또한 VFP 및 NEON 장치를 별도의 실행 단위로 처리하는 대신 코어에 완전히 통합합니다. 서버 프로세서와 마찬가지로 대용량 메모리에 액세스하도록 설계되었으며 40비트 물리적 주소를 지원할 수 있습니다. 즉, 시스템 수준 일관성을 지원하는 최신 AMBA 버스 프로토콜을 사용하여 최대 1TB의 메모리 주소를 지정할 수 있습니다. Cortex-A15는 동일한 프로세서에서 여러 운영 체제를 동시에 실행할 수 있는 특수 프로그램인 최신 운영 체제와 가상 머신을 실행하도록 설계되었으며 서버 및 클라우드 컴퓨팅 환경에서 다양한 소프트웨어 요구 사항을 가진 사용자를 지원하는 데 사용됩니다. Cortex-A15는 고급 전원 관리 기술을 사용하여 프로세서를 사용하지 않을 때 프로세서의 일부를 동적으로 종료합니다.

Cortex-A15 프로세서의 또 다른 특징은 클러스터당 4개의 코어를 구성하는 멀티 코어 프로세서이며 각 칩이 여러 클러스터를 가질 수 있다는 것입니다. Snoop 제어 장치는 클러스터 내 일관성을 제공하고 AMBA4 사양은 클러스터 전반에 걸쳐 캐싱 및 시스템 수준 일관성을 지원하는 프로토콜을 정의하며 AMBA4 버스는 동기식 작업도 지원합니다. 메모리 시스템도 더 빠르고 안정적입니다. Cortex-A15의 메모리 시스템은 SECDED(Single Error Correction, Double ErrorDetection) 오류 제어 코드를 사용합니다.

아래 그림은 Cortex-A15 코어의 파이프라인 개요를 보여줍니다. 5개의 가져오기 단계가 있으며 Cortex-A15에는 여러 유형의 분기 명령을 처리할 수 있는 복잡한 분기 예측기가 있으므로 가져오기가 더 복잡합니다. 디코딩, 이름 바꾸기 및 명령어 디스패치 단위는 7단계 간에 파이프라인됩니다. 레지스터 이름 바꾸기 장치와 명령어 창은 지정된 주기에서 실행할 준비가 된 명령어 세트를 발행하는 역할을 하기 때문에 비순차적 프로세서의 성능에 매우 중요합니다.

ARM Cortex-A15 프로세서 개요.

ARM Cortex-A15 MPCore 칩 구조 다이어그램.

Cortex-A15에는 여러 실행 파이프라인이 있습니다. 정수 ALU 및 분기 파이프라인은 각각 3주기를 사용하지만 곱셈 및 로드/저장 파이프라인은 더 깁니다. NEON/VFP 장치를 물리적으로 독립된 장치로 처리하는 다른 ARM 프로세서와 달리 Cortex-A15는 이를 코어에 통합하고 비순차적 파이프라인의 일부입니다. 이제 파이프라인을 더 자세히 살펴보겠습니다(아래 이미지 참조).

ARM Cortex-A15 프로세서의 파이프라인.

Cortex-A5 코어의 분기 예측기(Cortex-A8의 분기 예측기도 동일한 기능을 가짐)에는 직접 분기 예측기, 간접 분기 예측기, 복귀 주소를 예측하는 예측기가 포함됩니다. 간접 분기 예측기는 분기 명령의 PC를 기반으로 분기 대상을 예측하려고 시도하며 지정된 분기 및 해당 PC의 기록으로 색인화된 256개 항목을 갖습니다. 실제로 반환 명령어의 대상을 예측하기 위해 복잡한 분기 예측 논리가 필요하지 않습니다. 더 간단한 접근 방식은 함수가 호출될 때마다 반환 주소를 기록하고 이를 스택(반환 주소 스택, RAS라고 함)에 푸시하는 것입니다. 함수 호출은 후입선출 동작을 나타내기 때문에 간단히 RAS를 팝하고 함수에서 반환할 때 반환 주소 값을 가져와야 합니다. 마지막으로 더 넓은 문제 폭을 지원하기 위해 가져오기 단위는 명령어 캐시에서 한 번에 128비트를 가져오도록 설계되었습니다.

순환 버퍼(Cortex-A8에도 있음)는 디코딩 단계에 매우 흥미로운 추가 기능입니다. 일련의 명령이 루프에서 실행된다고 가정합니다. 다른 프로세서에서는 명령을 루프에서 반복적으로 가져오고 디코딩해야 하므로 에너지와 메모리 대역폭이 낭비됩니다. 이 프로세스는 모든 디코딩된 명령 패킷을 순환 버퍼에 저장함으로써 최적화될 수 있습니다. 그러면 루프 실행 시 가져오기 및 디코드 단위가 완전히 우회되고 레지스터 이름 변경 단계가 디코드 단위 또는 순환 버퍼에서 명령을 얻을 수 있습니다.

커널은 모든 명령어의 결과를 포함하는 ROB(재주문 버퍼)를 유지 관리합니다. ROB의 항목은 프로그램 순서대로 할당됩니다. 이름 바꾸기 단계에서는 피연산자를 ROB(ARM 문서에서는 결과 큐라고 함)의 항목에 매핑합니다. 예를 들어 명령어 3에 명령어 1에서 생성될 값이 필요한 경우 해당 피연산자는 명령어 1의 ROB 항목에 매핑됩니다. 그런 다음 모든 명령어는 명령어 창에 들어가 소스 피연산자가 준비될 때까지 기다립니다. 준비되면 적절한 파이프라인으로 전송됩니다. Cortex-A15에는 정수 ALU 2개, 분기 유닛 1개, 곱셈 유닛 1개, 로드/저장 유닛 2개가 있으며 NEON/VFP 유닛은 사이클당 2개의 명령어를 수용할 수 있습니다.

로드/저장 유닛에는 4단계 파이프라인이 있습니다. 정확한 예외 저장소를 보장하기 위해 명령이 ROB의 헤드에 도달할 때만 메모리 시스템에 저장소가 발행됩니다(파이프라인에는 이전 명령이 없습니다). 동시에 파이프라인의 동일한 주소에서 저장 작업을 수행하는 모든 로드 작업은 전달 경로를 통해 해당 값을 가져옵니다. 두 개의 L1 캐시(명령어 및 데이터)는 일반적으로 각각 32KB입니다.

Cortex-A15 프로세서는 활성 프리페처가 있는 16방향 집합 연관 캐시인 대형 L2 캐시(최대 4MB)를 지원합니다. L1 캐시와 L2 캐시는 캐시 일관성 프로토콜의 일부입니다. Cortex-A55는 디렉터리 기반 MESI 프로토콜을 사용합니다. 두 번째 수준 캐시에는 첫 번째 수준의 모든 디렉터리 복사본을 유지 관리하는 snoop 태그 배열이 포함되어 있습니다. I/O 작업에서 행을 수정하려는 경우 L2 캐시는 스누프 태그 배열을 사용하여 해당 행이 L1 캐시에 있는지 찾습니다. 첫 번째 수준 캐시에 행 복사본이 포함되어 있으면 해당 복사본은 유효하지 않습니다. 마찬가지로 DMA 읽기 작업이 있는 경우 L2 컨트롤러는 복사본이 포함된 L1 캐시에서 라인을 가져옵니다. 또한 이 프로토콜은 L3 캐시 및 다양한 주변 장치를 지원하도록 확장될 수 있습니다.

Cortex-A53은 ARMv8 명령어 세트 아키텍처를 지원하고 IP(지적 재산) 코어로 제공되는 구성 가능한 코어입니다. IP 코어는 임베디드, 개인용 모바일 장치 및 관련 시장에 대한 기술 제공의 기본 형태이며 수십억 개의 ARM 및 MIPS 프로세서가 이러한 IP 코어에서 생성되었습니다.

IP 코어는 Intel i7 멀티 코어 컴퓨터의 코어와 다릅니다. IP 코어(그 자체는 멀티코어일 수 있음)는 특수 프로세서(예: 비디오 인코더 또는 디코더), I/O 인터페이스 및 메모리 인터페이스를 포함하여 다른 로직(칩의 "코어"임)과 결합되도록 설계된 다음 특정 애플리케이션에 최적화된 프로세서로 제조됩니다. 프로세서 코어는 논리적으로 거의 동일하지만 결과 칩은 많이 다릅니다. 한 가지 매개변수는 L2 캐시의 크기이며 16배만큼 달라질 수 있습니다.

SPEC2006 정수 벤치마크용 ARM Cortex-A53의 CPI.

19.5.5.2 AMD 프로세서

이제 AMD 프로세서의 디자인을 살펴보겠습니다. AMD 프로세서는 x86 명령어 세트를 구현하고 모바일 장치, 넷북, 랩탑, 데스크탑 및 서버용 프로세서를 만듭니다. 이 섹션에서는 설계 스펙트럼의 반대쪽 끝에 있는 두 프로세서를 살펴봅니다. 모바일 장치, 태블릿 및 넷북용으로 제작된 AMD Bobcat 프로세서는 x86 명령어 세트의 하위 집합을 구현하고 전력 효율성 및 허용 가능한 성능 수준을 기본 목표로 설계되었습니다. AMD Bulldozer 프로세서는 스펙트럼의 반대편에 있으며 고급 서버용으로 설계되었습니다. 성능 및 명령 처리량에 최적화되어 있습니다. 또한 멀티스레딩을 가능하게 하는 조인트 코어라는 새로운 유형의 코어를 사용하는 AMD 최초의 멀티스레드 프로세서이기도 합니다.

AMD 밥캣

Bobcat 프로세서는 10~15W 전력 예산 내에서 작동하도록 설계되었습니다. 이 전력 예산 내에서 Bobcat 설계자는 여러 가지 복잡한 아키텍처 기능을 프로세서에 구현할 수 있었습니다. 예를 들어 Bobcat은 상당히 복잡한 2-실행 비순차적 파이프라인을 사용합니다. Bobcat의 파이프라인은 동일한 주기에서 2개의 명령어를 가져오도록 설계된 복잡한 분기 예측기를 사용합니다. 그런 다음 해당 속도로 디코딩하여 COP(복잡한 마이크로 작업)로 변환할 수 있습니다. AMD 용어의 복잡한 마이크로 연산은 메모리를 읽고 쓰는 CISC와 유사한 명령입니다. 그런 다음 이 COP 세트는 명령 대기열, 이름 바꾸기 엔진 및 스케줄러로 전송됩니다.

스케줄러는 명령어를 순차적으로 선택하여 이를 ALU, 메모리 주소 생성 장치 및 로드/저장 장치에 전달합니다. 성능을 향상시키기 위해 로드/저장 장치는 순서 없이 메모리 시스템에 요청을 보냅니다. 따라서 Bobcat이 약한 메모리 모델을 지원한다고 결론을 내리기 쉽습니다. 복잡한 마이크로아키텍처 기능 외에도 Bobcat은 SIMD 명령어 세트(최대 SSE 4), 프로세서 상태를 메모리에 저장하는 자동 방법, 64비트 명령어도 지원합니다. 프로세서의 전력 소비가 한도 내에 있도록 하기 위해 Bobcat에는 다양한 에너지 절약 최적화 기능이 포함되어 있습니다. 눈에 띄는 메커니즘 중 하나는 클럭 게이팅(clock gating)입니다. 사용되지 않은 장치의 경우 클록 신호가 로직 0으로 설정되어 사용되지 않은 장치에서 신호 전환이 없어 동적 전력 소비가 발생하지 않도록 합니다. 또한 Bobcat 프로세서는 가능할 때마다 데이터에 대한 포인터를 사용하고 프로세서의 다른 위치에 데이터를 복사하는 것을 최소화합니다.

아래 그림은 AMD Bobcat 프로세서의 파이프라인 블록 다이어그램을 보여줍니다. Bobcat 프로세서의 독특한 특징은 x86 ISA에서 이 문제를 해결할 수 있는 빠른 방법이 없기 때문에 명령이 분기인지 여부를 먼저 예측해야 하는 다소 복잡한 분기 예측기입니다. 명령어가 분기로 예측되면 그 결과(실행/실행되지 않음)와 목표를 계산해야 합니다. AMD는 분기 예측을 위해 고급 패턴 일치를 기반으로 하는 독점 알고리즘을 사용합니다. 분기 예측 후 가져오기 엔진은 I 캐시에서 한 번에 32바이트를 가져와 명령어 버퍼로 보냅니다.

AMD Bobcat 프로세서용 파이프라인.

디코더는 한 번에 22개의 명령어 바이트를 고려하고 명령어 경계를 묘사하려고 시도합니다. x86 명령 길이는 가변성이 클 수 있으므로 이는 느리고 계산 집약적인 프로세스입니다. 더 큰 프로세서는 명령을 두 번째로 더 쉽게 디코딩할 수 있도록 이 정보를 캐시하는 경우가 많습니다. Bobcat의 디코딩 처리량은 2개의 명령어로 제한되어 있으므로 이 기능이 없습니다. 이제 대부분의 x86 명령어 쌍은 22바이트 이내이므로 디코더는 대부분의 경우 두 x86 명령어의 내용을 모두 추출할 수 있습니다. 디코더는 일반적으로 각 x86 명령어를 1-2 Cops로 변환하고 일부 흔하지 않은 명령어의 경우 명령어를 마이크로코드 시퀀스로 대체합니다.

그런 다음 Cop가 56개 항목 ROB(재순서 버퍼)에 추가됩니다. Bobcat에는 두 개의 스케줄러가 있으며 정수 스케줄러에는 16개의 항목이 있고 부동 소수점 스케줄러에는 18개의 항목이 있습니다. 정수 스케줄러는 주기당 실행을 위해 두 개의 명령을 선택합니다. 정수 파이프라인에는 2개의 ALU와 2개의 주소 생성 장치(1개는 로드용, 1개는 저장용)가 있습니다. 부동 소수점 파이프라인은 주기당 두 개의 Co를 실행할 수도 있지만 몇 가지 제한 사항이 있습니다.

프로세서의 로드-저장 장치는 저장소의 값을 파이프라인의 로드 명령으로 전달합니다. Bobcat에는 32KB(8방향 연관) L1 D 및 I 캐시가 있으며 이는 512KB L2 캐시(16방향 세트 연관)에 연결되며 버스 인터페이스는 L2 캐시를 메인 메모리 및 시스템 버스에 연결합니다.

이제 파이프라인 일정을 고려해보세요. Bobcat 정수 파이프라인은 16단계로 나누어져 있으며 깊은 파이프라인으로 인해 코어는 1~2GHz 사이의 주파수에서 클럭킹될 수 있습니다. Bobcat 파이프라인에는 6개의 가져오기 주기와 3개의 디코드 주기가 있으며, 마지막 3개의 가져오기 주기는 디코드 주기와 겹치고 이름 바꾸기 엔진과 스케줄러는 4개의 주기를 사용합니다. 대부분의 정수 명령어의 경우 레지스터 파일을 읽는 데 1사이클, ALU에 액세스하는 데 1사이클, 결과를 레지스터 파일에 다시 쓰는 데 1사이클이 걸립니다. 부동 소수점 파이프라인에는 7개의 추가 단계가 있으며, 메모리 장치를 로드하려면 주소 생성 및 데이터 캐시 액세스를 위해 3개의 추가 단계가 필요합니다.

AMD 불도저

이름에서 알 수 있듯이 불도저 코어는 스펙트럼의 반대편에 있으며 주로 고급 데스크톱, 워크스테이션 및 서버에 사용됩니다. 근본적으로 잘못된 기계일 뿐만 아니라 멀티스레딩 기능도 갖추고 있습니다. 불도저는 실제로 멀티 코어, 단일 입도 멀티 스레드 프로세서와 SMT의 조합이며, 그 코어는 실제로 기능 단위를 공유하는 두 개의 작은 코어로 구성된 "조인트 코어"입니다.

두 개의 불도저 스레드는 획득 엔진(아래 이미지 참조)과 디코딩 로직을 공유합니다. 파이프라인의 이 부분(프런트 엔드라고 함)은 매 사이클마다 두 스레드 사이를 전환하고 정수, 로드 저장 및 분기 명령을 두 코어 중 하나에 전달합니다. 각 코어에는 명령 스케줄러, 레지스터 파일, 정수 실행 장치, L1 캐시 및 로드 저장 장치가 포함되어 있으며 각 코어는 명령 가져오기 및 디코딩 기능이 없는 자체 지원 코어로 볼 수 있습니다. 두 코어 모두 전용 스케줄러와 실행 장치가 있는 SMT 모드에서 실행되는 부동 소수점 장치를 공유합니다. 불도저 프로세서는 3~4GHz에서 서버 및 디지털 워크로드를 실행하도록 설계되었으며 최대 전력 소비 제한은 125~140W입니다.

불도저 프로세서 개요.

이제 아래 그림에서 프로세서를 더 자세히 살펴보겠습니다.

Bulldozer 프로세서의 읽기 폭은 Bobcat 프로세서의 두 배입니다. 사이클당 최대 4개의 x86 명령어를 가져오고 디코딩할 수 있습니다. Bobcat과 유사하게 Bulldozer 프로세서에는 명령이 분기인지 여부, 분기 결과 및 분기 대상을 예측할 수 있는 정교한 분기 예측 논리가 있습니다. 여기에는 약 5500개의 분기 명령에 대한 예측 분기 대상을 보유할 수 있는 다중 레벨 분기 대상 버퍼가 있습니다.

디코딩 엔진은 x86 명령을 COP로 변환합니다. AMD의 COP는 CISC 명령어이며 때로는 원래 x86 명령어보다 간단하지만 대부분의 x86 명령어는 COP로만 변환됩니다. 그러나 일부 명령어는 여러 COP로 변환되며 때로는 명령어 변환을 위해 마이크로코드 메모리를 사용해야 합니다. 디코드 엔진의 흥미로운 측면은 명령을 동적으로 결합하여 더 큰 명령을 생성할 수 있다는 것입니다. 예를 들어 비교 명령과 후속 분기 명령을 단일 Cop로 결합할 수 있습니다. 이를 매크로 융합이라고 합니다.

이어서 정수 명령이 실행을 위해 코어로 전달됩니다. 각 코어에는 이름 바꾸기 엔진, 명령 스케줄러(40개 항목), 레지스터 파일 및 128개 항목 ROB가 있습니다. 코어의 실행 유닛은 4개의 독립적인 파이프라인으로 구성되며, 2개의 파이프라인에는 ALU가 있고, 나머지 2개의 파이프라인은 메모리 주소 생성 전용입니다. 로드-저장 장치는 메모리에 대한 액세스를 조정하고, 저장소 간에 데이터를 로드로 전달하며, 스트라이드 프리페처를 사용하여 공격적인 프리페치를 수행합니다. 스트라이드 프리페처는 배열 액세스를 자동으로 추론하고 향후 액세스 가능성이 가장 높은 배열 인덱스에서 가져올 수 있습니다.

두 코어는 64KB 명령 캐시를 공유하고, 각 코어에는 16KB L1 쓰기 캐시가 있으며, 각 로드 액세스에는 4사이클이 소요됩니다. L1 캐시는 18사이클 대기 시간으로 코어 간에 공유되는 다양한 크기의 L2 캐시에 연결됩니다.

부동 소수점 단위는 두 코어 간에 공유되며 단순한 기능 단위 이상입니다. 두 스레드의 명령을 동시에 예약하고 실행하는 SMT 프로세서로 볼 수 있습니다. 여기에는 자체 명령 창, 레지스터 파일, 이름 바꾸기 및 깨우기 선택(비순차적 스케줄링) 논리가 있습니다. Bulldozer의 부동 소수점 장치에는 SIMD 명령(정수 및 부동 소수점)과 일반 부동 소수점 명령을 처리하는 4개의 파이프라인이 있습니다. 처음 두 파이프라인에는 FMAC 장치라고 하는 128비트 부동 소수점 ALU가 있습니다. FMAC(부동 소수점 곱셈 누산) 장치는 일반 부동 소수점 연산뿐만 아니라 (a <-- a+b*c) 형식의 연산도 수행할 수 있습니다. 마지막 두 파이프라인에는 128비트 정수 SIMD 단위가 있으며 마지막 파이프라인은 결과를 메모리에 저장하는 데에도 사용됩니다. 부동 소수점 장치에는 코어의 캐시에 액세스하기 위한 전용 로드-저장 장치가 있습니다.

19.5.5.3 인텔 프로세서

이전 몇 년 동안의 Intel 프로세서는 노트북과 데스크탑 시장을 지배했습니다. 이 섹션에서는 매우 다른 두 가지 Intel 프로세서의 디자인을 살펴보겠습니다. 첫 번째 프로세서는 휴대폰, 태블릿, 임베디드 컴퓨터용으로 설계된 Intel Atom이었습니다. 스펙트럼의 반대편에는 고급 데스크탑 및 서버에 사용되는 Intel Core i7 프로세서 제품군의 일부인 Sandy Bridge 멀티 코어 프로세서가 있습니다. 두 프로세서 모두 비즈니스 요구 사항이 매우 다르기 때문에 서로 다른 두 가지 디자인이 만들어집니다.

x86의 조상은 1972년에 생산을 시작한 최초의 마이크로프로세서였습니다. Intel 4004 및 8008은 매우 단순한 4비트 및 8비트 누산기 기반 아키텍처였으며 Morse et al. 1970년대 후반 8080에서 8086을 더 나은 처리량을 갖춘 16비트 아키텍처로 발전시켰습니다. 당시에는 거의 모든 마이크로프로세서 프로그래밍이 어셈블리 언어로 이루어졌고, 메모리와 컴파일러도 부족했습니다. Intel은 8080 사용자 기반을 유지하기를 원했기 때문에 8086은 8080과 "호환"되도록 설계되었습니다. 8086은 8080과 호환되는 객체 코드가 아니었지만 아키텍처는 어셈블리 언어 프로그램의 번역이 자동으로 수행될 수 있을 만큼 충분히 유사했습니다.

1980년 초, IBM은 IBM PC에서 사용하기 위해 8088이라는 8비트 외부 버스가 있는 8086 버전을 선택했습니다. 그들은 아키텍처 비용을 줄이기 위해 8비트 버전을 선택했고, 이 선택은 IBM PC의 엄청난 성공과 결합되어 8086 아키텍처를 유비쿼터스하게 만들었습니다. IBM PC의 성공은 부분적으로 IBM이 PC 아키텍처를 개방하고 PC 복제 산업이 번창할 수 있게 한 데서 비롯되었습니다. 80286, 80386, 80486, Pentium, Pentium Pro, Pentium II, Pentium III, Pentium 4 및 AMD64는 아키텍처를 확장하고 다양한 성능 향상을 제공합니다.

68000이 Macintosh용으로 선택되었지만 Mac은 PC만큼 인기를 얻지 못했습니다. 부분적으로는 Apple이 68000 기반 Mac 복제를 허용하지 않았고 68000에는 8086이 즐겼던 소프트웨어가 없었기 때문입니다. Motorola 68000은 8086보다 기술적으로 더 중요했을 수 있지만 IBM의 선택과 개방형 아키텍처 전략의 영향이 시장에서 68000의 기술적 이점을 지배했습니다.

어떤 사람들은 x86 명령 세트의 엉뚱함은 불가피하며 모든 아키텍처가 큰 성공을 거두려면 지불해야 하는 대가라고 믿습니다. 우리는 이러한 견해를 거부합니다. 분명히 성공적인 아키텍처는 이전 구현에 추가된 기능을 버릴 수 없으며 시간이 지남에 따라 일부 기능은 바람직하지 않은 것으로 간주될 수 있습니다. x86 문제는 8086 명령어 세트의 핵심에서 시작되며 8087, 80286, 80386, MMX, SSE, SSE2, SSE3, SSE4, AMD64(EM64T) 및 AVX에서 발견되는 구조적으로 일관되지 않은 확장으로 인해 더욱 악화됩니다.

이에 대한 반례는 x86보다 훨씬 오래되었으며 x86이 PC 시장을 지배했던 것처럼 메인프레임 시장을 지배하고 있는 IBM 360/370 아키텍처입니다. 더 나은 기반과 더 많은 호환성 향상 덕분에 이 명령어 세트가 처음 구현된 지 50년이 지난 x86보다 더 의미가 있다는 것은 의심의 여지가 없습니다. x86을 64비트 주소 지정으로 확장한다는 것은 아키텍처가 수십 년 동안 지속될 가능성이 높으며 미래의 명령어 세트 인류학자가 최초의 마이크로프로세서에서 아티팩트를 발견할 때까지 이러한 아키텍처에서 레이어를 하나씩 벗겨낼 것임을 의미합니다. 이러한 결과를 바탕으로 그들은 오늘날의 컴퓨터 아키텍처를 어떻게 판단할 것인가?

인텔 코어 마이크로아키텍처

수퍼스칼라 설계의 개념은 종종 RISC 아키텍처와 연관되어 있지만 동일한 수퍼스칼라 원칙이 CISC 시스템에도 적용될 수 있으며 그 대표적인 예가 Intel x86 아키텍처입니다. Intel 계열의 수퍼스칼라 개념의 진화는 주목할 가치가 있습니다. 386은 전통적인 CISC 비파이프라인 기계였습니다. 486은 최초의 파이프라인 x86 프로세서를 도입하여 정수 연산의 평균 대기 시간을 2~4사이클에서 1사이클로 줄였지만 여전히 사이클당 하나의 명령 실행으로 제한되었으며 수퍼스칼라 요소가 없었습니다. 원래 Pentium에는 두 개의 독립적인 정수 실행 장치로 구성된 적당한 수퍼스칼라 구성 요소가 있었던 반면, Pentium Pro는 비순차적 실행 기능을 갖춘 완전한 수퍼스칼라 설계를 도입했습니다. 후속 x86 모델에서는 수퍼스칼라 디자인이 개선되고 향상되었습니다.

아래 그림은 x86 파이프라인 아키텍처의 버전을 보여줍니다. 인텔은 파이프라인 아키텍처를 마이크로아키텍처라고 부릅니다. 마이크로아키텍처는 Intel 코어 마이크로아키텍처라고도 알려진 기계 명령어 세트 아키텍처의 기반이자 구현입니다. 이는 향상된 Intel Core 마이크로아키텍처와 함께 Intel Core 2 및 Intel Xeon 프로세서 제품군의 모든 프로세서 코어에 구현됩니다. 두 마이크로아키텍처의 주요 차이점은 후자가 세 번째 수준 캐시를 제공한다는 것입니다.

인텔 코어 마이크로아키텍처.

Intel Core i7-990X 구조 다이어그램.

인텔 8085 CPU 구조 다이어그램.

다음 표는 캐시 아키텍처의 일부 매개변수와 성능 특성을 보여줍니다. 모든 캐시는 후기입 업데이트 전략을 사용하며 명령이 메모리 위치에서 데이터를 읽을 때 프로세서는 캐시와 주 메모리에서 이 데이터가 포함된 캐시 라인을 다음 순서로 찾습니다.

  • 코어의 L1 데이터 캐시를 시작합니다.

  • L1 데이터 캐시 및 기타 코어용 L2 캐시.

  • 시스템 메모리.

인텔 코어 마이크로아키텍처 기반 프로세서의 캐시/메모리 매개변수 및 성능.

L2 캐시의 캐시 라인 가용성이나 상태에 관계없이 캐시 라인이 수정된 경우에만 다른 코어의 L1 데이터 캐시에서 캐시 라인을 가져옵니다. 위의 표 b는 메모리 클러스터의 여러 위치에서 처음 4바이트를 가져오는 특성을 보여줍니다. 대기 시간 열은 액세스 대기 시간의 추정치를 제공하지만 실제 대기 시간은 캐시 로드, 메모리 구성 요소 및 해당 매개변수에 따라 달라질 수 있습니다.

Intel Core 마이크로아키텍처 파이프라인에는 다음이 포함됩니다.

  • 메모리에서 명령어 스트림을 가져오는 순차 커밋 프런트엔드이며, 비순차 실행 코어에 디코딩된 명령어를 제공하는 4개의 명령어 디코더가 있습니다. 각 명령어는 마이크로 연산(마이크로 연산 또는 마이크로 연산)이라고 하는 하나 이상의 고정 길이 RISC 명령어로 변환됩니다.

  • 소스가 준비되고 실행 리소스를 사용할 수 있을 때 사이클당 최대 6개의 마이크로 연산을 실행하고 실행을 위해 마이크로 연산을 재정렬할 수 있는 비순차적 수퍼스칼라 실행 코어입니다.

  • 마이크로 연산의 실행 결과가 처리되고 아키텍처 상태와 프로세서의 레지스터 세트가 원래 프로그램 순서에 따라 업데이트되도록 보장하는 정렬된 폐기 장치입니다.

실제로 Intel 코어 마이크로아키텍처는 RISC 마이크로아키텍처에 CISC 명령어 세트 아키텍처를 구현합니다. 내부 RISC 마이크로 작업은 최소 14단계의 파이프라인을 통과하며 경우에 따라 마이크로 작업에는 여러 실행 단계가 필요하므로 이전 Intel x86 프로세서 및 Pentium에서 사용된 5단계 파이프라인과 달리 파이프라인이 더 길어집니다.

다음으로 프론트엔드(Front End)에 대해 설명한다.

먼저 분기 예측 단위에 대해 설명하겠습니다. 프런트엔드는 디코딩된 명령(마이크로 연산)을 제공하고 6단계 비순차적 엔진에 대한 흐름을 유지해야 합니다. 이 엔진은 분기 예측 장치(BPU), 명령 가져오기 및 사전 디코딩 장치, 명령 대기열 및 디코딩 장치의 세 가지 주요 구성 요소로 구성됩니다.

분기 예측 유닛 이 유닛은 명령어 가져오기 유닛이 다양한 분기 유형(조건부, 간접, 직접, 호출 및 반환)을 예측하여 실행될 가능성이 가장 높은 명령어를 얻도록 도와줍니다. BPU는 각 분기 유형에 대해 전용 하드웨어를 사용합니다. 분기 예측을 통해 프로세서는 분기 결과가 결정되기 훨씬 전에 명령 실행을 시작할 수 있습니다.

마이크로아키텍처는 최근 실행된 분기 명령의 기록을 기반으로 하는 동적 분기 예측 전략을 사용합니다. 최근 발생한 분기 명령에 대한 정보를 캐시하는 분기 대상 버퍼(BTB)를 유지 관리합니다. 명령어 스트림에서 분기 명령어를 만날 때마다 BTB를 검사하고, BTB에 항목이 이미 존재하는 경우 명령어 유닛은 예측된 분기가 취해졌는지 여부를 결정할 때 해당 항목에 대한 이력 정보에 따라 안내됩니다. 분기가 예측되면 해당 항목과 관련된 분기 대상 주소가 분기 대상 명령어를 프리페치하는 데 사용됩니다.

명령어가 실행되면 해당 항목의 기록 부분이 업데이트되어 분기 명령어의 결과를 반영합니다. 이 명령어가 BTB에 표시되지 않으면 이 명령어의 주소가 BTB의 항목에 로드되고 필요한 경우 이전 항목이 삭제됩니다.

처음 두 단락의 설명은 일반적으로 원래 Pentium 모델과 이후 Pentium 모델(이후 Intel 모델 포함)에 사용된 분기 예측 전략에 적용되지만, Pentium의 경우 비교적 간단한 2비트 히스토리 체계가 사용됩니다. 최신 모델에는 더 긴 파이프라인(인텔 코어 마이크로아키텍처의 경우 14단계, 펜티엄의 경우 5단계)이 있으므로 잘못된 예측에 대한 처벌이 더 큽니다. 따라서 이후 모델에서는 예측 오류율을 줄이기 위해 더 많은 과거 비트를 사용하는 더 복잡한 분기 예측 방식을 사용합니다.

정적 예측 알고리즘을 사용하여 다음 규칙에 따라 BTB에 이력이 없는 조건부 분기를 예측합니다.

  • 명령어 포인터(IP)와 독립적인 분기 주소의 경우 분기가 반환이면 실행이 예측되고, 그렇지 않으면 실행이 수행되지 않습니다.

  • IP 상대 역방향 조건 분기의 경우 예측을 수행합니다. 이 규칙은 루프의 일반적인 동작을 반영합니다.

  • IP 상대 순방향 조건 분기의 경우 예측이 수행되지 않습니다.

명령어 가져오기 및 사전 디코딩 단위에 대해 다시 설명합니다. 명령어 가져오기 장치에는 ITLB(명령어 변환 참조 버퍼), 명령어 프리페처, 명령어 캐시 및 프리디코딩 논리가 포함됩니다.

명령어 가져오기는 L1 명령어 캐시에서 수행됩니다. L1 캐시 누락이 발생하면 순서대로 프런트 엔드가 L2 캐시에서 L1 캐시로 한 번에 64바이트씩 새로운 명령을 보냅니다. 기본적으로 명령어는 순차적으로 가져오기 때문에 각 L2 캐시 라인 가져오기에는 가져올 다음 명령어가 포함되어 있으며 분기 예측 장치를 통한 분기 예측은 이 순차적 가져오기 작업을 변경할 수 있습니다. ITLB는 주어진 선형 IP 주소를 L2 캐시에 액세스하는 데 필요한 물리적 주소로 변환하고 프런트 엔드의 정적 분기 예측을 사용하여 가져올 다음 명령을 결정합니다.

프리디코드 장치는 명령어 캐시 또는 프리페치 버퍼에서 16바이트를 받아들이고 다음 작업을 수행합니다.

  • 명령의 길이를 결정합니다.

  • 명령어와 관련된 모든 접두사를 디코딩합니다.

  • 디코더 명령을 표시하는 다양한 속성(예: "is 분기")

프리코딩 장치는 사이클당 최대 6개의 명령어를 명령어 대기열에 쓸 수 있습니다. 가져오기에 6개 이상의 명령어가 포함된 경우 프리디코더는 가져오기의 모든 명령어가 명령어 큐에 기록될 때까지 사이클당 최대 6개의 명령어를 계속 디코딩합니다. 후속 추출은 현재 추출이 완료된 후에만 프리코딩에 들어갈 수 있습니다.

마지막으로 명령어 큐와 디코딩 단위에 대해 설명합니다. 가져온 명령어는 명령어 큐에 배치되고 거기에서 디코드 유닛은 바이트를 스캔하여 명령어 경계를 결정합니다. 이는 x86 명령어의 가변 길이로 인해 필요한 작업입니다. 디코더는 각 기계 명령어를 1~4개의 마이크로 연산으로 변환하며, 각 마이크로 연산은 118비트 RISC 명령어입니다. 대부분의 순수 RISC 시스템은 명령 길이가 32비트이므로 더 복잡한 x86 명령을 수용하려면 더 긴 마이크로 연산 길이가 필요합니다. 그럼에도 불구하고 마이크로 작업은 파생된 원래 지침보다 관리하기가 더 쉽습니다.

일부 명령어에는 4개 이상의 마이크로 연산이 필요하며 이러한 명령어는 복잡한 기계 명령어와 관련된 일련의 마이크로 연산(5개 이상)을 포함하는 마이크로코드 ROM으로 전송됩니다. 예를 들어, 문자열 명령어는 매우 큰(심지어 수백 개) 반복되는 마이크로 작업 시퀀스로 변환될 수 있습니다. 따라서 마이크로코드 ROM은 마이크로프로그램 제어 장치입니다.

생성된 마이크로 작업 시퀀스는 이름 바꾸기/분배 모듈로 전달됩니다.

위에서 프런트 엔드의 세 가지 구성 요소를 설명한 후 다음에는 비순차적 실행 로직에 대해 설명합니다.

프로세서의 이 부분은 입력 피연산자가 준비되는 즉시 실행할 수 있도록 마이크로 연산의 순서를 변경합니다. 할당 단계에서는 실행에 필요한 자원을 할당하고 다음 기능을 수행합니다.

  • 한 클록 주기에 할당자에 도착하는 세 개의 마이크로 연산 중 하나가 필요한 리소스(예: 레지스터)를 사용할 수 없는 경우 할당자는 파이프라인을 정지시킵니다.

  • 할당자는 언제든지 진행 중일 수 있는 126개의 마이크로 작업 중 하나의 완료 상태를 추적하는 ROB(재주문 버퍼) 항목을 할당합니다.

  • 할당자는 마이크로 연산의 결과 데이터 값에 대해 128개의 정수 또는 부동 소수점 레지스터 항목 중 하나를 할당하고 머신 파이프라인의 48개 로드 또는 24개 저장 중 하나를 추적하는 데 사용되는 로드 또는 저장 버퍼를 할당할 수 있습니다.

  • 할당자는 명령 스케줄러 앞에 있는 두 개의 마이크로 연산 대기열 중 하나에 항목을 할당합니다.

ROB는 최대 126개의 마이크로 연산을 보유할 수 있고 128개의 하드웨어 레지스터도 포함하는 순환 버퍼입니다. 각 버퍼 항목은 다음 필드로 구성됩니다.

  • 상태: 이 마이크로 작업이 실행되도록 예약되었는지, 실행이 예약되었는지, 실행이 완료되어 종료할 준비가 되었는지 여부를 나타냅니다.

  • 메모리 주소: 마이크로 연산을 생성한 펜티엄 명령어의 주소입니다.

  • 마이크로 오퍼레이션: 실제 오퍼레이션.

  • 별칭 레지스터: 마이크로 연산이 16개의 아키텍처 레지스터 중 하나를 참조하는 경우 이 항목은 해당 참조를 128개의 하드웨어 레지스터 중 하나로 리디렉션합니다.

마이크로 연산은 순서대로 ROB에 들어가고 ROB에서 스케줄링/실행 단위로 순서대로 전송되며, 스케줄링의 기준은 이 마이크로 연산에 필요한 적절한 실행 단위와 필요한 모든 데이터 항목이 사용 가능한지 여부이며 마지막으로 마이크로 연산은 순서대로 ROB에서 나옵니다. 질서정연한 정리를 달성하기 위해 각 마이크로 작업이 정리 준비가 된 것으로 지정된 후 마이크로 작업이 먼저 정리됩니다.

이름 변경 단계에서는 16개의 아키텍처 레지스터(8개의 부동 소수점 레지스터와 EAX, EBX, ECX, EDX, ESI, EDI, EBP 및 ESP)에 대한 참조를 128개의 물리적 레지스터 세트로 다시 매핑합니다. 이 단계에서는 실제 데이터 종속성(쓰기 후 읽기)을 유지하면서 제한된 수의 아키텍처 레지스터로 인해 발생하는 잘못된 종속성을 제거합니다.

리소스 할당 및 레지스터 이름 변경 후 micro-op은 두 개의 micro-op 대기열 중 하나에 배치되며 스케줄러에 공간이 생길 때까지 보관됩니다. 두 개의 큐 중 하나는 메모리 작업(로드 및 저장)에 사용되고 다른 하나는 메모리 참조를 포함하지 않는 마이크로 작업에 사용됩니다. 각 대기열은 FIFO(선입선출) 규칙을 따르지만 대기열 간에 순서가 유지되지는 않습니다. 즉, 마이크로 연산은 다른 큐의 마이크로 연산에 비해 한 큐에서 순서 없이 읽혀질 수 있습니다. 이 이동은 스케줄러에 더 큰 유연성을 제공합니다.

스케줄러는 마이크로 작업 큐에서 마이크로 작업을 검색하고 실행을 위해 이러한 마이크로 작업을 디스패치하는 일을 담당합니다. 각 스케줄러는 마이크로 연산에 모든 피연산자가 있음을 나타내는 상태의 마이크로 연산을 찾고, 해당 마이크로 연산에 필요한 실행 단위를 사용할 수 있는 경우 스케줄러는 마이크로 연산을 가져와 적절한 실행 단위로 디스패치합니다. 한 주기에 최대 6개의 마이크로 작업을 예약할 수 있으며, 특정 실행 단위에 대해 여러 개의 마이크로 작업을 사용할 수 있는 경우 스케줄러는 이를 큐에서 순차적으로 디스패치합니다. 이는 순차 실행을 선호하는 FIFO 규칙이지만 이 시점에서 명령 흐름은 종속성과 분기에 의해 재배열되었으며 본질적으로 순서가 잘못되었습니다.

4개의 포트는 스케줄러를 실행 장치에 연결하며, 포트 0은 정수 및 부동 소수점 명령에 사용됩니다. 단, 간단한 정수 연산 및 분기 예측 오류 처리는 포트 1에 할당됩니다. 또한 MMX 실행 장치는 이 두 포트 사이에 나누어지며 나머지 포트는 메모리 로드 및 저장에 사용됩니다.

다음으로 정수 및 부동 소수점 실행 유닛에 대해 설명합니다.

정수 및 부동 소수점 레지스터 파일은 레지스터 파일과 L1 데이터 캐시에서 값을 검색하는 실행 단위에 대해 보류 중인 작업의 소스입니다. 별도의 파이프라인 단계는 일반적으로 분기 명령에 대한 입력인 플래그(예: 0, 음수)를 계산하는 데 사용됩니다.

후속 파이프라인 단계에서는 실제 분기 결과와 예측 결과를 비교하는 기능인 분기 검사를 수행합니다. 분기 예측이 잘못된 것으로 판명되면 파이프라인에서 제거해야 하는 다양한 처리 단계의 마이크로 작업이 있습니다. 그러면 올바른 분기 대상이 드라이버 단계의 분기 예측기에 제공되어 새 대상 주소에서 전체 파이프라인이 다시 시작됩니다.

인텔 아톰

Intel Atom 프로세서에는 처음부터 고유한 요구 사항이 있습니다. 설계자는 전력 효율성이 매우 높고 상용 운영 체제와 웹 브라우저를 실행할 수 있을 만큼 강력하며 x86과 완전히 호환되는 코어를 설계해야 합니다. 전력 소비를 줄이는 대략적인 방법은 x86 ISA의 하위 집합을 구현하는 것입니다. 이를 통해 더 간단하고 전력 효율적인 디코더를 얻을 수 있습니다. 디코딩 로직은 x86 프로세서에서 전력을 많이 소모하는 것으로 알려져 있으므로 복잡성을 줄이는 것이 전력 소비를 줄이는 가장 쉬운 방법 중 하나이지만 전체 x86 호환성에서는 이 옵션이 제외됩니다.

따라서 설계자는 성능 저하 없이 에너지 효율성이 매우 높은 새로운 설계를 고려해야 했으며 파이프라인을 단순화하고 두 가지 문제만 고려하기로 결정했습니다. 비순차적 파이프라인은 명령어 간의 종속성을 결정하고 비순차적으로 명령어를 실행하기 위한 복잡한 구조를 가지고 있습니다. 이러한 구조 중 일부는 명령 창, 이름 변경 논리, 스케줄러 및 웨이크 선택 논리로, 이는 프로세서의 복잡성을 가중시키고 전력 소비를 증가시킵니다.

둘째, 대부분의 Intel 프로세서는 일반적으로 CISC 명령을 파이프라인에서 일반 RISC 명령처럼 실행되는 RISC와 유사한 마이크로 연산으로 변환합니다. 명령어 번역 프로세스는 많은 에너지를 소비합니다. Intel Atom의 설계자는 명령어 변환을 포기하기로 결정했으며 Atom 파이프라인은 CISC 명령어를 직접 처리합니다. 일부 매우 복잡한 명령어의 경우 Atom 프로세서는 마이크로코드 ROM을 사용하여 이를 더 간단한 CISC 명령어로 변환합니다. 그러나 이는 표준보다 예외에 가깝습니다.

CISC 프로세서의 가져오기 및 디코딩 단계는 명령어의 길이가 다양하고 명령어 경계를 구분하는 과정이 지루하기 때문에 RISC 프로세서에 비해 더 복잡합니다. 둘째, 디코딩 프로세스도 더 복잡합니다. Atom은 아래 그림과 같이 명령어 가져오기 및 디코딩을 위해 16단계 파이프라인 중 6단계를 사용하며 나머지 단계는 레지스터 액세스, 데이터 캐시 액세스 및 명령어 실행과 같은 전통적인 기능을 수행합니다. 더 단순한 파이프라인 외에도 Intel Atom 프로세서의 또 다른 특징은 양방향 멀티스레딩을 지원한다는 것입니다. 최신 모바일 장치는 일반적으로 멀티 태스킹 운영 체제를 실행하며 사용자는 동시에 여러 프로그램을 실행합니다. 멀티스레딩은 추가 병렬성을 지원하고 프로세서 파이프라인의 유휴 시간을 줄여 이러한 요구 사항을 지원할 수 있습니다. 파이프라인의 마지막 3단계는 예외 처리, 멀티스레딩 관련 이벤트 처리, 레지스터나 메모리에 데이터 다시 쓰기를 전담합니다. 모든 최신 프로세서와 마찬가지로 저장 명령은 중요한 경로에 있지 않습니다. 일반적으로 순차적 일관성을 준수하지 않는 프로세서는 저장된 값을 쓰기 버퍼에 쓰고 후속 명령을 계속 실행합니다.

Intel Atom 프로세서의 파이프라인.

이제 그 디자인이 더 자세히 설명됩니다. 추출 및 디코딩 단계부터 시작합니다(아래 이미지 참조). 가져오기 단계에서 Atom 프로세서는 분기의 방향과 대상을 예측하고 바이트 스트림을 명령어 프리페치 버퍼로 가져옵니다. 다음 작업은 가져온 바이트 스트림의 명령어를 나누는 것입니다. x86 명령어의 경계는 파이프라인의 이 부분에서 수행되는 가장 복잡한 작업 중 하나이므로 Atom 프로세서에는 2레벨 사전 디코딩 단계가 있습니다. 명령어를 처음으로 디코딩한 후 명령어 사이에 1비트 마커를 추가합니다. 이 단계는 ILD(명령 길이 디코더) 장치에 의해 수행되며 I 캐시에 명령어를 저장합니다. 그 후, I-캐시에서 가져온 사전 디코딩된 명령어는 길이가 알려져 있으므로 사전 디코딩 단계를 우회하고 직접 디코딩 단계로 이동할 수 있습니다. 이러한 추가 태그를 저장하면 I-cache의 유효 크기인 36KB가 줄어들지만, 태그를 추가한 후에는 실제로 32KB가 됩니다. 디코더는 대부분의 CISC 명령어를 RISC와 유사한 마이크로 연산으로 변환하지 않습니다. 그러나 일부 복잡한 x86 명령어의 경우 마이크로코드 메모리에 액세스하여 이를 더 간단한 마이크로 연산으로 변환해야 합니다.

인텔 아톰 프로세서 내부 구조 다이어그램.

이어서, 정수 명령이 정수 실행 장치로 디스패치되고, FP 명령이 FP 실행 장치로 디스패치됩니다. Atom에는 2개의 정수 ALU, 2개의 FP ALU 및 메모리 작업을 위한 2개의 주소 생성 장치가 있습니다. 멀티스레딩을 지원하려면 명령 대기열 복사본 2개(스레드당 1개)와 정수 및 FP 레지스터 파일 복사본 2개가 필요합니다. 명령 대기열과 같은 하드웨어 구조의 복사본을 만드는 대신 Intel은 다른 접근 방식을 취합니다. 예를 들어, Atom 프로세서에서는 32개 항목의 명령 대기열이 두 부분으로 분할되고(각 부분에는 16개의 항목이 있음) 각 스레드는 명령 대기열의 해당 부분을 사용합니다.

이제 멀티스레딩에 대한 일반적인 사항을 논의해 보겠습니다. 멀티스레딩은 유휴 시간을 줄여 칩의 리소스 활용도를 향상시키므로 이상적으로는 멀티스레드 프로세서가 더 높은 전력 오버헤드(활동이 높기 때문에)와 더 나은 명령 처리량을 가져야 합니다. 프로세서를 현명하게 설계하지 않으면 처리량이 예기치 않게 증가할 수 있습니다. 멀티스레딩은 캐시, TLB, 명령어 스케줄링/스케줄링 로직과 같은 공유 리소스의 경합을 증가시킵니다. 특히, 캐시가 스레드별로 분할되어 미스율이 높아질 것으로 예상됩니다. 상황은 TLB와 유사합니다. 반면, 파이프라인은 2차 누락이 있거나 프로그램의 낮은 ILP(명령 수준 병렬성) 단계에서 유휴 상태를 유지할 필요가 없습니다. 따라서 멀티스레딩에는 장단점이 있으며, 좋은 효과(성능 향상 효과)가 나쁜 효과(경합 증가 효과)보다 클 때만 성능상의 이점을 얻을 수 있습니다.

인텔 샌디 브릿지

이제 시중에서 판매되는 Intel Core i7 프로세서의 일부인 Sandy Bridge 프로세서라는 고성능 Intel 프로세서의 설계에 대해 논의합니다. Sandy Bridge 프로세서는 주로 새로운 멀티미디어 작업 부하, 숫자 집약적인 응용 프로그램 및 멀티 코어 친화적인 병렬 응용 프로그램을 지원하도록 설계되었습니다.

Sandy Bridge 프로세서의 가장 주목할만한 특징은 온칩 그래픽 프로세서가 포함되어 있다는 것입니다. 그래픽 프로세서에는 이미지 렌더링, 비디오 인코딩/디코딩 및 사용자 정의 이미지 처리를 수행하는 전용 장치가 장착되어 있습니다. CPU와 GPU는 대규모 공유 온칩 L3 캐시를 통해 통신합니다. Sandy Bridge 프로세서의 개요는 아래 그림에 나와 있습니다.

칩의 구성 요소가 증가함에 따라 CPU도 광범위한 수정을 거쳤습니다. Sandy Bridge 프로세서는 각 SIMD 장치에 대해 4개의 배정밀도 연산 또는 8개의 단정밀도 연산을 동시에 수행할 수 있는 256비트 SIMD 명령어 세트인 새로운 AVX 명령어 세트를 완벽하게 지원합니다. 고성능 기능이 너무 많이 추가되었으니 에너지 절약 기능도 많이 추가하는 것이 합리적입니다. 사용하지 않는 장치의 경우 DVFS(동적 전압 주파수 스케일링), 클록 게이팅(클럭 끄기) 및 전력 게이팅(기능 장치 그룹의 전원 끄기)과 같은 기술이 이미 일반적입니다. 또한 Sandy Bridge의 설계자들은 셀 간의 복사 값을 최소화하기 위해 코어 설계를 수정했으며(AMD Bobcat의 설계자들도 유사한 설계 결정을 내렸습니다) 전력 효율성을 높이기 위해 분기 예측기 및 분기 대상 버퍼와 같은 일부 코어 구조의 설계를 근본적으로 변경했습니다.

Intel Sandy Bridge와 같은 프로세서는 다중 코어(4~8)를 지원하도록 설계되었으며 각 코어는 양방향 멀티스레딩을 지원하므로 8코어 시스템에서 16개의 스레드를 실행할 수 있습니다. 이러한 스레드는 L3 캐시 뱅크, GPU 및 온칩 Northbridge 컨트롤러 간에 활발하게 통신할 수 있습니다. 통신 개체가 너무 많기 때문에 고대역폭과 낮은 지연 시간의 통신을 촉진하도록 유연한 온칩 네트워크를 설계해야 합니다.

Sandy Bridge의 설계자는 기존 버스 대신 링 기반 상호 연결을 선택했습니다. Sandy Bridge 프로세서는 32nm 1 반도체 공정용으로 설계되었으며 그 후속 제품은 Intel Ivy Bridge 프로세서로 설계는 동일하지만 22nm 공정용으로 설계되었습니다.

이제 아래 그림에서 Sandy Bridge 코어의 세부 설계를 고려하십시오. 사이클당 4개의 x86 명령어를 전달할 수 있는 32KB 명령어 캐시가 있습니다. x86 명령 스트림을 디코딩하는 첫 번째 단계는 경계를 묘사하는 것입니다(프리코딩이라고 함). 4개의 명령어가 사전 인코딩되면 디코더로 전송됩니다. Sandy Bridge에는 4개의 디코더가 있는데, 그 중 3개는 단순 디코더이고, 1개의 디코더는 마이크로프로그램 메모리를 사용하는 복합 디코더로 알려져 있으며, 모두 CISC 명령어를 RISC와 유사한 마이크로 연산으로 변환합니다. Sandy Bridge에는 약 1500개의 마이크로 작업을 저장할 수 있는 마이크로 작업용 L0 캐시가 있습니다. L0 마이크로 연산 캐시는 성능과 전력 소비 측면에서 성능상의 이점을 가지고 있습니다. 분기 대상의 명령을 L0 캐시에서 사용할 수 있는 경우 분기 예측 지연 시간이 줄어들 수 있습니다. 프로그램 내 대부분의 브랜치가 브랜치에 가깝기 때문에 L0 캐시의 적중률이 높을 뿐만 아니라 전력 소모도 절약될 것으로 예상됩니다. L0 캐시에서 명령어의 마이크로 연산을 사용할 수 있는 경우 명령어를 다시 가져오고, 프리코딩하고, 디코딩할 필요가 없으므로 이러한 전력 소모 작업을 피할 수 있습니다.

Sandy Bridge 프로세서의 내부 구조.

메모리 구성 요소가 포함된 코어 i7 파이프라인.

분기 예측 변수와 관련하여 디자이너가 내린 흥미로운 디자인 결정은 컴퓨터 아키텍처의 많은 유사한 문제를 나타냅니다. 작은 구조를 복잡한 항목으로 디자인해야 할까요, 아니면 큰 구조를 간단한 항목으로 디자인해야 할까요? 예를 들어 4방향 16KB 연관 캐시를 사용해야 합니까, 아니면 양방향 32KB 연관 캐시를 사용해야 합니까? 일반적으로 이러한 성격의 질문에 대한 명확한 대답은 없으며 대상 워크로드의 성격에 따라 크게 달라집니다. Sandy Bridge 프로세서의 경우 설계자는 2비트 포화 카운터가 있는 분기 예측기 또는 더 많은 항목과 1비트 포화 카운터가 있는 예측기 중에서 선택할 수 있습니다. 전력과 성능의 균형은 후자의 설계에서 더 나은 것으로 확인되어 1비트 카운터를 선택했습니다.

이어서, 비순차적 스케줄링을 수행하는 이름 바꾸기 및 스케줄링 장치로 4개의 마이크로 연산이 전송됩니다. 초기 프로세서(예: Nehalem 프로세서)에서는 명령 실행의 임시 결과가 ROB에 보관되었으며 명령이 완료되면 레지스터에 복사되었습니다. 이 작업에는 데이터 복사가 포함되었으며 전력 측면에서 효율적이지 않았습니다. Sandy Bridge는 이를 방지하고 고성능 RISC 프로세서와 유사하게 결과를 물리적 레지스터에 직접 저장합니다. 명령어가 이름 바꾸기 단계에 도달하면 이름 바꾸기 테이블의 매핑을 확인하고 소스 피연산자 값을 포함하는 물리적 레지스터의 ID를 찾거나 미래의 특정 시점에 이러한 값을 포함해야 합니다. 그 후, 물리적 레지스터 파일을 읽거나 해당 값이 생성되기를 기다립니다. 물리적 레지스터 파일을 사용하는 것은 완료되지 않은 명령의 결과를 ROB에 저장한 다음 그 결과를 다시 레지스터 파일에 복사하는 다른 방법을 사용하는 것보다 더 나은 접근 방식입니다. 물리적 레지스터를 사용하는 것은 빠르고 간단하며 에너지 효율적입니다. Sandy Bridge 프로세서에서 이 접근 방식을 사용하면 ROB가 단순화되고 언제든지 168개의 명령이 실행될 수 있습니다.

Sandy Bridge 프로세서에는 정수 ALU 3개, 로드 유닛 1개, 로드/저장 유닛 1개가 있습니다. 정수 단위는 160 항목 레지스터 파일에서 피연산자를 읽고 씁니다. 부동 소수점 연산을 지원하기 위해 AVX SIMD 명령어 세트(단정밀도 및 이중 정밀도 숫자 세트에 대해 256비트 연산 수행)를 지원하는 FP 덧셈 장치와 FP 곱셈 장치가 있습니다. 또한 256비트 작업을 지원하기 위해 Intel은 x86 AVX ISA에 새로운 256비트 벡터 레지스터(YMM 레지스터)를 추가했습니다.

AVX 명령어 세트를 구현하려면 32KB 데이터 캐시에서 256비트 전송이 지원되어야 합니다. Sandy Bridge 프로세서는 주기당 2개의 128비트 로드와 1개의 128비트 저장을 수행할 수 있습니다. YMM(256비트) 레지스터를 로드하는 경우 두 개의 128비트 로드 작업이 하나의(256비트) 로드 작업으로 통합됩니다. Sandy Bridge에는 256KB L2 캐시와 대용량(1-8MB) L3 캐시가 있습니다. L3 캐시는 여러 그룹으로 나뉩니다. L3 그룹, 코어, GPU 및 Northbridge 컨트롤러는 단방향 링 기반 상호 연결을 사용하여 연결됩니다. 메시지는 한 방향으로만 전송될 수 있으므로 단방향 링의 직경은 (N-1)입니다. 이러한 제한을 극복하기 위해 각 노드는 실제로 서로 정확히 반대되는 링의 두 지점에 연결되므로 유효 직경은 N=2에 가깝습니다.

샌디브릿지 프로세서에는 터보 모드라는 독특한 기능이 있는데, 그 아이디어는 다음과 같다. 프로세서가 비활성 기간(활성이 적음) 동안 있다고 가정하면 모든 코어의 온도는 상대적으로 낮게 유지됩니다. 또한 사용자가 수치적으로 집약적인 계산이 필요하고 많은 전력이 필요한 계산 집약적인 활동을 수행하기로 결정했다고 가정해 보겠습니다. 모든 프로세서에는 프로세서가 소비할 수 있는 최대 전력인 TDP(열 설계 전력) 등급이 있습니다. 터보 모드에서는 프로세서가 짧은 시간(20~25초) 동안 TDP보다 더 많은 전력을 소비할 수 있습니다. 이는 프로세서가 공칭 값보다 더 높은 주파수에서 모든 장치를 실행할 수 있음을 의미합니다. 온도가 특정 임계값에 도달하면 터보 모드가 자동으로 꺼지고 프로세서가 정상 작동을 재개합니다. 주목해야 할 주요 포인트는 짧은 시간 동안 많은 전력을 소비하는 것은 문제가 되지 않지만 매우 높은 온도는 짧은 시간이라도 허용되지 않습니다. 왜냐하면 높은 온도는 칩에 영구적인 손상을 줄 수 있기 때문입니다. 예를 들어 와이어 하나가 녹으면 칩 전체가 파괴될 수 있습니다. 프로세서가 가열되는 데 몇 초가 걸리기 때문에 고주파 및 고전력 위상을 사용하여 이상한 작업을 신속하게 완료함으로써 이 효과를 활용할 수 있습니다. 터보 모드는 몇 시간이 걸리는 장기 실행 작업에는 적합하지 않습니다.

인텔 CPU 아키텍처

차세대 Intel 프로세서에 포함될 수 있는 Intel 아키텍처 명령 확장 및 기능을 위한 소프트웨어 프로그래밍 인터페이스입니다. 명령어 세트 확장은 다양한 응용 분야 및 프로그래밍 용도를 포괄합니다. 512비트

인텔 고급 벡터 확장 512(Intel AVX-512) 명령어라고 하는 SIMD 벡터 SIMD 확장은 벡터 확장(IntelAVX) 및 인텔 고급 벡터 확장(IntelAVX2) 명령어에 비해 포괄적인 기능과 더 높은 성능을 제공합니다.

512비트 SIMD 명령 확장의 기본은 Intel AVX 및 Intel AVX2 시리즈 SIMD 명령의 확장을 포함하지만 512비트 벡터 레지스터, 64비트 모드에서 최대 32개의 벡터 레지스터 및 opmask 레지스터를 사용한 조건부 처리를 지원하는 새로운 인코딩 체계를 사용하여 인코딩된 Intel AVX-512 Foundation 명령이라고 합니다. Intel 프로세서 관련 기능에는 고급 매트릭스 확장(AMX), ENQCMD/ENWCMDS 명령 및 가상화 지원, TSX 보류 중인 로드 주소 추적, 선형 주소 변환, 아키텍처 마지막 분기 레코드(LBRS), 비쓰기 저장 잠금 비활성화 아키텍처, 버스 잠금 및 VM 알림 기능, 리소스 관리자 기술, EHFI(향상된 하드웨어 피드백 인터페이스), LAM(선형 주소 마스킹), Sapphire Rapids 기반 프로세서용 기계 오류 코드가 포함되지만 이에 국한되지는 않습니다. 마이크로아키텍처, IPI 가상화 등에 대한 자세한 설명은 Intel의 문서인 Intel Architecture Instruction Set Extensions and Future Feature를 참조하십시오.

Xeon E5-2600/4600의 칩 아키텍처는 다음과 같습니다.

2021년 인텔 아키텍처는 하이브리드 스칼라, 벡터, 매트릭스 및 공간 컴퓨팅을 지원합니다.

단일 패키지에 통합된 하이브리드 컴퓨팅 클러스터:

다양한 고성능 및 에너지 절약형 코어를 포함하여 새로운 아키텍처의 기본 구성 요소는 다음과 같습니다.

효율적인 x86 코어를 위해 다음 10년 동안의 처리량 효율성 요구 사항을 충족할 수 있는 확장성이 뛰어난 아키텍처를 갖추고 있습니다.

처리량을 위해 설계되어 최신 멀티태스킹을 위한 확장 가능한 멀티스레드 성능을 지원합니다. 다음을 통해 전력 및 밀도 효율적인 처리량에 최적화되었습니다.

  • 심층적인 프런트엔드, 주문형 길이 디코딩.

  • 실행 포트가 많은 광대역 백엔드.

  • 최신 트랜지스터 기술에 최적화된 설계.

명령어 제어를 위해 주문형 명령어 길이 디코더가 포함된 대형 명령어 캐시(64KB)는 대량의 코드를 소비하는 최신 워크로드를 가속화합니다. 깊은 분기 기록과 큰 구조 크기를 가진 분기를 정확하게 예측합니다.

전력 및 대기 시간을 유지하면서 사이클당 최대 6개의 명령을 디코딩할 수 있는 듀얼 삼중 폭 비순차 디코더:

데이터 실행의 경우 5개 와이드 할당, 8개 와이드 회수, 256개 항목 비순차 창 검색 데이터 병렬 처리, 17개 실행 포트가 데이터 병렬 처리를 수행합니다.

데이터 실행과 관련된 하드웨어 구성 요소, 수량 및 레이아웃은 다음과 같습니다.

메모리 하위 시스템 측면에서:

  • 듀얼 로딩 + 듀얼 스토리지.

  • 최대 4MB의 L2 캐시: L2는 17개의 대기 시간 주기에서 64바이트/주기 대역폭으로 4개의 코어 간에 공유됩니다.

  • 고급 프리페처: 모든 캐시 수준에서 다양한 스트림을 감지합니다.

  • 깊이 버퍼링(깊은 버퍼링, 깊이 버퍼 아님): 64개의 누락을 지원합니다.

  • 인텔 리소스 디렉터 기술: 소프트웨어가 코어 간 및 서로 다른 소프트웨어 스레드 간 공정성을 제어할 수 있도록 합니다.

최신 명령 수준 기능:

  • 안전해요.

  • 처리량이 2배인 부동 소수점 곱셈 누산(FMA) 명령입니다.

  • 정수 AI 처리량(VNNI)을 달성하기 위한 주요 지침이 추가되었습니다.

  • 넓은 벡터. 명령어 세트 아키텍처.

  • Intel VT-rp(가상화 기술 리디렉션 보호)를 지원합니다.

  • 고급 투기 실행 검증 방법.

  • 인텔 제어 흐름 적용 기술은 방어 깊이를 높이도록 설계되었습니다.

  • AI 확장으로 고급 벡터 지침을 지원합니다.

트랜지스터당 전력 및 성능 효율성:

  • 코어 수 확장이 가능하도록 영역 효율성을 극대화하기 위해 기능 선택 및 설계 구현 비용에 중점을 둡니다.

  • 명령당 낮은 스위칭 에너지는 전력이 제한된 처리량을 극대화하며 오늘날의 처리량 중심 워크로드의 핵심입니다.

  • 성능 범위를 확장하면서 모든 주파수에서 전력을 전송하는 데 필요한 작동 전압을 줄입니다.

프로세서 전력 계산 공식.

딜레이 성능은 ISO 파워에서는 40% 이상 성능이 향상되고, ISO 파워에서는 파워가 40% 이상 감소합니다.

처리 성능 측면에서는 성능을 80% 향상시키고 전력을 80% 줄입니다.

처리량을 위해 설계되어 최신 멀티태스킹을 위한 확장 가능한 멀티스레드 성능을 지원합니다. 다음을 통해 전력 및 밀도 효율적인 처리량에 최적화되었습니다.

  • 심층적인 프런트엔드, 주문형 길이 디코딩.

  • 실행 포트가 많은 넓은 백엔드.

  • 최신 트랜지스터 기술에 최적화된 설계.

다음으로 x86 코어의 성능에 대해 설명한다. 아키텍처의 목표는 다음과 같습니다.

  • 일반적인 CPU 성능을 제공하는 단계 함수입니다.

  • 변화하는 워크로드 패턴의 추세에 적응할 수 있는 새로운 기능으로 Arch/uArch를 개선합니다.

  • AI 성능 가속화를 혁신합니다.

새로운 x86 아키텍처는 속도를 위해 설계되었으며 다음을 통해 짧은 대기 시간 및 단일 스레드 애플리케이션 성능의 한계를 뛰어 넘었습니다.

  • 더 넓어요.

  • 더 깊게.

  • 더 똑똑해졌습니다.

대규모 코드 공간과 대규모 데이터 세트, 보조 프로세서를 통한 행렬 곱셈을 위한 새로운 AI 가속 기술, 세분화된 전력 예산 관리를 위한 새로운 지능형 PM 컨트롤러를 사용하여 워크로드를 가속화합니다.

프런트 엔드는 명령어를 가져와 μ 수준 작업으로 디코딩합니다.

  • 더 큰 코드: 4K iTLB 128에서 256으로 증가, 2M/4M iTLB 16에서 32로 증가.

  • 더 넓어짐: 디코딩 길이가 16B에서 32B로 늘어났습니다.

  • 더 스마트해짐: 향상된 분기 예측 정확도, 더 스마트해진 코드 프리페칭 메커니즘.

  • μ 대기열: 스레드당 항목이 70에서 72로 증가하고 단일 스레드가 70에서 144로 증가했습니다.

  • μ 작업: 2.25K를 4K로 업그레이드하고 적중률을 개선하며 프런트 엔드 대역폭을 늘립니다.

비순차적 엔진은 μ 작업 종속성을 추적하고 준비된 μ 작업을 실행 단위로 전달합니다.

  • 더 넓어짐: 할당 너비가 5에서 6으로 증가하고, 실행 포트가 10에서 12로 증가했습니다.

  • 더 깊어짐: 512개 항목 재정렬 버퍼 및 더 큰 스케줄러 크기.

  • 더 스마트해짐: 이름 바꾸기/할당 단계에서 더 많은 명령을 "실행"합니다.

정수 실행 장치는 산술 계산에도 사용되는 5개 포트 모두에 5세대 정수 실행 포트/ALU, 1사이클 LEA를 추가합니다.

벡터 산술 장치에는 새로운 고속 가산기(FADD) 기능이 탑재되어 전력 효율적이고 대기 시간이 낮으며 FMA 장치는 FP16 데이터 유형을 지원합니다. 복소수 지원을 포함하여 Intel AVX512에 FP16이 추가되었습니다.

L1 캐시 및 메모리 하위 시스템의 기능은 다음과 같습니다.

  • 더 넓어짐: 로딩 포트가 2에서 3으로 증가했습니다: 3x256비트 로딩, 2x512비트 로딩.

  • 더 깊게: 로드 버퍼와 저장 버퍼가 더 깊어지면 더 많은 메모리 병렬 처리가 노출됩니다.

  • 더 스마트해짐: 페이로드 대기 시간이 줄어들고 분기 메모리가 더 빨리 제거됩니다.

  • 더 큰 데이터: DTLB(data fast table)가 64에서 96으로 증가하고, L1 데이터 캐시가 향상된 프리페처로 12에서 16으로 증가했으며, 페이지 워커(page walker)가 2에서 4로 증가했습니다.

L2 캐시 및 메모리 하위 시스템의 기능은 다음과 같습니다.

  • 더 큰: 1.25M(클라이언트) 또는 2M(데이터 센터).

  • 더 빠르게: 최대 필수 실패 횟수가 32회에서 48회로 증가했습니다.

  • 더 스마트해짐: L2 캐시 모드 기반 다중 경로 프리페처, 피드백 기반 프리페치 조절, DRAM 읽기를 줄이기 위한 전체 라인 쓰기 예측 대역폭 최적화.

일반 성능 및 11세대 Intel Core, ISO 주파수에서 성능 19% 향상:

Intel AMX(Intel Advanced Matrix Extensions), 블록 매트릭스 곱셈 가속기 - 데이터 센터, VNNI가 256 int8에서 AMX의 2048 int8로 증가하여 작업, 클럭 주기 및 코어가 8배 증가했습니다.

AMX 아키텍처에는 두 가지 구성 요소가 있습니다.

  • 타일:

새로운 확장 가능한 2D 레지스터 파일 - 8개의 새로운 레지스터, 각각 1Kb: T0-T7.

  • 레지스터 파일은 기본 데이터 연산자(로드/저장, 지우기, 상수로 설정 등)를 지원합니다.

  • 상태는 청크로 선언되고 XSAVE 아키텍처에 의해 운영 체제에 의해 관리됩니다.

  • TMUL:

TILE의 첫 번째 연산자인 행렬 곱셈 명령 세트입니다.

  • MAC는 그리드 컴퓨팅 데이터의 "청크"를 계산합니다.

  • TMUL은 3개의 블록 레지스터(T2=+T1T0)를 사용하여 행렬 덧셈과 곱셈(C=+AC)을 수행합니다.

  • TMUL을 사용하려면 청크를 렌더링해야 합니다.

Intel AMX(Intel Advanced Matrix Extensions) 아키텍처는 다음과 같습니다.

x86 코어는 향후 10년 동안 컴퓨팅 CPU 아키텍처 성능의 다음 단계 기능으로, 높은 전력 효율성으로 IPC를 크게 개선하고 더 넓고, 더 깊고, 더 스마트해집니다. 대규모 데이터 세트 및 대규모 코드 공간 애플리케이션에 대한 더 나은 지원, 기계 학습 기술: Intel AMX의 타일식 곱셈, 주파수와 전력을 증가시키는 향상된 전력 관리. 모두 노트북에서 데스크탑, 데이터 센터에 이르기까지 전체 제품 범위에 서비스를 제공하도록 맞춤화된 확장 가능한 아키텍처를 갖추고 있습니다.

Intel의 스칼라 아키텍처 로드맵은 다음과 같습니다.

단일 스레드/대기 시간과 다중 스레드 성능/처리량 간의 2차원 관계는 다음과 같습니다.

인텔 스레드 디렉터:

  • 스케줄링의 목표는 소프트웨어 투명성, 실시간 적응성, 모바일에서 데스크톱으로의 확장성입니다.

  • 코어에 직접 내장된 인텔리전스는 사용자 입력 없이 열 설계 지점, 작동 조건 및 전력 설정을 기반으로 지침을 동적으로 조정합니다.

  • 운영 체제에 런타임 피드백을 제공하여 모든 워크로드 또는 워크플로우에 대한 최적의 일정 결정을 내릴 수 있습니다.

  • 각 스레드의 런타임 명령 혼합과 각 코어의 상태를 나노초 단위의 정밀도로 모니터링합니다.

Thread Director 스케줄링 사례는 다음과 같습니다.

1: P 코어에 예약된 우선순위 작업입니다.

2: E 코어에 예약된 백그라운드 작업입니다.

3: 새로운 AI 스레드가 준비되었습니다.

4: AI 스레드는 P 코어에서 먼저 예약됩니다.

5-7: 스핀 대기가 P 코어에서 E 코어로 이동합니다.

다음으로 멀티 코어 아키텍처 Alder Lake를 소개합니다. Alder Lake의 특징은 다음과 같습니다.

  • 단일 확장 가능한 SoC 아키텍처. 9W부터 125W까지 모든 클라이언트 세그먼트는 Intel 7 프로세스를 기반으로 구축되었습니다.

  • 새로운 코어 디자인. Intel 스레드 컨트롤러를 사용한 성능 하이브리드.

  • 업계 최고의 메모리 및 I/O. DDR5, PCIe Gen5, Thunderbolt 4, Wi-Fi 6E 등을 지원합니다.

확장 가능한 클라이언트 아키텍처:

아키텍처 빌딩 블록:

Alder Lake의 핵심 및 캐시 데이터는 다음과 같습니다.

  • 최대 16개 코어, 8개 성능 코어, 8개 효율성 코어.

  • 최대 24개의 스레드, P 코어당 2개의 스레드, E 코어당 1개의 스레드.

  • 최대 30M 캐시, 비포함, LL 캐시.

Alder Lake의 메모리는 업계의 DDR5로의 전환을 주도하여 4가지 주요 메모리 기술, 동적 전압-주파수 확장 및 향상된 오버클럭 지원을 모두 지원합니다.

PCIe는 Gen4에 비해 최대 2배의 대역폭, 최대 64GB/s, x16 채널을 제공하는 PCIe Gen5로의 업계 전환을 선도합니다.

상호 연결의 특징:

  • 컴퓨팅 구조: 최대 1000GB/s, 동적 대기 시간 최적화.

  • I/O 구조: 최대 64GB/s, 실시간, 수요 기반 대역폭 제어.

  • 메모리 아키텍처: 최대 204GB/s, 동적 버스 폭 및 주파수.

Intel의 통합 그래픽 칩Xe HPG의 Render Slice 아키텍처는 아래와 같습니다. 여기에는 4개의 코어, 샘플러, 레이 트레이싱 장치(기하학 처리, 래스터라이제이션, HiZ 등을 위한 각 1개)와 2개의 픽셀 백엔드가 포함됩니다.

Xe HPG에는 8개의 렌더 슬라이스를 포함하여 확장 가능한 그래픽 엔진이 있습니다. 성능과 전력 관계는 다음과 같습니다.

Xe HPC 기반 GPU의 컴퓨팅 빌딩 블록:

벡터 엔진과 매트릭스 엔진의 매개변수와 위치는 다음과 같습니다.

단일 슬라이스의 구조는 다음과 같습니다.

여러 조각으로 구성된 스택은 다음과 같습니다.

물론 여러 개의 스택이 더 큰 구조를 형성할 수도 있습니다. 연결은 확장성이 뛰어나며 2점 상호 연결, 4점 상호 연결, 6점 상호 연결 및 8점 상호 연결도 지원합니다.

Sapphire Rappids는 마이크로서비스 및 AI 워크로드, 획기적인 고급 메모리 및 IO 변환을 위해 설계된 데이터 센터 아키텍처의 새로운 표준인 차세대 Intel Xeon 확장 가능 프로세서입니다. Intel Xeon의 노드 성능은 스칼라 성능, 데이터 병렬 성능, 캐시 및 메모리 하위 시스템 아키텍처, 소켓 내/소켓 간 확장에 반영됩니다.

Intel Xeon의 데이터 센터 성능은 통합 및 비즈니스 프로세스, 성능 일관성, 탄력적이고 효율적인 데이터 센터 활용, 인프라 및 프레임워크 오버헤드 등의 측면에서 입증됩니다.

모듈식 아키텍처를 통해 모놀리식 CPU(예: Ice Lake)의 기존 소프트웨어 패러다임을 활용하여 확장 가능하고 균형 잡힌 아키텍처(예: Sapphire Rapids)를 제공합니다.

Sapphire Rapids는 각 스레드가 모든 타일 캐시, 메모리, IO에 대한 전체 액세스를 제공하는 멀티 칩, 단일 CPU 아키텍처로 전체 SoC에 걸쳐 일관되게 낮은 대기 시간과 높은 단면적 대역폭을 제공합니다. Sapphire Rapids SoC의 물리적 아키텍처는 다음과 같습니다.

Sapphire Rapids의 주요 구성 요소는 다음과 같습니다.

데이터 센터용으로 구축된 성능 코어, 주요 마이크로아키텍처 및 IPC 개선, 대규모 코드/데이터 공간에 대한 지원 개선, 고주파 자동/빠른 PM.

성능 코어, DC 워크로드 및 사용량에 대한 아키텍처 개선:

  • AI: Intel Advanced Matrix Extensions-AMX, 추론 및 훈련 가속화를 위한 타일링 매트릭스 작업.

  • 연결된 장치: 가속기 인터페이스 아키텍처-AiA, 효율적인 스케줄링, 사용자 수준의 신호 및 동기화.

  • FP16: 절반 정밀도, 더 높은 처리량과 더 낮은 정밀도를 지원합니다.

  • 캐시 관리: CLDEMOTE, 캐시 콘텐츠를 사전에 배치합니다.

Sapphire Rapids 가속 엔진은 공통 모드 작업 오프로딩을 실현하여 완벽하게 통합된 가속 엔진, 기본 스케줄링, 사용자 공간의 신호 및 동기화, 가속기 인터페이스 아키텍처, 코어와 가속 엔진 간의 일관된 공유 스토리지 공간, 동시에 공유되는 프로세스, 컨테이너 및 VM을 통해 코어 효율성을 향상시킵니다.

Intel 데이터 스트리밍 가속 엔진은 소켓당 최대 4개의 인스턴스, 짧은 지연 시간의 호출, 메모리 고정 오버헤드 없음, DSA 오프로드 후 CPU 코어 주기의 추가 39%를 통해 스트리밍 데이터 이동 및 변환 작업을 최적화합니다.

Intel Quick Assist 기술 가속 엔진은 암호화 및 데이터 제거/압축, 최대 400Gb/s 대칭 암호화, 최대 160Gb/s 압축 + 160Gb/s 압축 해제, 통합 작업 속도를 높이고 QAT 오프로드 후 워크로드 용량을 98% 늘립니다.

Sapphire Rapids I/O 고급 기능:

  • Compute ePressLink(CXL) 1.1 소개, 데이터 센터의 가속기 및 메모리 확장.

  • PCIe 5.0 및 연결성을 통해 장치 성능을 확장하고 Intel® UPI(Ultra Path Interconnect) 2.0으로 다중 슬롯 확장을 개선합니다.

  • 16GT/s에서 작동하는 최대 4개의 x24 UPI 링크, 새로운 8S-4UPI 성능 최적화 토폴로지, 향상된 DDIO 및 QoS 기능.

Sapphire Rapids의 인메모리 및 최종 레벨 캐시:

  • 모든 코어에서 공유되는 최대 용량이 100MB가 넘는 LLC로 공유 LLC(마지막 레벨 캐시)가 증가했습니다.

  • DDR 5 메모리, 4개 메모리 컨트롤러, 8개 채널 지원으로 대역폭, 보안 및 안정성이 향상되었습니다.

  • Intel Optane 영구 메모리 300 시리즈.

Sapphire Rapids는 고대역폭 메모리를 제공합니다.

  • 8채널 DDR 5를 탑재한 기본 Xeon SP에 비해 메모리 대역폭이 크게 향상되었습니다.

  • 용량과 대역폭이 증가하여 일부 용도에서는 DDR의 필요성이 완전히 제거될 수 있습니다.

  • 2가지 모드:

HBM 플랫 모드. HBM 및 DRAM을 갖춘 플랫 메모리 영역.

  • HBM 캐시 모드. DRAM 백업 캐시.

Sapphire Rapids는 탄력적인 컴퓨팅 모델을 위한 마이크로서비스를 구축하고 있으며, 새로운 클라우드 네이티브 및 SaaS 애플리케이션의 80% 이상이 마이크로서비스로 구축될 것으로 예상됩니다. 목표는 대기 시간 요구 사항을 충족하면서 더 높은 처리량을 달성하고 수천 개의 마이크로 서비스를 실행, 모니터링 및 조정하는 인프라 오버헤드를 줄이는 것입니다. 성능과 서비스 품질을 개선하고, 인프라 오버헤드를 줄이고, 분산 통신을 개선합니다. 마이크로서비스 성능 비교는 다음과 같습니다.

베키오 다리(Ponte Vecchio)에는 다음이 있습니다:

  • 새로운 확인 방법.

  • 새로운 SOC 아키텍처.

  • 새로운 IP 아키텍처.

  • 새로운 메모리 아키텍처.

  • 새로운 I/O 아키텍처.

  • 새로운 패키징 기술.

  • 새로운 전력 전달 기술.

  • 새로운 상호 연결.

  • 새로운 신호 무결성 기술.

  • 새로운 신뢰성 방법.

  • 새로운 소프트웨어.

Ponte Vecchio SoC에는 1000억 개가 넘는 트랜지스터, 47개의 활성 블록, 5개의 프로세스 노드가 있습니다. 내부 구조 다이어그램은 다음과 같습니다.

다양한 속도 향상 비율을 갖춘 가속 컴퓨팅 시스템:

CPU 최적화 스택 및 GPU 최적화 스택을 포함하여 별도의 CPU 및 GPU 소프트웨어 스택을 극복합니다.

19.6 메모리 시스템

지금까지 우리는 메모리 시스템을 큰 바이트 배열로 보았으며 이 추상화는 명령어 세트 설계, 어셈블리 언어 학습, 심지어 복잡한 파이프라인이 있는 기본 프로세서 설계에도 충분합니다. 그러나 실용적인 관점에서 볼 때, 빠른 메모리 시스템을 설계하기 위해서는 이러한 추상화를 더욱 재정의할 필요가 있습니다. 이전 장에서 소개한 기본 파이프라인에서는 데이터 및 명령어 메모리에 액세스하는 데 1사이클이 필요하다고 가정했는데, 이 장에서는 이것이 항상 정확하지는 않습니다. 실제로 1사이클이라는 이상적인 지연 시간에 접근하려면 메모리 시스템의 상당한 최적화가 필요하며, 대용량 메모리와 낮은 지연 시간이라는 이중 문제를 해결하려면 '캐시'와 계층적 메모리 시스템의 개념을 도입해야 합니다.

둘째, 지금까지는 하나의 프로그램만 시스템에서 실행된다고 가정했지만 대부분의 프로세서는 일반적으로 시간 공유 기반으로 여러 프로그램을 실행합니다. 예를 들어 두 개의 프로그램 A와 B가 있는 경우 최신 데스크톱이나 랩톱은 일반적으로 프로그램 A를 몇 밀리초 동안 실행하고 프로그램 B를 몇 밀리초 동안 실행한 다음 앞뒤로 전환합니다. 실제로 시스템은 웹 브라우저, 오디오 플레이어, 캘린더 애플리케이션과 같은 다른 많은 프로그램을 실행합니다. 일반적으로 말해서, 중단이 발생하는 시간 척도는 인간의 두뇌가 인지할 수 있는 시간 척도보다 훨씬 낮기 때문에 사용자는 어떠한 방해도 인지하지 못합니다. 예를 들어 일반적인 비디오는 초당 30번씩 새 이미지를 표시하거나 33밀리초마다 새 이미지를 표시합니다. 인간의 두뇌는 그림을 연결하여 부드럽게 움직이는 물체의 환상을 만듭니다. 프로세서가 33밀리초 전에 비디오 시퀀스의 다음 사진 처리를 마치면 다른 프로그램의 일부를 실행할 수 있습니다. 인간의 뇌는 그 차이를 구분할 수 없습니다. 여기서 요점은 우리가 모르는 사이에 프로세서가 운영 체제와 협력하여 초당 여러 번 여러 프로그램 사이를 전환한다는 것입니다. 운영 체제 자체는 프로세서가 자신과 다른 프로그램을 관리하는 데 도움이 되는 특수 프로그램입니다. Windows와 Linux는 널리 사용되는 운영 체제의 예입니다.

여러 프로그램을 지원하려면 메모리 시스템에 특별한 지원이 필요합니다. 이 지원이 없으면 여러 프로그램이 서로의 데이터를 덮어쓰게 되는데, 이는 바람직하지 않은 동작입니다. 둘째, 우리는 무한한 메모리를 가지고 있다고 가정했는데, 이는 역시 사실이 아니며, 우리가 가지고 있는 메모리의 양은 0이고, 메모리를 많이 사용하는 대규모 프로그램에 의해 고갈될 수 있습니다. 따라서 우리는 이러한 대규모 프로그램을 계속 실행할 수 있는 메커니즘이 있어야 하며, 여러 프로그램을 실행하고 대규모 메모리 집약적인 프로그램을 처리하는 두 가지 문제를 해결하기 위해 가상 메모리 개념을 도입할 것입니다.

컴퓨터 시스템의 메모리 계층 구조.

컴퓨터 메모리라는 복잡한 주제는 메모리 시스템을 핵심 특성에 따라 분류하면 관리하기가 더 쉽습니다. 그 중 가장 중요한 내용이 아래 표에 나열되어 있습니다.

표에서 위치라는 용어는 메모리가 컴퓨터 내부에 있는지 외부에 있는지를 나타냅니다. 내부 메모리는 일반적으로 주 메모리와 동일하지만 다른 형태의 내부 메모리도 있습니다. 프로세서에는 레지스터 형태의 자체 로컬 메모리가 필요하고, 프로세서의 제어 장치 부분에도 자체 내부 메모리가 필요할 수 있으며, 캐시는 또 다른 형태의 메모리입니다. 외부 메모리는 프로세서가 I/O 컨트롤러를 통해 액세스할 수 있는 주변 저장 장치(예: 디스크 및 테이프)로 구성됩니다.

메모리의 분명한 특징은 용량입니다. 내부 메모리의 경우 일반적으로 바이트(1바이트 = 8비트) 또는 워드 단위로 표현됩니다. 일반적인 단어 길이는 8, 16, 32비트입니다. 외부 메모리 용량은 일반적으로 바이트 단위로 표시됩니다.

관련된 개념은 전송 단위입니다. 내부 메모리의 경우 전송 단위는 메모리 모듈에 들어오고 나가는 와이어 수와 동일합니다. 이는 워드 길이와 같을 수도 있지만 일반적으로 64, 128 또는 256비트와 같이 더 큽니다. 이 점을 설명하기 위해 내부 메모리와 관련된 세 가지 개념을 고려하십시오.

  • 워드: 메모리 구성의 "자연스러운" 단위인 워드의 크기는 일반적으로 정수 및 명령어 길이를 나타내는 데 사용되는 비트 수와 같습니다. 불행하게도 64비트 단어 크기를 가지고 있지만 46비트 정수 표현을 사용하는 CRAY C90(기존 CRAY 슈퍼컴퓨터)과 같은 많은 예외가 있습니다. Intel x86 아키텍처는 바이트의 배수로 표현되는 다양한 명령 길이를 가지며 워드 크기는 32비트입니다.

  • 주소 지정 가능 단위: 일부 시스템에서는 주소 지정 가능 단위가 단어이지만 많은 시스템에서는 바이트 수준에서 주소 지정을 허용합니다. 어쨌든, 주소의 비트 A 길이와 주소 지정 가능한 단위 수 N 사이의 관계는 2A=N입니다.

  • 전송 단위: 주 메모리의 경우 메모리에서 한 번에 읽거나 쓰는 비트 수입니다. 전송 단위는 단어 또는 주소 지정 가능 단위와 동일할 필요는 없습니다. 외부 메모리의 경우 데이터는 일반적으로 단어보다 훨씬 큰 단위로 전송되며 이러한 단위를 블록이라고 합니다.

메모리 유형 간의 또 다른 차이점은 다음을 포함하는 데이터 단위에 액세스하는 방법입니다.

  • 순차적 액세스: 메모리는 레코드라고 하는 데이터 단위로 구성됩니다. 액세스는 특정 선형 순서로 이루어져야 하며, 저장된 주소 지정 정보는 레코드를 분리하고 검색 프로세스를 지원하는 데 사용됩니다. 공유된 읽기-쓰기 메커니즘을 사용하면 각 중간 레코드를 현재 위치에서 원하는 위치로 이동하고 전달하고 거부해야 하므로 레코드에 액세스하는 데 걸리는 시간은 매우 다양합니다.

  • 직접 액세스: 순차 액세스와 마찬가지로 직접 액세스에는 공유된 읽기 및 쓰기 메커니즘이 포함되지만 단일 블록이나 레코드는 물리적 위치를 기반으로 하는 고유한 주소를 갖습니다. 일반적인 인근 위치에 도달하기 위한 직접 접근과 순차적 검색, 계산 또는 최종 위치 도달 대기를 통해 접근이 이루어집니다. 마찬가지로 액세스 시간도 가변적입니다.

  • 랜덤 액세스: 메모리에서 주소 지정 가능한 각 위치에는 고유한 물리적 와이어 주소 지정 메커니즘이 있으며, 지정된 위치에 액세스하는 시간은 이전 액세스 순서와 무관하며 일정합니다. 따라서 임의의 위치를 ​​무작위로 선택하고 직접 주소를 지정하고 액세스할 수 있습니다. 주 메모리와 일부 캐시 시스템은 무작위로 액세스됩니다.

  • 연관: 단어 내에서 필요한 비트 위치를 비교하여 지정된 일치 항목을 얻고 모든 단어를 동시에 비교할 수 있는 임의 액세스 유형의 메모리입니다. 따라서 단어는 주소가 아닌 내용의 일부를 기반으로 검색됩니다. 일반 랜덤 액세스 메모리와 마찬가지로 각 위치에는 고유한 주소 지정 메커니즘이 있으며 검색 시간은 위치나 이전 액세스 패턴과 무관합니다. 캐시 메모리는 연관적으로 액세스할 수 있습니다.

사용자 관점에서 볼 때 메모리의 가장 중요한 두 가지 특성은 용량과 성능입니다. 성능에는 세 가지 매개변수가 포함됩니다.

  • 액세스 시간(대기 시간): 랜덤 액세스 메모리의 경우 읽기 또는 쓰기 작업을 수행하는 데 필요한 시간, 즉 주소가 메모리에 제공되는 순간부터 데이터가 저장되거나 사용할 수 있게 되는 순간까지의 시간입니다. 비 랜덤 액세스 메모리의 경우 액세스 시간은 읽기-쓰기 메커니즘을 원하는 위치에 배치하는 데 필요한 시간입니다.

  • 메모리 주기 시간: 주로 랜덤 액세스 메모리에 적용되며 액세스 시간과 두 번째 액세스가 시작되기 전에 필요한 추가 시간이 포함됩니다. 신호 라인의 과도 현상이 사라지거나 데이터가 파괴적으로 읽히면 데이터를 재생성하는 데 추가 시간이 필요할 수 있습니다. 메모리 주기 시간은 프로세서가 아닌 시스템 버스와 관련이 있습니다.

  • 전송 속도: 데이터가 메모리 장치로 전송되거나 메모리 장치에서 전송될 수 있는 속도입니다. 랜덤 액세스 메모리의 경우 1/(사이클 시간)과 동일하고 비 랜덤 액세스 메모리의 경우 다음 관계가 유지됩니다.

Tn=TA+nR

그 중에는:

Tn=n비트를 읽거나 쓰는 데 걸리는 평균 시간입니다.

  • TA=평균 접속 시간.

  • n=자리수.

  • R=전송 속도, 단위는 비트/초(bps)입니다.

다양한 물리적 유형의 메모리가 설명되었습니다. 오늘날 가장 일반적인 것은 반도체 메모리, 자기 디스크 및 테이프용 자기 표면 메모리, 광학 및 광자기 메모리입니다.

19.6.1 메모리 시스템 소개

19.6.1.1 빠른 메모리 시스템 요구 사항

이제 빠른 스토리지 시스템을 구축하기 위한 기술적 요구 사항을 살펴보겠습니다. 메모리 구성 요소를 설계하는 데 사용할 수 있는 기본 회로에는 래치, SRAM 셀, CAM 셀, DRAM 셀 등 4가지가 있습니다. 여기에는 절충점이 있습니다. 래치 및 SRAM 셀은 DRAM 또는 CAM 셀보다 훨씬 빠르지만 래치, CAM 또는 SRAM 셀은 DRAM 셀보다 면적이 훨씬 더 크고 훨씬 더 많은 전력을 소비합니다. 래치는 네거티브 클록 에지에서 데이터를 읽고 감지하도록 설계되었으며 클록 사이클의 일부 내에 데이터를 저장하고 검색할 수 있는 빠른 회로입니다. 반면 SRAM 셀은 일반적으로 디코더 및 감지 증폭기와 함께 더 큰 SRAM 셀 어레이의 일부로 사용되도록 설계됩니다. 이러한 추가 오버헤드로 인해 SRAM 셀은 일반적으로 일반적인 에지 트리거 래치보다 느립니다. 이와 대조적으로 CAM 셀은 콘텐츠 의존형 메모리에 가장 적합한 반면, DRAM 셀은 대용량 메모리에 가장 적합합니다.

이제 파이프라인은 메모리 액세스에 1사이클이 걸린다고 가정하고, 이 요구 사항을 충족하려면 전체 메모리를 작은 래치 어레이 또는 SRAM 셀로 구축해야 합니다. 아래 표는 2012년 현재 일반적인 래치, SRAM 셀, DRAM 셀의 크기를 보여줍니다.

단위 유형 지역 일반적인 지연

마스터-슬레이브 D 플립플롭 0.8μm2 한 클럭 사이클의 분수

SRAM 셀 0.08μm2 1-5 클록 사이클

DRAM 셀 0.005μm2 50-200 클록 사이클

일반적인 래치(마스터-슬레이브 D 플립플롭)는 SRAM 셀보다 10배 더 크며 이는 DRAM 셀보다 약 16배 더 큽니다. 즉, 특정 양의 실리콘이 주어지면 DRAM 셀을 사용하면 160배 더 많은 데이터를 저장할 수 있지만 200배 더 느립니다(DRAM 셀의 대표적인 배열을 고려하는 경우). 분명히 용량과 속도 사이에는 균형이 있지만 실제로는 둘 다 필요합니다.

먼저 저장 용량 문제를 고려해 보겠습니다. 기술 및 제조 가능성의 여러 제한으로 인해 2012년 현재 400-500mm²보다 큰 면적의 칩을 제조할 수 없었기 때문에 칩에 보유할 수 있는 전체 메모리 용량은 제한되어 있지만 메모리 셀을 특별히 포함하는 추가 칩으로 사용 가능한 메모리 용량을 보충하는 것은 전적으로 가능합니다. 오프칩 메모리는 속도가 더 느리고 프로세서가 이러한 메모리 모듈에 액세스하는 데 수십 사이클이 걸린다는 점을 명심하세요. 1-사이클 메모리 액세스 목표를 달성하려면 대부분의 경우 상대적으로 더 빠른 온칩 메모리를 사용해야 하지만 옵션도 제한되어 있으며 래치로만 구성된 메모리 시스템을 감당할 수 없습니다. 프로그램 수가 많으면 모든 데이터를 메모리에 저장할 수 없습니다. 예를 들어 최신 프로그램에는 종종 수백 메가바이트의 메모리가 필요하고 일부 대형 과학 프로그램에는 기가바이트의 메모리가 필요합니다. 둘째, 기술적 한계로 인해 대형 DRAM 어레이와 프로세서를 동일한 칩에 통합하는 것이 어렵고 설계자는 온칩 메모리에 대형 SRAM 어레이를 사용해야 합니다. 위 표에서 볼 수 있듯이 SRAM 셀(어레이)은 DRAM 셀(어레이)보다 훨씬 크기 때문에 용량도 훨씬 작습니다.

그러나 대기 시간 요구 사항이 충돌합니다. 저장 공간을 최대화하고 메모리를 DRAM 셀로만 구성하고 DRAM에 액세스하는 데 100사이클의 대기 시간을 주기로 결정했다고 가정해 보겠습니다. 명령어의 1/3이 메모리 명령어라고 가정하면 완벽한 5단계 프로세서 파이프라인의 유효 CPI는 1+1/3×(1001)=34로 계산됩니다. CPI가 34배나 증가했는데 이는 전혀 받아들일 수 없는 수치입니다!

따라서 지연 시간과 스토리지 간의 공정한 균형이 필요합니다. 가능한 한 많은 데이터를 저장하되 IPC가 매우 낮아서는 안 됩니다. 불행히도 메모리 액세스가 완전히 무작위라고 가정하면 이 상황에서 벗어날 수 있는 방법이 없습니다. 높은 저장 용량과 낮은 대기 시간이라는 두 가지 장점을 최대한 활용할 수 있도록 메모리 액세스에 특정 패턴이 있으면 더 좋을 것입니다.

19.6.1.2 메모리 액세스 모드

두 가지 메모리 액세스 모드가 있습니다.

  • 시간적 지역성. 특정 시점에 리소스에 액세스하면 짧은 시간 내에 다시 액세스될 가능성이 높습니다.

  • 공간적 지역성. 특정 시점에 리소스에 액세스하면 가까운 시일 내에 유사한 리소스에 액세스할 가능성이 높습니다.

메모리 액세스에 시간적, 공간적 지역성이 있습니까?

어느 정도 시간적, 공간적 지역성이 있는 경우 대규모 메모리 요구 사항과 짧은 대기 시간이라는 두 가지 문제를 해결하는 데 도움이 되는 몇 가지 주요 최적화가 이루어질 수 있습니다. 컴퓨터 아키텍처에서는 시간적, 공간적 지역성과 같은 속성을 활용하여 문제를 해결하는 경우가 많습니다.

19.6.1.3 명령어 접근의 시공간 지역성

이 문제를 해결하기 위한 표준 접근 방식은 SPEC 벤치마크와 같은 대표적인 프로젝트 세트에서 지역성을 측정하고 특성화하는 것입니다. 메모리 액세스는 크게 명령어와 데이터라는 두 가지 범주로 나눌 수 있습니다. 명령어 액세스는 비공식적으로 분석하기가 더 쉽기 때문에 먼저 이를 살펴보겠습니다.

일반적인 프로그램에는 할당문, 분기문(if, else) 및 루프가 있습니다. 대규모 프로그램의 코드 대부분은 루프 또는 일부 일반 코드의 일부입니다. 컴퓨터 아키텍처에는 90%의 코드가 10%의 시간 동안 실행되고, 10%의 코드가 90%의 시간 동안 실행된다는 표준 경험 법칙이 있습니다. 워드 프로세서의 경우 사용자 입력을 처리하고 결과를 화면에 표시하는 코드가 도움말 화면을 표시하는 코드보다 더 자주 실행됩니다. 마찬가지로 과학적 응용의 경우 대부분의 시간은 프로그램의 몇 가지 루프에서 소비됩니다. 실제로 대부분의 일반적인 애플리케이션에서는 이 패턴을 사용합니다. 따라서 컴퓨터 설계자는 명령 액세스의 시간적 지역성이 대부분의 프로그램에 적용된다는 결론을 내립니다.

이제 명령어 접근의 공간적 지역성을 고려해보세요. 분기 문이 없으면 다음 프로그램 카운터는 현재 프로그램 카운터에 ISA(일반 ISA의 경우) 4바이트를 더한 값입니다. 메모리 주소가 서로 가까우면 두 개의 액세스가 "유사하다"고 간주하며 여기에 공간적 지역성이 분명히 있습니다. 프로그램의 대부분의 명령은 비분기형이며 공간적 지역성이 유지됩니다. 또한 대부분의 프로그램에서 분기에 대한 좋은 패턴은 분기 대상이 실제로 그렇게 멀리 떨어져 있지 않다는 것입니다. 간단한 If-else 문이나 for 루프를 고려하면 분기 대상의 거리는 루프 또는 문의 If 부분의 길이와 같습니다. 대부분의 프로그램에서 이는 일반적으로 수천 개의 명령어 길이가 아니라 10~100개의 명령어 길이입니다. 따라서 설계자들은 명령어 메모리 액세스도 상당한 공간적 지역성을 나타낸다고 결론지었습니다.

데이터 액세스 상황은 약간 더 복잡하지만 크게 다르지는 않습니다. 데이터 액세스의 경우 동일한 데이터를 재사용하고 유사한 데이터 항목에 액세스하는 경향이 있습니다.

19.6.1.4 시간적 지역성 특성

프로그램에서 시간적 지역성을 특성화하기 위해 스택 거리 방법이라는 방법을 설명하겠습니다.

우리는 액세스된 데이터 주소의 스택을 유지합니다. 각 메모리 명령(로드/저장)에 대해 스택에서 해당 주소를 검색하고 항목이 발견된 위치(발견된 경우)를 "스택 거리"라고 합니다. 거리는 거리가 0인 스택 상단에서 측정되며 100번째 항목의 스택 거리는 99입니다. 스택에서 항목을 감지할 때마다 해당 항목을 제거하고 스택 상단으로 푸시합니다.

메모리 주소를 찾을 수 없으면 새 항목이 생성되어 스택 맨 위로 푸시됩니다. 일반적으로 스택의 깊이는 길이 L로 제한됩니다. 새 항목 추가로 인해 스택의 항목 수가 L을 초과하는 경우 스택 맨 아래에 있는 항목을 제거해야 합니다. 둘째, 새 항목을 추가할 때 스택 거리가 정의되지 않습니다. 제한된 스택을 고려하기 때문에 새 항목을 스택에 있는 항목과 구별할 방법은 없지만 스택의 맨 아래에 있으므로 제거해야 합니다. 따라서 이 경우 스택 거리를 L(스택 깊이로 제한)과 동일하게 설정합니다.

스택 거리의 개념은 시간적 지역성을 나타냅니다. 액세스의 시간적 지역성이 높으면 평균 스택 거리가 더 낮아질 것으로 예상됩니다. 대조적으로, 메모리 액세스의 시간적 지역성이 낮은 경우 평균 스택 거리가 높으므로 스택 거리 분포를 사용하여 프로그램의 시간적 지역성을 측정할 수 있습니다.

다양한 Perl 프로그램을 실행하고 스택 거리를 추적하기 위해 카운터를 유지하는 SPEC2006 벤치마크 Perlbench를 사용하여 간단한 실험을 수행할 수 있습니다. 백만 번째 메모리 액세스는 워밍업 기간으로, 이 기간 동안 스택은 변경되지 않지만 카운터는 증가하지 않습니다. 다음 백만 개의 메모리 액세스에 대해 스택은 변경되지 않은 상태로 유지되고 카운터는 증가됩니다. 아래 그림은 스택 거리의 히스토그램을 보여줍니다. 스택 크기는 1000개 항목으로 제한되어 있으며 이는 대부분의 메모리 액세스를 캡처하기에 충분합니다.

스택 거리 분포도.

위에서 볼 수 있듯이 대부분의 액세스는 매우 낮은 스택 거리를 가지며 0-9 사이의 스택 거리가 가장 일반적인 값이며 모든 액세스의 약 27%가 이 저장소에 있습니다. 실제로 메모리 액세스의 2/3 이상이 스택 거리가 100 미만입니다. 100을 초과하면 분포가 점차 감소하지만 상당히 안정적으로 유지됩니다. 스택 거리의 분포는 종종 두꺼운 꼬리 분포를 따른다고 합니다. 즉, 분포가 작은 스택 거리 쪽으로 크게 치우쳐 있지만 큰 스택 거리가 드문 일이 아니라는 의미입니다. 스택 거리가 큰 경우 분포의 꼬리는 0이 아닌 상태로 유지되며 위 플롯은 유사한 동작을 보여줍니다.

연구원들은 로그 정규 분포를 사용하여 스택 거리를 근사화하려고 했습니다.

f(x)=\frac{1}{x \sigma \sqrt{2 \pi} e^{-\frac{(\ln (x)-\mu)^{2}{2 \sigma^{2&#125;&#125;

19.6.1.5 공간적 지역성 특성

스택 거리와 관련하여 주소 거리라는 용어를 정의합니다. 여기서 i 번째 주소 거리는 i 번째 메모리 액세스의 메모리 주소와 로드 또는 저장될 수 있는 마지막 K 메모리 액세스 집합에서 가장 가까운 주소 간의 차이입니다. 이런 방식으로 잘못된 주소 거리를 제거하는 데에는 직관적인 이유가 있습니다. 프로그램은 종종 동일한 시간 간격 내에 주 메모리의 다른 영역에 액세스합니다. 배열에 대한 작업을 수행하고, 배열 항목에 액세스한 다음 일부 상수에 액세스하고, 작업을 수행하고, 결과를 저장한 다음 For 루프를 사용하여 다음 배열 항목으로 이동합니다. 여기에는 분명히 공간적 지역성이 있습니다. 즉, 루프 액세스의 연속적인 반복이 배열의 주소에 가깝습니다. 그러나 이를 정량화하려면 마지막 K개 액세스 중에서 가장 가까운 액세스(메모리 주소로 표시)를 검색해야 합니다. 여기서 K는 둘러싸는 루프의 각 반복에서 메모리 액세스 수입니다. 이 경우 주소 거리가 작은 값으로 나타나며 공간적 집약성이 높다는 것을 의미한다. 그러나 K는 신중하게 선택해야 하며, 너무 작거나 커서는 안 됩니다. 경험에 따르면 K = 10은 대규모 프로그램 세트에 적합한 값입니다.

요약하면, 평균 주소 거리가 작다는 것은 프로그램이 공간적 지역성이 높다는 것을 의미하며 프로그램은 동일한 시간 간격 내에서 가까운 메모리 주소에 높은 확률로 접근하는 경향이 있습니다. 반면, 주소 거리가 멀면 액세스가 서로 멀리 떨어져 프로그램이 공간적 지역성을 나타내지 않습니다.

SPEC2006 벤치마크 Perlbench를 사용하여 이전에 설명한 실험을 반복하면 처음 1백만 방문에 대한 주소 거리 분포가 제공됩니다. 아래 그림을 참조하세요.

방문의 4분의 1 이상이 -5와 +5 사이의 주소 거리를 가지며, 방문의 2/3 이상이 -25와 +25 사이의 주소 거리를 갖습니다. ±50 이후에는 주소 거리 분포가 점차 감소합니다. 경험적으로, 이 분포는 꼬리가 두꺼운 특성도 가지고 있습니다.

19.6.1.6 시공간적 지역성 활용

이전 섹션에서는 샘플 프로그램의 스택 및 주소 거리 분포를 보여줍니다. 컴퓨터 게임, 워드 프로세서, 데이터베이스, 스프레드시트 애플리케이션, 날씨 시뮬레이션 프로그램, 금융 애플리케이션, 모바일 컴퓨터에서 실행되는 소프트웨어 애플리케이션 등 사용자가 일상 생활에서 사용하는 수천 개의 프로그램에 대해 유사한 실험이 수행되었습니다. 거의 모두 시간과 공간에서 매우 높은 지역성을 나타냅니다. 즉, 시간과 공간의 지역성은 우리가 무엇을 하든(예를 들어 책을 집어들거나 프로그램을 작성하는 등) 변함없이 유지되는 인간의 기본 특징입니다. 이는 단지 경험적 관찰일 뿐이며 어떤 형태의 시간적, 공간적 지역성을 나타내지 않는 프로그램을 작성하는 것도 가능하며 상용 프로그램에서 이러한 특성을 나타내지 않는 코드 영역을 찾는 것도 가능합니다. 그러나 이는 표준이 아닌 예외입니다. 우리는 특별한 경우보다는 루틴에 맞게 컴퓨터 시스템을 설계해야 하며, 이것이 바로 대부분의 사용자가 실행하기를 기대하는 프로그램의 성능을 향상시키는 방법입니다.

이제부터 시간적, 공간적 지역성을 당연하게 여기고 저장 용량을 저하시키지 않으면서 메모리 시스템의 성능을 향상시키기 위해 무엇을 할 수 있는지 알아보세요. 먼저 시간적 지역성을 살펴보자.

시간적 지역성 활용: 계층적 기억 시스템

우리는 캐시라고 불리는 메모리 저장 위치를 설계할 수 있습니다. 캐시의 각 항목은 개념적으로 메모리 주소와 값이라는 두 개의 필드를 포함하며 아래 그림과 같이 캐시 계층 구조를 정의합니다.

메모리 계층.

주 메모리(물리적 메모리)는 프로세서가 사용하는 모든 메모리 위치의 값을 포함하는 대규모 DRAM 어레이입니다.

L1 캐시는 일반적으로 작은 SRAM 어레이(8 - 64KB)이고 L2 캐시는 더 큰 SRAM 어레이(128KB - 4MB)입니다. 일부 프로세서(예: Intel Sandybridge 프로세서)에는 L3 캐시(4MB+)라는 또 다른 수준의 캐시가 있습니다. L2/L3 캐시 아래에는 주 메모리 또는 물리적 메모리라고 하는 모든 메모리 위치를 포함하는 대형 DRAM 어레이가 있습니다. L1 캐시 등 L2 캐시 값의 하위 집합을 포괄적 캐시가 있는 시스템에서 유지하는 것이 더 쉽습니다. 따라서 포괄적인 캐시 계층 구조의 경우 다음과 같습니다.  값 ​​(L1) 값 ​​(L2) 값 ​​( 주 메모리 ). 또는 상위 수준 캐시가 반드시 하위 수준 캐시의 값 하위 집합을 포함하지 않는 독점 캐시를 사용할 수 있습니다. 지금까지 비독점 캐시는 디자인의 단순성, 단순성 및 일부 미묘한 정확성 문제로 인해 모든 프로세서에서 일반적으로 사용되었습니다. 그러나 2012년 현재 범용 프로세서의 실용성은 아직 확립되지 않았다.

n번째 레벨 캐시에 포함된 메모리 값의 집합이 (n+1)번째 레벨 캐시에 있는 모든 값의 하위 집합인 메모리 시스템을 포괄적 캐시 계층 구조라고 합니다. 엄격한 포함을 따르지 않는 메모리 시스템을 배타적 캐시 계층 구조라고 합니다.

이제 위 이미지에 표시된 캐시 계층 구조를 다시 살펴보세요. L1 캐시가 더 작기 때문에 액세스가 더 빠르며 액세스 시간은 일반적으로 1-2주기입니다. L2 캐시는 더 크며 일반적으로 액세스하는 데 5-15주기가 걸립니다. 주 메모리는 크기가 크고 DRAM 셀을 사용하기 때문에 훨씬 느리며 액세스 시간은 일반적으로 100~300사이클로 매우 높습니다. 메모리 액세스 프로토콜은 저자가 책에 액세스하는 방법과 유사합니다.

메모리 접근 프로토콜은 다음과 같습니다. 메모리 액세스(로드 또는 저장)가 있을 때마다 프로세서는 먼저 L1 캐시를 확인합니다. 캐시의 각 항목은 개념적으로 메모리 주소와 값을 포함합니다. 데이터 항목이 1차 캐시에 존재하면 캐시 히트(Cache Hit), 그렇지 않으면 캐시 미스(Cache Miss)이다. 캐시 적중이 있고 메모리 요청이 읽기인 경우 값은 단순히 프로세서에 반환되고, 메모리 요청이 쓰기인 경우 프로세서는 새 값을 캐시 항목에 씁니다. 그런 다음 변경 사항을 더 낮은 수준으로 전파하거나 처리를 재개할 수 있습니다. 이후 장에서는 다양한 쓰기 전략과 캐시 쓰기를 수행하는 다양한 방법에 대해 설명합니다. 그러나 캐시 미스가 있는 경우 추가 처리가 필요합니다.

캐시 히트: 캐시에 메모리 위치가 존재할 때 이벤트를 캐시 히트라고 합니다.

캐시 미스: 캐시에 메모리 위치가 존재하지 않는 경우를 캐시 미스라고 합니다.

L1 캐시 누락이 발생하는 경우 프로세서는 L2 캐시에 액세스하여 데이터 항목을 검색해야 합니다. 항목이 발견되면(캐시 적중) 프로토콜은 L1 캐시와 동일합니다. 이 기사에서는 포괄적 캐싱을 고려하므로 데이터 항목을 L1 캐시로 가져와야 합니다. L2 누락이 있는 경우 다른 L3 캐시나 주 메모리 등 더 낮은 수준에 액세스해야 합니다. 가장 낮은 수준(예: 주 메모리)에서는 주 메모리에 모든 메모리 위치에 대한 항목이 포함되어 있다고 가정하므로 누락이 발생하지 않도록 보장합니다.

단일 플랫 메모리 시스템을 사용하는 대신 프로세서는 계층형 메모리 시스템을 사용하여 성능을 극대화합니다. 이는 이상적인 단일 주기 대기 시간으로 대용량 메모리의 환상을 제공하도록 설계되었습니다.

구체적인 예로 다음 구성에 대한 평균 메모리 액세스 대기 시간을 찾아보세요.

레벨 미스율(%) 지연

L1 10 1

L2 10 10

주 메모리 0 100

메모리 시스템 구성 1.

레벨 미스율(%) 지연

메인 메모리 0 100

메모리 시스템 구성 2.

답변: 먼저 구성 1을 고려해 보겠습니다. 액세스의 90%는 L1 캐시에서 발생하며 이러한 히트에 대한 메모리 액세스 시간은 1주기입니다. L1 캐시에서 누락된 액세스라도 캐시에서 액세스가 적중할지 실패할지 알 수 없기 때문에 여전히 1주기 대기 시간이 발생합니다. 이후 L2 캐시에 대한 액세스 중 90%가 캐시에 도달하게 되며 10사이클의 대기 시간이 발생합니다. 마지막으로 나머지 액세스(1%)가 주 메모리에 도달하여 추가 대기 시간이 발생합니다. 따라서 평균 메모리 액세스 시간(T)은 다음과 같습니다.

T=1+0.1×(10+0.1×100)=1+1+1=3

따라서 구성 1의 계층형 메모리 시스템에 대한 평균 메모리 대기 시간은 3사이클입니다.

구성 2는 평균 메모리 액세스 시간이 100사이클인 모든 액세스에 주 메모리를 사용하는 평면 계층 구조입니다. 따라서 계층화된 메모리 시스템을 사용하면 속도를 100/3=33.3배까지 높일 수 있습니다.

위의 예는 계층적 메모리 시스템을 사용할 때의 성능 향상이 단일 계층 구조의 플랫 메모리 시스템보다 33.33배 더 높다는 것을 보여줍니다. 성능 향상은 다양한 캐시의 적중률과 지연 시간에 따라 달라집니다. 또한, 캐시 적중률은 프로그램의 스택 거리 분포와 캐시 관리 전략에 따라 달라집니다. 마찬가지로 캐시 액세스 대기 시간은 캐시 제조 기술, 캐시 설계 및 캐시 관리 방식에 따라 달라집니다. 캐시 액세스 최적화는 지난 20년 동안 컴퓨터 아키텍처 연구에서 매우 중요한 주제였으며, 연구자들은 이 분야에 대한 수천 편의 논문을 발표했습니다. 이 문서에서는 기본 메커니즘 중 일부에 대해서만 설명합니다.

공간적 지역성 활용: 캐시 블록

이제 공간적 지역성을 고려하면 위 그림은 대부분의 액세스가 ±25 바이트 내에 있음을 보여줍니다. 주소 거리 분포는 일련의 메모리 위치가 블록으로 그룹화되고 하위 레벨에서 한 번에 가져오는 경우 액세스에 공간적 지역성이 높기 때문에 캐시 적중 수가 증가할 수 있음을 보여줍니다.

따라서 거의 모든 프로세서는 연속된 주소 블록을 생성하고 캐시는 각 블록을 원자 단위로 처리합니다. 전체 블록을 한 번에 하위 레벨에서 가져오고, 필요한 경우 전체 블록을 캐시에서도 제거합니다. 캐시 블록은 캐시 라인이라고도 합니다. 일반적인 캐시 블록이나 라인의 길이는 32-128바이트입니다. 주소 지정을 용이하게 하려면 크기가 2의 엄격한 거듭제곱이어야 합니다.

캐시 블록 또는 캐시 라인은 캐시에서 데이터의 원자 단위로 간주되는 연속적인 메모리 위치 집합입니다.

따라서 캐시 항목의 개념을 약간 재정의할 필요가 있습니다. 각 메모리 주소에 대한 항목을 생성하는 대신 각 캐시 라인에 대해 별도의 항목을 생성합니다. 이 기사에서는 캐시 라인과 블록이라는 용어를 동의어로 사용할 것이며 L1 캐시와 L2 캐시에서 동일한 캐시 라인 크기를 가질 필요는 없으며 서로 다를 수 있다는 점에 유의하십시오. 그러나 캐시를 포괄적으로 유지하고 추가 메모리 액세스를 최소화하려면 일반적으로 L2에서 L1과 동일하거나 더 큰 블록 크기를 사용해야 합니다.

지금까지 배운 핵심 사항:

  • 시간적 및 공간적 지역성은 대부분의 인간 행동에 내재된 속성이며 책을 읽는 것과 컴퓨터 프로그램을 작성하는 데 동일하게 적용됩니다.

  • 시간적 집약성은 스택 거리로 정량화할 수 있고, 공간적 집약성은 주소 거리로 정량화할 수 있습니다.

  • 시간과 공간의 지역성을 활용하는 메모리 시스템 설계가 필요하다.

  • 시간적 지역성을 활용하기 위해 캐시 세트로 구성된 계층적 메모리 시스템을 사용합니다. L1 캐시는 일반적으로 대부분의 메모리 액세스를 신속하게 충족하도록 설계된 작고 빠른 구조입니다. 낮은 수준의 캐시는 더 많은 양의 데이터를 저장하고 액세스 빈도가 낮으며 액세스하는 데 더 오랜 시간이 걸립니다.

  • 공간적 지역성을 활용하기 위해 연속된 메모리 위치 집합을 캐시에서 원자 데이터 단위로 처리되는 블록(행이라고도 함)으로 그룹화합니다.

캐싱에 대한 요구 사항은 이전에 질적으로 연구되었으며 캐싱 설계는 나중에 논의됩니다.

19.6.2 캐시

19.6.2.1 캐싱 개요

캐시는 아래 그림 A와 같이 값비싼 고속 메모리의 메모리 액세스 시간과 저렴한 저속 메모리의 대용량 메모리를 결합하도록 설계되었습니다. 아래 그림 b는 다중 레벨 캐시의 사용을 보여줍니다. L2 캐시는 L1 캐시보다 느리고 일반적으로 큰 반면, L3 캐시는 L2 캐시보다 느리고 일반적으로 큽니다.

다음 그림은 캐시/주 메모리 시스템의 구조를 보여줍니다. 주 메모리는 각각 고유한 n비트 주소를 갖는 최대 2n개의 주소 지정 가능한 단어로 구성됩니다. 매핑 목적을 위해 메모리는 여러 개의 고정 길이 블록으로 구성되는 것으로 간주되며, 각 블록은 K 단어를 포함합니다. 즉, 주 메모리에는 M=2n/K 블록이 있습니다. 캐시는 라인이라고 불리는 m개의 블록으로 구성됩니다. 각 라인에는 K 단어와 몇 비트의 태그가 포함됩니다. 각 행에는 행이 캐시에 로드된 이후 수정되었는지 여부를 나타내는 비트와 같은 제어 비트(표시되지 않음)도 포함됩니다. 행의 길이(플래그 및 제어 비트 제외)가 행 크기입니다.

다음 그림은 읽기 작업을 보여줍니다. 프로세서는 읽을 워드의 읽기 주소(RA)를 생성하고, 해당 워드가 캐시에 포함되어 있으면 프로세서에 전달합니다. 그렇지 않으면 해당 단어가 포함된 블록이 캐시에 로드되고 해당 단어가 프로세서로 전달됩니다. 그림은 병렬로 발생하는 마지막 두 작업을 보여 주며 아래 그림에 표시된 일반적인 현대 캐시 구성을 반영합니다. 이 조직에서 캐시는 데이터, 제어 및 주소 라인을 통해 프로세서에 연결됩니다. 데이터 및 주소 라인은 주 메모리에 액세스하는 시스템 버스에 연결된 데이터 및 주소 버퍼에도 연결됩니다. 캐시 적중이 발생하면 데이터 및 주소 버퍼가 비활성화되고 프로세서와 캐시 사이에는 통신만 있고 시스템 버스 통신은 없습니다. 캐시 미스가 발생하면 필요한 주소가 시스템 버스에 로드되고 데이터는 데이터 버퍼를 통해 캐시와 프로세서로 반환됩니다. 다른 조직에서는 모든 데이터, 주소 및 제어 라인에 대해 프로세서와 주 메모리 사이에 캐시가 물리적으로 삽입됩니다. 후자의 경우 캐시 미스의 경우 필요한 단어가 먼저 캐시로 읽혀진 다음 캐시에서 프로세서로 전송됩니다.

캐시 읽기 작업.

일반적인 캐시 구성.

가상 주소를 사용할 때 시스템 설계자는 프로세서와 MMU 사이 또는 MMU와 주 메모리 사이에 캐시를 배치하도록 선택할 수 있습니다(아래 이미지). 논리적 캐시(가상 캐시라고도 함)는 가상 주소를 사용하여 데이터를 저장합니다. 프로세서는 MMU를 거치지 않고 캐시에 직접 접근합니다. 물리적 캐시는 메인 메모리의 물리적 주소를 사용하여 데이터를 저장합니다.

아래 그림 a는 캐시의 직접 매핑 방법을 보여줍니다. 처음 m개의 주 메모리 블록이 매핑되고, 각 주 메모리 블록이 캐시의 고유한 행에 매핑되고, 다음 m개의 주 메모리 블록이 동일한 방식으로 캐시에 매핑됩니다.

캐시된 직접 및 연관 매핑.

직접 매핑된 캐시 구성.

완전한 연관 매핑 캐시 구성.

이 외에도 K-way(예: 2, 4, 8, 16) 캐시 매핑 방법도 있습니다. 아래 그림을 참조하세요.

k-way 세트 연관 캐시 구성.

캐시 크기 변화에 따른 다양한 매핑 방법의 상관 곡선은 다음과 같습니다.

다음 그림은 한때 인기를 끌었던 Pentium 4의 구조 다이어그램으로, 캐시의 구조를 명확하게 보여줍니다.

19.6.2.2 기본 캐시 작업

아래 그림과 같이 캐시를 블랙박스라고 생각해보자. 로드 작업의 경우 입력은 메모리 주소이고, 캐시 히트가 발생하면 출력은 메모리 위치의 값입니다. 캐시에는 요청이 적중되었는지 아니면 실패했는지를 나타내는 상태 표시줄이 있다고 가정합니다. 작업이 저장인 경우 캐시는 메모리 주소와 값이라는 두 가지 입력을 받아들입니다. 캐시 적중이 있으면 캐시는 메모리 위치에 해당하는 항목에 값을 저장하고, 그렇지 않으면 캐시 미스를 나타냅니다.

블랙박스로 캐시합니다.

이제 SRAM 어레이를 빌딩 블록으로 사용하는 이 블랙박스를 구현하는 방법을 살펴보겠습니다.

디자인에 영감을 주기 위해 블록 크기가 64바이트인 32비트 시스템의 예를 고려해 보십시오. 이 머신에는 226개의 블록이 있습니다. L1 캐시의 크기는 8KB이며 128개의 블록을 포함합니다. 따라서 L1 캐시는 어느 시점에서든 최대 128개의 2^{26} 블록을 포함하는 전체 메모리 주소 공간의 매우 작은 하위 집합으로 간주될 수 있습니다. 주어진 블록이 L1 캐시에 존재하는지 확인하려면 128개 항목 중 해당 블록이 포함되어 있는지 확인해야 합니다.

첫 번째 수준 캐시가 전체적으로 읽기와 쓰기라는 두 가지 기본 요청을 지원하는 메모리 계층 구조의 일부라고 가정합니다. 그러나 이러한 두 가지 고급 작업을 구현하려면 캐시 수준에서 많은 기본 작업이 필요합니다.

메모리 주소와 유사하게 블록 주소는 메모리 주소의 26MSB 비트로 정의됩니다. 첫 번째 문제는 주어진 블록 주소를 가진 블록이 캐시에 존재하는지 확인하는 것인데, 이를 위해서는 조회 작업이 필요합니다. 블록이 캐시에 존재하는 경우(즉, 캐시 적중) 블록에 대한 포인터가 반환됩니다. 요청을 처리하려면 데이터 읽기 및 데이터 쓰기라는 두 가지 기본 작업이 필요합니다. 즉, 블록의 내용을 읽거나 쓰며 인수로 블록에 대한 포인터가 필요합니다.

캐시 누락이 있는 경우 메모리 계층의 하위 수준에서 블록을 가져와 캐시에 삽입해야 합니다. 메모리 계층의 하위 수준에서 블록을 검색하여 캐시에 삽입하는 프로세스를 채우기 작업이라고 합니다. 채우기 작업은 복잡한 작업이며 많은 원자 하위 작업을 사용합니다. 블록을 가져오려면 먼저 로드 요청을 하위 수준 캐시로 보낸 다음 L1 캐시에 삽입해야 합니다.

삽입 과정도 복잡합니다. 먼저 주어진 블록 세트에 새 블록을 삽입할 공간이 있는지 확인해야 합니다. 공간이 충분하면 삽입 작업을 사용하여 항목 중 하나를 채울 수 있습니다. 그러나 블록이 삽입될 캐시의 모든 위치가 이미 점유된 경우 기존 블록을 캐시에서 제거해야 합니다. 따라서 제거해야 하는 캐시 블록을 종료하려면 교체 작업을 호출해야 합니다. 적절한 대체 후보 블록이 발견되면 제거 작업을 사용하여 캐시에서 제거해야 합니다.

요약하면 캐싱을 구현하려면 조회, 데이터 읽기, 데이터 쓰기, 삽입, 교체, 제거 등의 기본 작업이 광범위하게 필요합니다. 채우기 작업은 단순히 메모리 계층 구조의 다양한 수준에서 찾기, 삽입 및 바꾸기 작업의 순서입니다. 마찬가지로 읽기 작업은 기본적으로 탐색 작업이거나 찾기 및 채우기 작업의 조합입니다.

19.6.2.3 캐시 조회 및 캐시 설계

8KB 캐시가 블록 크기가 64바이트인 32비트 시스템용으로 설계되었다고 가정합니다. 효율적인 캐시 조회를 수행하려면 캐시에 있는 128개 항목 중 26비트 블록 주소가 존재하는지 여부를 찾는 효율적인 방법을 찾아야 합니다. 두 가지 문제가 있습니다. 첫 번째는 주어진 항목을 빠르게 찾는 것이고, 두 번째는 읽기/쓰기 작업을 수행하는 것입니다. 두 가지 문제를 해결하기 위해 단일 SRAM 어레이를 사용하는 대신 아래 이미지에 표시된 대로 이를 두 개의 어레이로 분할합니다.

캐시 구조.

완전 연관(FA) 캐시

일반적인 설계에서 캐시 항목은 두 개의 SRAM 기반 어레이에 보관됩니다. 하나는 블록 주소와 관련된 정보를 포함하는 태그 어레이라고 하는 SRAM 어레이이고, 다른 하나는 블록에 대한 데이터를 포함하는 데이터 어레이라고 하는 SRRAM 어레이입니다. 태그 배열에는 블록을 고유하게 식별하는 태그가 포함되어 있습니다. 태그는 일반적으로 블록 주소의 일부이며 캐시 유형에 따라 다릅니다. 태그 및 데이터 배열 외에도 캐시 액세스 알고리즘을 수행하는 전용 캐시 컨트롤러가 있습니다.

먼저 블록 위치를 지정하는 매우 간단한 방법을 생각해 보십시오. 캐시의 128개 항목을 동시에 검사하여 블록 주소가 캐시 항목의 블록 주소와 동일한지 확인할 수 있습니다. 이 캐시를 완전 연관 캐시 또는 콘텐츠 주소 지정 가능 캐시라고 하며, "완전 연관"이란 특정 블록이 캐시의 모든 항목과 연관될 수 있음을 의미합니다.

따라서 FA(완전 연관) 캐시의 각 캐시 항목에는 태그와 데이터라는 두 개의 필드가 포함되어야 합니다. 이 경우 태그는 블록 주소와 동일하게 설정할 수 있으며, 블록 주소는 블록마다 고유하므로 태그의 정의에 맞습니다. 블록 데이터는 블록의 내용을 나타냅니다(이 경우 64바이트). 이 예에서 블록 주소에는 26비트가 필요하고 블록 데이터에는 64바이트가 필요합니다. 검색 작업은 전체 캐시에 걸쳐 이루어져야 하며 항목이 발견되면 데이터를 읽거나 새 값을 써야 합니다.

먼저 태그 배열을 살펴보겠습니다. 이 경우 각 태그는 26비트 블록 주소와 동일합니다. 메모리 요청이 캐시에 도달하면 첫 번째 단계는 최상위 26비트를 추출하여 태그를 계산하는 것입니다. 그런 다음 추출된 태그는 일련의 비교기를 사용하여 태그 배열의 각 항목과 일치해야 합니다. 일치하는 항목이 없으면 캐시 미스가 선언되고 추가 처리될 수 있습니다. 캐시 적중이 있는 경우 태그와 일치하는 항목 번호를 사용하여 데이터 항목에 액세스해야 합니다. 예를 들어, 128개의 항목이 포함된 8KB 캐시에서 태그 배열의 53번째 항목은 태그와 일치할 수 있습니다. 이 경우 캐시 컨트롤러는 읽기 액세스의 경우 데이터 배열에서 53번째 항목을 가져와야 하고, 쓰기 액세스의 경우 53번째 항목을 써야 합니다.

완전 연관 캐시에서 태그 배열을 구현하는 방법에는 두 가지가 있습니다. 캐시 컨트롤러가 각 항목을 반복하고 이를 주어진 태그와 비교하는 일반 SRAM 배열로 설계하거나 각 행에 대한 비교기가 있는 CAM 배열을 사용할 수 있습니다. 레이블 값을 행에 저장된 데이터와 비교하고 비교 결과에 따라 출력(1 또는 0)을 생성할 수 있습니다. CAM 배열은 일반적으로 인코더를 사용하여 결과와 일치하는 행 수를 계산합니다. 주로 배열의 순차적 반복에 시간이 많이 걸리기 때문에 완전히 연관적으로 캐시된 태그 배열의 CAM 구현이 더 일반적입니다.

아래 이미지는 이 개념을 보여줍니다. CAM 어레이의 각 행은 해당 워드 라인을 1로 설정하여 활성화됩니다. 이어서 CAM 셀에 내장된 비교기는 각 행의 내용을 태그와 비교하여 출력을 생성합니다. OR 게이트를 사용하여 출력이 1인지 확인하고, 출력이 1이면 캐시 적중을 의미하고, 그렇지 않으면 캐시 누락을 의미합니다. 이러한 각 출력 라인은 일치하는 행의 인덱스를 생성하는 인코더에도 연결되며, 이 인덱스를 사용하여 데이터 배열에 액세스하고 블록의 데이터를 읽습니다. 쓰기의 경우 블록을 읽는 대신 블록을 씁니다.

완전한 연관 캐시.

완전 연관 캐시는 작은 구조(일반적으로 2-32개) 항목에 유용하지만 더 큰 구조에는 CAM 어레이를 사용할 수 없으며 비교 및 인코딩의 영역 및 전력 오버헤드가 매우 높습니다. 또한 태그 어레이의 SRAM 구현의 각 항목을 순차적으로 탐색하는 것도 불가능하며 이는 매우 시간이 많이 걸립니다. 따라서 더 큰 구조에서 데이터를 찾는 더 나은 방법을 찾아야 합니다.

직접 매핑(DM) 캐시

완전 연관 캐시에서는 모든 블록이 캐시의 어느 위치에나 저장될 수 있습니다. 이 방식은 매우 유연하지만 주로 면적 및 전력 오버헤드로 인해 캐시에 항목 수가 많은 경우에는 사용할 수 없습니다. 우리는 블록이 캐시의 어느 곳에도 저장되는 것을 허용하지 않고 대신 주어진 블록에 대해 고정된 위치만 지정합니다. 이는 다음과 같이 수행할 수 있습니다.

이 예에는 128개의 항목이 있는 8KB 캐시가 있으며 캐시에서 64바이트 청크가 들어가는 위치를 제한할 수 있습니다. 각 블록에 대해 해당 주소에 해당하는 태그를 저장할 수 있는 태그 배열의 고유한 위치를 지정하면 아래와 같이 고유한 위치가 생성될 수 있습니다. 블록 a의 주소와 캐시에 있는 항목 수(128)를 고려하여 a%128을 계산해 보겠습니다. % 연산자는 a를 128로 나눈 나머지를 계산합니다. a는 이진 값이고 128은 2의 거듭제곱이므로 나머지를 계산하는 것은 매우 쉽습니다. 26비트 블록 주소에서 7 LSB 비트만 추출하면 됩니다. 이 7비트는 태그 배열에 액세스하는 데 사용될 수 있습니다. 태그 배열에 저장된 태그 값은 블록 주소를 기준으로 계산된 태그 값과 비교하여 적중 여부를 판단할 수 있습니다.

완전 연관 캐시처럼 태그 배열에 블록 주소를 저장하는 대신 설계를 약간 최적화하는 것도 가능합니다. 블록 주소의 26비트 중 7비트는 태그 배열의 태그에 액세스하는 데 사용됩니다. 즉, 태그 배열의 특정 항목에 매핑될 수 있는 모든 블록은 마지막 7비트를 공통으로 갖게 됩니다. 따라서 이 7비트는 태그의 일부로 명시적으로 저장할 필요가 없으며 블록 주소의 나머지 19비트만 블록 간에 변경될 수 있습니다. 따라서 직접 매핑된 캐시 구현의 태그에는 19비트만 포함하면 됩니다.

아래 이미지는 이 개념을 그래픽으로 보여줍니다. 32비트 주소를 세 부분으로 분할합니다. 가장 중요한 19비트는 태그를 포함하고, 다음 7비트는 인덱스(태그 배열의 인덱스)라고 하며, 나머지 6비트는 블록의 바이트 오프셋을 가리키며, 액세스 프로토콜의 나머지 부분은 개념적으로 완전 연관 캐시와 유사합니다. 이 경우 인덱스를 사용하여 태그 배열의 해당 위치에 액세스하고 내용을 읽고 이를 계산된 태그와 비교합니다. 동일하면 캐시 적중을 선언하고, 그렇지 않으면 캐시 미스를 선언합니다. 캐시 적중이 발생한 경우 인덱스를 사용하여 데이터 배열에 액세스하고 캐시 적중/실패 결과를 사용하여 데이터 배열을 활성화/비활성화합니다.

직접 매핑된 캐시.

지금까지 우리는 완전 연관 캐시와 직접 매핑 캐시를 살펴보았습니다.

  • 완전 연관 캐시는 블록이 캐시의 모든 항목에 저장될 수 있으므로 매우 유연한 구조이지만 대기 시간과 전력 소비가 더 높습니다. 주어진 블록은 캐시의 더 많은 항목에 할당될 수 있으므로 직접 매핑된 캐시보다 적중률이 더 높습니다.

  • 직접 매핑된 캐시는 더 빠르고 더 낮은 전력 구조입니다. 블록은 캐시의 한 항목에만 상주할 수 있으며 이 캐시의 예상 적중률은 완전 연관 캐시의 적중률보다 낮습니다.

완전 연관 캐시와 직접 매핑 캐시 사이에는 전력, 대기 시간 및 적중률 간에 균형이 있습니다.

세트 연관 캐시

완전 연관 캐시는 캐시의 모든 항목에서 블록을 검색해야 하기 때문에 전력을 더 많이 소모합니다. 대조적으로, 직접 매핑된 캐시는 하나의 항목만 확인하면 되지만 적중률이 허용할 수 없을 정도로 낮기 때문에 더 빠르고 효율적입니다. 그러므로 이 두 가지 패러다임을 결합해 보십시오.

캐시에 있는 여러 항목 집합 중 하나에 블록이 잠재적으로 상주할 수 있는 캐시를 설계해 보겠습니다. 완전 연관 캐시와 같이 캐시의 항목 집합을 블록 주소와 연결하면 적중 또는 실패를 선언하기 전에 집합의 모든 항목을 확인해야 합니다. 이 접근 방식은 완전 연관 매핑 방식과 직접 매핑 방식의 장점을 결합합니다. 세트에 4개 또는 8개의 항목이 포함된 경우 값비싼 CAM 구조를 사용하고 모든 항목을 순차적으로 반복하는 대신 태그 배열에서 세트의 모든 항목을 병렬로 읽고 모든 항목을 블록 주소의 태그 부분과 병렬로 비교할 수 있습니다. 일치하는 항목이 있으면 데이터 배열에서 해당 항목을 읽을 수 있습니다. 여러 청크가 세트와 연관될 수 있으므로 이 설계를 세트 연관 캐싱이라고 합니다. 집합에 있는 블록의 수를 캐시의 연관성이라고 하며, 집합에 있는 각 항목을 웨이라고 합니다.

연관성: 컬렉션에 포함된 블록 수는 캐시의 연관성을 정의합니다.

Way: 컬렉션의 각 항목을 Way라고 합니다.

8KB 캐시와 64바이트 블록이 있는 32비트 메모리 시스템을 고려하여 캐시 항목을 세트로 그룹화하는 간단한 방법을 설명하겠습니다. 아래 그림에 표시된 것처럼 32비트 주소에서 가장 낮은 6비트가 먼저 제거됩니다. 이는 블록의 바이트 주소를 지정하고 나머지 26비트는 블록 주소를 지정하므로 8KB 캐시에 총 128개의 항목이 있습니다. 각각 4개의 항목으로 구성된 세트를 생성하려면 모든 캐시 항목을 4개의 항목으로 분할해야 하며 이러한 세트는 32(25)개가 됩니다.

직접 매핑된 캐시에서는 26비트 블록 주소 중 가장 낮은 7비트를 사용하여 캐시 항목의 인덱스를 지정합니다. 이제 위 이미지에 표시된 것처럼 이 7비트를 두 부분으로 나눌 수 있습니다. 한 부분은 5비트를 포함하며 컬렉션의 주소를 나타내고, 두 번째 부분은 2비트를 포함하며 무시됩니다. 컬렉션의 주소를 나타내는 5비트 그룹을 컬렉션 인덱스라고 합니다.

세트 인덱스 i를 계산한 후에는 세트에 속하는 태그 배열의 모든 요소에 액세스해야 합니다. 태그 배열은 세트의 블록 수가 S이면 세트에 속한 모든 항목을 연속적으로 그룹화할 수 있는 방식으로 배열될 수 있습니다. i번째 세트의 경우 태그 배열의 요소 iS, $(iS+1) $ ... $(iS+S - 1) $에 액세스해야 합니다.

태그 배열의 각 항목에 대해 항목에 포함된 태그를 블록 주소의 태그 부분과 비교해야 합니다. 일치하는 항목이 있으면 히트를 선언할 수 있습니다. 컬렉션 연관 캐시의 태그 개념은 매우 까다롭습니다. 위 그림과 같이 인덱스에 속하지 않는 비트들로 구성되며, 블록 주소의 (21=265)MSB 비트이다. 태그 비트 수를 결정하는 데 사용되는 논리는 다음과 같습니다.

각 세트는 5비트 세트 인덱스로 지정됩니다. 이 5비트는 주어진 세트에 매핑될 수 있는 모든 블록에 공통입니다. 나머지 비트(21=265)는 동일한 세트에 매핑된 서로 다른 블록을 구별하는 데 필요합니다. 따라서 세트 연관 캐시의 태그 크기는 직접 매핑 캐시(19)와 완전 연관 캐시(26)의 크기 사이에 있습니다.

다음 그림은 컬렉션 연관 캐시의 디자인을 보여줍니다. 세트 인덱스는 먼저 비트 7-11을 사용하여 블록의 주소에서 계산되며, 세트 인덱스는 태그 어레이 인덱스 생성기를 사용하여 태그 어레이의 해당 4개 항목에 대한 인덱스를 생성하는 데 사용됩니다. 그런 다음 태그 배열의 네 항목을 모두 병렬로 액세스하고 해당 값을 읽습니다. 여기서는 CAM 어레이를 사용하는 대신 단일 멀티포트(다중 입력, 다중 출력) SRAM 어레이를 사용할 수 있습니다. 다음으로, 각 요소가 토큰과 비교되어 출력(0 또는 1)이 생성됩니다. 출력 중 하나라도 1과 같으면(OR 게이트에 의해 결정됨) 캐시가 적중되고, 그렇지 않으면 캐시가 누락됩니다. 4방향 연관 캐시를 가정할 때 인코더의 출력이 00에서 11 사이이기 때문에 인코더를 사용하여 일치 세트의 태그를 인덱싱합니다. 이어서, 멀티플렉서를 사용하여 태그 배열에서 일치하는 항목의 인덱스를 선택합니다. 이 인덱스를 사용하여 데이터 배열에 액세스할 수 있습니다. 데이터 배열의 해당 항목에는 읽거나 쓸 수 있는 블록의 데이터가 포함되어 있습니다.

읽기 작업에 대해 약간의 최적화를 수행할 수 있습니다. 읽기 작업의 경우 데이터 배열과 태그 배열에 대한 액세스가 병렬로 발생할 수 있습니다. 세트에 4가지 방법이 있는 경우 태그 일치를 계산할 때 세트의 4가지 방법에 해당하는 4개의 데이터 블록을 읽을 수 있습니다. 나중에 캐시 적중이 발생하는 경우 태그 배열에서 일치하는 항목을 계산한 후 멀티플렉서를 사용하여 올바른 데이터 블록을 선택할 수 있습니다. 이 경우 데이터 배열에서 청크를 읽는 데 필요한 시간의 일부 또는 전부가 실제로 태그 계산, 태그 배열 액세스 및 일치 작업과 겹칩니다.

컬렉션 연관 캐시.

요약하면, 집합 연관 캐시는 현재 가장 일반적인 캐시 설계이며 매우 큰 캐시에서도 허용 가능한 전력 소비 및 대기 시간을 갖습니다. 집합 연관 캐시의 연관성은 일반적으로 2, 4 또는 8입니다. 연관성이 K인 집합을 K-way 연관 캐시라고도 합니다.

컬렉션 연관 캐시를 설계할 때 우리는 심오한 질문에 대답해야 합니다. 인덱스 비트 설정과 비트 무시의 상대적 순서는 무엇입니까? 무시된 비트는 인덱스 비트(MSB)의 왼쪽에 있어야 합니까, 아니면 인덱스 비트(LSB)의 오른쪽에 있어야 합니까? 위 그림에서는 MSB가 선택되었습니다. 그 뒤에있는 논리는 무엇입니까?

답변:

인덱스 비트의 왼쪽(MSB)에 무시된 비트가 있는 경우 인접한 블록은 다른 세트에 매핑됩니다. 그러나 무시된 비트가 인덱스 비트의 오른쪽(LSB)에 있는 반대의 경우에는 연속 블록이 동일한 세트에 매핑됩니다. 전자의 방식을 non-CONT라 하고 후자의 방식을 CONT라 하며 설계에서는 non-CONT를 선택하였다.

두 개의 배열 A와 B를 고려하여 A와 B의 크기를 캐시 크기보다 훨씬 작게 하고 구성 블록 중 일부를 동일한 집합 집합에 매핑하도록 합니다. 아래 그림은 CONT 및 NON-CONT 방식의 배열을 저장하는 캐시 영역의 개념도를 보여줍니다. 캐시에 충분한 공간이 있더라도 CONT 체계를 사용하여 캐시에 두 개의 배열을 동시에 저장하는 것이 불가능하다는 것을 관찰했습니다. 메모리 공간은 캐시의 한 영역에서 겹치므로 두 프로그램의 데이터를 동시에 캐시에 보관할 수 없습니다. 그러나 NON-CONT 방식은 블록을 모든 세트에 균등하게 분배하려고 시도합니다. 따라서 두 개의 어레이를 동시에 캐시에 보관할 수 있습니다.

이는 프로그램에서 자주 발생하는 패턴입니다. CONT 방식은 캐시의 전체 영역을 예약하므로 충돌 세트에 매핑된 다른 데이터 구조를 수용하는 것이 불가능합니다. 그러나 데이터가 캐시에 분산되면 더 많은 데이터 구조를 수용할 수 있고 충돌이 줄어들 수 있습니다.

구체적인 예를 들자면 32비트 시스템에서 캐시에는 다음 매개변수가 있습니다.

매개변수 가치

크기 N

관련성

블록 크기

그렇다면 대부분의 라벨 크기는 무엇입니까? 답변:

  • 블록 내 바이트를 지정하는 데 필요한 비트 수는 log(B)입니다.

  • 블록 수는 N=B, 세트 수는 N/(BK)와 같습니다.

  • 설정된 인덱스 자릿수는 log(N)log(B)log(K)와 같습니다.

  • 나머지 비트는 레이블 비트입니다. 32(log(N)log(B)log(K)+log(B))=32log(N)+log(K).

19.6.2.4 데이터 읽기 및 쓰기 작업

특정 블록이 캐시에 존재한다고 판단되면 기본 읽기 작업을 사용하여 데이터 배열에서 메모리 위치 값을 가져옵니다. 조회 작업에서 캐시 적중이 반환되면 해당 블록이 캐시에 존재하는지 여부가 결정됩니다. 캐시에 누락이 있는 경우 캐시 컨트롤러는 하위 수준 캐시에 읽기 요청을 보내고 블록을 가져와야 합니다. 데이터 읽기 작업은 데이터를 사용할 수 있게 되는 즉시 시작할 수 있습니다.

첫 번째 단계는 일치하는 태그 항목에 해당하는 데이터 배열의 블록을 읽은 다음 블록의 모든 바이트에서 적절한 바이트 세트를 선택하는 것입니다. 이는 멀티플렉서 세트를 사용하여 수행할 수 있습니다. 탐색 작업 후에 데이터 읽기 작업을 엄격하게 시작할 필요는 없습니다. 두 작업 사이에 상당한 중복이 있을 수 있습니다. 태그 배열과 데이터 배열을 병렬로 읽을 수 있습니다. 일치하는 토큰이 계산된 후 멀티플렉서를 사용하여 올바른 값 집합을 선택할 수 있습니다.

값을 쓰기 전에 전체 블록이 이미 캐시에 있는지 확인하는 것이 중요합니다. 새로운 데이터가 생성되기 때문에 블록의 이전 값이 필요하지 않다고 주장할 수는 없습니다. 그 이유는 단일 메모리 액세스의 경우 일반적으로 4바이트 또는 최대 8바이트가 기록되지만 블록 길이는 최소 32 또는 64바이트이고 블록은 캐시의 원자 단위이므로 다른 위치에 다른 부분을 가질 수 없기 때문입니다. 예를 들어, 첫 번째 레벨 캐시에 블록의 4바이트를 보관하고 두 번째 레벨 캐시에 나머지 바이트를 보관할 수 없습니다. 둘째, 이를 수행하려면 업데이트하기 위해 작성된 바이트를 추적하기 위해 추가 상태를 유지해야 합니다. 따라서 간단하게 유지하려면 1바이트만 쓰려는 경우에도 전체 블록으로 캐시를 채워야 합니다.

그런 다음 적절한 워드 라인과 비트 라인 세트를 활성화하여 새로운 값을 데이터 배열에 기록해야 합니다. 이는 디멀티플렉서 회로 세트를 사용하여 쉽게 수행할 수 있습니다.

데이터 쓰기를 수행하는 방법에는 두 가지가 있습니다.

  • 연속 기입. Write-through는 값이 데이터 배열에 기록될 때마다 쓰기도 하위 수준 캐시로 전송되는 비교적 간단한 방식입니다. 이 접근 방식은 캐시 트래픽을 늘리지만 수정된 블록이 캐시에 저장된 후 이를 추적할 필요가 없기 때문에 구현이 더 간단합니다. 따라서 필요한 경우 행을 캐시에서 원활하게 제거할 수 있습니다. 캐시 제거 및 교체도 간단하지만 쓰기 비용이 발생합니다. L1 캐시가 Write-Through 프로토콜을 따르면 다중 프로세서에 대한 캐시를 쉽게 구현할 수 있습니다.

  • 다시 쓰기. 이 체계는 쓰기 작업을 사용하여 수정된 블록을 명시적으로 추적합니다. 이 정보는 태그 배열의 추가 비트를 사용하여 유지 관리할 수 있습니다. 이 비트를 종종 수정 비트라고 합니다. 메모리 계층의 하위 레벨에서 블록을 얻을 때마다 수정 비트는 0입니다. 그러나 데이터 쓰기가 이루어지고 데이터 배열이 업데이트되면 태그 배열의 수정 비트가 1로 설정됩니다. 행을 제거하려면 추가 처리가 필요합니다. 쓰기 저장 프로토콜의 경우 쓰기 비용은 더 낮고 제거 작업 비용은 더 높습니다. 여기서의 절충은 연속 쓰기 캐싱의 절충과 반대입니다.

추가 수정 비트가 있는 태그 배열의 항목 구조는 아래 그림에 나와 있습니다.

수정된 비트가 있는 태그 배열의 항목입니다.

19.6.2.5 삽입 작업

이 섹션에서는 캐시에 블록을 삽입하는 프로토콜에 대해 설명합니다. 이 작업은 하위 레벨에서 블록이 도착할 때 호출됩니다. 먼저 주어진 블록이 컬렉션에 매핑되는 모든 방식을 살펴보고 빈 항목이 있는지 확인해야 합니다. 빈 항목이 있는 경우 항목 중 하나를 임의로 선택하여 지정된 블록의 내용으로 채울 수 있습니다. 빈 항목이 없으면 교체 및 제거 작업을 호출하여 컬렉션에서 기존 블록을 선택하고 제거해야 합니다.

특정 항목이 비어 있는지 또는 비어 있지 않은지 확인하려면 일부 추가 상태 정보를 유지해야 합니다. 컴퓨터 아키텍처에서는 이러한 상태를 각각 유효하지 않음 및 유효함이라고도 합니다. 블록의 상태를 나타내기 위해 유효한 비트라고 하는 단 하나의 추가 비트만 태그 배열에 저장하면 됩니다. 태그 배열은 데이터 배열보다 작고 일반적으로 빠르기 때문에 항목에 대한 추가 정보를 보관하려면 사용하세요. 유효한 비트가 추가된 태그 배열의 항목 구조는 다음과 같습니다.

수정된 유효한 비트를 포함하는 태그 배열의 항목입니다.

캐시 컨트롤러는 잘못된 항목을 검색할 때 각 태그의 유효한 비트를 확인해야 합니다. 캐시의 모든 항목은 처음에는 유효하지 않습니다. 유효하지 않은 항목이 발견되면 데이터 배열의 해당 항목이 블록의 내용으로 채워질 수 있으며 이는 유효합니다. 그러나 유효하지 않은 항목이 없으면 하나의 항목을 캐시에 삽입해야 하는 특정 블록으로 대체해야 합니다.

19.6.2.6 교체 작업

여기서의 작업은 컬렉션에서 새 항목으로 대체될 수 있는 항목을 찾는 것입니다. 자주 액세스하는 요소를 교체하면 캐시 누락 수가 증가하므로 이를 원하지 않습니다. 이상적으로는 향후에 액세스할 가능성이 가장 낮은 요소를 교체하고 싶지만 향후 이벤트를 예측하기는 어렵습니다. 과거의 행동을 기반으로 합리적인 추측이 이루어져야 하며, 캐시의 블록을 교체하는 전략은 다양할 수 있습니다. 이를 대체 시나리오 또는 대체 전략이라고 합니다.

캐시 교체 체계 또는 교체 정책은 컬렉션의 항목을 새 항목으로 바꾸는 방법입니다.

일반적으로 사용되는 교체 전략은 다음과 같습니다.

  • 무작위 교체 전략. 이 전략은 가장 간단하고 일반적이며 무작위로 블록을 선택하여 교체합니다. 그러나 프로그램의 동작과 메모리 접근 패턴의 성격을 고려하지 않기 때문에 성능 측면에서는 그다지 이상적이지는 않습니다. 이 방식은 매우 자주 액세스되는 블록을 자주 교체하게 됩니다.

  • FIFO 교체 전략. FIFO(선입선출) 교체 전략의 가정은 가장 빠른 시점에 캐시에 가져온 블록이 향후 액세스될 가능성이 가장 낮다는 것입니다. FIFO 교체 전략을 구현하려면 태그 배열에 카운터를 추가해야 합니다. 블록이 도입될 때마다 0과 같은 카운터 값이 할당되고 나머지 블록에 대해 카운터 값이 증가됩니다. 카운터가 클수록 블록이 캐시에 더 빨리 들어갑니다.

또한 교체 후보를 찾으려면 가장 큰 카운터 값을 가진 항목을 찾아야 하며, 이는 가장 빠른 블록이어야 합니다. 불행하게도 FIFO 방식은 시간적 지역성 원칙을 엄격하게 준수하지 않습니다. 이는 캐시에서 수명이 긴 블록에 불이익을 줍니다. 실제로는 매우 자주 액세스되어 처음부터 제거되어서는 안 되는 블록일 수 있습니다.

이제 FIFO 교체 전략 구현의 실제적인 측면을 고려하십시오. 카운터의 최대 크기는 컬렉션의 요소 수, 즉 캐시의 연관성과 동일해야 합니다. 예를 들어, 캐시의 연관성이 8이면 캐시에는 3비트 카운터가 있어야 하며, 교체해야 하는 항목은 가장 큰 카운터 값을 가져야 합니다.

이 경우 새 값을 캐시에 가져오는 프로세스는 비용이 많이 들기 때문에 컬렉션의 한 요소를 제외한 모든 요소에 대해 카운터를 증가시켜야 합니다. 그러나 캐시 미스는 캐시 적중보다 더 드뭅니다. 따라서 실제로 오버헤드가 크지 않으며 큰 성능 오버헤드 없이 구현이 가능하다.

  • LRU 교체 전략. LRU(최근에 사용됨) 교체 전략은 가장 효과적인 방식 중 하나로 간주됩니다. LRU 방식은 스택 거리 정의에서 직접 시작됩니다. 이상적으로는 향후 접근 가능성이 가장 낮은 블록을 교체하고 싶습니다. 스택 거리 개념에 따르면, 미래에 접근할 확률은 최근 접근할 확률과 관련이 있다. 프로세서가 n(n은 그리 큰 숫자는 아님) 액세스의 마지막 창에서 블록에 자주 액세스하는 경우 가까운 미래에 해당 블록에 액세스할 확률이 높습니다. 그러나 블록에 오래 전에 마지막으로 액세스한 경우 해당 블록에 곧 액세스할 가능성은 거의 없습니다.

LRU 교체 전략에서는 블록에 마지막으로 액세스한 시간을 유지하고 가장 빠른 시점에 마지막으로 액세스한 블록을 교체 후보로 선택합니다. 이 전략은 각 블록의 타임스탬프를 유지하며, 블록에 액세스할 때마다 해당 타임스탬프가 현재 시간과 일치하도록 업데이트됩니다. 적합한 대체 후보를 찾으려면 컬렉션에서 타임스탬프가 가장 작은 항목을 찾아야 합니다.

LRU 전략을 구현할 때 가장 큰 문제는 캐시에 대한 읽기 및 쓰기 액세스마다 추가 작업이 필요하다는 것입니다. 이는 일반적으로 명령의 1/3이 메모리 액세스이기 때문에 성능에 상당한 영향을 미칩니다. 둘째, 충분히 큰 타임스탬프를 유지하려면 전용 비트가 필요합니다. 그렇지 않으면 세트의 각 블록에 대한 타임스탬프를 자주 재설정해야 하므로 프로세스가 더욱 느려지고 캐시 컨트롤러가 더욱 복잡해집니다. 따라서 큰 오버헤드 없이 이상에 최대한 가까운 LRU 방식을 달성하는 것은 어려운 작업입니다.

작은 타임스탬프(보통 1~3비트)를 사용하고 유사 LRU 체계라고 하는 LRU 전략을 대략적으로 따르는 LRU 체계를 설계해 볼 수 있습니다. 기본적인 pseudo-LRU를 구현하는 간단한 방법이 아래에 설명되어 있습니다. 가장 최근에 사용한 요소를 명시적으로 표시하는 대신 가장 최근에 사용한 요소를 표시해 보세요. 태그가 지정되지 않은 요소는 가장 최근에 사용된 요소로 자동 분류됩니다.

먼저 태그 배열의 각 블록에 카운터를 연결해 보겠습니다. 블록에 액세스(읽기/쓰기)할 때마다 카운터가 증가하고, 카운터가 최대값에 도달하면 증가가 중지됩니다. 예를 들어, 2비트 카운터를 사용하는 경우 카운터를 3 이상으로 증가시키지 마십시오. 각 블록과 관련된 카운터가 결국 3에 도달하고 값을 유지한다는 사실을 인식하려면 컬렉션에 있는 각 블록의 카운터를 주기적으로 1씩 감소시키거나 심지어 0으로 재설정할 수도 있습니다. 그런 다음 일부 카운터는 다시 증가하기 시작합니다.

이렇게 하면 대부분의 경우 카운터 값을 확인하여 가장 최근에 사용된 블록을 식별할 수 있습니다. 카운터의 가장 낮은 값과 관련된 블록은 최근에 가장 적게 사용된 블록 중 하나이며, 가장 최근에 사용된 블록일 가능성이 높습니다. 이 접근 방식에는 액세스당 어느 정도의 활동이 포함되지만 작은 카운터를 증가시키는 데 따른 추가 오버헤드는 거의 없습니다. 둘째, 타이밍 측면에서 중요한 경로에 있지 않으며 병렬 또는 나중에 실행될 수 있습니다. 교체 후보를 찾는 것은 그룹의 모든 카운터를 보고 가장 낮은 카운터 값을 가진 블록을 찾는 것을 포함합니다. 블록을 새 블록으로 교체한 후 대부분의 프로세서는 일반적으로 새 블록의 카운터를 가능한 최대 값으로 설정합니다. 이는 새 블록이 교체 후보에 비해 가장 낮은 우선순위를 가져야 함을 캐시 컨트롤러에 나타냅니다.

19.6.2.7 퇴거 작업

캐시가 연속 쓰기 정책을 따르는 경우 조치가 필요하지 않으며 블록을 간단히 삭제할 수 있습니다. 그러나 캐시가 쓰기 저장 정책을 따르는 경우 수정된 비트를 확인해야 합니다. 데이터가 수정되지 않은 경우 원활하게 검색할 수 있습니다. 그러나 데이터가 수정된 경우에는 하위 수준 캐시에 다시 기록해야 합니다.

19.6.2.8 종합운용

캐시 읽기 작업의 단계 순서는 조회 작업부터 시작하여 아래 그림에 나와 있습니다. 조회 작업과 데이터 읽기 작업 사이에 약간의 겹침이 있을 수 있습니다. 캐시 적중이 있는 경우 캐시는 값을 프로세서나 더 높은 수준의 캐시(둘 중 해당하는 경우)에 반환합니다. 그러나 캐시 누락이 있는 경우 데이터 읽기 작업을 취소하고 하위 캐시로 요청을 보내야 합니다. 낮은 수준의 캐시는 동일한 액세스 순서를 수행하고 전체 캐시 블록(단지 4바이트가 아님)을 반환합니다. 그런 다음 캐시 컨트롤러는 요청된 데이터를 블록에서 추출하여 프로세서로 보낼 수 있으며, 캐시 컨트롤러는 삽입 작업을 호출하여 블록을 캐시에 삽입합니다. 컬렉션에 유효하지 않은 항목이 있는 경우 해당 블록을 지정된 블록으로 교체할 수 있지만 컬렉션의 모든 메서드가 유효한 경우 교체 후보를 찾기 위해 교체 작업을 호출해야 합니다. 작업이 항상 호출되는 것은 아니기 때문에(컬렉션에 대한 모든 경로에 유효한 데이터가 포함된 경우에만) 다이어그램에서는 작업에 물음표를 추가합니다. 그런 다음 블록을 제거해야 하며, 행이 수정되면 더 낮은 수준의 캐시에 기록될 수 있으며 후기입 캐시가 사용됩니다. 그런 다음 캐시 컨트롤러는 삽입 작업을 호출하는데, 이번에는 확실히 성공할 것입니다.

읽기 작업.

다음 그림은 후기입 캐시에 대한 캐시 쓰기 작업의 순서를 보여줍니다. 작업 순서는 캐시 읽기와 거의 유사합니다. 캐시 적중이 있는 경우 수정된 비트를 1로 설정하여 데이터 쓰기 작업이 호출되고, 그렇지 않으면 해당 블록에 대한 읽기 요청이 하위 수준 캐시로 발행됩니다. 블록이 도착하면 대부분의 캐시 컨트롤러는 일반적으로 이를 작은 임시 버퍼에 저장하고, 이 시점에서 4바이트가 버퍼에 기록된 후 반환됩니다. 일부 프로세서에서는 캐시 컨트롤러가 모든 하위 작업이 완료될 때까지 기다릴 수 있습니다. 임시 버퍼에 쓴 후(위 다이어그램에서 블록 쓰기 작업) 블록의 내용(수정된 내용)을 쓰기 위해 삽입 작업이 호출됩니다. 이 작업이 성공하지 못한 경우(모든 메서드가 유효하기 때문에) 읽기 작업과 동일한 단계 시퀀스(교체, 제거 및 삽입)가 수행됩니다.

쓰기 작업(다시 쓰기 캐시).

다음 그림은 연속 기입 캐싱에 대한 작업 순서를 보여줍니다. 첫 번째 차이점은 요청이 캐시에 적중하더라도 블록이 더 낮은 수준에 기록된다는 것입니다. 두 번째 차이점은 값이 임시 버퍼에 기록된 후(미스 이후) 블록의 새 내용도 하위 수준 캐시에 다시 기록된다는 것입니다. 나머지 단계는 다시 쓰기 캐싱을 위해 수행되는 단계 순서와 유사합니다.

쓰기 작업(연속 캐시).

19.6.3 메모리 시스템 메커니즘

우리는 캐시 계층 구조를 사용하여 메모리 내 시스템을 구축하여 캐시 작동 및 모든 구성 작업에 대한 합리적인 이해를 발전시켰습니다. 메모리 시스템은 전체적으로 읽기 및 쓰기, 로드 및 저장이라는 두 가지 기본 작업을 지원합니다.

캐시에는 두 가지 최고 수준이 있습니다. 데이터 캐시(L1 캐시라고도 함)와 명령어 캐시(I 캐시라고도 함)입니다. 거의 항상 여기에는 다양한 메모리 위치 세트가 포함됩니다. I 캐시와 L1 캐시에 액세스하는 데 사용되는 프로토콜은 동일합니다. 중복을 피하기 위해 나중에는 첫 번째 레벨 캐시에만 집중하게 됩니다. 명령어 캐시에 대한 액세스가 동일한 단계 순서를 따른다는 점만 기억하면 됩니다.

프로세서는 L1 캐시에 액세스하여 시작됩니다. L1 히트가 발생하면 보통 1~2사이클 이내에 값을 받습니다. 그렇지 않으면 요청이 두 번째 수준 캐시 또는 주 메모리와 같은 하위 수준 캐시로 이동해야 합니다. 이 경우 요청에는 수십 또는 수백 주기가 걸릴 수 있습니다. 이 섹션에서는 캐시 시스템을 전체적으로 살펴보고 이를 메모리 시스템이라는 블랙박스로 취급합니다.

포괄적 캐싱을 고려하면 메모리 시스템의 전체 크기는 주 메모리의 크기와 같습니다. 예를 들어, 시스템에 1GB의 주 메모리가 있는 경우 메모리 시스템의 크기는 1GB와 같습니다. 메모리 시스템 내부에는 성능 향상을 위해 사용되는 캐시 계층 구조가 있을 수 있지만 주 메모리에 포함된 데이터의 하위 집합만 포함되므로 전체 저장 용량을 늘리지는 않습니다. 또한 프로세서의 메모리 액세스 논리는 전체 메모리 시스템을 개념적으로 대규모 바이트 배열로 모델링된 단일 단위로 처리합니다. 이를 물리적 메모리 시스템 또는 물리적 주소 공간이라고도 합니다.

물리적 주소 공간에는 캐시와 주 메모리에 포함된 모든 메모리 위치 집합이 포함됩니다.

19.6.3.1 메모리 시스템의 수학적 모델

성능

메모리 시스템은 읽기 및 쓰기 요청만 처리하는 블랙박스라고 생각할 수 있습니다. 요청에 걸리는 시간은 요청이 도착하는 메모리 시스템의 수준에 따라 달라집니다. 파이프라인은 MA(메모리 액세스) 단계에서 메모리 시스템에 연결하고 요청합니다. 응답이 주기에 속하지 않는 경우 5단계 순차 파이프라인에 추가 거품을 도입해야 합니다.

평균 메모리 액세스 시간이 AMAT(주기로 측정됨)이고 로드/저장 명령의 비율이 fmem이라고 가정하면 CPI는 다음과 같이 표현될 수 있습니다.

\begin{정렬됨} C P I &=C P I_{\text {이상적인 }+\text { 실속\_속도 } * \text { 실속\_주기 } \\ &=C P I_{\text {이상적 }+f_{\text {mem } \times(A M A T-1) \end{정렬됨}

CPIideal은 모든 액세스에 대해 1사이클 대기 시간을 갖는 완벽한 메모리 시스템을 가정하는 CPI입니다. 5단계 순차 파이프라인에서 이상적인 명령어 처리량은 사이클당 명령어 1개이며 메모리 수준에서 1사이클이 할당됩니다. 실제로 메모리 액세스에 n 사이클이 걸리면 n-1 일시 중지 사이클이 발생하며 이를 설명하기 위해 위 공식이 필요합니다. 이 공식에서는 각 메모리 액세스가 AMAT-1 주기 일시 중지를 경험한다고 암시적으로 가정합니다. 실제로는 그렇지 않습니다. 대부분의 명령이 L1 캐시에 도달하고 L1 캐시에는 일반적으로 1주기 대기 시간이 있기 때문입니다. 따라서 L1 캐시에 도달하는 액세스는 중단되지 않습니다. 그러나 L1 및 L2 캐시의 액세스 실패로 인해 일시 중지 기간이 길어집니다.

그럼에도 불구하고 우리는 많은 수의 명령에 대한 평균 CPI에만 관심이 있기 때문에 위 공식은 여전히 ​​유효합니다. 이 방정식은 많은 수의 명령어를 고려하고 모든 메모리 정지 주기를 합산하고 명령어당 평균 주기 수를 계산하여 도출할 수 있습니다.

평균 메모리 액세스 시간

위 공식에서 CPIideal은 프로그램의 성격과 파이프라인의 다른 단계(MA 제외)의 속성에 의해 결정되며, fmem도 프로세서에서 실행되는 프로그램의 고유 속성입니다. AMAT를 계산하려면 위 공식과 유사하게 계산할 수 있는 공식이 필요합니다. 다음과 같은 L1 및 L2 캐시가 있는 메모리 시스템을 가정해 보겠습니다.

\begin{정렬됨} \text { AMAT } &=L 1_{\text {적중 시간 }+L 1_{\text {실패율 } \times L 1_{\text {미스 페널티 } \\ &=L 1_{\text {적중률 }+L 1_{\text {실패율 } \times\left(\text { L2 } _{\text {적중률 }+L 2_{\text {실패율 } \times L 2_{\text {미스 페널티 }\right) \end{정렬됨}

모든 메모리 액세스에는 적중 또는 실패에 관계없이 L1 캐시에 대한 액세스가 필요하므로 L1 적중 시간과 동일한 지연 시간이 발생해야 합니다. 액세스의 일부(L1_{\text {miss rate})가 L1 캐시에서 손실되고 L2 캐시로 이동됩니다. 또한, 적중이든 미스이든 L2_{\text {hit time} 주기의 지연이 발생해야 합니다. L2 캐시의 일부 접근(L2_{\text {miss rate})이 누락되면 계속해서 메인 메모리에 접근해야 합니다. 모든 접근은 메인 메모리에서 일어난다고 가정합니다. 따라서 L2_{\text {miss Penalty} 페널티는 주 메모리 액세스 시간과 동일합니다.

첫 번째 수준이 L1 캐시이고 마지막 수준이 주 메모리인 n 수준 메모리 시스템이 있다고 가정하면 비슷한 공식을 사용할 수 있습니다.

\begin{정렬됨} \text { AM AT } &=L 1_{\text {적중 시간 }+L 1_{\text {실패율 } \times L 1_{\text {미스 페널티 } \\ L 1_{\text {페널티 미스 } &=L 2_{\text {적중 시간 }+L 2_{\text {미스 비율 } \times L 2_{\text {페널티 미스 } \\ L 2_{\text {페널티 미스 } &=L 3_{\text {적중 시간 }+L 3_{\text {미스 비율 } \times L 3_{\text {페널티 미스 } \\ \cdots &=\ldots \\ L(n-1)_{\text {페널티 미스 } &=L n_{\text {적중 시간 } \end{정렬됨}

특정 수준 i에 대해 이러한 방정식에 사용된 미스 비율은 해당 수준의 미스 수를 해당 수준의 총 액세스 수로 나눈 값과 동일하며 이를 로컬 미스 비율(로컬 미스 비율)이라고 합니다. 대조적으로, 레벨 i에서 전역 미스 비율을 정의할 수 있습니다. 이는 레벨 미스 수를 총 메모리 액세스 수로 나눈 것과 같습니다.

로컬 미스율: 레벨 i의 캐시 미스 횟수를 레벨 i의 총 액세스 횟수로 나눈 값과 같습니다.

전역 실패율: i 수준 캐시의 실패 횟수를 총 메모리 액세스 횟수로 나눈 값과 같습니다.

미스율이나 미스 페널티를 줄이거나 적중 시간을 줄임으로써 시스템 성능을 향상시킬 수 있습니다. 먼저 미스율을 살펴보겠습니다.

19.6.3.2 캐시 미스

캐시 누락 분류

먼저 캐시의 다양한 유형의 누락을 분류해 보겠습니다.

  • 강제 미스 또는 콜드 미스. 데이터가 처음 캐시에 로드되면 데이터 값이 캐시에 없기 때문에 이러한 누락이 발생하기 마련입니다.

  • 용량 부족. 프로그램에 필요한 메모리 양이 캐시 크기보다 클 때 용량 누락이 발생합니다. 예를 들어, 프로그램이 배열의 모든 요소에 반복적으로 액세스하고 배열 크기가 1MB이고 L2 캐시 크기가 512KB라고 가정합니다. 이 경우 L2 캐시는 너무 작아서 모든 데이터를 담을 수 없기 때문에 용량 부족이 발생합니다.

  • 충돌 미스. 일반적인 시간 간격 동안 프로그램이 액세스하는 블록 세트를 작업 세트라고 합니다. 캐시의 크기가 프로그램의 작업 세트보다 작을 경우 충돌 미스가 발생한다고도 할 수 있습니다. 간격의 길이는 주관적인 것으로 간주되므로 작업 세트의 정의는 다소 부정확합니다. 그러나 시간 간격의 의미는 프로그램 실행의 총 시간에 비해 작은 간격이라는 것입니다. 이러한 누락은 시스템 동작이 안정된 상태에 도달할 수 있을 만큼 충분히 큰 한 직접 매핑 및 집합 연관 캐시에서 발생합니다. 예를 들어, 4방향 집합 연관 캐시를 고려하면 프로그램의 작업 집합에 동일한 집합에 매핑된 블록이 5개 있는 경우 액세스된 블록 수가 집합에 포함될 수 있는 항목의 최대 수보다 크기 때문에 필연적으로 캐시 누락이 발생합니다.

짧은 시간 간격 동안 프로그램이 액세스하는 메모리 위치에는 해당 시점의 프로그램 작업 세트가 포함됩니다.

이로부터 미스는 세 가지 범주, 즉 힘(force), 역량(capacity), 갈등(3C)으로 나눌 수 있습니다.

실패율 감소

높은 IPC를 유지하기 위해서는 캐시 미스율을 줄여야 합니다. 다양한 유형의 캐시 누락을 줄이려면 다양한 전략을 채택해야 합니다.

강제 이직부터 시작하겠습니다. 앞으로 어떤 블록에 액세스할지 예측하고 해당 블록을 미리 가져올 수 있는 방법이 필요합니다. 공간적 지역성을 활용하는 방식이 효과적인 예측변수인 경우가 많습니다. 따라서 블록 크기를 늘리는 것이 강제 누락 횟수를 줄이는 데 도움이 됩니다. 그러나 특정 한도 이상으로 블록 크기를 늘리면 부정적인 결과를 초래할 수도 있습니다. 캐시에 보관할 수 있는 블록 수가 줄어들고, 두 번째로 추가적인 이점이 미미할 수 있습니다. 마지막으로, 메모리 시스템의 낮은 수준에서 더 큰 블록을 읽고 전송하는 데는 더 많은 시간이 걸립니다. 따라서 설계자는 지나치게 큰 블록 크기를 피합니다. 32~128바이트 사이의 값이 적합합니다.

최신 프로세서에는 현재 액세스 패턴을 기반으로 향후 액세스할 수 있는 블록의 주소를 예측하는 복잡한 예측 변수가 있는 경우가 많습니다. 그런 다음 누락률을 줄이기 위해 메모리 계층의 하위 수준에서 예측된 블록을 가져옵니다. 예를 들어 대규모 배열의 요소에 순차적으로 액세스하는 경우 액세스 패턴을 기반으로 향후 액세스를 예측할 수 있습니다. 때때로 우리는 인덱스가 고정된 값만큼 다른 배열의 요소에 액세스합니다. 예를 들어 배열의 네 번째 요소마다 액세스하는 알고리즘이 있을 수 있습니다. 이 경우에도 연속 접속의 주소는 같은 값만큼 다르기 때문에 패턴을 분석하고 향후 접속을 예측하는 것도 가능하다. 이 장치를 하드웨어 프리페처라고 합니다. 이는 대부분의 최신 프로세서에 존재하며 복잡한 알고리즘을 사용하여 블록을 "프리페치"하여 누락률을 줄입니다. 하드웨어 프리페처는 매우 공격적이어서는 안 됩니다. 그렇지 않으면 가져온 것보다 캐시에서 더 많은 유용한 데이터를 제거하는 경향이 있습니다.

하드웨어 프리페처는 가까운 미래의 메모리 액세스를 예측하고 메모리 시스템의 하위 수준에서 이를 가져오는 특수 하드웨어 장치입니다.

먼저 용량 미스에 대해 설명하겠습니다. 유일한 효과적인 해결책은 캐시 크기를 늘리는 것입니다. 안타깝게도 이 문서에 제시된 캐시 설계에서는 캐시 크기가 2바이트(바이트)의 거듭제곱과 같아야 합니다. 일부 고급 기술을 사용하면 이 규칙을 위반할 수 있습니다. 그러나 일반적으로 상용 프로세서에 있는 대부분의 캐시 크기는 2의 거듭제곱입니다. 따라서 캐시 크기를 늘리면 크기가 최소한 두 배로 늘어납니다. 캐시 크기를 두 배로 늘리려면 두 배의 영역이 필요하고 속도가 느려지며 전력 소비가 늘어납니다. 프리페치는 현명하게 사용하면 도움이 될 수도 있습니다.

충돌 실패 수를 줄이는 일반적인 솔루션은 캐시의 연관성을 높이는 것이지만, 이렇게 하면 캐시의 대기 시간과 전력 소비도 늘어납니다. 설계자는 세트 연관 캐시의 추가 적중률과 추가 대기 시간의 균형을 신중하게 조정해야 합니다. 때로는 캐시의 일부 컬렉션에서 충돌 미스가 발생하는데, 이 경우 피해자 캐시라고 하는 작은 완전 연관 캐시와 기본 캐시를 사용할 수 있습니다. 메인 캐시에서 이동된 모든 블록은 희생 캐시에 기록될 수 있으며, 캐시 컨트롤러는 메인 캐시를 먼저 확인해야 하며, 누락이 있는 경우 다음 레벨로 진행하기 전에 피해자 캐시를 확인해야 합니다. 따라서 레벨 i의 피해자 캐시는 레벨 (i+1)에 도착하는 일부 요청을 필터링할 수 있습니다.

하드웨어 기술과 함께 시간적, 공간적 지역성을 최대화하는 "캐시 친화적" 방식으로 프로그램을 작성하고 컴파일러를 사용하여 주어진 메모리 시스템에 맞게 코드를 최적화하는 것이 가능합니다. 둘째, 컴파일러는 실제로 사용되기 전에 블록을 캐시에 프리페치하기 위한 프리페치 코드를 삽입할 수 있습니다.

이제 경험적으로는 대략적으로 사실이지만 이론적으로는 완전히 사실이 아닌 두 가지 경험 법칙에 대해 간단히 언급하겠습니다.

첫 번째는 제곱근 규칙(Square Root Rule)으로, 미스율이 캐시 크기의 제곱근에 비례한다는 것입니다.

miss\ rate \propto \frac{1}{\sqrt{\text { 캐시 크기 &#125;&#125; \ \ \ \ \ [제곱근의 법칙]

Hartsteinet al. [Hartstein et al., 2006]은 이 규칙에 대한 이론적 기초를 찾으려고 노력했으며 확률 이론의 결과를 사용하여 규칙의 기초를 설명했습니다. 실험 결과를 바탕으로 제곱근 규칙의 캐시 크기 지수가 -0.3에서 -0.7까지 다양하다는 규칙의 일반화된 버전이 파생되었습니다.

두 번째 규칙은 연관성 규칙(Associativity Rule)이라고 하며, 연관성을 두 배로 늘리는 것은 원래 연관성으로 캐시 크기를 두 배로 늘리는 것과 거의 동일한 효과를 갖는다고 명시합니다. 64KB 4방향 연관 캐시는 128KB 2방향 연관 캐시와 거의 동일한 실패율을 갖습니다.

상관 규칙과 제곱근 규칙은 경험적 규칙일 뿐이며 완전히 확립된 것은 아닙니다. 개념적 보조 수단으로만 사용됩니다. 우리는 항상 이러한 규칙을 위반하는 예제를 구성할 수 있습니다.

적중 시간 감소 및 불이익을 놓침

적중 시간과 미스 페널티(Miss Penalty)를 줄여 평균 메모리 액세스 시간도 줄일 수 있습니다. 적중 시간을 줄이기 위해 작고 간단한 캐시를 사용하지만, 이로 인해 미스율도 높아집니다.

이제 단점을 줄이는 방법에 대해 논의해 보겠습니다. 레벨 i의 미스 페널티는 레벨 (i+1)에서 시작하는 메모리 시스템의 메모리 대기 시간과 같습니다. 적중 시간과 미스율을 줄이는 전통적인 방법은 주어진 수준에서 미스 불이익을 줄이기 위해 항상 사용될 수 있으며, 현재 연구는 미스 불이익을 줄이는 것을 특별히 목표로 하고 있습니다. 먼저 L1 캐시의 쓰기 실패를 살펴보세요. 이 경우 전체 블록을 L2 캐시에서 캐시로 가져와야 하는데, 이는 시간이 걸리며(>10사이클), 둘째, 쓰기가 완료되지 않으면 파이프라인을 복구할 수 없습니다. 프로세서 설계자는 아래 그림과 같이 쓰기 버퍼라고 하는 작은 연관 캐시 모음을 사용합니다. 프로세서는 쓰기 버퍼에 값을 쓴 다음 다시 시작할 수 있으며, L1 캐시에 누락이 있는 경우에만 쓰기 버퍼에 쓸 수 있습니다(가정). 후속 읽기에서는 일반적으로 매우 작고 빠른(4-8개 항목) L1 캐시에 액세스하는 동안 쓰기 버퍼를 확인해야 합니다.

데이터가 L1 캐시에 도달하면 해당 항목이 쓰기 버퍼에서 제거될 수 있습니다. 쓰기 버퍼에 사용 가능한 여유 항목이 없으면 파이프라인을 일시 중지해야 합니다. 둘째, 쓰기 미스가 하위 수준 캐시에서 처리되기 전에 동일한 주소에 대한 또 다른 쓰기가 있을 수 있으며, 이는 쓰기 버퍼에 지정된 주소에 대한 할당 항목을 작성하여 원활하게 처리할 수 있습니다.

이제 읽기 실패를 살펴보세요. 프로세서는 일반적으로 메모리 액세스당 최대 4바이트에만 관심이 있으며 이러한 중요한 4바이트가 제공되면 파이프라인이 복구될 수 있습니다. 그러나 작업이 완료되기 전에 메모리 시스템은 일반적으로 크기가 32~128바이트 사이인 전체 블록을 채워야 합니다. 따라서 메모리 시스템이 프로세서에 필요한 정확한 바이트 세트를 알고 있다면 여기서 최적화를 도입할 수 있습니다. 이 경우 메모리 시스템은 먼저 필요한 메모리 단어(4바이트)를 가져온 다음 또는 병렬로 나머지 블록을 가져올 수 있습니다. 이러한 유형의 최적화를 중요한 단어 우선이라고 합니다. 그런 다음 이 데이터를 파이프라인으로 신속하게 전송하여 작업을 재개할 수 있습니다. 이러한 최적화를 조기 재시작이라고 합니다. 이 두 가지 최적화를 구현하면 메모리 시스템의 복잡성이 증가합니다. 하지만 키워드 우선순위화와 조기 재시작은 누락으로 인한 불이익을 줄이는 데 상당히 효과적입니다.

메모리 시스템 최적화 기술 개요

다음 표에는 스토리지 시스템을 최적화하기 위해 도입한 다양한 기술이 요약되어 있습니다. 각 기술에는 몇 가지 부정적인 부작용이 있으며, 기술이 한 측면에서 메모리 시스템을 개선하면 다른 측면에서는 해로울 수 있습니다. 예를 들어 캐시 크기를 늘리면 용량 누락 횟수는 줄어들지만 면적, 대기 시간 및 전력도 늘어납니다.

기술 신청 단점

블록 크기 강제 미스 캐시된 블록 수 줄이기

미리 가져오기 강제 미스, 용량 미스 캐시에서 유용한 데이터를 교체하는 데 따른 복잡성과 위험이 더욱 증가함

큰 캐시 크기 용량 부족 높은 대기 시간, 높은 전력, 더 넓은 영역

관련성 증가 갈등 미스 높은 대기 시간, 높은 전력

캐시를 희생하다 갈등 미스 추가적인 복잡성

컴파일러 기반 기술 모든 유형의 실수 그다지 다재다능하지 않음

작고 간단한 캐시 적중 시간 높은 미스율

쓰기 버퍼 나쁜 실수 추가적인 복잡성

키워드 우선순위 나쁜 실수 추가적인 복잡성 및 상태

미리 다시 시작하세요 나쁜 실수 추가적인 복잡성

대체로 메모리 시스템은 매우 신중하게 설계되어야 합니다. 대상 워크로드의 요구 사항은 설계자가 설정한 제약 조건 및 제조 기술의 한계와 신중하게 균형을 이루어야 하며, 전력, 면적 및 복잡성 제약을 염두에 두면서 성능을 극대화해야 합니다.

19.6.4 가상 메모리

프로세서는 여러 프로그램 사이를 빠르게 전환하여 여러 프로그램을 실행할 수 있습니다. 예를 들어, 사용자가 게임을 하는 동안 그의 프로세서는 이메일을 받을 수 있습니다. 중단이 느껴지지 않는 이유는 프로세서가 프로그램 간에 전환하는 시간(보통 몇 밀리초)이 인간이 인지할 수 있는 것보다 훨씬 작기 때문입니다.

지금까지 우리는 프로그램에 필요한 모든 데이터가 주 메모리에 있다고 가정했지만 이는 잘못된 것입니다. 과거에는 메인 메모리의 크기가 몇 메가바이트 수준이었고 사용자는 수백 메가바이트의 데이터가 필요한 매우 큰 프로그램을 실행할 수 있었습니다. 지금도 메인 메모리보다 훨씬 큰 데이터를 처리하는 것이 가능하다. 사용자는 기계에 포함된 물리적 메모리 양보다 큰 데이터 구조를 생성하는 C 프로그램을 작성하여 이 명령문을 쉽게 확인할 수 있습니다. 대부분의 시스템에서 이 C 프로그램은 성공적으로 컴파일되고 실행됩니다.

이 섹션에서는 메모리 시스템을 약간 변경하여 위의 요구 사항을 충족할 수 있습니다. 이 섹션을 읽으려면 특정 운영 체제 지식(예: 프로세스, 스레드, 메모리 등)이 필요합니다. 언리얼 렌더링 시스템 분석(18) - 운영 체제를 참조하세요.

19.6.4.1 메모리의 가상 보기

동일한 시점에 여러 프로세스가 활성화되기 때문에 프로세스 간에 메모리를 나누어야 합니다. 이것이 완료되지 않으면 프로세스는 결국 서로의 값을 수정하게 될 수 있으며 프로그래머나 컴파일러가 여러 프로세스가 존재한다는 사실을 알기를 원하지 않습니다. 그렇지 않으면 불필요한 복잡성이 발생하고, 둘째, 특정 프로그램이 특정 메모리 맵으로 컴파일되면 메모리 맵이 겹치는 프로세스가 있는 다른 시스템에서 실행되지 않을 수 있습니다. 더 나쁜 것은 동일한 프로그램의 두 복사본을 실행하는 것이 불가능하다는 것입니다. 따라서 각 프로그램은 전체 메모리 시스템을 소유하고 있다고 가정하는 메모리의 가상 보기를 확인해야 합니다.

이 두 가지 요구 사항은 서로 충돌하는 것으로 보입니다. 메모리 시스템과 운영 체제는 서로 다른 프로세스가 서로 다른 메모리 주소에 액세스할 것으로 예상하지만 프로그래머와 컴파일러는 이 요구 사항을 알고 싶어하지 않습니다. 게다가 프로그래머는 원하는 대로 메모리 맵을 배치하고 싶어합니다. 프로그래머와 운영체제 모두를 만족시킬 수 있는 방법이 있는 것으로 밝혀졌습니다.

우리는 메모리의 가상 및 물리적 뷰를 정의해야 합니다. 메모리의 물리적 관점에서 서로 다른 프로세스는 메모리 공간의 겹치지 않는 영역에서 작동합니다. 그러나 가상 보기에서는 각 프로세스가 원하는 모든 주소에 액세스하고 서로 다른 프로세스의 가상 보기가 겹칠 수 있습니다. 해결책은 페이지 매김입니다. 메모리의 가상 뷰는 가상 메모리라고도 하는데, 이는 하나의 프로세스가 다른 프로세스의 간섭 없이 전체 메모리 공간을 소유한다고 가정하는 가상 메모리 시스템으로 정의됩니다.

가상 메모리 시스템은 하나의 프로세스가 다른 프로세스의 간섭 없이 전체 메모리 공간을 소유한다고 가정하는 가상의 메모리 시스템으로 정의됩니다. 메모리 크기는 시스템의 전체 주소 지정 가능 메모리만큼 큽니다. 예를 들어, 32비트 시스템에서 가상 메모리의 크기는 2^{32}바이트(4GB)입니다. 가상 메모리의 모든 메모리 위치 집합을 가상 주소 공간이라고 합니다.

아래 그림은 32비트 Linux 운영 체제에서 프로세스의 메모리 맵을 간략하게 보여줍니다. 맨 아래(가장 낮은 주소)부터 시작해 보겠습니다. 첫 번째 섹션에는 프로세스, 형식 및 대상 컴퓨터에 대한 세부 정보로 시작하는 헤더가 포함되어 있습니다. 헤더에는 메모리 맵의 각 세그먼트에 대한 세부 정보가 포함되어 있습니다. 예를 들어 크기, 시작 주소 및 기타 속성을 포함하여 프로그램 코드가 포함된 텍스트 부분에 대한 세부 정보가 포함되어 있습니다. 텍스트 세그먼트는 헤더 뒤에서 시작되며, 프로그램이 로드되면 운영 체제는 프로그램 카운터를 텍스트 세그먼트의 시작 부분으로 설정합니다. 프로그램의 모든 명령은 일반적으로 텍스트 세그먼트에 포함됩니다.

Linux 운영 체제(32비트)의 프로세스 메모리 매핑입니다.

텍스트 세그먼트 다음에는 정적 변수와 전역 변수를 포함하는 두 개의 추가 세그먼트가 옵니다. 선택적으로 일부 운영 체제에는 상수와 같은 읽기 전용 데이터를 포함하는 추가 영역도 있습니다. 텍스트 섹션 다음에는 일반적으로 프로그래머가 초기화한 모든 정적/전역 변수를 포함하는 데이터 섹션이 옵니다. 다음 형식(C 또는 C++)의 선언을 고려해 보겠습니다.

cpp
static int val = 5;

변수 val에 해당하는 4바이트가 데이터 세그먼트에 저장됩니다.

데이터 세그먼트 다음에는 프로그래머가 명시적으로 초기화하지 않은 정적 변수와 전역 변수를 저장하는 bss 세그먼트가 옵니다. 대부분의 운영 체제에서 bss 세그먼트에 해당하는 모든 메모리 영역은 0입니다. 이는 안전상의 이유로 수행되어야 합니다. 프로그램 A가 실행되어 그 값을 bss 세그먼트에 쓰고 이어서 프로그램 B가 실행된다고 가정해 보겠습니다. B는 bss 세그먼트에 쓰기 전에 항상 변수 값을 읽으려고 시도할 수 있습니다. 이 경우 프로그램 A가 작성한 값을 가져오지만 이는 이상적인 동작이 아닙니다. 프로그램 A는 bss 세그먼트에 비밀번호나 신용 카드 번호와 같은 일부 민감한 데이터를 저장했을 수 있습니다. 따라서 프로그램 B는 프로그램 A가 모르는 사이에 이 민감한 데이터에 접근할 수 있으며 이를 오용할 수도 있습니다. 따라서 이러한 보안 오류가 발생하지 않도록 bss 세그먼트를 0으로 채워야 합니다.

BSS 세그먼트 뒤에는 프로그램에서 동적으로 할당된 변수를 저장하는 데 사용되는 힙이라는 메모리 영역이 있습니다. C 프로그램은 일반적으로 malloc 호출을 사용하여 새 데이터를 할당하고 Java 및 C++는 new 연산자를 사용합니다. 몇 가지 예를 살펴보겠습니다.

cpp
int *intarray  = (int*) malloc(10 * sizeof(int)); // [C]
int *intarray  = new int[10];                     // [C++]
int[] intarray = new int[10];                     // [Java]

동적으로 할당된 배열은 컴파일 타임에 크기를 알 수 없기 때문에 이러한 언어에서 유용합니다. 데이터를 힙에 저장하는 것의 또 다른 이점은 함수 호출 후에도 데이터가 유지된다는 것입니다. 스택의 데이터는 함수 호출 중에만 유효한 상태로 유지되며 이후에는 삭제됩니다. 그러나 힙의 데이터는 프로그램 수명 동안 유지되며 프로그램의 모든 함수에서 사용할 수 있으며 힙의 다양한 데이터 구조에 대한 포인터를 함수 간에 공유할 수 있습니다. 힙은 위쪽으로(더 높은 주소 쪽으로) 증가합니다. 둘째, 힙에서 메모리를 관리하는 것은 고급 언어에서 힙의 영역이 malloc/new 호출로 동적으로 할당되고 free/delete 호출로 해제되기 때문에 다소 어려운 작업입니다. 할당된 메모리 영역이 해제되면 메모리 맵에 구멍이 형성됩니다. 구멍의 크기가 구멍의 크기보다 작은 경우 다른 데이터 구조가 구멍에 할당될 수 있습니다. 이 경우 메모리 맵에 또 다른 작은 구멍이 생성됩니다. 시간이 지남에 따라 점점 더 많은 데이터 구조가 할당되고 할당 해제됨에 따라 홀 수가 증가하는 경향이 있는데, 이를 조각화라고 합니다. 따라서 힙의 홀 수를 줄이기 위해서는 효율적인 메모리 관리자가 필요합니다. 아래 이미지는 홀과 할당된 메모리가 있는 힙의 보기를 보여줍니다.

힙 메모리 매핑.

다음 섹션은 메모리 매핑된 파일 및 동적으로 연결된 라이브러리에 해당하는 데이터를 저장하는 데 사용됩니다. 대부분의 경우 운영 체제는 파일의 내용(예: 음악, 텍스트 또는 비디오 파일)을 메모리 매핑 파일이라는 메모리 영역으로 전송하고 파일의 속성을 일반 배열로 처리합니다. 둘째, 프로그램은 때때로 다른 프로그램(라이브러리라고 함)의 내용을 동적으로 읽고 해당 텍스트 세그먼트의 내용을 메모리 맵으로 전송할 수 있습니다. 이러한 라이브러리를 DLL(동적 링크 라이브러리)이라고 합니다. 이 메모리 매핑된 구조의 내용은 프로세스의 메모리 맵에 있는 전용 세그먼트에 저장됩니다.

다음 세그먼트는 스택으로, 메모리 맵의 맨 위에서 시작하여 아래쪽으로(더 작은 주소로) 증가하며, 프로그램의 동작에 따라 스택이 늘어나고 줄어듭니다. 위 이미지는 실제 크기와 동일하지 않으니 주의하시기 바랍니다. 32비트 메모리 시스템을 고려하면 가상 메모리의 총량은 4GB이지만 프로그램이 사용할 수 있는 메모리의 총량은 일반적으로 수백 메가바이트로 제한됩니다. 따라서 힙 시작과 스택 사이의 매핑에는 거대한 빈 영역이 있습니다.

운영 체제는 매우 자주 실행되어야 하고 장치 요청을 처리해야 하며 프로세스 관리를 수행해야 합니다. 한 프로세스에서 다른 프로세스로 메모리의 가상 보기를 변경하는 것은 약간 비용이 많이 들기 때문에 대부분의 운영 체제는 가상 메모리를 사용자 프로세스와 커널로 나눕니다. 예를 들어 Linux는 하위 3GB를 사용자 프로세스용으로, 상위 1GB를 커널용으로 예약합니다. 마찬가지로 Windows에서는 상위 2GB를 커널용으로, 하위 2GB를 사용자 프로세스용으로 예약합니다. 프로세서가 사용자 프로세스에서 커널로 전환할 때 메모리 뷰를 변경할 필요가 없습니다. 둘째, 2GB 또는 3GB는 프로그램의 일반적인 메모리 공간보다 훨씬 크므로 이 작은 수정으로 인해 프로그램 성능이 크게 저하되지는 않습니다. 게다가 이 트릭은 가상 메모리의 개념과 충돌하지 않으며, 프로그램은 메모리 공간이 줄어든다고 가정하면 됩니다(Linux의 경우 4GB에서 3GB로). 아래 그림을 참조하세요.

Linux 및 Windows 사용자와 커널 메모리 매핑.

19.6.4.2 중복 및 크기 문제

우리는 두 가지 가상 메모리 문제를 해결해야 합니다.

  • 중복 문제. 프로그래머와 컴파일러는 자신이 전체 메모리 공간을 소유하고 있고 원하는 곳에 쓸 수 있다고 가정하여 프로그램을 작성합니다. 불행하게도 동시에 활성화된 모든 프로세스는 동일한 가정을 합니다. 별도의 조치를 취하지 않으면 본의 아니게 서로의 메모리 공간에 쓰기를 하여 서로의 데이터를 손상시킬 수 있습니다. 실제로, 동일한 메모리 맵을 사용하고 하드웨어가 서로 다른 프로세스가 서로 격리되도록 해야 한다는 점을 고려하면 조잡한 시스템에서 이런 일이 발생할 가능성은 매우 높습니다. 이것이 중첩 문제입니다.

  • 크기 문제. 때로는 사용 가능한 물리적 메모리보다 더 많은 메모리가 필요한 프로세스를 실행해야 할 때도 있습니다. 다른 저장 매체(예: 하드 드라이브)의 일부 공간을 프로세스의 메모리 공간을 저장하기 위해 용도를 변경할 수 있다면 이상적입니다. 이를 크기 문제라고 합니다.

모든 가상 메모리 구현은 크기 및 중복 문제를 효율적으로 해결해야 합니다.

19.6.4.3 페이징

이 섹션에 포함된 개념은 다음과 같이 분석됩니다.

  • 가상 주소: 가상 주소 공간에서 프로그램이 지정한 주소입니다.

  • 물리적 주소: 주소 변환 후 메모리 시스템에 제공되는 주소입니다.

  • 페이지: 가상 주소 공간의 메모리 조각입니다.

  • 프레임: 물리적 주소 공간의 메모리 조각입니다. 페이지와 프레임 크기는 동일합니다.

  • 페이지 테이블: 각 페이지의 주소를 프레임의 주소에 매핑하는 매핑 테이블입니다. 각 프로세스에는 자체 페이지 테이블이 있습니다.

프로세서, 운영 체제, 컴파일러 및 프로그래머의 요구 사항의 균형을 맞추기 위해 프로세스에서 생성된 주소를 메모리 시스템에서 사용할 수 있는 주소로 변환하도록 변환 시스템을 설계해야 합니다. 변환기를 사용하면 가상 메모리가 필요한 프로그래머/컴파일러와 물리적 메모리가 필요한 프로세서/메모리 시스템의 요구를 충족할 수 있습니다. 번역기 시스템은 실제 번역기와 유사합니다. 예를 들어 러시아 대표단이 두바이를 방문하는 경우 러시아어를 아랍어로 번역할 수 있는 번역가가 필요합니다. 그러면 양측 모두 각자의 언어로 말할 수 있고 행복감을 느낄 수 있습니다. 번역 시스템의 개념도는 아래와 같습니다.

이제 두 부분으로 나눌 수 있는 32비트 메모리 주소를 생각해 보세요. 4KB 페이지를 고려하면 하위 12비트는 오프셋이라고 하는 페이지의 바이트 주소(이유: 212=4096=4KB)를 지정하고 상위 20비트는 페이지 번호를 지정합니다(아래 그림). 마찬가지로 물리적 주소는 프레임 번호와 오프셋의 두 부분으로 나눌 수 있습니다. 아래에 표시된 변환 프로세스는 먼저 20비트 페이지 번호를 해당하는 20비트 프레임 번호로 바꾼 다음 12비트 오프셋을 물리적 프레임 번호에 추가합니다.

가상주소를 물리주소로 변환합니다.

구현된 솔루션에는 페이지 테이블의 레벨에 따라 레벨 1과 레벨 2 페이지 테이블이 있다. 그들의 개략도는 다음과 같습니다:

상위: 레벨 1 페이지 테이블; 하단: 레벨 2 페이지 테이블.

Intel Itanium 및 PowerPC 603과 같은 일부 프로세서는 페이지 테이블에 대해 다른 디자인을 사용합니다. 페이지 테이블 주소를 지정하기 위해 페이지 번호를 사용하는 대신 프레임 번호를 사용하여 페이지 주소를 지정합니다. 이 경우 전체 시스템에 대해 하나의 페이지 테이블만 있습니다. 프레임은 일반적으로 프로세스의 페이지에 고유하게 매핑되므로 이 역방향 페이지 테이블의 각 항목에는 프로세스 ID와 페이지 번호가 포함됩니다. 아래 그림 (a)는 역 페이지 테이블의 구조를 나타낸 것으로, 각 프로세스마다 별도의 페이지 테이블을 유지할 필요가 없다는 것이 가장 큰 장점이다. 프로세스가 많고 물리적 메모리 크기가 작은 경우 공간을 절약할 수 있습니다.

페이지 테이블을 반전시킵니다.

역방향 페이지 테이블의 가장 큰 어려움은 가상 주소를 찾는 것입니다. 모든 항목을 검색하는 것은 매우 느린 프로세스이므로 실용적이지 않습니다. 따라서 (프로세스 ID, 페이지 번호) 쌍을 해시 테이블의 인덱스에 매핑하는 해시 함수가 필요하며, 해시 테이블의 이 인덱스는 역방향 페이지 테이블의 항목을 가리켜야 합니다. 여러 가상 주소가 해시 테이블의 동일한 항목을 가리킬 수 있으므로 (프로세스 ID, 페이지 번호)가 역방향 페이지 테이블에 저장된 항목과 일치하는지 확인해야 합니다.

위의 그림 (b)는 역방향 페이지 테이블을 사용한 솔루션을 보여줍니다. 페이지 번호와 프로세스 ID 쌍의 해시를 계산한 후, 해시의 내용으로 색인화된 해시 테이블에 액세스하며, 해시 테이블 항목의 내용은 주어진 페이지에 매핑될 수 있는 프레임 f를 가리킵니다. 그러나 해시 함수가 여러 페이지를 동일한 프레임에 매핑할 수 있으므로 이를 확인해야 합니다. 그런 다음 역방향 페이지 테이블에 액세스하고 항목 f에 액세스합니다. 역방향 페이지 테이블의 항목에는 지정된 항목(또는 지정된 프레임)에 매핑되는 페이지 번호, 프로세스 ID 쌍이 포함됩니다. 내용이 일치하지 않는 것으로 확인되면 후속 K 항목에서 페이지 번호와 프로세스 ID 쌍을 계속 검색합니다. 선형 프로빙이라고 하는 이 방법은 일치하는 항목을 찾을 때까지 대상 데이터 구조를 검색합니다. K 항목 중에서 일치하는 항목이 발견되지 않으면 페이지가 매핑되지 않았다고 결론을 내릴 수 있습니다. 그런 다음 항목(캐시와 유사)을 제거하고 이를 역방향 페이지 테이블에서 제거된 모든 항목을 저장하는 주 메모리의 전용 영역에 작성하여 맵을 생성해야 하며, 항상 해시 테이블이 가리키는 항목과 맵을 포함하는 실제 항목 간의 차이가 K 항목을 넘지 않도록 해야 합니다. 사용 가능한 슬롯이 없으면 항목을 제거해야 합니다.

해시 엔진의 출력을 사용하여 역방향 페이지 테이블에 직접 액세스할 수 있다고 생각할 수도 있습니다. 일반적으로 해시 테이블에 대한 액세스를 중간 단계로 추가합니다. 사용되는 실제 프레임 세트를 더 효과적으로 제어할 수 있기 때문입니다. 이 프로세스를 사용하면 다른 목적으로 사용될 수 있는 특정 프레임의 매핑을 억제할 수 있습니다. 마지막으로 해시 테이블을 유지하고 업데이트하는 오버헤드가 시스템 전체 페이지 테이블을 사용하는 것의 이점보다 크다는 점입니다. 따라서 역방향 페이지 테이블은 일반적으로 상용 시스템에서 사용되지 않습니다.

가상 메모리에는 TLB, 공간 교체, MMU 및 페이지 오류와 같은 개념과 메커니즘도 포함됩니다. 이는 가상 메모리에서 지원될 수 있으며 이 문서에서는 다시 설명하지 않습니다.

주소 번역 과정.

19.6.4.4 페이징 시스템의 고급 기능

페이지 테이블 메커니즘을 사용하여 몇 가지 흥미로운 작업을 수행할 수 있다는 것이 밝혀졌습니다. 몇 가지 예를 살펴보겠습니다.

  • 공유 메모리

두 프로세스가 데이터를 교환할 수 있도록 서로 메모리를 공유하려고 한다고 가정하면 각 프로세스는 이를 커널에 알려야 합니다. 커널은 두 가상 주소 공간의 두 페이지를 동일한 프레임에 매핑할 수 있으며, 그 후 각 프로세스는 자신의 가상 주소 공간에 페이지를 쓸 수 있으며 마술처럼 데이터가 다른 프로세스의 가상 주소에 반영됩니다. 때로는 여러 프로세스가 서로 통신해야 하며, 공유 메모리 메커니즘은 가장 빠른 방법 중 하나입니다.

  • 보호

컴퓨터 바이러스는 일반적으로 실행 중인 프로세스의 코드를 변경하여 자신의 코드를 실행할 수 있도록 하며, 종종 프로그램에 잘못된 입력의 특정 순서를 제공합니다. 제대로 확인하지 않으면 프로그램의 특정 변수 값을 덮어쓰게 됩니다. 일부 변수는 텍스트 섹션에 대한 포인터로 변경될 수 있으며 이 메커니즘을 활용하여 텍스트 섹션 섹션 내의 지침을 변경할 수 있습니다. 이 문제는 텍스트 세그먼트의 모든 페이지를 읽기 전용으로 표시하여 런타임 시 해당 내용을 수정할 수 없도록 하여 해결할 수 있습니다.

  • 세분화

우리는 항상 프로그래머가 원하는 대로 메모리 맵을 자유롭게 레이아웃할 수 있다고 가정해 왔습니다. 예를 들어, 프로그래머는 매우 높은 주소(예: 0xFFFFFFFF8)에서 스택을 시작하기로 결정할 수 있습니다. 그러나 프로그램의 메모리 공간이 매우 작더라도 16비트 주소를 사용하는 시스템에서는 코드가 실행되지 않을 수 있습니다. 둘째, 시스템은 가상 메모리의 특정 세그먼트를 예약하여 프로세스에서 사용할 수 없게 만들 수 있습니다. 예를 들어 운영 체제는 일반적으로 커널용으로 상위 1GB 또는 2GB를 예약합니다. 이러한 문제를 해결하려면 가상 메모리 위에 또 다른 가상 레이어를 만들어야 합니다.

세그먼트화된 메모리(x86 시스템용)에는 텍스트, 데이터 및 스택 세그먼트에 대한 특정 세그먼트 레지스터가 있습니다. 각 가상 주소는 특정 세그먼트 레지스터에 대한 오프셋으로 지정됩니다. 기본적으로 명령어는 코드 세그먼트 레지스터를 사용하고 데이터는 데이터 세그먼트 레지스터를 사용합니다. 파이프라인의 메모리 액세스(MA) 단계에서는 세그먼트 레지스터에 저장된 값에 오프셋을 추가하여 가상 주소를 생성합니다. 그런 다음 MMU는 이 가상 주소를 사용하여 물리적 주소를 생성합니다.

19.7 다중 프로세서 시스템

프로세서의 설계 및 구현은 파이프라인과 같은 성능을 최적화하는 여러 가지 방법뿐만 아니라 이전에 자세히 논의되었습니다. 프로세서와 메모리 시스템을 최적화하면 프로그램 성능이 크게 향상될 수 있습니다. 문제는 이것으로 충분합니까? 더 잘하는 것이 가능할까요?

짧은 대답: 아마도 그렇지 않을 것입니다. 프로세서 성능에는 한계가 있다는 사실부터 시작하십시오. 매우 정교한 슈퍼스칼라 프로세서와 고도로 최적화된 메모리 시스템을 사용해도 프로세서 속도만으로는 프로세서 속도를 높이는 것이 불가능하며, 일반적으로 IPC를 50% 이상 높이는 것도 불가능합니다. 둘째, 전력 및 온도 문제로 인해 프로세서 주파수를 3GHz 이상으로 높이는 것은 어렵습니다. 지난 수년 동안 프로세서 주파수는 본질적으로 변하지 않았으므로 CPU 성능은 매우 느리게 증가했습니다.

위의 논의는 다음 두 그림으로 설명됩니다. 아래 차트는 Intel, AMD, Sun, Qualcomm 및 Fujitsu를 포함한 여러 공급업체에서 2001년부터 2010년까지 출시한 프로세서의 최고 주파수를 보여줍니다. 우리는 주파수가 거의 동일하게 유지되는 것을 관찰했으며(주로 1GHz와 2.5GHz 사이) 이러한 추세는 주파수가 점진적으로 증가하지 않음을 나타냅니다. 가까운 시일 내에 프로세서 주파수도 3GHz로 제한될 것으로 예상됩니다.

CPU 주파수.

아래 차트는 2001년부터 2010년까지 동일한 프로세서 세트에 대한 평균 Spec Int 2006 점수를 보여줍니다. 시간이 지남에 따라 CPU 성능이 점차 포화되고 성능 개선이 점점 더 어려워지는 것을 관찰했습니다.

CPU 성능.

개별 프로세서의 성능이 앞으로도 크게 향상될 것으로 예상되지는 않지만, 프로세서 제조 기술이 꾸준히 향상되어 트랜지스터가 더 작고 빨라지기 때문에 컴퓨터 아키텍처의 미래는 암울하지 않습니다. 1990년대 후반까지 프로세서 설계자들은 트랜지스터 기술의 발전을 활용하여 더 많은 기능을 구현함으로써 프로세서의 복잡성을 높였습니다. 그러나 복잡성과 전력 소비 제약으로 인해 설계자는 2005년 이후 더 단순한 프로세서로 전환했습니다. 프로세서에 더 많은 기능을 구현하는 대신 공급업체는 여러 프로그램을 동시에 실행하는 데 도움이 되는 여러 프로세서를 단일 칩에 설치하기로 결정했습니다. 또는 단일 프로그램을 여러 부분으로 분할하여 모두 병렬로 실행할 수 있습니다.

병렬로 실행되는 여러 컴퓨팅 장치를 사용하는 이러한 패러다임을 다중 처리라고 합니다. 멀티프로세싱은 병렬로 작동하는 동일한 칩 내의 여러 프로세서 또는 칩 전체의 여러 병렬 프로세서를 나타낼 수 있는 상당히 일반적인 용어입니다. 멀티프로세서는 멀티프로세싱을 지원하는 하드웨어입니다. 칩에 여러 프로세서가 있는 경우 각 프로세서를 코어라고 하고 칩을 멀티코어 프로세서라고 합니다.

우리는 멀티프로세서, 특히 멀티코어 시스템의 시대에 살고 있습니다. 칩당 코어 수는 약 2년마다 3배로 증가하며 추가 하드웨어를 활용하기 위해 새로운 애플리케이션이 작성되고 있습니다. 대부분의 전문가들은 컴퓨팅의 미래가 다중 프로세서 시스템에 있다고 믿습니다.

다양한 유형의 멀티프로세서 설계를 시작하기 전에 멀티프로세서의 배경과 역사를 살펴보겠습니다.

19.7.1 다중 프로세서 배경

1960년대와 1970년대에는 메인프레임 컴퓨터가 주로 은행과 금융 기관에서 사용되었습니다. 그들은 점점 더 많은 소비자를 보유하고 있으므로 초당 더 많은 트랜잭션을 수행할 수 있는 컴퓨터가 필요합니다. 프로세서 하나만으로 필요한 계산 처리량을 제공하기에 부족한 경우가 종종 있습니다. 따라서 초기 컴퓨터 설계자들은 단일 컴퓨터에 여러 프로세서를 설치하기로 결정했습니다. 프로세서는 계산 부하를 공유하여 전체 시스템의 계산 처리량을 높일 수 있습니다.

최초의 멀티프로세서 중 하나는 A와 B라는 두 개의 프로세서가 있는 Burroughs 5000이었습니다. A는 메인 프로세서이고 B는 보조 프로세서입니다. 로드가 높으면 프로세서 A는 프로세서 B에게 수행할 작업을 제공합니다. 당시 거의 모든 주요 공급업체에는 메인 프로세서에 연결된 두 번째 CPU 칩을 지원하는 컴퓨터인 IBM 370, PDP 11/74, VAX-11/782 및 Univac 1108-II와 같은 다중 프로세서 제품이 있었습니다. 이러한 모든 초기 시스템에서 두 번째 CPU는 전선이나 케이블을 통해 첫 번째 CPU에 물리적으로 연결된 두 번째 칩에 있었습니다. 대칭비대칭의 두 가지 유형이 있습니다. 대칭형 다중 프로세서는 각각 동일한 유형을 가지며 운영 체제 및 주변 장치에서 제공하는 서비스에 액세스할 수 있는 여러 프로세서로 구성됩니다. 비대칭 다중 프로세서는 서로 다른 프로세서에 서로 다른 역할을 할당합니다. 일반적으로 운영 체제와 주변 장치를 제어하는 ​​고유한 프로세서가 있습니다. 나머지 프로세서는 메인 프로세서에서 작업을 받고 결과를 반환하는 슬레이브입니다.

대칭형 다중 처리: 이 패러다임은 다중 프로세서 시스템의 모든 구성 요소 프로세서를 동일하게 취급하며 각 프로세서는 운영 체제 및 I/O 주변 장치에 대해 동일한 액세스 권한을 갖습니다. SMP 시스템이라고도 합니다.

비대칭 다중 처리: 이 패러다임은 다중 프로세서 시스템의 모든 구성 요소 프로세서를 동일하게 처리하지 않습니다. 일반적으로 운영 체제와 I/O 장치를 독점적으로 제어하고 작업을 다른 프로세서에 배포하는 메인 프로세서가 있습니다.

초기에는 두 번째 프로세서가 일반적으로 메인 컴퓨터의 다른 영역에 있는 케이블 세트를 사용하여 메인 프로세서에 연결되었습니다. 그 당시 컴퓨터는 방만큼 컸습니다. 소형화가 진행되면서 두 프로세서는 점차 서로 가까워지고 있다. 1980년대 후반과 1990년대 초반에 기업들은 동일한 마더보드에 여러 프로세서를 설치하기 시작했습니다. 마더보드는 컴퓨터에서 사용되는 모든 칩이 포함된 인쇄 회로 기판입니다. 칩과 금속 와이어가 있는 커다란 녹색 회로 기판이 마더보드입니다. 1990년대 후반에는 단일 마더보드에 4~8개의 프로세서가 있을 수 있었고 전용 고속 버스를 통해 서로 연결되었습니다.

점차적으로 동일한 칩에 여러 프로세서가 포함되는 멀티 코어 프로세서 시대가 시작되었습니다. 2001년 IBM은 Power 4라는 듀얼 코어(2코어) 멀티 코어 프로세서를 출시하는 데 앞장섰습니다. 2005년에는 Intel과 AMD도 유사한 제품을 출시했습니다. 2022년부터 16, 32, 64개 및 그 이상의 코어를 갖춘 멀티 코어 프로세서가 출시됩니다.

이제 1960년부터 2012년 사이에 프로세서 세계에서 무슨 일이 일어났는지 자세히 살펴보겠습니다. 1960년대에는 컴퓨터가 대개 방 크기만 했습니다. 오늘날 당신은 주머니에 컴퓨터를 가지고 다닙니다. 1960년대 초 휴대폰의 프로세서는 IBM 360 시스템보다 약 160만 배 빨랐으며 전력 효율성도 훨씬 더 높았습니다. 컴퓨터 기술의 지속적인 발전의 주요 원동력은 트랜지스터의 소형화입니다. 트랜지스터의 소형화는 60년대에 한때 채널 길이가 몇 밀리미터였지만 현재는 길이가 약 20-30나노미터입니다. 1971년에는 일반적인 칩에 2,000~3,000개의 트랜지스터가 있었습니다. 오늘날 칩에는 수십억 개의 트랜지스터가 있습니다.

지난 40~50년 동안 칩당 트랜지스터 수는 약 1~2년마다 두 배로 늘어났습니다. 실제로 Intel의 공동 창업자인 Gordon Moore는 1965년에 이러한 추세를 예측했습니다. 무어의 법칙에 따르면 칩의 트랜지스터 수가 1~2년마다 두 배로 늘어날 것으로 예상됩니다. 처음에 무어는 2배의 시간을 1년으로 예측했는데, 이후 약 2년이 되었습니다. 이는 제조 기술, 신소재, 제조 기술의 꾸준한 발전으로 인해 발생할 것으로 예상됩니다.

무어의 법칙은 1960년대 중반에 제안된 이후 거의 항상 사실이었습니다. 오늘날에는 거의 2년마다 트랜지스터의 크기가 2배로 줄어들어 트랜지스터의 면적이 두 배로 늘어나 칩의 트랜지스터 수가 두 배로 늘어납니다. 피처 크기를 칩에 제작할 수 있는 가장 작은 구조의 크기로 정의하겠습니다. 아래 표는 지난 10년간 Intel 프로세서의 기능 크기를 보여줍니다. 우리는 피처 크기가 2년마다 약 2(1.41)배씩 감소하여 트랜지스터 수가 두 배로 늘어나는 것을 관찰했습니다.

연도 피처 크기

2001년 130nm

2003년 90nm

2005년 65nm

2007년 45nm

2009년 32nm

2011년 22nm

무어의 법칙은 경험적 법칙이라는 점에 유의하세요. 그러나 지난 40년 동안의 추세를 정확하게 예측했기 때문에 기술 문헌에서 널리 인용됩니다. 이는 트랜지스터 크기의 소형화를 직접적으로 예측하며, 트랜지스터가 작을수록 전력 효율이 높고 속도가 더 빠릅니다. 전통적으로 설계자는 이러한 이점을 활용하여 추가 트랜지스터를 갖춘 더 큰 프로세서를 설계해 왔습니다. 그들은 추가 트랜지스터 예산을 사용하여 다양한 장치의 복잡성을 높이고, 캐시 크기를 늘리고, 문제 폭을 늘리고, 기능 장치 수를 늘렸습니다. 둘째, 파이프라인 단계의 수도 2002년경까지 꾸준히 증가했고, 클럭 주파수도 증가했다. 그러나 2002년 이후 컴퓨터 아키텍처의 세계는 근본적으로 바뀌었습니다. 갑자기 전력과 온도가 주요 관심사가 되었습니다. 프로세서 전력 소비 곡선은 100와트를 초과하기 시작하고 칩 온도는 섭씨 100도를 초과하기 시작합니다. 이러한 제한으로 인해 프로세서 복잡성 및 클럭 주파수의 확장이 크게 종료됩니다.

대신 설계자는 기본 설계를 변경하지 않고 각 칩에 더 많은 코어를 패키징하기 시작하여 코어당 트랜지스터 수가 동일하게 유지되도록 했습니다. 무어의 법칙에 따르면 코어 수가 2년마다 두 배로 늘어나 멀티 코어 프로세서 시대가 열렸고 프로세서 공급업체는 칩의 코어 수를 두 배로 늘리기 시작했습니다. 가까운 미래에는 칩당 코어 수가 64개, 128개 또는 그 이상에 이를 것으로 예상됩니다.

기존 멀티 코어 프로세서 외에도 또 다른 중요한 개발이 있습니다. 칩당 4개의 대형 코어 외에도 그래픽 프로세서와 같이 칩에 64~256개의 매우 작은 코어가 있는 아키텍처도 있습니다. 이러한 프로세서는 또한 무어의 법칙을 따르며 2년마다 코어 수가 두 배로 증가하며 컴퓨터 그래픽, 수치 컴퓨팅 및 과학 컴퓨팅에 점점 더 많이 사용되고 있습니다. 두 개의 프로그램 카운터를 지원하고 동시에 두 개의 프로그램을 실행하도록 프로세서의 리소스를 분할하는 것도 가능합니다. 이러한 특별한 유형의 프로세서를 멀티스레드 프로세서라고 합니다.

이 장에서는 독자에게 다중 프로세서 설계의 광범위한 추세에 대한 이해를 제공합니다. 소프트웨어 관점에서 멀티프로세싱을 살펴보는 것부터 시작하여, 소프트웨어 요구 사항이 식별되면 멀티프로세싱을 지원하는 하드웨어 설계로 넘어갑니다. 멀티코어, 멀티스레드 및 벡터 프로세서가 광범위하게 고려됩니다.

19.7.2 다중 프로세서 시스템 소프트웨어

19.7.2.1 강력하고 느슨하게 결합된 다중 처리

느슨하게 결합된 다중 처리는 여러 프로세서에서 관련되지 않은 여러 프로그램을 병렬로 실행합니다.

강력 결합 다중 처리는 메모리 공간, 데이터, 코드, 파일 및 네트워크 연결을 공유하는 여러 프로세서에서 병렬로 실행되는 프로그램 세트입니다.

이 기사에서는 강력하게 결합된 다중 처리를 연구하고 대량의 데이터와 코드를 공유하여 프로그램 그룹을 함께 실행할 수 있는 시스템에 주로 중점을 둘 것입니다.

19.7.2.2 공유 메모리 및 메시지 전달

컴퓨터 설계자는 다양한 패턴에 따라 다중 프로세서용 프로토콜 세트를 설계했습니다. 첫 번째 패러다임은 공유 메모리라고 하며, 모든 개별 프로그램은 메모리 시스템의 동일한 보기를 봅니다. 프로그램 A가 x 값을 5로 변경하면 프로그램 B는 즉시 변경 사항을 확인합니다. 두 번째 설정은 메시징이라고 하며, 여러 프로그램이 메시지를 전달하여 서로 통신합니다. **공유 메모리 패러다임은 강력하게 결합된 다중 프로세서에 더 적합하고, 메시지 전달 패러다임은 느슨하게 결합된 다중 프로세서에 더 적합합니다.**메시지 전달은 강력하게 결합된 다중 프로세서에서 구현될 수 있습니다. 마찬가지로, 분산 공유 메모리라고 하는 느슨하게 결합된 다중 프로세서에서 공유 메모리 추상화를 구현하는 것이 가능하지만 이는 일반적으로 표준이 아닙니다.

공유 메모리

다중 프로세서를 사용하여 n개의 숫자를 병렬로 추가해 보겠습니다. 해당 코드는 아래와 같으며 OpenMP 언어 확장을 사용하여 C++로 코딩되었습니다. 모든 숫자가 숫자라는 배열에 저장되어 있다고 가정합니다. 배열 번호에는 SIZE 항목이 있으며 시작할 수 있는 병렬 서브루틴 수가 N과 같다고 가정합니다.

cpp
/* 변수선언 */
int partialSums[N];
int numbers[SIZE];
int result = 0;

/* 초기화(Initialize)배열 */
(...)
    
/* 병렬 코드 */
#pragma omp parallel 
{ 
    /* get my processor id */
    int myId = omp_get_thread_num();

    /* add my portion of numbers */
    int startIdx = myId * SIZE/N;
    int endIdx = startIdx + SIZE/N;
    for(int jdx = startIdx; jdx < endIdx; jdx++)
        partialSums[myId] += numbers[jdx];
}

/* 코드 */
for(int idx=0; idx < N; idx++)
    result += partialSums[idx];

병렬 프로그램에 추가된 유일한 추가 의미상 차이점은 이 루프의 각 반복을 별도의 서브루틴으로 시작하고 이러한 각 서브루틴을 스레드라고 부르는 #pragma omp Parallel 지시어를 제외하고 코드를 일반 순차 프로그램으로 착각하기 쉽다는 것입니다. 스레드는 공유 메모리 공간의 메모리 위치 값을 수정하여 서로 통신합니다. 각 스레드에는 다른 스레드에서 액세스할 수 없는 자체 지역 변수 세트가 있습니다.

반복 횟수 또는 시작된 병렬 스레드 수는 미리 설정된 시스템 매개변수이며 일반적으로 프로세서 수와 동일하며 위 코드에서 N과 같습니다. 따라서 코드의 병렬 부분에 대한 N 복사본이 병렬로 실행되고, 각 복사본은 별도의 프로세서에서 실행됩니다. 프로그램의 각 복사본은 병렬 부분이 호출되기 전에 선언된 모든 변수에 액세스할 수 있습니다. 예를 들어 부분 합계 및 숫자 배열에 액세스할 수 있습니다. 각 프로세서는 스레드의 ID를 반환하는 omp_get_thread_num 함수를 호출합니다. 각 스레드는 스레드 ID를 사용하여 추가해야 하는 배열의 범위를 찾고, 배열의 관련 부분에 모든 항목을 추가하고, 결과를 부분Sums 배열의 해당 항목에 저장합니다. 모든 스레드가 작업을 완료하면 순차적인 부분이 시작됩니다. 이 순차 코드는 모든 프로세서에서 실행될 수 있으며 운영 체제 또는 병렬 프로그래밍 프레임워크에 의해 런타임 시 동적으로 생성됩니다. 최종 결과를 얻으려면 순차 부분의 모든 부분합을 더해야 합니다.

계산의 그래픽 표현은 아래와 같습니다. 상위 스레드는 자체 작업을 수행하기 위해 일련의 하위 스레드를 생성하고, 완료되면 최종적으로 연결되고 상위 스레드가 인계받아 병렬 결과를 집계합니다. 이 예는 Fork-Join 패러다임의 구체적인 예이기도 합니다.

병렬 추가 프로그램의 그래픽 표현.

주의할 점이 몇 가지 있습니다. 각 스레드에는 자체 스택이 있으며 해당 스택을 사용하여 지역 변수를 선언할 수 있습니다. 완료되면 스택의 모든 지역 변수가 삭제됩니다. 상위 스레드와 하위 스레드 간에 데이터를 전달하려면 두 스레드 모두에서 액세스할 수 있는 변수를 사용해야 합니다. 모든 스레드는 이러한 변수에 전역적으로 액세스할 수 있어야 합니다. 하위 스레드는 이러한 변수를 자유롭게 수정할 수 있으며 이를 사용하여 서로 통신할 수도 있습니다. 또한 자유롭게 운영 체제를 호출하고 외부 파일 및 네트워크 장치에 쓸 수 있습니다. 모든 스레드의 실행이 끝나면 조인 작업을 수행하고 상태를 해제하며 상위 스레드가 결과를 집계하는 역할을 맡아 완료합니다. 조인은 스레드 간 동기화 작업의 한 예이며 스레드 간에는 다른 많은 유형의 동기화 작업이 있을 수 있습니다. 스레드가 매우 복잡한 작업을 수행하기 위해 조정하는 데 사용할 수 있는 복잡한 구조 집합이 있습니다. 숫자 집합을 추가하는 것은 매우 간단한 예입니다. 멀티스레드 프로그램은 행렬 대수와 같은 다른 복잡한 작업을 수행하고 심지어 미분 방정식을 병렬로 푸는 데에도 사용할 수 있습니다.

메시지 전통

다음은 독자에게 메시징 프로그램의 개요만 제공하기 위한 메시징에 대한 간략한 설명입니다. 여기서 각 프로그램은 별도의 엔터티이며 다른 프로그램과 코드나 데이터를 공유하지 않습니다. 프로세스는 프로그램의 실행 중인 인스턴스로 정의되며 일반적으로 다른 프로세스와 주소 공간을 공유하지 않는 프로세스입니다.

이제 다음 표에 표시된 대로 주로 보내기 및 받기라는 두 가지 기능을 사용하여 메시지 전달 의미 체계를 빠르게 정의합니다. send(pid, val) 함수는 id가 pid와 같은 프로세스에 정수(val)를 보내는 데 사용되고, receive(pid)는 id가 pid와 같은 프로세스가 보낸 정수를 받는 데 사용됩니다. pid가 ANYSOURCE와 같으면 수신 함수는 모든 프로세스에서 보낸 값을 반환할 수 있습니다. 우리의 의미 체계는 널리 사용되는 병렬 프로그래밍 프레임워크 MPI(Message Passing Interface)를 기반으로 합니다. MPI 호출에는 더 많은 매개변수가 있으며 구문은 상대적으로 복잡합니다.

기능 의미론적

보내기(pid,val) id가 pid와 동일한 프로세스에 정수 val을 보냅니다.

수신(PID)

  1. 프로세스 pid로부터 정수를 받습니다.
  2. 함수는 값을 얻을 때까지 차단됩니다.
  3. pid가 ANYSOURCE와 같으면 수신 함수는 모든 프로세스에서 보낸 값을 반환합니다.

이제 n 개의 숫자를 병렬로 추가하는 다음 예제를 고려하십시오. 모든 숫자가 숫자 배열에 저장되어 있고 이 배열을 모든 N개의 프로세서에서 사용할 수 있으며 숫자 요소의 개수가 SIZE라고 가정합니다. 단순화를 위해 SIZE를 N으로 나눌 수 있다고 가정합니다.

cpp
/* start all the parallel processes */
SpawnAllParallelProcesses();

/* For each process execute the following code */
int myId = getMyProcessId();

/* 계산/산출(Calculate) 및 */
int startIdx = myId * SIZE/N;
int endIdx = startIdx + SIZE/N;
int partialSum = 0;
for(int jdx = startIdx; jdx < endIdx; jdx++)
    partialSum += numbers[jdx];

/* 포인트 및 까지 */
if(myId != 0) 
{
    send (0, partialSum);
}
else 
{
    /* 처리(Process)포인트 */
    int sum = partialSum;
    for (int pid = 1; pid < N; pid++) 
    {
        sum += receive(ANYSOURCE);
    }
    
    /* 비활성화(Disable) */
    shutDownAllProcesses();
    
    return sum;
}

19.7.3 다중 프로세서 디자인 공간

Michael J. Flynn은 1966년에 다양한 프로세서의 통합이 코드, 데이터 또는 둘 다를 공유할 수 있다는 관찰에서 시작하여 Flynn의 다중 프로세서 분류를 제안한 것으로 유명합니다. 가능한 선택 사항은 SISD(단일 명령어 단일 데이터), SIMD(단일 명령어 다중 데이터), MISD(다중 명령어 단일 데이터) 및 MIMD(다중 명령어 다중 데이터)의 네 가지입니다. 이러한 유형의 다중 프로세서는 아래에 설명되어 있습니다.

  • SISD: 단일 파이프라인이 있는 표준 단일 프로세서입니다. SISD 프로세서는 단일 프로세서로만 구성된 다중 프로세서 세트의 특별한 경우로 생각할 수 있습니다.

  • SIMD: SIMD 프로세서는 하나의 명령으로 여러 데이터 스트림을 처리할 수 있습니다. 예를 들어 SIMD 명령어는 하나의 명령어로 4개의 숫자 세트를 추가할 수 있습니다. 최신 프로세서는 SIMD 명령어를 명령어 세트에 통합하고 SIMD 명령어 세트의 SSE 세트를 포함하는 x86 프로세서와 같은 특수 SIMD 실행 장치를 갖습니다. 그래픽 프로세서와 벡터 프로세서는 매우 성공적인 SIMD 프로세서의 특별한 예입니다.

멀티 스레드 SIMD 프로세서 데이터 경로의 단순화된 블록 다이어그램. .

  • MISD: MISD 시스템은 실제로는 매우 드물며 신뢰성 요구 사항이 매우 높은 시스템에 주로 사용됩니다. 예를 들어, 대형 상업용 항공기에는 동일한 프로그램의 다양한 버전을 실행하는 여러 프로세서가 있는 경우가 많으며 최종 결과는 투표로 결정됩니다. 예를 들어, 항공기에는 MIPS 프로세서, ARM 프로세서 및 x86 프로세서가 있을 수 있으며, 각각은 여러 명령어 스트림이 있지만 데이터 소스는 하나만 있는 자동 조종 시스템과 같은 동일한 프로그램의 서로 다른 버전을 실행합니다. 전용 투표 회로는 3개 출력의 과반수 투표를 계산합니다. 예를 들어, 프로그램이나 프로세서의 버그로 인해 시스템 중 하나가 좌회전하기로 잘못 결정할 수 있고, 다른 두 시스템 모두 우회전하기로 올바른 결정을 내릴 수 있으며, 이 경우 투표 회로는 우회전하기로 결정합니다. MISD 시스템은 특별한 경우를 제외하고는 실제로 거의 사용되지 않으므로 이 기사에서는 다루지 않습니다.

  • MIMD: MIMD 시스템은 현재 가장 널리 사용되는 다중 프로세서 시스템입니다. 여기에는 여러 명령 스트림과 여러 데이터 스트림이 있습니다. 멀티 코어 프로세서와 대형 서버는 모두 MIMD 시스템입니다. 다중 명령 스트림은 명령이 여러 소스에서 나오며 각각 고유한 위치와 관련 프로그램 카운터가 있음을 의미합니다. MIMD 패러다임의 두 가지 중요한 분야가 지난 몇 년 동안 형성되었습니다.

첫 번째는 SPMD(Single Program Multiple Data)이고, 두 번째는 MPMD(Multiple Program Multiple Data)입니다. 대부분의 병렬 프로그램은 SPMD 스타일로 작성됩니다. 동일한 프로그램의 여러 복사본이 서로 다른 코어 또는 별도의 프로세서에서 실행되지만 각 개별 처리 장치에는 별도의 프로그램 카운터가 있으므로 서로 다른 명령 스트림을 인식합니다. 때때로 SPMD 프로그램은 스레드 ID를 기반으로 서로 다른 작업을 수행하는 방식으로 작성됩니다. SPMD의 장점은 서로 다른 프로세서에 대해 서로 다른 프로그램을 작성할 필요가 없다는 것입니다. 동일한 프로그램의 일부는 다르게 동작하더라도 모든 프로세서에서 실행될 수 있습니다.

대조적인 패러다임은 MPMD로, 서로 다른 프로세서에서 실행되는 프로그램은 실제로 다르며 이기종 처리 장치를 갖춘 특수 프로세서에 더 유용합니다. 일반적으로 슬레이브 프로그램에 작업을 분배하는 마스터 프로그램은 하나뿐입니다. 슬레이브 프로그램은 할당된 작업 부하를 완료한 다음 결과를 마스터 프로그램에 반환합니다. 이 두 프로그램의 작업 성격은 실제로 매우 다르며, 이를 하나의 프로그램으로 완벽하게 결합하는 것이 불가능한 경우가 많습니다.

MIMD 조직에서 프로세서는 보편적이며 각 프로세서는 적절한 데이터 변환을 수행하는 데 필요한 모든 명령을 처리할 수 있습니다. MIMD는 프로세서가 통신하는 방식에 따라 더 세분화될 수 있습니다(아래 이미지).

프로세서가 공통 메모리를 공유하는 경우 각 프로세서는 공유 메모리에 저장된 프로그램과 데이터에 액세스하여 프로세서 간 통신을 수행합니다. 이러한 시스템의 가장 일반적인 형태는 대칭형 다중 프로세서(SMP)입니다. SMP에서는 여러 프로세서가 공유 버스 또는 기타 상호 연결 메커니즘을 통해 단일 메모리 또는 메모리 풀을 공유하며, 특정 메모리 영역에 대한 메모리 액세스 시간이 각 프로세서에서 거의 동일하다는 특징이 있습니다. 지난 몇 년간의 발전은 아래 그림에 설명된 NUMA(Non-Uniform Memory Access) 조직입니다. 이름에서 알 수 있듯이 NUMA 프로세서는 메모리 영역마다 메모리 액세스 시간이 다를 수 있습니다.

위의 설명에서 우리가 집중해야 할 시스템은 SIMD와 MIMD라는 것이 분명해졌습니다. MISD 시스템은 거의 사용되지 않으므로 다시 논의하지 않습니다. MIMD 다중 처리에 대해서는 아래에서 설명합니다. SPMD가 가장 일반적인 방법이므로 MIMD 다중 처리의 SPMD 변형만 설명합니다.

19.7.4 MIMD 다중 프로세서

이제 강력하게 결합된 공유 메모리를 기반으로 하는 MIMD 머신을 자세히 살펴보겠습니다. 먼저 소프트웨어 관점에서 살펴보겠습니다. 소프트웨어 관점에서 이러한 기계의 광범위한 사양을 공식화한 후 하드웨어 설계에 대한 간략한 개요로 넘어갈 수 있습니다. 병렬 MIMD 기계의 설계를 설명하려면 책 전체가 필요합니다.

공유 메모리 MIMD 시스템의 소프트웨어 인터페이스를 논리적 관점이라고 부르고, 멀티프로세서의 실제 물리적 설계를 물리적 관점이라고 부릅니다. 논리 관점을 설명할 때 주요 관심사는 다중 프로세서가 소프트웨어와 관련하여 동작하는 방식, 하드웨어의 동작에 대한 보장, 정확성, 성능 및 오류 복구를 포함하여 소프트웨어가 기대할 수 있는 것입니다. 물리적 관점은 프로세서, 스토리지 시스템 및 상호 연결 네트워크의 물리적 설계를 포함하여 멀티프로세서의 실제 설계와 관련됩니다. 물리적 각도는 논리적 각도와 일치해야 합니다. 여기서는 단일 프로세서와 유사한 접근 방식을 취합니다. 먼저 어셈블리 코드를 살펴봄으로써 소프트웨어 뷰(아키텍처)를 설명하고, 그런 다음 파이프라인 프로세서(조직)를 설명하여 어셈블리 코드에 대한 구현을 제공합니다.

19.7.4.1 논리적 관점

다음 그림은 공유 메모리 MIMD 다중 프로세서의 논리적 보기를 보여줍니다. 각 프로세서는 코드와 데이터가 저장되는 메모리 시스템에 연결되어 있으며, 프로그램 카운터는 실행 중인 명령의 위치, 즉 메모리의 코드 세그먼트를 가리킵니다. 이 세그먼트는 일반적으로 읽기 전용이므로 여러 프로세서가 있다는 사실의 영향을 받지 않습니다.

멀티프로세서 시스템의 논리적 관점.

공유 메모리 다중 프로세서 구현의 주요 과제는 데이터 액세스를 올바르게 처리하는 것입니다. 위 다이어그램은 각 컴퓨팅 프로세서가 메모리에 연결되어 블랙박스로 처리되는 시나리오를 보여줍니다. 서로 다른 가상 주소 공간을 가진 프로세스 시스템을 고려하면 문제가 없습니다. 각 프로세서는 해당 데이터의 개인 복사본에 대해 작업할 수 있으며, 메모리 공간이 효과적으로 분리되어 있으므로 이 시스템에서 일련의 병렬 프로세스를 쉽게 실행할 수 있습니다. 그러나 다중 스레드로 공유 메모리 프로그램을 연구할 때 심각한 문제가 발생하며 스레드 간에 데이터가 공유됩니다. 또한 서로 다른 가상 페이지를 동일한 물리적 프레임에 매핑하여 프로세스 간에 메모리를 공유할 수 있으며, 이 상황을 병렬 다중 스레드 소프트웨어의 특별한 경우로 처리할 수 있습니다.

병렬 스레드 그룹은 일반적으로 가상 및 물리적 주소 공간을 공유하지만 스레드에는 스택에 보관되는 개인 데이터도 있습니다. 분리된 스택을 구현하는 방법에는 두 가지가 있습니다. 첫째, 모든 스레드는 동일한 가상 주소 공간을 가질 수 있으며, 다른 스택 포인터는 가상 주소 공간의 다른 지점에서 시작할 수 있습니다. 스레드 스택의 크기가 다른 스레드의 스택과 겹칠 만큼 크지 않은지 확인해야 합니다. 또 다른 접근 방식은 서로 다른 스레드의 가상 주소 공간의 스택 부분을 서로 다른 메모리 프레임에 매핑하는 것입니다. 각 스레드는 스택 부분에 대한 페이지 테이블의 다른 항목을 가질 수 있지만 나머지 가상 주소 공간(예: 코드, 읽기 전용 데이터, 상수 및 힙 변수)에 대한 공통 항목을 가질 수 있습니다.

어쨌든 병렬 소프트웨어 복잡성의 주요 문제는 코드가 읽기 전용이기 때문도 아니고, 스레드 간에 공유되지 않는 로컬 변수 때문도 아니고, 데이터 값이 여러 스레드 간에 공유될 수 있기 때문입니다. 이것이 병렬 프로그램의 힘이며 병렬 프로그램을 매우 복잡하게 만드는 이유입니다. 앞서 보여드린 숫자 집합을 병렬로 추가하는 예에서는 공유 메모리를 통해 값과 계산 결과를 공유함으로써 얻을 수 있는 이점을 명확하게 볼 수 있습니다.

그러나 스레드 간에 값을 공유하는 것은 그리 간단하지 않고 다소 깊은 주제입니다. 이 기사에서는 일관성과 메모리 일관성이라는 두 가지 중요한 주제를 간략하게 살펴봅니다. 캐싱과 관련하여 일관성이 언급되면 일관성을 캐시 일관성이라고도 합니다. 그러나 일관성은 캐싱에만 국한되지 않고 일반적인 용어입니다.

일관성

메모리 시스템의 일관성은 여러 스레드가 동일한 위치에 액세스하는 방식을 나타냅니다. 여러 스레드가 동일한 메모리 위치에 액세스하면 다양한 동작이 가능하며 그 중 일부는 직관적으로 잘못되었지만 가능합니다. 일관성을 살펴보기 전에 메모리 시스템에는 캐시, 쓰기 버퍼, 다양한 유형의 임시 버퍼 등 다양한 엔터티가 있다는 점에 유의하는 것이 중요합니다. 프로세서는 일반적으로 임시 버퍼에 값을 쓴 다음 작업을 재개합니다. 메모리 시스템의 역할은 이러한 버퍼의 데이터를 캐시 하위 시스템의 위치로 전송하는 것입니다. 따라서 내부적으로 주어진 메모리 주소는 주어진 시점에서 다양한 물리적 위치와 연관될 수 있습니다. 둘째, 프로세서에서 메모리 시스템(일반적으로 캐시 블록)의 올바른 위치로 데이터를 전송하는 프로세스는 즉각적이지 않으며, 메모리 읽기 또는 쓰기 요청이 해당 위치에 도달하는 데 수십 사이클 이상이 걸리는 경우도 있습니다. 메모리 트래픽이 많은 경우 이러한 메모리 요청 메시지는 더 오래 기다릴 수 있으며 메시지는 나중에 전송되는 다른 메시지로 재정렬될 수 있습니다.

내부적으로는 읽기/쓰기 작업을 위한 간단한 논리적 추상화를 제공하려고 노력하는 다양한 구성 요소의 복잡한 네트워크이지만 메모리가 모든 프로세서에 큰 바이트 배열처럼 보인다고 가정해 보겠습니다. 다중 프로세서 메모리 시스템의 내부 복잡성으로 인해 동일한 공유 변수 세트에 액세스하는 프로그램의 몇 가지 흥미로운 동작이 발생합니다.

일련의 예를 고려해 봅시다. 각 예시에서 모든 공유 값은 0으로 초기화되고 t1, t2, t3 등 모든 지역 변수는 t로 시작됩니다. 스레드 1이 스레드 간에 공유되는 변수 x에 쓴 후 즉시 스레드 2가 해당 값을 읽으려고 시도한다고 가정합니다.

cpp
// Thread 1:
x = 1

// Thread 2:
t1 = x

스레드 2가 1을 읽는 것이 보장됩니까? 아니면 이전 값을 0으로 가져올 수 있나요? 스레드 2가 x 2ns 또는 심지어 10ns 이후의 값을 읽는다면 어떻게 될까요? 한 스레드의 쓰기가 다른 스레드로 전파되는 데 얼마나 걸리나요? 이러한 질문에 대한 답은 메모리 시스템의 구현에 따라 달라집니다. 메모리 시스템에 빠른 버스와 빠른 캐시가 있으면 쓰기가 다른 스레드로 매우 빠르게 전파될 수 있습니다. 그러나 버스와 캐시가 느린 경우 다른 스레드가 공유 변수에 대한 쓰기를 확인하는 데 더 많은 시간이 걸릴 수 있습니다.

이제 이 예제를 더욱 복잡하게 만들기 위해 스레드 1이 x에 두 번 쓴다고 가정합니다.

cpp
// Thread 1:
x = 1
x = 2
    
// Thread 2:
t1 = x
t2 = x

이제 일련의 가능한 결과를 살펴보겠습니다. (t1,t2) = (1,2), (t1,t2) = (0,1)이 모두 가능합니다. 이는 스레드 1이 시작되기 전에 t1이 작성되고 스레드 1의 첫 번째 명령문이 완료된 후에 t2가 작성될 때 가능합니다. 마찬가지로 (0,0), (0,1), (0,2), (1,1), (1,2), (2,2) 등 가능한 모든 결과 집합을 체계적으로 열거할 수 있습니다. 흥미로운 질문은 결과 (2,1)이 가능한가입니다. x에 대한 첫 번째 쓰기가 메모리 시스템에서 지연되고 두 번째 쓰기가 이를 초과한 경우 이것이 가능할 수 있지만 문제는 이 동작을 허용해야 하는지 여부입니다.

대답은 '아니요'입니다. 이러한 동작을 허용한다면 다중 프로세서 메모리 시스템을 구현하는 것은 의심할 여지 없이 더 간단해질 것이지만 병렬 프로그램에 대한 작성 및 추론은 매우 어려워질 것입니다. 따라서 대부분의 다중 프로세서 시스템에서는 이 동작을 허용하지 않습니다.

이제 동일한 메모리 위치에 액세스하는 여러 스레드의 문제를 좀 더 공식적으로 살펴보겠습니다. 우리는 이상적으로 메모리 시스템이 일관성을 갖기를 원합니다. 즉, 동일한 메모리 주소에 대한 다양한 액세스를 처리할 때 프로그램 작성을 더 쉽게 만들기 위해 일련의 규칙을 따라야 한다는 의미입니다.

동일한 메모리 주소에 액세스하는 메모리의 동작을 일관성이라고 합니다.

일반적으로 일관성에는 두 가지 원칙이 있습니다.

  • 완료: 쓰기가 최종적으로 완료되어야 합니다. 이 공리는 메모리 시스템에서 쓰기가 손실되지 않는다는 것을 나타냅니다. 예를 들어 변수 x에 값 10을 쓰는 것이 불가능하고 쓰기 요청이 메모리 시스템에 의해 삭제되고 x에 해당하는 메모리 위치에 도달한 다음 해당 값을 업데이트해야 하며 나중에 다른 쓰기 요청으로 덮어쓸 수 있습니다. 그러나 중요한 점은 쓰기 요청이 향후 어느 시점에 메모리 위치 업데이트를 요구한다는 것입니다.

  • 순서: 동일한 메모리 주소에 대한 모든 쓰기는 모든 스레드에서 동일한 순서로 확인되어야 합니다. 이 공리는 모든 스레드가 메모리 위치에 대한 모든 쓰기를 동일한 순서로 인식한다는 것을 의미합니다. 즉, 스레드 1은 2가 저장 위치 x에 1 이후에 기록되었음을 알고 있기 때문에 위의 경우 (2, 1)을 읽는 것이 불가능하다는 것을 의미합니다. 순서 공리에 따르면 다른 모든 스레드는 x에 대한 동일한 쓰기 순서를 인식해야 합니다. 일관성 공리에 대한 인식은 직관적으로 이해되며 기본적으로 모든 쓰기가 결국 완료된다는 것을 의미하며 이는 단일 프로세서 시스템에 해당됩니다. 둘째, 모든 프로세서는 단일 메모리 위치에 대해 동일한 뷰를 봅니다. 해당 값이 0에서 1, 2로 변경되면 모든 프로세서는 동일한 변경 순서(0-1-2)를 보게 되며 어떤 프로세서도 다른 순서로 업데이트를 볼 수 없습니다. 이는 또한 메모리 시스템이 내부적으로 어떻게 구현되는지에 관계없이 외부적으로 모든 메모리 위치가 전역적으로 액세스 가능한 단일 위치로 처리된다는 것을 의미합니다.

메모리 일관성

일관성은 동일한 메모리 위치에 대한 액세스를 의미하며, 다른 저장 위치에 액세스하는 방법은 무엇입니까? 이는 일련의 예를 통해 설명될 수 있습니다.

cpp
// Thread 1:
x = 1;
y = 1;
    
// Thread 2:
t1 = y;
t2 = x;

이제 직관적인 관점에서 t1과 t2의 허용된 값을 보면 스레드 1이 스레드 1보다 먼저 예약될 때 발생할 수 있는 (t1, t2) = (0, 0)을 항상 얻을 수 있습니다. 또한 스레드 1이 완료된 후 스레드 2가 예약될 때 발생하는 (t1, t2) = (1, 1)을 얻을 수도 있습니다. 마찬가지로 (t1, t2) = (0, 1)을 읽을 수 있습니다. 아래 이미지는 세 가지 결과를 모두 얻는 방법을 보여줍니다.

가능한 모든 결과를 보여줍니다.

흥미로운 질문은 (t1, t2) = (1,0)이 허용되는지 여부입니다. 이는 메모리 시스템에 의해 x에 대한 쓰기가 어떤 방식으로든 지연되는 반면 y에 대한 쓰기는 빠르게 완료될 때 발생합니다. 이 경우 t1은 업데이트된 y 값을 가져오고 t2는 x의 이전 값을 가져옵니다. 이런 행동이 허용되나요? 이런 행위가 허용된다면 소프트웨어와 병렬 알고리즘의 정확성을 추론하기 어렵고 프로그래밍도 어려워질 것은 자명하다. 그러나 이 동작이 허용되면 소프트웨어에 대해 강력한 보증을 제공할 필요가 없으므로 하드웨어 설계가 더 단순해집니다.

정답도 없고 틀린 답도 없는 것 같죠? 그것은 모두 우리가 소프트웨어를 프로그래밍하는 방법과 하드웨어 디자이너가 소프트웨어 작성자를 위해 무엇을 만들고 싶은지에 달려 있습니다. 그러나 이 예에는 (t1, t2) = (1,0)이라는 특별한 경우라는 매우 심오한 내용이 있습니다. 그 이유를 알아보기 위해 위의 다이어그램을 다시 살펴보면 두 스레드의 명령어 사이에 인터리빙을 생성하여 세 가지 결과에 대해 추론할 수 있었습니다. 이러한 인터리빙에서 동일한 스레드의 명령어 순서는 프로그램 순서라고 하는 프로그램에 지정된 순서와 동일합니다.

각 구성 스레드의 제어 흐름 의미와 일치하는 명령(여러 스레드에 속할 수 있음)의 순서를 프로그램 순서라고 합니다. 스레드의 제어 흐름 의미 체계는 주어진 명령 다음에 어떤 명령을 실행할 수 있는지 결정하는 규칙 집합으로 정의됩니다. 예를 들어, 단일 사이클 프로세서에 의해 실행되는 명령어 세트는 항상 프로그램 순서대로 실행됩니다.

프로그램 순서대로 스레드를 인터리브하여 결과 (t1, t2) = (1,0)을 생성할 수 없다는 것은 명백합니다.

가능한 출력 집합에서 출력(1,0)을 제외할 수 있다면 좋을 것입니다. 이를 통해 병렬 소프트웨어를 작성하여 가능한 결과를 쉽게 예측할 수 있습니다. 병렬 프로그램의 가능한 결과 집합을 결정하는 메모리 시스템 모델을 메모리 모델이라고 합니다.

병렬 프로그램의 가능한 결과 집합을 결정하는 메모리 시스템 모델을 메모리 모델이라고 합니다.

순차적 일관성

다양한 유형의 프로세서에 해당하는 다양한 유형의 메모리 모델을 가질 수 있습니다. 가장 중요한 메모리 모델 중 하나는 순차 일관성(SC)입니다. 순차 일관성이란 프로그램 순서에 따라 스레드를 인터리빙하여 해당 결과만 생성되도록 허용하는 것을 의미합니다. 즉, 프로그램 순서를 위반하지 않고 스레드 1과 스레드 2를 가능한 모든 방법으로 인터리빙하여 생성되므로 위 그림에 표시된 모든 결과가 허용된다는 의미입니다. 그러나 결과 (t1, t2) = (1,0)은 프로그램 순서를 위반하므로 허용되지 않으며 순차적으로 일관된 메모리 모델에서는 허용되지 않습니다. 프로그램 순서에 따라 여러 스레드를 인터리브하면 프로세서가 한 주기에 한 스레드의 명령을 실행하고 다음 주기에 다른 스레드의 명령을 실행할 수도 있다는 뜻입니다. 따라서 여러 스레드를 처리하는 단일 프로세서는 SC 실행을 생성합니다. 실제로 모델 이름을 살펴보면 "순차적"이라는 단어는 실행이 단일 프로세서가 모든 스레드의 명령을 어떤 순서로 순차적으로 실행하는 것과 동일하다는 개념에서 유래합니다.

병렬 스레드 세트의 결과가 모든 스레드의 명령을 어떤 순서로 실행하는 단일 프로세서의 결과와 동일하면 메모리 모델은 순차적으로 일관성이 있습니다. 대안으로, 시퀀스 일관성은 가능한 결과 세트가 프로그램 순서에 따라 스레드 세트를 인터리브하여 생성될 수 있는 메모리 모델로 정의될 수 있습니다.

시퀀스 일관성은 컴퓨터 아키텍처 및 분산 시스템 분야에서 널리 연구되어 온 매우 중요한 개념입니다. 병렬 시스템에서의 실행을 순차 시스템에서의 실행과 동일시함으로써 병렬 시스템을 프로세서가 하나인 직렬 시스템으로 축소합니다. 한 가지 주목해야 할 점은 SC가 일련의 병렬 프로그램의 실행 결과가 항상 동일하다는 것을 의미하지 않는다는 것입니다. 스레드가 인터리브되는 방식과 스레드가 도착하는 시기에 따라 다르지만 일부 결과는 허용되지 않습니다.

약한 일관성

SC를 구현하려면 비용이 많이 들기 때문에 소프트웨어는 단순해지지만 하드웨어는 매우 느려집니다. SC를 지원하려면 일반적으로 다음 읽기 또는 쓰기를 메모리 시스템에 보내기 전에 읽기 또는 쓰기가 완료될 때까지 기다려야 합니다. 쓰기 요청 W는 모든 프로세서의 모든 후속 읽기가 W가 쓴 값 또는 나중에 동일한 위치에 쓴 값을 얻을 때 완료됩니다. 데이터를 읽은 후 읽기 요청이 완료되고 원래 데이터를 쓴 쓰기 요청이 완료됩니다.

이러한 요구 사항/제한 사항은 고성능 시스템에서 병목 현상을 일으키므로 컴퓨터 아키텍처 커뮤니티는 SC를 위반하는 약한 메모리 모델로 전환했습니다. 약한 메모리 모델에서는 다음 다중 스레드 코드 조각에서 결과 (t1, t2) = (1,0)이 허용됩니다.

cpp
// Thread 1:
x = 1
y = 1

// Thread 2:
t1 = y
t2 = x

약한 일관성(WC) 메모리 모델은 SC와 호환되지 않으며 일반적으로 임의의 메모리 순서를 허용합니다.

약한 메모리 모델에는 다양한 유형이 있으며, 일반적인 변형은 약한 일관성(WC)입니다. 이제 스레드 1이 코어 1에서 실행되고 스레드 2가 코어 2에서 실행된다고 가정하고 WC가 (1,0) 결과를 허용하는 이유를 알아보세요. 또한 x에 해당하는 메모리 위치가 코어 2 근처에 있고 y에 해당하는 메모리 위치가 코어 1 근처에 있다고 가정합니다. 또한 코어 1 주변에서 코어 2로 요청을 보내는 데 수십 사이클이 걸리고 대기 시간이 가변적이라고 가정합니다.

먼저 핵심 1 파이프라인의 동작을 연구합니다. 코어 1 파이프라인 관점에서 메모리 쓰기 요청이 메모리 시스템으로 전달되면 메모리 쓰기 명령이 완료된 것으로 간주되고 명령이 RW 단계로 들어갑니다. 따라서 이 경우 프로세서는 -n번째 사이클에서 x에 대한 쓰기를 메모리 시스템으로 전달한 다음 (n+1)번째 사이클에서 y에 쓰기를 전달합니다. y에 대한 쓰기는 y의 메모리 위치에 빠르게 도달하는 반면, x에 대한 쓰기는 오랜 시간이 걸립니다.

동시에 코어 2는 y 값을 읽으려고 시도합니다. (y에 대한) 쓰기 요청이 y에 도착한 후 읽기 요청이 y의 메모리 위치에 도착한다고 가정하면, y의 새로운 값은 1과 동일하게 획득됩니다. 이어서 코어 2는 x에 대한 읽기 작업을 발행하고 x에 대한 읽기 작업은 x에 대한 쓰기 작업이 x에 도달하기 전에 x의 메모리 위치에 도달할 수 있습니다. 이 경우 x의 이전 값인 0을 얻습니다. 따라서 약한 메모리 모델에서는 결과 (1,0)가 가능합니다.

이를 방지하려면 y에 쓰기 요청을 보내기 전에 x에 대한 쓰기가 완전히 완료될 때까지 기다릴 수 있습니다. 이는 정확하지만 일반적으로 공유 메모리 위치에 쓸 때 다른 스레드는 정확히 동일한 시점에 이를 읽지 않습니다. 메모리 액세스 패턴이 프로세서 간에 공유되지 않기 때문에 런타임 시 이 두 가지 경우를 구별할 수 없습니다. 성능을 향상시키려면 이전 메모리 요청이 완료될 때까지 각 메모리 요청을 지연하는 것은 가치가 없습니다. 따라서 고성능 구현에서는 동일한 스레드의 메모리 액세스가 메모리 시스템에 의해 재정렬되도록 허용하는 메모리 모델을 선호합니다. 다음 하위 섹션에서는 (1,0) 결과를 방지하는 방법을 살펴보겠습니다.

대부분의 프로세서는 메모리 요청이 파이프라인을 떠난 후 특정 시점에 즉시 완료된다고 가정하고, 더 나아가 모든 스레드는 메모리 요청이 정확히 동일한 시점에 즉시 완료된다고 가정합니다. 메모리 요청의 이러한 속성을 원자성이라고 합니다. 둘째, 메모리 요청은 프로그램 순서와 다른 순서로 완료될 수 있다는 점에 유의하는 것이 중요합니다. 완료 순서가 각 스레드의 프로그램 순서와 동일한 경우 메모리 모델은 SC를 따르고, 완료 순서가 프로그램 순서와 다르면 메모리 모델은 WC의 변형입니다.

메모리 요청이 발행된 후 특정 시점에 모든 스레드에 의해 즉각적인 것으로 인식되는 경우 이를 원자성 또는 원자성 관찰이라고 합니다.

정확하게 말하면 각 메모리 요청에는 시작, 종료, 완료라는 세 가지 관심 이벤트가 있습니다. 쓰기 요청을 고려해 보겠습니다. 명령이 MA 단계의 L1 캐시에 요청을 보낼 때 요청이 시작됩니다. 명령이 RW 단계로 이동하면 요청이 완료됩니다. 최신 프로세서에서는 메모리 요청이 완료될 때 쓰기가 대상 메모리 위치에 도달한다는 보장이 없습니다. 쓰기 요청이 메모리 위치에 도달하고 쓰기가 모든 프로세서에 표시되는 시점을 완료 시간이라고 합니다. 단순 프로세서에서 요청을 완료하는 데 걸리는 시간은 시작 시간과 종료 시간 사이입니다. 그러나 고성능 프로세서에서는 그렇지 않습니다. 이 개념은 아래 이미지에 설명되어 있습니다.

읽기 요청은 어떻습니까? 대부분의 사람들은 읽기 완료 시간이 메모리 위치 값을 반환해야 하기 때문에 시작 시간과 종료 시간 사이에 있다고 순진하게 가정합니다. 그러나 읽기가 아직 완료되지 않은 쓰기 값을 반환할 수 있으므로 이는 완전히 정확하지는 않습니다. 쓰기 원자성(쓰기가 즉각적으로 발생한다는 착각)이 필요한 메모리 모델에서는 해당 쓰기 요청이 완료될 때만 읽기가 완료됩니다. 쓰기 원자성을 가정하는 모든 메모리 일관성 모델은 메모리 액세스가 완료되는 순서의 속성을 사용하여 정의됩니다.

약한 메모리 모델에서는 동일한 스레드에서 독립적인 메모리 작업 간의 순서가 따르지 않습니다. 예를 들어, x에 쓴 다음 y에 쓰면 스레드 2는 그 순서가 반대라는 것을 확인합니다. 단, 동일한 스레드에 속한 슬레이브 메모리 명령어의 연산 순서는 항상 따른다. 예를 들어, 변수 x의 값을 1로 설정한 다음 동일한 스레드에서 이를 읽으면 1 이상의 x 값을 쓰게 되고 다른 모든 스레드는 동일한 순서로 메모리 요청을 인식하게 됩니다. 동일한 스레드에 의한 슬레이브 메모리 액세스 간에는 메모리 순서 충돌이 전혀 없습니다(아래 그림 참조).

멀티 스레드 프로그램에서 메모리 요청의 실제 완료 시간입니다.

이제 순서 규칙을 따르지 않는 약한 메모리 모델을 사용할 때의 어려움을 설명합니다. 순차적으로 일관된 시스템을 가정하고 병렬 추가 프로그램을 작성해 보겠습니다. OpenMP는 메모리 모델이 약한 시스템에서 프로그램이 올바르게 실행되도록 보장하기 위해 뒤에서 많은 작업을 수행하므로 OpenMP는 사용되지 않습니다. 코드 블록을 병렬로 실행하는 병렬 구성과 스레드의 식별자를 반환하는 getThreadId() 함수를 정의해 보겠습니다. 스레드 ID의 범위는 0에서 N-1까지입니다. 병렬 추가 기능에 대한 코드는 다음과 같습니다. 병렬 부분이 시작되기 전에 모든 배열이 0으로 초기화된다고 가정합니다. 병렬 부분에서 각 스레드는 부분 번호를 추가하고 결과를 부분 합계 배열의 해당 항목에 씁니다. 완료되면 완료 배열의 항목을 1로 설정합니다.

cpp
/* variable declaration */
int partialSums[N];
int finished[N];
int numbers[SIZE];
int result = 0;
int doneInit = 0;

/* initialise all the elements in partialSums and finished to 0 */
(...)
doneInit = 1;

/* parallel section */
parallel 
{
    /* wait till initialisation */
    while (!doneInit()){};
    
    /* compute the partial sum */
    int myId = getThreadId();
    int startIdx = myId * SIZE/N;
    int endIdx = startIdx + SIZE/N;
    for(int jdx = startIdx; jdx < endIdx; jdx++)
        partialSums[myId] += numbers[jdx];
    
    /* set an entry in the finished array */
    finished[myId] = 1;
}

/* wait till all the threads are done */
do 
{
    flag = 1;
    for (int i=0; i < N; i++)
    {
        if(finished[i] == 0)
        {
            flag = 0;
            break;
        }
    }
} while (flag == 0);

/* compute the final result */
for(int idx=0; idx < N; idx++)
    result += partialSums[idx];

이제 결과를 집계해야 하는 스레드의 경우 모든 스레드가 부분 합계 계산을 마칠 때까지 기다려야 합니다. 완성된 배열의 모든 항목이 1이 될 때까지 기다리면서 이를 수행합니다. 완성된 배열의 모든 항목이 1과 같다고 판단되면 모든 부분합을 추가하여 최종 결과를 얻습니다. 순차적으로 일관된 시스템이 가정되면 이 코드가 올바르게 실행된다는 것을 쉽게 확인할 수 있습니다. 그녀가 주목해야 할 것은 배열의 모든 항목을 읽어 1이 완료된 경우에만 결과가 계산된다는 것입니다. 부분합이 계산되어 부분합계 배열에 기록되면 완성 배열의 항목은 1과 같습니다. 최종 결과를 계산하기 위해 부분합계 배열의 요소를 추가했기 때문에 올바르게 계산되었다고 결론을 내릴 수 있습니다.

이제 순차 일관성이 있는 위의 예에서 마지막 스레드가 done[i]를 1로 읽을 때 부분 합계[i]에 부분 합계 값이 포함되어 있다고 암시적으로 가정하는 약한 메모리 모델을 생각해 보세요. 그러나 약한 메모리 모델이 가정되는 경우, 메모리 시스템이 완료된[i] 및 부분적 합계[i]에 대한 쓰기 순서를 변경할 수 있으므로 이 가정은 유지되지 않습니다. 따라서 메모리 모델이 약한 시스템에서는 부분 합계 배열에 쓰기 전에 완성된 배열에 쓰기가 발생할 수 있습니다. 이 경우, done[i]가 1이라는 사실이 부분Sums[i]에 업데이트된 값이 포함된다는 것을 보장하지 않습니다. 이러한 구별은 순차 일관성이 프로그래머에게 매우 친숙한 이유입니다.

약한 메모리 모델에서 동일한 스레드에 의해 실행된 메모리 액세스는 항상 해당 스레드에 의해 프로그램 순서대로 간주됩니다. 그러나 다른 스레드에서는 메모리 액세스 순서를 다르게 인식할 수 있습니다.

병렬 추가 예제가 올바르게 실행되는지 확인하는 방법으로 돌아갑니다. 이 곤경에서 벗어나는 유일한 방법은 다른 스레드의 읽기 완료[i]가 1이 되기 전에 부분적 합계[i]에 대한 쓰기가 완료되도록 보장하는 메커니즘을 갖는 것입니다. 우리는 펜스라는 일반 명령어를 사용할 수 있습니다. 펜스 이후에 읽기 또는 쓰기가 시작되기 전에 펜스 이전에 실행된 모든 읽기 및 쓰기가 완료되도록 보장할 수 있습니다. 간단히 말해서, 각 명령 뒤에 울타리를 삽입하여 약한 메모리 모델을 순차적으로 일관된 모델로 변환할 수 있습니다. 그러나 이로 인해 상당한 오버헤드가 발생할 수 있으므로 필요할 경우 최소한의 펜스 명령 수를 도입하는 것이 좋습니다. 다음으로, 펜스 명령을 추가하여 약한 메모리 모델에 대해 일련의 숫자를 병렬로 추가합니다.

cpp
/* variable declaration */
int partialSums[N];
int finished[N];
int numbers[SIZE];
int result = 0;

/* initialise all the elements in partialSums and finished to 0 */
(...)
    
/* fence */
/* 병렬 로써 로드/읽기(Read)초기화(Initialize)의 배열 */
fence();

/* All the data is present in all the arrays at this point */
/* parallel section */
parallel 
{
    /* get the current thread id */
    int myId = getThreadId();
    
    /* compute the partial sum */
    int startIdx = myId * SIZE/N;
    int endIdx = startIdx + SIZE/N;
    for(int jdx = startIdx; jdx < endIdx; jdx++)
        partialSums[myId] += numbers[jdx];
    
    /* fence */
    /* partialSums[i]기록/쓰기(Write)finished[i] */
    fence();
    
    /* set the value of done */
    finished[myId] = 1;
}

/* wait till all the threads are done */
do 
{
    flag = 1;
    for (int i=0; i < N; i++)
    {
        if(finished[i] == 0)
        {
            flag = 0;
            break;
        }
    }
} while (flag == 0) ;

/* sequential section */
for(int idx=0; idx < N; idx++)
    result += partialSums[idx];

위의 코드는 약한 메모리 모델에 대한 코드를 보여줍니다. 코드는 순차적으로 일관된 메모리 모델의 코드와 거의 동일합니다. 유일한 차이점은 울타리 지침 두 개를 추가했다는 것입니다. Fence()라는 함수가 내부적으로 Fence 명령을 호출한다고 가정합니다. 모든 병렬 스레드를 호출하기 전에 초기화된 데이터 구조에 대한 모든 쓰기가 완료되었는지 확인하기 위해 울타리()가 먼저 호출됩니다. 그런 다음 병렬 스레드가 시작되어 부분 합계 계산 및 쓰기 프로세스를 완료한 다음 차단 작업을 다시 호출하여 모든 부분 합계가 계산되어 완료[myId]가 1로 설정되기 전에 메모리의 해당 위치에 기록되었는지 확인합니다. 둘째, 마지막 스레드가 종료[i]를 1로 읽으면 부분 합계[i] 값이 최신이고 올바른지 확인할 수 있습니다. 따라서 약한 메모리 모델에도 불구하고 프로그램은 여전히 ​​올바르게 실행됩니다.

따라서 프로그래머가 약한 메모리 모델을 인식하고 올바른 위치에 울타리를 삽입하면 약한 메모리 모델은 정확성에 영향을 미치지 않습니다. 그럼에도 불구하고 프로그래머는 약한 메모리 모델을 이해하는 것이 필요합니다. 그렇지 않으면 프로그래머가 기본 메모리 모델을 고려하지 않기 때문에 병렬 프로그램에서 많은 미묘한 오류가 발생합니다. 약한 메모리 모델은 고성능 메모리 시스템을 구축할 수 있기 때문에 현재 대부분의 프로세서에서 사용됩니다. 이에 비해 순차 일관성은 매우 제한적입니다. MIPS R10000을 제외하면 다른 주요 공급업체에서는 순차 일관성이 있는 시스템을 제공하지 않으며 현재의 모든 x86 및 ARM 기반 시스템은 서로 다른 버전의 약한 메모리 모델을 사용합니다.

19.7.4.2 물리적 관점

우리는 다중 프로세서 메모리 시스템의 논리적 관점에서 일관성과 일관성이라는 두 가지 중요한 측면과 두 가지 속성을 모두 고려한 메모리 시스템 구현의 필요성을 연구합니다. 이 섹션에서는 다중 프로세서 메모리 시스템의 설계 공간을 조사하고 설계 대안에 대한 개요를 제공합니다. 다중 프로세서 메모리 시스템용 캐시를 설계하는 데에는 두 가지 접근 방식이 있습니다. 첫 번째 설계는 단일 캐시가 여러 프로세서 간에 공유되는 공유 캐시라고 합니다. 두 번째 디자인은 일반적으로 각 프로세서 또는 프로세서 그룹마다 하나씩 있는 전용 캐시 세트를 사용합니다. 모든 개인 캐시는 협력하여 공유 캐시라는 환상을 제공하는데, 이를 캐시 일관성이라고 합니다.

이 섹션에서는 공유 캐시의 설계와 개인 캐시의 설계를 검토하고 메모리 일관성 보장 문제를 소개하며 궁극적으로 주어진 일관성 모델(예: 순차적 일관성 또는 약한 일관성)의 효율적인 구현이 어렵고 고급 컴퓨터 아키텍처 과정의 연구 주제라는 결론을 내릴 것입니다. 본 논문에서는 간단한 해결책을 제안합니다.

다중 프로세서 메모리 시스템: 공유 및 개인 캐시

첫 번째 수준 캐시를 고려하세요. 각 프로세서에는 별도의 명령어 캐시가 제공될 수 있습니다. 여기서 명령어는 일반적으로 프로그램 실행 중에 변경되지 않는 읽기 전용 데이터를 나타냅니다. 공유는 문제가 되지 않으므로 각 프로세서는 작은 전용 명령어 캐시의 이점을 누리며, 주요 문제는 데이터 캐시입니다. 데이터 캐시를 설계하는 방법에는 두 가지가 있습니다. 공유 캐시 또는 개인 캐시를 사용할 수 있습니다. 공유 캐시는 모든 프로세서가 액세스할 수 있는 단일 캐시이고, 개인 캐시는 하나의 프로세서 또는 프로세서 그룹에서만 액세스할 수 있습니다. 아래 그림에 표시된 것처럼 동일한 시스템에는 공유 캐시 계층, 개인 캐시 계층 또는 공유 캐시와 개인 캐시의 조합이 있을 수 있습니다.

공유 및 개인 캐시가 있는 시스템의 예.

이제 공유 캐싱과 개인 캐싱 간의 장단점을 평가해 보세요. 공유 캐시는 모든 프로세서에 액세스할 수 있으며 캐시 메모리 위치에 대한 단일 항목을 포함하고 통신 프로토콜은 일반적인 캐시 액세스와 마찬가지로 간단합니다. 추가적인 복잡성은 주로 다양한 프로세서의 요청을 올바르게 예약해야 한다는 사실 때문입니다. 그러나 단순성을 희생하는 대신 공유 캐시에도 문제가 있습니다. 모든 프로세서의 요청을 처리하려면 공유 캐시에 요청을 동시에 처리할 수 있는 많은 수의 읽기 및 쓰기 포트가 필요합니다. 불행하게도 캐시의 크기는 대략 포트 수의 제곱입니다. 또한 공유 캐시는 현재 실행 중인 모든 스레드의 작업 세트를 수용해야 하므로 공유 캐시가 매우 커지고 느려지는 경향이 있습니다. 물리적 제약으로 인해 공유 캐시를 모든 프로세서 가까이에 배치하는 것은 어렵습니다. 이에 비해 개인 캐시는 일반적으로 요청을 처리하는 코어 수가 적고 읽기/쓰기 포트 수가 적어 훨씬 작습니다. 따라서 연결된 프로세서 가까이에 배치할 수 있습니다. 따라서 개인 캐시는 프로세서에 더 가까이 배치할 수 있고 크기가 훨씬 작기 때문에 훨씬 더 빠릅니다.

공유 캐시 문제를 해결하기 위해 디자이너는 특히 메모리 계층의 더 높은 수준에서 개인 캐시를 사용하는 경우가 많습니다. 개인 캐시는 하나의 프로세서 또는 소규모 프로세서 그룹에서만 액세스할 수 있으며 크기가 작고 빠르며 전력 소비도 거의 없습니다. 개인 캐시의 주요 문제점은 프로그래머에게 두 개의 프로세서가 있는 시스템 및 각 프로세서와 관련된 전용 데이터 캐시와 같은 공유 캐시의 환상을 제공해야 한다는 것입니다. 한 프로세서가 메모리 주소 x에 쓰면 다른 프로세서는 쓰기에 대해 알아야 합니다. 그러나 개인 캐시에만 액세스하는 경우 쓰기 주소 x를 결코 알 수 없습니다. 즉, 쓰기 주소 x가 손실되어 시스템이 일관성이 없다는 의미입니다. 따라서 모든 프로세서의 개인 캐시를 통합된 공유 캐시처럼 보이고 일관성 규칙을 따르도록 바인딩해야 합니다. 캐싱과 관련된 일관성을 종종 캐시 일관성이라고 합니다. 캐시 일관성을 유지하는 것은 개인 캐시의 또 다른 복잡성 원인이며 확장성을 제한합니다. 소규모 개인 캐시에는 잘 작동하지만 대규모 개인 캐시의 경우 일관성을 유지하는 오버헤드가 엄청나게 커집니다. 대규모 저수준 캐시의 경우 공유 캐시가 더 적합합니다. 둘째, 일반적으로 여러 개인 캐시에 일부 데이터 복사가 있어 공간이 낭비됩니다.

개인 캐시 컨텍스트 집합 내의 일관성을 캐시 일관성이라고 합니다.

캐시 일관성 프로토콜을 구현함으로써 분리된 개인 캐시 세트를 소프트웨어 공유 캐시로 변환할 수 있습니다. 다음 표에서는 공유 캐시와 개인 캐시 간의 주요 장단점을 간략하게 설명합니다.

속성 개인 캐시 공유 캐시

지역 낮음 높은

속도 빠르게 천천히

프로세서에 가깝다 가까운 멀리

차원 확장성 낮음 높은

데이터 복제 예 아니요

복잡성 높음(캐시 일관성 필요) 낮음

L1 캐시는 대기 시간이 짧고 처리량이 높기 때문에 개인 캐시가 바람직하다는 것이 표에서 분명하게 드러납니다. 그러나 낮은 수준에서는 더 큰 크기가 필요하고 훨씬 적은 수의 요청을 처리하므로 공유 캐시를 포함해야 합니다. 다음으로 일관된 개인 캐시와 대규모 공유 캐시의 설계에 대해 설명합니다. 단순화를 위해 계층적 개인 캐시가 아닌 단일 계층 개인 캐시만 고려되므로 복잡성이 더해집니다. 공유 캐시 디자인은 더 간단하기 때문에 먼저 논의됩니다.

19.7.4.3 공유 캐시

공유 캐시의 가장 간단한 구현 사례에서는 단일 프로세서에서 일반 캐시로 구현될 수 있지만 실제로는 단일 프로세서에서는 하나의 스레드만 캐시에 액세스하기 때문에 매우 나쁜 접근 방식으로 판명됩니다. 그러나 다중 프로세서에서는 여러 스레드가 캐시에 액세스할 수 있으므로 더 많은 대역폭을 제공해야 합니다. 모든 스레드가 동일한 데이터 및 태그 어레이에 액세스해야 하는 경우 요청을 일시 중지하거나 어레이의 포트 수를 늘려야 하므로 면적과 전력에 매우 부정적인 결과를 초래합니다. 마지막으로 무어의 법칙에 따르면 캐시 크기(특히 L2 및 L3)는 대략 두 배로 늘어났으며 오늘날 온칩 캐시 크기는 4~16MB 이상에 달할 수 있습니다. 전체 캐시에 단일 태그 배열을 사용하면 크기가 매우 크고 느려집니다. LLC(최종 레벨 캐시)라는 용어는 메모리 계층 구조에서 가장 낮은 온칩 캐시(메인 메모리가 가장 낮음)로 정의됩니다. 예를 들어, 멀티 코어 프로세서에 주 메모리에 연결된 온칩 L3 캐시가 있는 경우 LLC는 L3 캐시 메모리입니다. 아래에서는 LLC라는 용어가 자주 사용됩니다.

여러 스레드를 동시에 지원할 수 있는 멀티 메가바이트 LLC를 만들려면 여러 하위 캐시로 분할해야 합니다. 4MB LLC를 가정하면 일반적인 설계에서는 각각 256~512KB의 작은 하위 캐시 8~16개로 분할되며 이는 허용 가능한 크기입니다. 각 하위 캐시는 캐시 뱅크라고 불리는 캐시 그 자체입니다. 따라서 실제로 대규모 캐시는 캐시 뱅크 세트로 분할되어 직접 매핑되거나 연관되도록 설정할 수 있습니다. 다중 저장소 캐시에 액세스하는 것은 2단계 프로세스입니다. 먼저 저장소 주소를 계산한 다음 저장소에서 일반 캐시 액세스를 수행합니다. 예를 들어 설명하기 위해 16개 뱅크, 4MB 캐시를 고려하면 각 뱅크에는 256KB의 데이터가 포함되어 있으며 4MB = 222바이트이며 비트 19-22는 메모리 주소를 선택하는 데 전용으로 사용될 수 있습니다. 이 경우 라이브러리 선택은 연관성과 아무런 관련이 없습니다. 라이브러리를 선택한 후 나머지 28비트는 블록 내 오프셋, 컬렉션 인덱스 및 레이블 간에 분할될 수 있습니다.

캐시를 여러 라이브러리로 분할하면 두 가지 장점이 있습니다. 첫째, 라이브러리당 경합량이 줄어듭니다. 4개의 스레드와 16개의 라이브러리가 있는 경우 2개의 스레드가 동일한 라이브러리에 액세스할 확률은 낮습니다. 둘째, 각 라이브러리는 더 작은 캐시이므로 전력 효율성이 높고 속도가 더 빠릅니다. 따라서 우리는 멀티스레딩 지원과 빠른 캐시 설계라는 두 가지 목표를 달성합니다.

19.7.4.4 개인 캐시

우리의 의도는 일련의 개인 캐시가 하나의 대규모 공유 캐시처럼 작동하도록 만드는 것이며 소프트웨어 관점에서 캐시가 개인인지 공유인지 알 수 없어야 합니다. 프로세서 그룹과 관련 캐시를 보여주는 시스템의 개념 다이어그램이 아래에 나와 있습니다. 이 캐시 그룹은 캐시 그룹을 형성하며 전체 캐시 그룹은 캐시로 표시되어야 합니다.

많은 프로세서와 개인 캐시가 있는 시스템. 왼쪽은 소프트웨어 관점이고, 오른쪽은 하드웨어 관점입니다.

이러한 캐시는 단순한 공유 버스 유형 토폴로지에서 보다 복잡한 토폴로지에 이르기까지 다양한 내부 네트워크를 통해 연결됩니다. 모든 캐시는 공유 버스에 연결되어 있어 언제든지 단일 작성자와 여러 리더를 사용할 수 있다고 가정합니다. 하나의 캐시가 버스에 메시지를 쓰면 다른 모든 캐시가 해당 메시지를 읽을 수 있습니다. 토폴로지는 아래 그림과 같습니다. 버스는 메시지가 기록되는 특정 시점에 하나의 캐시에만 독점적인 액세스를 제공하므로 모든 캐시는 동일한 메시지 순서를 인식합니다. 공유 버스에 연결된 캐시와의 캐시 일관성을 달성하기 위한 프로토콜을 스누피 프로토콜이라고 합니다.

ng)

공유 버스에 연결된 캐시.

이제 두 가지 일관성 원칙의 관점에서 스누피 프로토콜의 작동을 살펴보겠습니다. 쓰기는 항상 완료되며(완료 원칙), 모든 프로세서는 동일한 블록에 대한 쓰기를 동일한 순서로 확인합니다(시퀀스 원칙). 캐시가 블록에 쓰기 작업을 수행하려면 결국 쓰기가 다른 모든 캐시에 표시되어야 합니다. 쓰기 요청은 손실되지 않으므로 완료 공리를 충족하려면 이 작업을 수행해야 합니다. 둘째, 동일한 블록에 대한 서로 다른 쓰기는 해당 블록을 포함할 수 있는 모든 캐시에 동일한 순서로 도착해야 합니다(순서 공리). 이는 특정 블록에 대해 모든 캐시가 동일한 업데이트 순서를 인식하도록 보장하기 위한 것입니다. 공유 버스는 자동으로 시퀀스 공리의 요구 사항을 충족합니다.

두 가지 청취 프로토콜의 설계는 다음과 같습니다: 쓰기-업데이트 및 쓰기-무효화.

업데이트 계약 쓰기

이제 개인 캐시가 쓰기 요청의 복사본을 보유하고 쓰기 요청을 모든 캐시에 브로드캐스트한다고 가정하여 프로토콜을 설계해 보겠습니다. 이 정책은 쓰기가 손실되지 않고 모든 캐시가 동일한 블록에 대한 쓰기 메시지를 동일한 순서로 인식하도록 보장합니다. 이 전략은 쓰기가 이루어질 때마다 브로드캐스팅을 요구하며 이는 상당한 오버헤드이지만 이 전략은 여전히 ​​유효합니다.

이제 읽기가 프로토콜에 통합되었습니다. x 위치에 대한 읽기는 먼저 개인 캐시를 확인하여 해당 캐시의 복사본이 이미 사용 가능한지 확인할 수 있습니다. 유효한 복사본이 있으면 해당 값을 요청 처리기로 전달할 수 있습니다. 그러나 캐시 미스가 있는 경우 캐시 그룹 내 다른 자매 캐시와 함께 존재하거나 하위 수준에서 가져와야 할 수도 있습니다. 먼저 자매 캐시에 값이 있는지 확인하고 여기에서도 동일한 프로세스를 따르고 캐시는 모든 캐시에 읽기 요청을 브로드캐스트합니다. 캐시 중 하나에 값이 있으면 응답하여 요청 캐시에 값을 보냅니다. 요청 캐시는 값을 삽입하고 이를 프로세서에 전달합니다. 그러나 다른 캐시로부터 응답을 받지 못하면 더 낮은 수준으로 읽기를 시작합니다.

이 프로토콜을 쓰기-업데이트 프로토콜이라고 합니다. 각 캐시 블록은 M, S, I의 세 가지 상태를 유지해야 합니다. M은 캐시가 블록을 수정했음을 나타내는 수정된 상태를 나타내고, S(공유)는 캐시가 블록을 수정하지 않았음을 나타내고, I(잘못됨)는 블록에 유효한 데이터가 포함되어 있지 않음을 나타냅니다.

아래 그림은 각 캐시 블록에 대한 FSM(Finite State Machine)을 보여줍니다. FSM은 캐시 컨트롤러에 의해 실행됩니다. 상태 전환은 이벤트/작업 형식입니다. 캐시 컨트롤러에 이벤트가 전송되면 상태 전환을 포함한 적절한 조치를 취합니다. 어떤 경우에는 작업 필드가 비어 있으므로 이러한 경우에는 아무런 작업도 수행되지 않습니다. 캐시 블록의 상태는 태그 배열 항목의 일부이며, 블록이 캐시에 없으면 상태가 유효하지 않은 것으로 간주됩니다(I). 다음 그림은 프로세서에 의해 생성된 이벤트의 변환을 보여주고 버스를 통해 전송된 이벤트에 대한 캐시 그룹의 다른 캐시의 작동을 보여주지 않는다는 점을 언급할 가치가 있습니다.

업데이트 프로토콜에 상태 전이 다이어그램을 작성합니다.

모든 블록은 처음에는 I 상태입니다. 읽기 미스가 있는 경우 S 상태로 이동하고 캐시 그룹의 모든 캐시에 읽기 미스를 브로드캐스팅하고 자매 캐시 또는 하위 수준에서 값을 가져와야 합니다. 자매 캐시가 하위 레벨에 다시 쓰지 않고 블록을 수정했을 수 있으므로 자매 캐시의 우선순위를 먼저 지정합니다. 마찬가지로 I 상태에 쓰기 누락이 있는 경우 다른 자매 캐시(사용 가능한 경우)에서 블록을 읽어 M 상태로 이동해야 합니다. 다른 자매 캐시에 해당 블록이 없으면 메모리 계층의 하위 수준에서 해당 블록을 읽어야 합니다.

S 상태에서 읽기 적중이 발생하면 데이터가 프로세서에 원활하게 전달될 수 있습니다. 그러나 상태 S의 블록에 쓰려면 업데이트된 값을 얻을 수 있도록 다른 모든 캐시에 쓰기를 브로드캐스트해야 합니다. 캐시가 버스에서 쓰기 요청의 복사본을 얻은 후에는 해당 값을 블록에 쓰고 상태를 M으로 변경할 수 있습니다. S 상태의 블록을 제거하려면 간단히 캐시에서 제거하면 됩니다. 블록이 아직 수정되지 않았으므로 이 시점에서 해당 값을 다시 쓸 필요가 없습니다.

이제 M 상태를 고려해보세요. M 상태의 블록을 읽어야 하는 경우 캐시에서 해당 블록을 읽고 그 값을 프로세서로 보낼 수 있습니다. 메시지를 보낼 필요는 없지만, 쓰고 싶다면 버스에서 쓰기 요청을 보내야 합니다. 캐시가 공유 버스에 도착하는 자체 쓰기 요청을 확인하면 해당 값을 개인 캐시의 메모리 위치에 쓸 수 있습니다. M 상태의 블록을 회수하려면 해당 블록이 수정되었기 때문에 메모리 계층의 하위 레벨에 다시 작성해야 합니다.

각 버스에는 중재자(arbiter)라는 전용 구조가 있는데, 이 구조는 서로 다른 캐시로부터 버스 사용 요청을 수신하고 버스를 FIFO 순서로 캐시에 할당합니다. 버스 중재기의 개략도는 아래 그림에 나와 있습니다. 이는 버스에서 전송되는 요청 대기열을 포함하는 매우 간단한 구조입니다. 각 사이클마다 큐에서 요청을 가져오고 버스에서 메시지를 전송할 수 있는 해당 캐시 권한을 부여합니다.

버스 중재자 구조.

이제 자매 캐시를 고려해 보세요. 버스로부터 Miss 메시지를 받을 때마다 캐시를 ​​확인하여 블록이 사용 가능한지 확인합니다. 캐시 적중이 있는 경우 블록을 버스로 보내거나 요청 캐시로 직접 보냅니다. 다른 캐시로부터 쓰기 알림을 받으면 캐시에 있는 블록의 내용을 업데이트합니다.

디렉터리 프로토콜

청취 프로토콜에서는 항상 쓰기, 읽기 실패 또는 쓰기 실패를 브로드캐스트하며 실제로는 블록 복사본이 포함된 캐시에만 메시지를 보내면 됩니다. 디렉토리 프로토콜은 각 블록 주소에 대한 공유자 목록을 유지하는 디렉토리라는 특수 구조를 사용하여 이 정보를 유지합니다. 공유자는 블록을 포함할 수 있는 캐시의 ID이며, 공유자 목록은 일반적으로 주어진 블록을 포함할 수 있는 캐시의 상위 집합입니다. 공유자 목록을 비트 벡터(공유자당 1비트)로 유지할 수 있습니다. 비트가 1이면 캐시에 복사본이 포함되고 그렇지 않으면 그렇지 않습니다.

디렉터리가 있는 쓰기 업데이트 프로토콜은 다음과 같이 수정됩니다. 버스에서 데이터를 브로드캐스트하는 대신 캐시는 모든 메시지를 디렉토리로 보냅니다. 읽기 또는 쓰기 실패의 경우 디렉터리는 자매 캐시(사본이 있는 경우)에서 블록을 가져온 다음 해당 블록을 요청 캐시로 전달합니다. 마찬가지로 쓰기의 경우 디렉터리는 블록 복사본이 있을 수 있는 캐시에만 쓰기 메시지를 보내고 캐시가 블록을 삽입하거나 회수할 때 공유자 목록을 업데이트해야 합니다. 마지막으로 일관성을 유지하기 위해 디렉터리는 모든 캐시가 동일한 순서로 메시지를 받고 메시지가 손실되지 않도록 해야 합니다. 디렉터리 프로토콜은 전송해야 하는 메시지 수를 최소화하므로 확장성이 더 높습니다.

스누피 프로토콜: 스누피 프로토콜에서는 모든 캐시가 공유 버스에 연결됩니다. 캐시는 각 메시지를 다른 캐시로 브로드캐스트합니다.

디렉터리 프로토콜: 디렉토리 프로토콜에서는 디렉토리라는 특수 구조를 추가하여 메시지 수를 줄입니다. 이 디렉터리는 블록 복사본을 포함할 수 있는 캐시 목록을 유지 관리하고 지정된 블록 주소에 대한 메시지만 목록에 있는 캐시로 보냅니다.

쓰기를 수행하기 위해 버스에서 브로드캐스트를 기다려야 하는 이유는 무엇입니까?

답변: 그렇지 않고 프로세서 1이 x에 1을 쓰고 프로세서 2가 x에 2를 쓰려고 한다고 가정해 보겠습니다. 그런 다음 먼저 x의 복사본에 각각 1과 2를 쓴 다음 쓰기를 브로드캐스트하므로 두 프로세서 모두 x에 대한 쓰기를 다른 순서로 볼 수 있습니다. 이는 질서의 공리를 위반한다. 그러나 쓰기 요청 복사본이 버스에서 도착할 때까지 기다리면 동일한 순서로 x를 씁니다. 버스는 프로세서 1과 2 사이의 충돌을 효과적으로 해결하고 요청을 명령합니다.

잘못된 프로토콜 쓰기

모든 쓰기에 대해 쓰기 요청을 브로드캐스팅하는 것은 불필요한 오버헤드입니다. 애초에 대부분의 블록이 공유되지 않을 수 있으므로 쓰기마다 추가 메시지를 보낼 필요가 없습니다. 쓰기 무효화 프로토콜을 제시하여 쓰기 업데이트 프로토콜의 메시지 수를 줄여 보겠습니다. 여기서는 수신 프로토콜을 사용하거나 디렉터리 프로토콜을 사용할 수 있습니다. 청취 프로토콜의 예가 아래에 나와 있습니다.

각 블록에 대해 M, S, I의 세 가지 상태를 유지하지만 상태의 의미를 변경합니다.

  • 유효하지 않은 상태(I)는 동일한 의미를 유지합니다. 즉, 해당 항목이 실제로 캐시에 존재하지 않음을 의미합니다.

  • 공유 상태(S)는 캐시가 블록을 읽을 수 있지만 블록을 쓸 수 없음을 의미합니다. 공유 상태에서는 서로 다른 캐시에 동일한 블록의 여러 복사본이 있을 수 있습니다. 공유 상태에서는 블록이 읽기 전용이라고 가정하므로 블록의 복사본이 여러 개 있어도 캐시 일관성에 영향을 주지 않습니다.

  • M(수정) 상태는 캐시가 블록을 쓸 수 있음을 나타냅니다. 블록이 M 상태에 있는 경우 캐시 그룹의 다른 모든 캐시는 해당 블록을 I 상태에 있어야 합니다. 다른 캐시는 S 또는 M 상태의 블록의 유효한 복사본을 가질 수 없습니다. 여기서 쓰기 무효화 프로토콜은 쓰기 업데이트 프로토콜과 다릅니다. 한 번에 하나의 쓰기만 허용하거나 한 번에 여러 읽기를 허용하며 동시에 읽기와 쓰기가 공존하는 것을 허용하지 않습니다. 언제든지 블록에 대한 쓰기 액세스 권한을 갖는 캐시 수를 제한하여 메시지 수를 줄일 수 있습니다.

내부 메커니즘은 다음과 같습니다. 쓰기-업데이트 프로토콜은 읽기 적중 시 메시지를 보낼 필요가 없으므로 쓰기 적중 시 추가 메시지가 전송되는데, 이를 제거하려고 합니다. 여러 캐시가 동시에 블록을 읽거나 쓸 수 있으므로 추가 메시지를 보내야 합니다. 쓰기 무효화 프로토콜은 이 동작을 제거했습니다. 블록이 M 상태에 있으면 다른 캐시에는 해당 블록의 유효한 복사본이 포함되어 있지 않습니다.

다음 그림은 프로세서의 동작으로 인한 상태 전이 다이어그램을 보여줍니다. 상태 전이 다이어그램은 기본적으로 쓰기 업데이트 프로토콜의 상태 전이 다이어그램과 동일합니다. 차이점을 살펴보겠습니다. 첫 번째는 버스에 배치될 세 가지 유형의 메시지, 즉 쓰기, 쓰기 미스, 읽기 미스를 정의하는 것입니다. I 상태에서 S 상태로 전환할 때 버스에 읽기 미스가 발생합니다. 자매 캐시에 응답 데이터가 없으면 캐시 컨트롤러는 하위 레벨에서 블록을 읽습니다. S 상태의 의미는 동일하게 유지됩니다. S 상태의 블록을 쓰려면 버스에 쓰기 메시지를 쓴 후 M 상태로 전환해야 합니다. 이제 블록이 M 상태에 있으면 다른 캐시에 유효한 복사본이 포함되어 있지 않다는 것을 확신할 수 있으며, 버스에 정보를 보낼 필요 없이 M 상태의 블록을 자유롭게 읽고 쓸 수 있습니다. 프로세서가 M 상태의 블록을 제거하기로 결정하면 해당 데이터를 하위 레벨에 기록해야 합니다.

프로세서 동작으로 인한 블록의 상태 전이 다이어그램입니다.

아래 그림은 버스에서 수신된 메시지로 인한 상태 전환을 보여줍니다. S 상태에서 읽기 실패가 발생하면 다른 캐시가 해당 블록에 대한 읽기 액세스를 원한다는 의미입니다. 블록을 포함하는 모든 캐시에는 블록 내용이 전송됩니다. 이 프로세스는 다음과 같이 조정될 수 있습니다. 블록 복사본이 있는 모든 캐시는 버스에 액세스하려고 시도합니다. 버스에 액세스하는 첫 번째 캐시는 블록 복사본을 요청 캐시로 보냅니다. 나머지 캐시는 블록의 내용이 전송되었음을 즉시 알 수 있습니다. 그런 다음 그들은 시도를 중단했습니다. S 상태에서 쓰기 또는 쓰기 실패 메시지를 받으면 블록은 I 상태로 전환됩니다.

이제 M 상태를 고려해 보겠습니다. 다른 캐시가 쓰기 실패 메시지를 보내면 해당 블록이 포함된 캐시의 캐시 컨트롤러는 해당 캐시에 블록 내용을 보내고 I 상태로 전환합니다. 그러나 읽기 미스가 발생하면 S 상태의 블록을 원활하게 회수할 수 있다는 가정 하에 일련의 단계가 필요하므로 S 상태로 이동하기 전에 하위 레벨에 데이터를 쓰는 것이 필요하다. 이어서, 원래 블록이 있던 캐시도 블록의 내용을 요청 캐시로 보내고 블록의 상태를 S 상태로 전환합니다.

버스의 메시지로 인한 블록 상태 전환 다이어그램.

디렉터리의 쓰기 무효화 프로토콜 사용

디렉토리를 사용하여 쓰기 무효화 프로토콜을 구현하는 것은 매우 간단합니다. 상태 전이 다이어그램은 거의 동일하게 유지되며 메시지는 브로드캐스트되지 않고 대신 디렉터리로 전송되어 블록의 공유자에게 메시지를 보냅니다.

이제 블록의 생명주기를 설명해보자. 하위 레벨에서 블록을 가져올 때마다 하위 레벨에서 블록을 가져온 캐시인 공유자가 하나만 있는 디렉토리 항목이 초기화됩니다. 이제 블록에 읽기 누락이 있는 경우 디렉터리는 계속해서 공유 프로그램을 추가합니다. 그러나 쓰기 미스가 있거나 프로세서가 블록을 쓰기로 결정하면 쓰기 또는 쓰기 미스 메시지가 디렉터리로 전송됩니다. 디렉터리는 공유자 목록을 정리하고 쓰기 액세스를 수행하는 프로세서인 하나의 공유자만 유지합니다. 블록이 제거되면 해당 캐시는 공유 프로그램을 제거하는 디렉터리에 이를 알립니다. 공유자 세트가 비어 있으면 디렉토리 항목을 삭제할 수 있습니다.

쓰기 무효화 및 업데이트 프로토콜은 배타적(E) 상태라는 추가 상태를 추가하여 개선할 수 있습니다. E 상태는 메모리 계층의 하위 레벨에서 얻은 각 캐시 블록의 초기 상태일 수 있습니다. 이 상태는 블록이 캐시에만 속한다는 사실을 저장합니다. 그러나 캐시에는 쓰기 액세스가 아닌 읽기 전용 액세스 권한이 있습니다. E에서 M으로 전환하는 경우 블록이 하나의 캐시에만 소유되므로 버스에 쓰기 미스나 쓰기 메시지를 보낼 필요가 없습니다. 필요한 경우 데이터를 E 상태에서 원활하게 제거할 수 있습니다.

19.7.4.5 MESI 프로토콜

SMP를 통해 캐시 일관성을 제공하기 위해 데이터 캐시는 종종 MESI라는 프로토콜을 지원합니다. MESI의 경우 데이터 캐시에는 태그당 2개의 상태 비트가 포함되어 있으므로 각 행은 다음 네 가지 상태 중 하나일 수 있습니다.

  • 수정됨, M: 캐시의 행이 수정되었으며(주 메모리와 달리) 이 캐시에서만 사용할 수 있습니다.

  • 배타적(E): 캐시에 있는 라인은 주 메모리의 라인과 동일하며 다른 캐시에는 존재하지 않습니다.

  • 공유(S): 캐시에 있는 라인이 주 메모리의 라인과 동일하며 다른 캐시에 존재할 수 있습니다.

  • 잘못됨(I): 캐시의 행에 유효한 데이터가 포함되어 있지 않습니다.

다음 표에는 네 가지 상태의 의미가 요약되어 있습니다.

엠 수정됨 E 독점

S 공유됨

나는 유효하지 않음

이 캐시 라인이 유효한가요? 예 예 예 아니요

메모리 카피는... 만료됨 유효한 유효한

  • 복사본이 다른 캐시에 존재합니까? 아니요 아니요 가능 가능

이 줄에 쓰기… 버스에 타지 않음 버스에 타지 않음 버스에 들어가 캐시를 업데이트하세요 버스로 바로 이동

아래 그림은 MESI 프로토콜의 상태 다이어그램을 보여줍니다. 캐시의 각 라인에는 자체 상태 비트가 있으므로 상태 다이어그램에도 자체 구현이 있습니다. 그림 a는 이 캐시에 연결된 프로세서에 의해 시작된 작업으로 인해 발생하는 전환을 보여주고, 그림 b는 공통 버스에서 스누핑된 이벤트로 인해 발생하는 전환을 보여줍니다. 프로세서 시작 및 버스 시작 작업에 대한 별도의 상태 다이어그램은 MESI 프로토콜의 논리를 명확하게 하는 데 도움이 됩니다. 언제든지 캐시 라인은 단일 상태에 있으며 다음 이벤트가 연결된 프로세서에서 발생하는 경우 다이어그램 a로 표시되고 다음 이벤트가 버스에서 발생하는 경우 다이어그램 b로 전환이 표시됩니다.

MESI 상태 전이 다이어그램.

읽기 실패: 로컬 캐시에서 읽기 실패가 발생하면 프로세서는 누락된 주소가 포함된 주 메모리 라인을 읽기 위해 메모리 읽기를 시작합니다. 프로세서는 다른 모든 프로세서/캐시 장치에 스누핑 트랜잭션을 알리기 위해 버스에 신호를 삽입합니다. 가능한 결과는 다양합니다.

  • 다른 캐시에 배타적 상태의 깨끗한(메모리 읽기 이후 수정되지 않은) 행 복사본이 있는 경우 행을 공유한다는 신호를 반환합니다. 그런 다음 응답 프로세서는 복사본 상태를 독점에서 공유로 전환하고, 시작 프로세서는 주 메모리에서 라인을 읽은 다음 캐시의 라인을 유효하지 않음에서 공유로 전환합니다.

  • 하나 이상의 캐시에 공유 상태의 깨끗한 행 복사본이 있는 경우 각 캐시는 행이 공유된다는 신호를 보냅니다. 시작 프로세서는 라인을 읽고 캐시의 라인을 유효하지 않은 라인에서 공유 라인으로 변환합니다.

  • 다른 캐시에 수정된 라인 복사본이 있는 경우 해당 캐시는 메모리 읽기를 차단하고 공유 버스를 통해 요청 캐시에 라인을 제공한 다음 응답 캐시는 해당 라인을 수정에서 공유로 변경합니다. 요청 캐시로 전송된 라인은 블록을 메모리에 저장하는 메모리 컨트롤러에 의해 수신되고 처리됩니다.

  • 다른 캐시에 해당 행의 복사본(삭제 또는 수정)이 없으면 신호가 반환되지 않습니다. 시작 프로세서는 라인을 읽고 캐시의 라인을 유효하지 않은 라인에서 배타적 라인으로 변환합니다.

읽기 적중: 로컬 캐시의 현재 행에서 읽기 적중이 발생하면 프로세서는 필요한 항목만 읽습니다. 상태 변경 없음: 상태가 수정됨, 공유됨 또는 배타적 상태로 유지됩니다.

쓰기 실패: 로컬 캐시에서 쓰기 실패가 발생하면 프로세서는 누락된 주소가 포함된 주 메모리 라인을 읽기 위해 메모리 읽기를 시작합니다. 이를 위해 프로세서는 RWITM(Read-with-intent-to-modify)을 나타내는 신호를 버스에 보냅니다. 행이 로드되자마자 수정된 것으로 표시됩니다. 다른 캐시의 경우 데이터 행을 로드하기 전에 가능한 두 가지 상황이 있습니다.

  • 첫째, 일부 다른 캐시에 행의 수정된 복사본이 있을 수 있습니다(상태 = 수정됨). 이 경우 알람 프로세서는 시작 프로세서에 다른 프로세서에 행의 수정된 복사본이 있다는 신호를 보내고 시작 프로세서는 버스를 포기하고 기다립니다. 다른 프로세서가 버스에 액세스하고, 수정된 캐시 라인을 주 메모리에 다시 쓰고, 캐시 라인의 상태를 유효하지 않은 상태로 전환합니다(시작 프로세서가 이 라인을 수정하기 때문입니다). 이어서 시작 프로세서는 RWITM의 버스에 다시 신호를 보낸 다음 주 메모리에서 라인을 읽고 캐시에서 라인을 수정한 다음 해당 라인을 수정된 것으로 표시합니다.

  • 둘째, 다른 캐시에는 요청된 라인의 수정된 복사본이 없습니다. 이 경우 신호가 반환되지 않으며 시작 프로세서는 계속해서 라인을 읽고 수정합니다. 동시에 하나 이상의 캐시에 공유 상태의 깨끗한 행 복사본이 있는 경우 각 캐시는 해당 행 복사본을 무효화하고, 한 캐시에 배타적 상태의 행 복사본이 있으면 해당 행 복사본이 무효화됩니다.

쓰기 적중: 로컬 캐시의 현재 라인에서 쓰기 적중이 발생하면 그 효과는 로컬 캐시에 있는 라인의 현재 상태에 따라 달라집니다.

  • 공유: 업데이트를 수행하기 전에 프로세서는 행의 독점 소유권을 얻어야 하며 프로세서는 버스에서 신호를 보내고 캐시에 행의 공유 복사본이 있는 각 프로세서는 섹터를 공유에서 유효하지 않은 섹터로 변환한 다음 시작 프로세서가 업데이트를 수행하고 행의 복사본을 공유에서 수정으로 변환합니다.

  • 독점: 프로세서는 이미 행에 대한 독점 제어권을 갖고 있으므로 업데이트를 수행하고 행 복사본을 독점에서 수정으로 변환하기만 하면 됩니다.

  • 수정됨: 프로세서가 이미 행에 대한 독점 제어권을 갖고 있고 해당 행을 수정된 것으로 표시하므로 업데이트만 수행하면 됩니다.

L1-L2 캐시 일관성: 지금까지 우리는 동일한 버스나 다른 SMP 상호 연결 시설에 연결된 캐시 간의 협력 활동 측면에서 캐시 일관성 프로토콜을 설명했습니다. 일반적으로 이러한 캐시는 L2 캐시이며 각 프로세서에는 버스에 직접 연결되지 않아 스누프 프로토콜에 참여할 수 없는 L1 캐시도 있으므로 SMP 구성의 모든 캐시와 레벨 캐시 모두에 대한 데이터 무결성을 유지하려면 일부 체계가 필요합니다.

전략은 L1 캐시의 각 라인에 상태를 나타내는 비트가 포함되도록 MESI 프로토콜(또는 모든 캐시 일관성 프로토콜)을 L1 캐시로 확장하는 것입니다. 목표는 L2 캐시 및 해당 L1 캐시에 있는 모든 라인에 대해 L1 라인 상태가 L2 라인의 상태를 추적해야 한다는 것입니다. 간단한 접근 방식은 L1 캐시에서 연속 쓰기 전략을 사용하는 것입니다. 이 경우 쓰기는 메모리가 아닌 L2 캐시에 이루어집니다. L1 연속 쓰기 정책은 L2 캐시의 L1 라인을 강제로 수정하여 다른 L2 캐시에 표시되도록 합니다. L1 연속 쓰기 전략을 사용하려면 L1 콘텐츠가 L2 콘텐츠의 하위 집합이어야 합니다. 이는 결국 두 번째 수준 캐시의 연관성이 첫 번째 수준 캐시의 연관성과 같거나 커야 함을 의미합니다. L1 연속 쓰기 정책은 IBM S/390 SMP에 사용됩니다.

첫 번째 수준 캐시에 후기입 정책이 있으면 두 캐시 간의 관계가 더 복잡해집니다. 유지 관리 방법에는 여러 가지가 있지만 이 기사의 범위를 벗어납니다.

19.7.4.6 메모리 일관성 모델

일반적인 메모리 일관성 모델은 동일한 스레드에서 실행되는 메모리 작업 간에 허용되는 재정렬 유형을 지정합니다. 예를 들어 순차적 일관성에서는 모든 읽기/쓰기 액세스가 프로그램 순서대로 수행되며 다른 모든 스레드도 프로그램 순서대로 스레드의 메모리 액세스를 인식합니다.

확실한 보장을 받아 일관성 있는 메모리 시스템을 구축해 봅시다. 모든 쓰기 작업이 완료 시간과 연관되어 있고 완료 즉시 실행된다고 가정하면 모든 읽기 작업이 완료되기 전에 쓴 값을 얻는 것은 불가능합니다. 쓰기 작업이 완료된 후 동일한 주소에 대한 모든 읽기는 쓰기 작업이나 업데이트된 쓰기 작업으로 쓰여진 값을 가져옵니다. 일관된 메모리가 가정되므로 동일한 메모리 주소에 대한 모든 쓰기는 모든 프로세서에서 동일한 순서로 표시됩니다. 둘째, 각 읽기 작업은 가장 최근에 완료된 쓰기 작업으로 해당 주소에 쓴 값을 반환합니다. 이제 프로세서 1이 주소 x에 쓰기 요청을 보내고 프로세서 2가 동일한 주소 x에 읽기 요청을 보내는 상황을 고려해 보겠습니다. 이 동작은 정의되지 않았습니다. 읽기는 동시 쓰기 작업으로 설정된 값을 가져오거나 이전 값을 가져올 수 있습니다. 그러나 읽기 작업이 동시 쓰기 작업에 의해 설정된 값을 얻는 경우 모든 프로세서에서 실행되는 모든 후속 읽기는 해당 값 또는 업데이트된 값을 가져와야 합니다. 메모리 위치의 값을 읽고 나면 읽기 작업이 완료되고 해당 데이터를 생성한 쓰기가 완료됩니다.

이제 각 프로세서가 이전에 발행된 모든 메모리 요청을 완료한 후 메모리 요청을 발행하는 다중 프로세서를 설계해 보겠습니다. 즉, 메모리 요청(읽기/쓰기)을 발행한 후 프로세서는 다음 메모리 요청을 발행하기 전에 완료될 때까지 기다립니다. 이 속성을 가진 다중 프로세서는 순차적으로 일관성이 있습니다.

이제 액세스 그래프라는 이론적 도구부터 시작하여 짧은 비공식 증명에 대해 설명합니다.

교통 지도

다음 그림은 두 스레드의 실행 및 이와 관련된 메모리 액세스 순서를 보여줍니다. 각 읽기 또는 쓰기 액세스에 대해 액세스 그래프에 원이나 노드를 만듭니다(그림 (c) 참조). 이 경우 프로그램 순서에 따라 하나의 액세스가 다른 액세스를 따르거나 서로 다른 스레드의 두 액세스 사이에 읽기-쓰기 종속성이 있는 경우 화살표(또는 가장자리)가 두 노드 사이에 추가됩니다. 예를 들어 스레드 1에서 x가 5로 설정되고 스레드 2의 읽기 작업이 이 x 값을 읽는 경우 x 읽기와 쓰기 사이에 상관 관계가 있으므로 액세스 그래프에 화살표가 추가되어 소스 요청 후에 대상 요청이 완료되어야 함을 나타냅니다.

메모리 액세스의 그래픽 표현.

액세스 그래프에 a에서 b로의 경로가 있는 경우 노드 a와 b 사이의 발생 전 관계를 정의합니다.

발생 전 관계는 액세스 그래프에 a에서 b까지의 경로가 있음을 나타냅니다. 이 관계는 a가 완료된 후 b가 실행을 완료해야 함을 의미합니다.

액세스 그래프는 동시 시스템을 추론하기 위한 일반적인 도구입니다. 이는 노드 집합으로 구성되며, 각 노드는 명령어(일반적으로 메모리 명령어)의 동적 인스턴스입니다. 노드 사이에 모서리가 있습니다. A-->B의 에지는 B가 A 후에 실행을 완료해야 함을 의미합니다. 위 그림에서는 두 가지 유형의 에지, 즉 프로그램 순서 에지와 인과성 에지가 추가되었습니다. 프로그램 순서 에지는 동일한 스레드에서 메모리 요청의 완료 순서를 나타냅니다. 명령어가 완료되기를 기다리는 경우 다음 명령어를 실행하기 전에 동일한 스레드의 연속 명령어 사이에 에지가 있습니다.

인과 경계는 스레드 간의 로드 및 저장 명령 사이에 있습니다. 예를 들어, 특정 명령어가 값을 쓰고 다른 명령어가 다른 스레드에서 해당 값을 읽는 경우 저장소에서 로드로 에지를 추가합니다.

순차적 일관성을 입증하려면 아래 설명된 대로 액세스 그래프에 추가 에지를 추가해야 합니다. 먼저 현자(모든 것을 알고 있는 가상의 존재)가 있다고 가정하면 일관된 메모리가 가정되므로 동일한 메모리 위치에 있는 모든 저장소는 순차적으로 정렬됩니다. 또한 동일한 메모리 위치에 대한 로드와 저장 사이에는 순서가 있습니다. 예를 들어, x를 1로 설정한 다음 x를 3으로 설정하고 t1=x, x=5를 읽으면 위치 x에는 저장-저장-로드-저장 순서가 있습니다. Sage는 각 메모리 위치에 대한 로드와 저장 간의 순서를 알고 있으며, Sage가 해당 발생 전 에지를 액세스 그래프에 추가한다고 가정합니다. 이 경우 저장과 로드 사이의 에지는 인과 관계 에지이고, 스토어-스토어 및 로드-스토어 에지는 일관성 에지의 예입니다.

다음으로 액세스 그래프를 사용하여 시스템의 속성을 증명하는 방법을 설명합니다. 첫째, 주어진 프로그램 실행을 기반으로 주어진 메모리 일관성 모델 M에 대해 프로그램의 액세스 그래프를 구성해야 하며, 메모리 액세스 동작을 기반으로 일관성 및 인과성 경계를 추가해야 합니다. 둘째, 일관성 모델을 기반으로 동일한 스레드의 명령어 사이에 프로그램 순서 가장자리가 추가됩니다. SC의 경우 연속 명령어 사이에 에지가 추가되고, WC의 경우 종속 명령어 사이, 일반 명령어 사이, 펜스 사이에 에지가 추가됩니다. 액세스 그래프는 이론적 도구이지만 일반적으로 실용적인 도구는 아니라는 점을 이해하는 것이 중요합니다. 특정 프로그램이나 시스템에 대한 액세스 그래프를 실제로 작성하지 않고 액세스 그래프의 속성에 대해 추론해 보겠습니다.

액세스 그래프에 사이클이 포함되어 있지 않으면 노드를 순차적으로 배열할 수 있습니다. 이제 이 사실을 증명해보자. 액세스 그래프에서 a에서 b로 가는 경로가 있으면 a를 b의 조상이라고 합니다. 순서는 반복 프로세스에 따라 생성될 수 있습니다. 먼저 조상이 없는 노드를 찾습니다. 특정 작업이 먼저 완료되어야 하기 때문에 그러한 노드가 있어야 합니다(그렇지 않으면 루프가 발생함). 액세스 그래프에서 이를 제거하고 위의 (d)에 표시된 대로 각 노드를 순서대로 추가하여 조상이 없는 다른 노드를 찾습니다. 각 단계에서 방문 그래프의 노드 수는 마지막으로 하나의 노드만 남고 시퀀스의 마지막 노드가 될 때까지 1씩 감소합니다. 이제 액세스 그래프에 사이클이 있는 경우에만 액세스 그래프에서 조상이 없는 노드를 찾는 것이 가능하고 따라서 일반적인 경우에는 불가능하다고 가정해 보겠습니다.

노드를 순서대로 배열하는 것은 액세스 그래프가 설계된 메모리 모델을 준수한다는 것을 증명하는 것과 같습니다. 사전 발생 관계를 위반하지 않고 노드를 순차적으로 나열할 수 있다는 사실은 실행이 단일 프로세서가 각 노드를 순차적으로 실행하는 것과 동일하다는 것을 의미하며, 이것이 바로 일관성 모델의 정의입니다. 모든 일관성 모델은 메모리 명령어와 일관성 가정 간의 순서 제약 조건으로 구성됩니다. 이 정의는 또한 단일 프로세서가 사전 발생 관계를 위반하지 않고 명령을 순서대로 실행할 수 있어야 함을 의미합니다.

이것이 바로 액세스 그래프를 동등한 순차 노드 목록으로 변환하여 달성한 것입니다. 이제 프로그램 순서, 인과성, 일관성 마진이 일관성 모델을 지정하는 데 충분하다는 사실은 훨씬 더 심오합니다.

따라서 액세스 그래프(메모리 모델 M의 경우)에 루프가 포함되어 있지 않으면 주어진 실행이 M을 따른다고 결론을 내릴 수 있습니다. 시스템이 생성할 수 있는 모든 가능한 액세스 그래프가 비순환적임을 입증할 수 있으면 전체 시스템이 M을 따른다고 결론을 내릴 수 있습니다.

순차적 일관성 증명

후속 메모리 요청을 발행하기 전에 메모리 요청이 완료되기를 기다리는 간단한 시스템에 의해 생성될 수 있는 모든 가능한 액세스 그래프(SC로 가정)가 비주기적임을 보여드리겠습니다. 액세스 그래프 G를 고려하면 G의 모든 메모리 액세스가 순차적으로 작성될 수 있음을 보여야 합니다. 즉, 노드 b가 노드 a 뒤에 있으면 G의 b에서 a로 가는 경로가 없습니다. 즉, 우리의 순서는 액세스 그래프에 표시된 액세스 순서를 따릅니다.

액세스 그래프에 동일한 스레드 t1에 속하는 노드 S 세트를 포함하는 사이클이 있다고 가정합니다. 여기서 a는 프로그램 순서에 따라 S에서 가장 빠른 노드이고 b는 S에서 가장 최신 노드입니다. 분명히 a는 b보다 먼저 발생합니다. 왜냐하면 메모리 명령은 프로그램 순서로 실행되고 동일한 스레드에서 다음 요청을 시작하기 전에 요청이 완료될 때까지 기다리기 때문입니다. 인과 경계로 인해 형성된 루프의 경우 b는 다른 메모리 읽기 요청(노드)에서 읽은 값을 써야 하며 c는 다른 스레드에 속합니다. 또는 b와 다른 스레드에 속하는 노드 c 사이에 일관성 가장자리가 있을 수 있습니다. 이제 순환이 존재하려면 c가 a보다 먼저 발생해야 합니다. c와 a 사이에 노드 체인이 있고, 노드 체인의 마지막 노드가 d라고 가정합니다. 정의에 따르면 dt1는 d가 저장소 위치에 쓰고 노드 a가 여기에서 읽거나 d에서 a까지 일관된 가장자리가 있음을 의미합니다. 노드 b에서 노드 a까지의 경로가 있기 때문에(c와 d를 통해) 노드 b와 관련된 요청은 노드 a의 요청보다 먼저 발생해야 합니다. 이는 노드 a 요청이 완료될 때까지 노드 b와 관련된 메모리 요청을 실행할 수 없기 때문에 불가능합니다. 따라서 액세스 그래프에서 루핑이 불가능하다는 모순이 있습니다. 따라서 실행은 SC에서 이루어집니다.

이제 성자의 개념을 명확히 해보자. 여기서 문제는 순서를 생성하는 것이 아니라 순서가 존재한다는 것을 증명하는 것이며, 후자의 문제가 해결되고 있기 때문에 가상의 개체가 액세스 그래프에 추가 에지를 추가한다고 가정하는 것은 항상 가능합니다. 결과적인 순차적 순서는 각 스레드의 프로그램 순서, 인과성 및 사전 발생 관계를 기반으로 한 일관성을 따릅니다. 그러므로 이것은 유효한 순서이다.

따라서 우리 시스템에서는 스레드에 대한 순서를 생성하는 것이 항상 가능하므로 다중 프로세서가 SC에 있다는 결론을 내릴 수 있습니다. 이제 우리 시스템이 순차적으로 일관성이 있음을 입증했으므로 가정을 사용하여 다중 프로세서를 구현하는 방법을 설명하겠습니다. 이는 아래 그림에 표시된 것과 같은 시스템으로 이어질 수 있습니다.

단순하고 순차적으로 일관된 시스템.

단순하고 순차적으로 일관된 머신

위의 다이어그램은 멀티프로세서의 모든 프로세서에 걸쳐 대규모 공유 L1 캐시가 있는 설계를 보여줍니다. 각 메모리 위치의 사본은 하나만 있고, 언제든지 하나의 읽기 또는 쓰기 액세스만 지원하여 일관성을 보장합니다. 둘째, L1 캐시의 메모리 위치 값이 변경되면 쓰기가 완료됩니다. 마찬가지로 L1 캐시에 있는 메모리 주소 값을 읽으면 읽기가 완료됩니다. 이전 섹션에서 설명한 간단한 순차 RISC 파이프라인을 수정하여 명령어가 읽기/쓰기 액세스를 완료한 후에만 메모리 액세스(MA) 단계를 벗어나도록 해야 합니다. 캐시 누락이 있는 경우 명령은 블록이 L1 캐시에 도달하고 액세스가 완료될 때까지 기다립니다. 이 간단한 시스템은 동일한 스레드의 메모리 요청이 프로그램 순서대로 완료되어 순차적으로 일관성을 유지하도록 보장합니다.

위 다이어그램에 묘사된 시스템은 일부 비현실적인 가정을 하고 있으므로 실용적이지 않습니다. 16개의 프로세서가 있고 메모리 명령어의 빈도가 1/3이라면 매 사이클마다 L1 캐시에 액세스하는 데 5-6개의 명령어가 필요합니다. 따라서 L1 캐시에는 최소 6개의 읽기/쓰기 포트가 필요하므로 구조가 너무 크고 느려집니다. 또한 L1 캐시는 모든 스레드의 작업 세트를 수용할 수 있을 만큼 커야 하므로 L1 캐시가 매우 크고 느려집니다. 따라서 이러한 캐시를 갖춘 다중 프로세서 시스템은 실제로 매우 느릴 수 있으며 최신 프로세서는 더 작은 캐시가 많은 더 복잡한 메모리 시스템을 갖춘 더 높은 성능 구현을 선택했습니다. 이러한 캐시는 서로 협력하여 더 큰 캐시라는 환상을 제공합니다.

복잡한 시스템이 순차 일관성(SC)을 준수한다는 것을 증명하는 것은 어렵고 설계자는 약한 메모리 모델을 사용하여 시스템을 설계하도록 선택합니다. 이 경우 펜스 지침이 올바르게 작동하는지 증명해야 하는데, 이는 복잡한 디자인에서 발생할 수 있는 모든 미묘한 코너 케이스를 고려하면 상당히 어려운 문제입니다.

약한 일관성 모델 구현

동일한 스레드에 있는 노드의 프로그램 순서를 나타내는 에지가 없는 약하게 일관된 시스템의 액세스 그래프를 생각해 보세요. 대조적으로, 일반 읽기/쓰기 노드와 차단 작업 사이에 가장자리가 있는 동일한 스레드의 노드의 경우 SC 사례에서와 마찬가지로 인과성 및 일관성 가장자리를 액세스 그래프에 추가해야 합니다.

약하게 일관된 머신을 구현하려면 이 액세스 그래프가 순환되지 않도록 해야 합니다. 다음 구현은 액세스 그래프에 사이클을 도입하지 않고 지정된 스레드의 프로그램 순서에 있는 모든 이전 명령어가 완료된 후에 차단 명령어가 시작되도록 보장한다는 것을 보여줄 수 있습니다. 펜스 명령어는 파이프라인의 끝에만 도달하면 되는 의사 명령어이며 타이밍 목적으로만 사용됩니다. 모든 이전 지침이 완료될 때까지 MA 단계에서 펜스 지침을 일시 중지합니다. 또한 이 전략은 후속 명령이 MA 단계에 도달하지 않도록 보장합니다. 이전 명령어가 모두 완료되면 차단 명령어는 RW 단계로 들어가고 후속 명령어는 메모리에 요청할 수 있습니다.

메모리 일관성 모델 구현 내용을 요약합니다. 순차 일관성 또는 약한 일관성과 같은 메모리 일관성 모델은 프로세서의 파이프라인을 수정하고 메모리 시스템이 메모리 요청 처리를 완료하자마자 프로세서에 승인을 보내도록 하여 구현할 수 있습니다. 고성능 구현에서는 많은 미묘한 코너 케이스가 가능하며 주어진 일관성 모델을 구현하는지 확인하는 것은 매우 복잡합니다.

메모리 배리어 적용 및 UE 구현에 대해서는 1.4.5 메모리 배리어를 참조하세요.

19.7.4.7 다중 스레드 프로세서

이제 다중 프로세서를 설계하는 또 다른 방법을 살펴보겠습니다. 지금까지 우리는 다중 프로세서를 생성하기 위해 물리적으로 분리된 파이프라인이 필요하다고 주장했으며, 각 파이프라인에 별도의 프로그램 카운터를 할당하는 설계를 살펴보았습니다. 그러나 동일한 파이프라인에서 스레드 그룹을 실행하는 다른 방법, 즉 멀티스레딩을 살펴보겠습니다. 별도의 파이프라인에서 별도의 스레드를 실행하는 대신 동일한 파이프라인에서 실행하십시오. 이 개념은 성긴 멀티스레딩이라는 가장 간단한 멀티스레딩 변형을 논의하여 설명됩니다.

멀티스레딩은 동일한 파이프라인에서 여러 스레드를 실행하는 설계 패러다임입니다.

멀티스레드 프로세서는 멀티스레딩을 구현하는 프로세서입니다.

대략적인 멀티스레딩

단일 파이프라인에서 4개의 스레드를 실행한다고 가정해 보겠습니다. 동일한 프로세스에 속하는 여러 스레드에는 자체 프로그램 카운터, 스택 및 레지스터가 있습니다. 그러나 그들은 기억에 대한 공통된 견해를 공유합니다. 4개의 스레드 모두 자신만의 독립적인 명령 스트림을 갖고 있으므로 4개의 프로세스가 별도로 실행되고 있다는 환상을 제공해야 합니다. 소프트웨어는 스레드가 다중 스레드 프로세서에서 실행된다는 사실을 무시해야 하며, 각 스레드에는 자체 전용 CPU가 있다는 점을 인식해야 합니다. 전통적인 일관성 및 일관성 보장 외에도 소프트웨어가 멀티스레딩을 무시해야 한다는 추가 보장이 제공되어야 합니다.

아래 그림과 같이 스레드 1이 n 루프를 실행한 다음 스레드 2로 전환하고 n 주기 동안 실행한 다음 스레드 3으로 전환하는 등의 간단한 시나리오를 생각해 보십시오. n 주기 동안 스레드 4를 실행한 후 스레드 1의 실행이 다시 시작됩니다. 스레드를 실행하려면 스레드의 상태나 컨텍스트를 로드해야 합니다. 프로그램의 컨텍스트에는 플래그 레지스터, 프로그램 카운터 및 레지스터 세트가 포함됩니다. 서로 다른 프로세스의 메모리 영역이 겹치지 않기 때문에 메인 메모리를 추적할 필요가 없습니다. 다중 스레드의 경우 모든 스레드는 명시적으로 동일한 메모리 공간을 공유할 것으로 예상됩니다.

더 간단한 접근 방식을 취할 수 있습니다. 스레드의 컨텍스트를 명시적으로 로드 및 언로드하는 대신 스레드의 컨텍스트를 파이프라인에 저장할 수 있습니다. 예를 들어, 대략적인 멀티스레딩을 지원하려는 경우 4개의 독립 플래그 레지스터, 4개의 프로그램 카운터 및 4개의 독립 레지스터(각 스레드당 하나씩)를 가질 수 있습니다. 현재 실행 중인 스레드의 ID가 포함된 전용 레지스터를 가질 수도 있습니다. 예를 들어 스레드 2가 실행 중이면 스레드 2의 컨텍스트를 사용하고, 스레드 3이 실행 중이면 스레드 3의 컨텍스트를 사용합니다. 이렇게 하면 여러 스레드가 서로의 상태를 덮어쓸 수 없습니다.

거친 멀티스레딩의 개념도

스레드의 컨텍스트를 명시적으로 로드 및 언로드하는 대신 더 간단한 접근 방식을 사용할 수 있습니다. 스레드의 컨텍스트를 파이프라인에 저장할 수 있습니다. 예를 들어, 대략적인 멀티스레딩을 지원하려는 경우 4개의 독립 플래그 레지스터, 4개의 프로그램 카운터 및 4개의 독립 레지스터 파일(각 스레드당 하나씩)을 가질 수 있습니다. 추가적으로 현재 실행 중인 스레드의 ID를 포함하는 전용 레지스터를 가질 수 있습니다. 예를 들어, 스레드 2를 실행하는 경우 스레드 2의 컨텍스트를 사용하고, 스레드 3을 실행하는 경우 스레드 3의 컨텍스트를 사용합니다. 이런 방식으로 여러 스레드가 서로의 상태를 덮어쓰는 것은 불가능합니다.

이제 몇 가지 미묘한 문제를 살펴보십시오. 파이프라인의 동일한 지점에 여러 스레드에 속하는 명령이 있을 수 있으며, 이는 한 스레드에서 다음 스레드로 전환할 때 발생할 수 있습니다. 명령 패킷에 스레드 ID 필드를 추가하고 전달 및 연동 논리가 스레드의 ID를 고려하고 스레드 간에 값을 전달하지 않도록 추가로 보장해 보겠습니다. 이러한 방식으로 스레드 간 전환 오버헤드를 무시할 수 있는 상태로 파이프라인에서 4개의 독립적인 스레드를 실행할 수 있습니다. 스레드의 컨텍스트를 저장하고 복원하기 위해 예외 처리기를 사용할 필요가 없으며 스레드 실행을 예약하기 위해 운영 체제를 호출할 필요도 없습니다.

이제 대략적인 멀티스레딩을 전체적으로 살펴보겠습니다. n개의 스레드를 빠르게 연속해서 루프 순서로 실행합니다. 또한 스레드가 서로의 상태를 파괴하지 않지만 동시에 4개의 스레드를 실행하지 않도록 스레드 간에 빠르게 전환하는 메커니즘이 있습니다. 그렇다면 이 솔루션의 장점은 무엇입니까?

메모리에 대한 불규칙한 액세스가 많은 메모리 집약적 스레드의 경우를 생각해 보십시오. L2 캐시에서 자주 누락되며 메모리에서 값이 반환될 때까지 파이프라인을 100~300주기 동안 일시 중지해야 합니다. 비순차적 파이프라인은 메모리 값에 의존하지 않는 다른 명령을 실행하여 일부 대기 시간을 숨길 수 있지만 오랜 시간 동안 정지되기도 합니다. 그러나 다른 스레드로 전환할 수 있다면 유용한 작업을 수행할 수 있습니다. 해당 스레드가 L2 캐시의 누락으로 인해 발생한 경우 다른 스레드로 전환하여 작업의 일부를 완료할 수 있으며, 이는 전체 시스템의 처리량을 최대화합니다. 두 가지 가능한 솔루션을 생각해 볼 수 있습니다. n 주기마다 주기적으로 전환하거나 두 번째 수준 캐시 누락과 같은 이벤트가 발생할 때 다른 스레드로 전환할 수 있습니다. 둘째, 스레드가 대기 시간이 긴 이벤트(예: 두 번째 수준 캐시 누락)를 기다리고 있는 경우 이 스레드로 전환할 필요가 없으며 실행할 명령 풀이 있는 스레드로 전환해야 합니다. 성긴 멀티스레드 시스템의 성능을 최적화하기 위해 수많은 경험적 방법을 설계할 수 있습니다.

소프트웨어 스레드와 하드웨어 스레드의 차이점:

  • 소프트웨어 스레드는 공통 목표를 향해 협력하기 위해 서로 통신할 수 있는 다른 소프트웨어 스레드와 주소 공간의 일부를 공유하는 서브루틴입니다.

  • 하드웨어 스레드는 파이프라인에서 실행되는 소프트웨어 스레드 또는 단일 스레드 프로그램의 인스턴스와 해당 실행 상태로 정의됩니다.

다중 스레드 프로세서는 스레드 전체에 리소스를 할당하여 동일한 프로세서에서 여러 하드웨어 스레드를 지원합니다. 소프트웨어 스레드는 이를 실행하는 데 사용되는 엔터티와 관계없이 별도의 프로세서 또는 하드웨어 스레드에 물리적으로 매핑될 수 있습니다. 주목해야 할 중요한 점은 소프트웨어 스레드는 프로그래밍 언어 개념인 반면, 하드웨어 스레드는 파이프라인의 리소스와 물리적으로 연결되어 있다는 것입니다.

이 기사에서는 "스레드"라는 단어를 사용하여 소프트웨어 스레드와 하드웨어 스레드를 모두 지칭하며, 올바른 사용법은 문맥을 통해 추론해야 합니다.

세밀한 멀티스레딩

세밀한 멀티스레딩은 대략적인 멀티스레딩의 특별한 경우로, 전환 간격 n이 매우 작으며(보통 1~2사이클), 이는 스레드 간 전환이 빠를 수 있음을 의미합니다. 메모리 집약적인 스레드를 실행하기 위해 세분화된 멀티스레딩을 활용할 수 있지만, 네거티브 멀티스레딩은 스레드 그룹을 실행하는 데에도 유용합니다(예: 나누기와 같은 긴 산술 연산). 일반적인 프로세서에서는 나눗셈 연산과 기타 특수 연산(예: 삼각법 또는 초월 연산)이 느립니다(3~10사이클). 이 시간 동안 원래 스레드는 작업이 완료될 때까지 기다리는 동안 다른 스레드로 전환하여 파이프라인 단계에서 사용되지 않은 명령 중 일부를 실행할 수 있습니다. 따라서 수학 연산이 많은 과학 프로그램에서 스레드 간을 빠르게 전환하는 기능을 활용하여 유휴 시간을 줄일 수 있습니다.

따라서 세분화된 멀티스레딩은 스레드 간을 빠르게 전환하고 유휴 단계를 활용하여 유용한 작업을 수행하는 보다 유연한 형태의 거친 멀티스레딩으로 생각할 수 있습니다. 이 개념은 말만큼 간단하지 않습니다. 멀티스레딩에 대한 자세한 지원은 일반적인 순차 또는 비순차 파이프라인의 모든 구조에서 제공되어야 하며, 각 스레드의 컨텍스트를 매우 신중하게 관리하고 지침이 누락되거나 오류가 발생하지 않도록 해야 합니다.

스레드 간 전환 논리는 간단하지 않습니다. 대부분의 경우 스레드 간 전환 논리는 시간 기반 기준(사이클 수)과 이벤트 기반 기준(L2 캐시 누락 또는 페이지 오류와 같은 대기 시간이 긴 이벤트)의 조합입니다. 멀티스레드 프로세서가 다양한 벤치마크에서 우수한 성능을 발휘하도록 하려면 휴리스틱을 신중하게 조정해야 합니다.

동시 멀티스레딩

단일 실행 파이프라인의 경우 복잡한 로직을 사용하여 스레드 간 전환을 통해 각 단계를 바쁘게 유지할 수 있다면 높은 효율성을 달성할 수 있습니다. 단일 실행 파이프라인의 모든 단계는 주기당 하나의 명령만 처리할 수 있습니다. 이에 비해 다중 실행 파이프라인은 사이클당 여러 명령을 처리할 수 있으며, 더욱이 실행 슬롯의 수는 파이프라인이 사이클당 처리할 수 있는 명령 수와 동일하게 설계되었습니다. 예를 들어, 3-실행 프로세서는 사이클당 최대 3개의 명령어를 가져오고, 디코딩하고, 궁극적으로 실행할 수 있습니다.

여러 실행 파이프라인에서 멀티스레딩을 구현하려면 스레드 내 명령어 간의 종속성도 고려해야 합니다. 세분화된 방식과 거친 방식은 스레드가 모든 실행 슬롯의 기능 단위에 대한 명령을 실행할 수 없기 때문에 제대로 수행되지 않을 수 있으며 이러한 스레드는 명령 수준 병렬성이 낮은 것으로 설명될 수 있습니다. 4개의 실행 파이프라인을 사용하고 프로그램의 종속성으로 인해 각 스레드의 최대 IPC가 1인 경우 3개의 실행 슬롯이 각 주기에서 유휴 상태로 유지됩니다. 따라서 4스레드 시스템의 전체 IPC는 1이 되며 멀티스레딩의 이점은 제한됩니다.

따라서 전체 시스템의 IPC를 높일 수 있도록 추가 실행 기간을 활용하는 것이 필요합니다. 간단한 접근 방식은 각 스레드에 실행 슬롯을 할당하는 것입니다. 둘째, 구조적 충돌을 피하기 위해 4개의 ALU를 갖고 각 스레드에 하나의 ALU를 할당할 수 있습니다. 그러나 스레드가 매 주기마다 명령을 실행하지 않을 수 있으므로 이는 파이프라인을 최적이 아닌 방식으로 사용하는 것입니다. 스레드 간에 실행 슬롯을 동적으로 나눌 수 있는 보다 유연한 솔루션, 즉 동시 멀티스레딩(SMT)이 있으면 더 좋을 것입니다. 예를 들어, 주어진 주기에서 스레드 2에서 2개의 명령어를 실행하고 스레드 3과 4에서 1개의 명령어를 실행할 수 있으며, 이 상황은 다음 주기에서 반전될 수 있습니다. 아래 그림은 SMT 접근 방식을 세분화된 멀티스레딩과 거친 멀티스레딩과 비교하면서 이 개념을 보여줍니다.

멀티 스레드 프로세서에서의 명령 실행.

위 그림의 열은 다중 실행 기계의 실행 슬롯을 나타내고, 행은 주기를 나타내며, 서로 다른 스레드에 속한 명령어는 서로 다른 색상을 나타냅니다. 그림 (a)는 각 스레드가 두 개의 연속 사이클을 실행하는 대략적인 시스템에서의 명령 실행을 보여줍니다. 충분한 수의 실행 가능한 명령어를 찾을 수 없기 때문에 많은 실행 슬롯이 비어 있습니다. 세분화된 멀티스레딩(그림 (b))에도 동일한 문제가 있습니다. 그러나 SMT 프로세서에서는 명령이 항상 실행 준비가 된 사용 가능한 스레드 집합에서 발견되기 때문에 일반적으로 대부분의 실행 슬롯을 바쁜 상태로 유지하는 것이 가능합니다. 어떤 이유로 한 스레드가 일시 중단되면 다른 스레드가 추가 명령을 실행하여 보상합니다. 실제로 모든 스레드에는 동시에 낮은 ILP(명령 수준 병렬성, 명령 수준 병렬성) 단계가 없으므로 SMT 접근 방식은 다중 실행 프로세서의 기능을 활용하는 매우 다양하고 효과적인 방법임이 입증되었습니다. Pentium 4(90년대 후반에 출시됨) 이후 대부분의 Intel 프로세서는 다양한 버전의 동시 멀티스레딩을 지원했습니다. Intel 용어로 SMT는 하이퍼스레딩이라고 하는 반면, IBM Power 7 프로세서에는 8개의 코어가 있고 각 코어는 4방향 SMT(각 코어는 4개의 스레드를 실행할 수 있음)입니다.

실행할 올바른 명령 세트를 선택하는 문제는 SMT 프로세서의 성능에 매우 중요합니다. 둘째, n-way SMT 프로세서의 메모리 대역폭 요구 사항은 동등한 단일 프로세서의 메모리 대역폭 요구 사항보다 높으며, 동일한 주기에 4개의 독립적인 프로그램 카운터에서 가져와야 하기 때문에 가져오기 논리도 훨씬 더 복잡합니다. 마지막으로, 일관성과 일관성을 유지하는 문제로 인해 상황은 더욱 복잡해졌습니다.

여러 스레드를 실행하는 다른 방법은 다음과 같습니다.

이에 대한 설명은 다음과 같습니다.

  • 수퍼스칼라: 멀티스레딩이 없는 기본적인 수퍼스칼라 방식입니다. 몇 년 전까지만 해도 프로세서 내에서 병렬성을 제공하는 가장 강력한 방법이었습니다. 일부 주기에서는 사용 가능한 모든 실행 슬롯(발행 슬롯)이 사용되지는 않습니다. 이러한 주기에서는 발행된 명령 수가 최대 명령 수보다 적습니다. 이를 수평 손실이라고 합니다. 다른 명령어 주기에서는 실행 슬롯이 사용되지 않고 명령어를 실행할 수 없는 경우를 수직 손실이라고 합니다.

  • 인터리브 멀티스레딩 수퍼스칼라: 각 주기마다 하나의 스레드에서 최대한 많은 명령을 실행합니다. 앞서 언급했듯이 이 기술을 사용하면 스레드 전환으로 인한 잠재적인 지연이 제거되지만 특정 주기에 실행되는 명령 수는 여전히 특정 스레드에 존재하는 종속성에 의해 제한됩니다.

  • 차단된 멀티스레드 수퍼스칼라: 마찬가지로 한 스레드의 명령만 모든 주기에서 실행될 수 있으며 차단 멀티스레딩이 사용됩니다.

  • 매우 긴 명령 단어(VLIW): VLIW 아키텍처(예: IA-64)는 일반적으로 컴파일러에 의해 구축된 여러 명령을 한 단어에 넣습니다. 이는 병렬로 실행될 수 있는 작업을 동일한 단어에 넣습니다. 간단한 VLIW 머신(위의 g)에서는 병렬로 발행된 명령으로 단어를 완전히 채울 수 없는 경우 작업이 사용되지 않습니다.

  • 인터리브 멀티스레딩 VLIW: 수퍼스칼라 아키텍처에서 인터리브 멀티스레딩이 제공하는 것과 유사한 효율성을 제공합니다.

  • 차단된 멀티스레딩 VLIW: 수퍼스칼라 아키텍처에서 차단된 멀티스레딩이 제공하는 것과 유사한 효율성을 제공합니다.

  • 동시 멀티스레딩: 위의 그림 j는 한 번에 8개의 명령을 내릴 수 있는 시스템을 보여줍니다. 스레드의 명령 수준 병렬 처리 수준이 높으면 일부 주기에서 모든 수평 슬롯을 채울 수 있습니다. 다른 주기에서는 두 개 이상의 스레드에서 명령이 실행될 수 있습니다. 충분한 스레드가 활성화되면 일반적으로 주기당 최대 명령 수를 발행하여 높은 효율성을 제공할 수 있습니다.

  • 멀티코어라고도 알려진 칩 멀티프로세서: 위의 그림 k는 4개의 코어를 포함하는 칩을 보여주며, 각 코어에는 두 가지 문제가 있는 수퍼스칼라 프로세서가 있습니다. 각 코어에는 사이클당 최대 2개의 명령을 발행할 수 있는 스레드가 할당됩니다.

19.7.5 SIMD 다중 프로세서

이 섹션에서는 SIMD 다중 프로세서에 대해 설명합니다. SIMD 프로세서는 일반적으로 과학 응용 프로그램, 고강도 게임 및 그래픽에 사용됩니다. 그들은 범용 용도가 많지 않습니다. 그러나 제한된 종류의 응용 분야에서는 SIMD 프로세서가 MIMD 프로세서보다 더 나은 경향이 있습니다.

SIMD 프로세서는 풍부한 역사를 가지고 있습니다. 옛날에는 프로세서를 배열로 배열했는데, 데이터는 일반적으로 프로세서의 첫 번째 행과 열을 통해 입력되었으며, 각 프로세서는 입력 메시지에 대해 작동하고 출력 메시지를 생성하여 해당 메시지를 이웃에게 보냈습니다. 이러한 유형의 프로세서를 수축기 배열이라고 합니다. 축소 배열은 행렬 곱셈 및 기타 선형 대수 연산에 사용됩니다. 몇몇 후속 공급업체, 특히 Cray는 더 빠르고 에너지 효율적인 슈퍼컴퓨터를 설계하기 위해 SIMD 지침을 프로세서에 통합했습니다. 오늘날 이러한 초기 노력의 대부분은 사라졌지만 단일 명령이 여러 데이터 스트림에서 작동하는 기존 SIMD 컴퓨터의 일부 측면이 최신 프로세서의 설계에 스며들었습니다.

우리는 현대 프로세서 설계의 중요한 발전, 고성능 프로세서에 SIMD 기능 장치 및 명령을 포함시키는 것에 대해 논의할 것입니다.

19.7.5.1 SIMD: 벡터 프로세서

두 개의 n-요소 배열을 추가하는 문제를 고려해 보겠습니다. 단일 스레드 구현에서는 피연산자를 메모리에서 로드하고 추가한 다음 결과를 메모리에 저장해야 합니다. 따라서 대상 배열의 각 요소를 계산하려면 두 개의 로드 명령어, 추가 명령어 및 저장 명령어가 필요합니다. 기존 프로세서는 (c[i]=a[i]+b[i]) 및 (c[j]=a[j]+b[j])가 병렬로 계산될 수 있다는 사실을 활용하여 속도 향상을 달성하려고 합니다. 왜냐하면 이 두 작업 사이에는 종속성이 없으며 이러한 많은 작업을 병렬로 실행하면 IPC가 증가할 수 있기 때문입니다.

이제 수퍼스칼라 프로세서를 고려해 보겠습니다. 사이클당 4개의 명령을 실행할 수 있는 경우 IPC는 단일 사이클 프로세서의 최대 4배가 될 수 있습니다. 실제로 4-실행 프로세서의 경우 본질적으로 병렬 어레이 처리 작업을 사용하여 단일 사이클 프로세서에서 달성할 수 있는 최대 속도 향상은 약 3~3.5배입니다. 둘째, 넓은 실행 폭을 통해 IPC를 늘리는 이 방법은 확장성이 없습니다. 실제로 파이프라인의 논리가 매우 복잡해지고 면적/전력 오버헤드가 엄청나기 때문에 실행 프로세서가 8개 또는 10개라는 것은 없습니다.

따라서 설계자들은 대규모 데이터 벡터(배열)에서 작동하는 벡터 연산에 대한 특별한 지원을 제공하기로 결정했습니다. 이러한 프로세서를 벡터 프로세서라고 합니다. 주요 아이디어는 전체 데이터 배열을 한 번에 처리하는 것입니다. 일반 프로세서는 정수 및 부동 소수점 숫자와 같은 일반 스칼라 데이터 유형을 사용합니다. 벡터 프로세서는 본질적으로 스칼라 데이터 유형의 배열인 벡터 데이터 유형을 사용합니다.

벡터 프로세서는 기본 데이터 유형(정수 또는 부동 소수점 숫자)의 벡터를 기본 정보 단위로 처리합니다. 전체 벡터에 대한 산술 연산을 한 번에 로드, 저장 및 수행할 수 있습니다. 데이터 벡터에 대해 작동하는 이 명령어를 벡터 명령어라고 합니다.

단일 벡터 추가 C=A+B 명령의 성능을 향상하려면 여러 기능 단위를 사용하십시오.

4채널 벡터 유닛 구조를 포함합니다.

벡터 프로세서를 주로 사용한 가장 상징적인 제품 중 하나는 Cray 1 슈퍼컴퓨터였습니다. 이 슈퍼컴퓨터는 주로 선형 대수 연산으로 구성된 과학 응용 분야에 사용되었습니다. 이러한 작업은 데이터 및 행렬의 벡터에서 작동하므로 벡터 프로세서에서 실행하는 데 이상적입니다. 안타깝게도 집약적인 과학 컴퓨팅 영역을 벗어나면 벡터 프로세서는 1990년대 후반까지 범용 시장에 진출하지 못했습니다.

1990년대 후반에 개인용 컴퓨터가 연구 및 과학 응용 프로그램 실행에 사용되기 시작했습니다. 둘째, 설계자는 슈퍼컴퓨터용 맞춤형 프로세서를 설계하는 대신 일반 상용 프로세서를 사용하여 슈퍼컴퓨터를 구축하기 시작했습니다. 그 이후로 이러한 추세는 그래픽 프로세서 개발로 이어졌습니다. 1995년부터 2010년 사이에 대부분의 슈퍼컴퓨터는 수천 개의 상용 프로세서로 구성되었습니다. 기존 프로세서에서 벡터 명령을 사용하는 또 다른 중요한 이유는 많은 그래픽 처리가 필요한 고강도 게임을 지원하기 위해서입니다. 예를 들어, 현대 게임은 여러 캐릭터와 수천 가지 시각 효과로 복잡한 장면을 렌더링합니다. 조명, 그림자, 애니메이션, 깊이, 색상 조작과 같은 대부분의 시각 효과는 점이나 픽셀이 포함된 행렬에 대한 기본 선형 대수 연산의 핵심입니다. 이러한 요인으로 인해 기존 프로세서는 제한된 벡터 지원을 도입하기 시작했습니다. 특히, Intel 프로세서는 MMX 및 SSE 1-4 벡터 명령어 세트를 제공했고, AMD 프로세서는 3DNow! 벡터 확장인 ARM 프로세서는 ARM Neon 벡터 ISA를 제공합니다. 이들 ISA 사이에는 많은 공통점이 있으므로 특정 ISA에 초점을 맞추지는 않습니다. 벡터 프로세서 설계 및 작동의 기본 원리를 살펴보겠습니다.

19.7.5.2 소프트웨어 인터페이스

먼저 벡터 레지스터 세트가 필요한 기계 모델을 고려해 보겠습니다. 예를 들어, x86 SSE(Streaming Single Instruction Multiple Data Extensions) 명령 세트는 16개의 128비트 레지스터(XMM0...XMM15)를 정의합니다. 각 레지스터에는 4개의 정수 또는 4개의 부동 소수점 값이 포함될 수 있으며, 8개의 2바이트 짧은 정수 또는 16개의 1바이트 문자가 포함될 수 있습니다. 같은 줄에서 모든 벡터 ISA에는 일반 레지스터보다 더 넓은 추가 벡터 레지스터가 필요합니다. 일반적으로 각 레지스터에는 여러 개의 부동 소수점 값이 포함될 수 있습니다. 여기서는 vr0...vr7이라는 8개의 128비트 벡터 레지스터를 정의하겠습니다.

이제 벡터 레지스터를 로드, 저장 및 조작하기 위한 지침이 필요합니다. 벡터 레지스터를 로드하는 경우 연속된 메모리 위치에서 값을 로드하는 것과 연속되지 않은 메모리 위치에서 값을 로드하는 두 가지 옵션이 있습니다. 전자의 경우는 더 특별하며 일반적으로 모든 배열 요소가 연속적인 메모리 위치에 저장되는 배열 기반 애플리케이션에 적용됩니다. ISA에 대한 대부분의 벡터 확장은 단순성과 규칙성으로 인해 로드 명령의 이러한 변형을 지원합니다. 여기서는 ISA용 벡터 로딩 명령어 v:ld를 설계하고 아래 그림에 표시된 의미를 고려할 수도 있습니다. 여기서 v:ld 명령은 메모리 위치([r1+12], [r1/16], [r2+20], [r 1+24])의 내용을 벡터 레지스터 vr1로 읽습니다. 아래 표에서 벡터 레지스터를 확인하세요.

예 문법 설명하다

v.ld vr1, 12[r1] v.ld , vr1 <-- ([r1+12], [r1+16], [r1+20], [r1+ 24])

이제 행렬의 경우를 고려해보세요. 10000개 요소로 구성된 행렬 a[100][100]이 있고 데이터가 행 우선 순서로 저장되어 있으며 행렬의 두 열에 대해 연산이 수행된다고 가정합니다. 이 경우 열의 요소가 서로 인접하여 저장되지 않기 때문에 문제가 있습니다. 따라서 입력 피연산자가 연속적인 메모리 위치에 유지된다는 가정에 의존하는 벡터 로드 명령어는 작동을 중지하므로 열의 위치에 대한 모든 데이터를 가져와 벡터 레지스터에 유지하려면 특수 지원이 필요합니다. 입력 피연산자가 기본적으로 주 메모리에 분산되어 있기 때문에 이 연산을 분산 수집 연산이라고 합니다.

우리는 그것들을 수집하여 벡터 레지스터라는 곳에 넣어야 합니다. 벡터 로드 명령의 분산-수집 변형을 고려하고 이를 v.sg.ld라고 부르겠습니다. 프로세서는 배열 요소의 위치를 ​​가정하는 대신 요소의 주소가 포함된 다른 벡터 레지스터를 읽습니다(의미는 아래 표 참조). 이 경우 전용 벡터 로드부는 vr2에 저장된 메모리 주소를 읽어와 메모리에서 해당 값을 추출하여 벡터 레지스터 vr1에 순차적으로 쓴다.

예 문법 설명하다

v.sg.ld vr1, 12[r1] v.sg.ld , vr1 <-- ([vr2[0]], [vr2[1]], [vr2[2]], [vr2[3]])

벡터 레지스터에 데이터가 로드되면 이러한 레지스터 두 개에 대한 작업을 직접 수행할 수 있습니다. 예를 들어 128비트 벡터 레지스터 vr1 및 vr2를 생각해 보세요. 그런 다음 어셈블리 문 v.add vr3, vr1, vr2는 입력 벡터 레지스터(vr1 및 vr2)에 저장된 해당 4바이트 부동 소수점 숫자의 각 쌍을 추가하고 결과를 출력 벡터 레지스터(vr3)의 관련 위치에 저장합니다. 여기서는 벡터 추가 명령(v.add)이 사용됩니다. 아래 그림은 벡터 덧셈 명령어의 예를 보여줍니다.

(multiprocessor19 - 원본 이미지 404 소실)multiprocessor19

Vector ISA는 벡터 곱셈, 나눗셈 및 논리 연산에 대해 유사한 연산을 정의합니다. 벡터 명령어는 항상 두 개의 입력 피연산자(예: 벡터)를 가질 필요는 없으며, 벡터에 스칼라를 곱할 수도 있고, 하나의 벡터 피연산자에서만 작동하는 명령어를 가질 수도 있습니다. 예를 들어, SSE 명령어 세트에는 벡터 레지스터의 부동 소수점 숫자 세트에 대한 sin 및 cos와 같은 삼각 함수를 계산하기 위한 특수 명령어가 있습니다. 벡터 명령어가 n개의 피연산자에 대해 동시에 연산을 수행할 수 있으면 n개의 데이터 경로가 있다고 하며 벡터 명령어는 n개의 데이터 경로 모두에 대해 동시에 연산을 수행합니다.

벡터 명령어가 n개의 피연산자에 대해 동시에 연산을 수행할 수 있다면 n개의 데이터 레인(lane)이 있다는 뜻이고, 벡터 명령어는 n개의 데이터 경로 모두에 대해 동시에 연산을 수행한다는 의미입니다.

마지막 단계는 벡터 레지스터를 메모리에 저장하는 것입니다. 두 가지 옵션이 있습니다. 인접한 메모리 위치에 저장하거나 인접하지 않은 위치에 저장할 수 있습니다. 벡터 저장 명령어의 두 가지 변형(연속 및 비연속)은 벡터 로드 명령어의 두 가지 변형(v.ld 및 v.sg.ld) 라인에서 설계될 수 있습니다. 스칼라 레지스터와 벡터 레지스터 간에 데이터를 전송하는 명령어를 도입해야 하는 경우도 있습니다.

19.7.5.3 SSE 명령 예

이제 실제 어셈블리 명령어를 사용하는 대신, gcc 내장이라고 불리는 어셈블리 명령어 주위의 래퍼 역할을 하는 gcc 컴파일러에서 제공하는 함수를 사용하는 대신 x86 기반 SSE 명령어 세트를 사용하는 실제 예제를 고려해보세요.

이제 i의 모든 값에 대해 c[i]=a[i]+b[i]를 계산하기를 희망하면서 두 개의 부동 소수점 배열을 추가하는 문제를 해결해 보겠습니다.

SSE 명령어 세트에는 128비트 레지스터가 포함되어 있으며 각 레지스터는 4개의 32비트 부동 소수점 숫자를 저장하는 데 사용할 수 있습니다. 따라서 N개의 숫자 배열이 있는 경우 각 루프에 최대 4쌍의 숫자를 추가할 수 있으므로 N/4 반복을 수행해야 합니다. 각 반복마다 벡터 레지스터를 로드하고, 누적하고, 결과를 메모리에 저장해야 합니다. 벡터 계산을 벡터 레지스터 크기에 따라 일련의 루프 반복으로 나누는 프로세스를 스트립 마이닝이라고 합니다.

배열 a와 b에 요소 쌍을 추가하고 x86 ISA의 SSE 확장을 사용하여 결과를 배열 C에 저장하는 함수를 C/C++로 작성합니다. a와 b의 항목 수가 동일하고 4의 배수라고 가정합니다. 한 구현에 대한 코드는 다음과 같습니다.

cpp
void sseAdd (const float a[], const float b[], float c[], int N)
{
    /* strip mining */
    int numIters = N / 4;

    /* iteration */
    for (int i = 0; i < numIters; i++) 
    {
        /* load the values */
        __m128 val1 = _mm_load_ps(a);
        __m128 val2 = _mm_load_ps(b);

        /* perform the vector addition */
        __m128 res = _mm_add_ps(val1, val2);

        /* store the result */
        _mm_store_ps(c, res);

        /* increment the pointers */
        a += 4 ; b += 4; c+= 4;
    }
}

위 코드 분석: 먼저 반복 횟수를 계산합니다. 각 반복에서 4개의 배열 요소로 구성된 블록을 고려하고, 4개의 부동 소수점 숫자 세트를 128비트 벡터 변수에 로드하고, val1.val1은 컴파일러에 의해 벡터 레지스터에 매핑되고, _mm_load_ps 함수를 사용하여 메모리에서 4개의 연속 부동 소수점 값 세트를 로드합니다. 예를 들어, _mm_load_ps(a) 함수는 a, a+4, a+8, a+12 위치의 부동 소수점 값 4개를 벡터 레지스터로 로드합니다. 마찬가지로 두 번째 벡터 레지스터 val2에는 메모리 주소 b에서 시작하는 4개의 부동 소수점 값이 로드됩니다. 그런 다음 벡터 추가가 수행되고 결과는 res 변수와 연관된 128비트 벡터 레지스터에 저장됩니다. 이를 위해 내장 함수 _mm_add_ps가 사용되며 이후 변수 res가 메모리 위치, 즉 c, c+4, c+8 및 c+12에 저장됩니다.

다음 반복을 계속하기 전에 포인터 a, b 및 c를 업데이트해야 합니다. 4개의 연속 배열 요소가 각 사이클마다 처리되므로 각 포인터는 4(4개의 배열 요소)로 업데이트됩니다.

벡터 명령어는 일괄 로드/저장 및 한 번에 쌍으로 숫자 집합을 추가하는 등의 일괄 계산을 용이하게 한다는 결론을 빠르게 내릴 수 있습니다. 이 함수의 성능을 쿼드 코어 Intel 코어 i7 시스템에서 벡터 명령어를 사용하지 않는 함수 버전과 비교하면 SSE 명령어가 포함된 코드는 백만 요소 배열에 대해 2~3배 더 빠르게 실행되었습니다. SSE 레지스터가 더 넓으면 속도가 더 빨라질 수 있습니다. x86 프로세서의 최신 AVX 벡터 ISA는 256비트 및 512비트 벡터 레지스터를 지원합니다.

19.7.5.4 판단 명령

지금까지 벡터 로드, 저장 및 ALU 연산을 살펴보았습니다. 지점은 어떻습니까? 일반적으로 분기는 벡터 프로세서의 맥락에서 다른 의미를 갖습니다. 예를 들어, 32개의 정수를 수용할 수 있을 만큼 넓은 벡터 레지스터를 갖춘 프로세서에는 18개의 정수만 쌍으로 덧셈한 다음 이를 메모리에 저장하는 프로그램이 있습니다. 이 경우 유효한 데이터를 덮어쓸 위험이 있으므로 벡터 레지스터 전체를 메모리에 저장할 수 없습니다.

또 다른 예를 생각해 봅시다. 배열의 모든 요소에 inc10(x) 함수를 적용한다고 가정해 보겠습니다. 이 경우 입력 피연산자 x가 10보다 작으면 여기에 10을 더하려고 합니다. 이 모드는 벡터 프로세서에서 실행되는 프로그램에서 매우 일반적이므로 이를 지원하려면 벡터 ISA의 추가 지원이 필요합니다.

cpp
function inc10(x):
    if (x < 10)
        x = x + 10;

규칙 명령어의 새로운 변형을 추가하고 이를 예측 명령어(ARM의 조건 명령어와 유사)라고 부르겠습니다. 예를 들어, 일반 로드, 저장 및 ALU 명령어의 조건자 변형을 생성할 수 있습니다. 판정 명령은 특정 조건이 참일 때 실행되고, 그렇지 않으면 전혀 실행되지 않습니다. 조건이 거짓인 경우 판정 명령은 nop와 동일합니다.

예측 명령어는 일반 로드, 저장 또는 ALU 명령어의 변형입니다. 특정 조건이 true이면 정상적으로 실행됩니다. 연관된 조건이 false이면 nop로 변환됩니다.

예를 들어, 마지막 비교가 동일하면 ARM ISA의 addeq 명령어는 일반 덧셈 명령어처럼 실행됩니다. 그러나 그렇지 않은 경우에는 add 명령이 전혀 실행되지 않습니다.

이제 판단에 대한 지원을 추가해 보겠습니다. 먼저 cmp 명령어의 벡터 형식을 만들고 이를 v.cmp라고 부릅니다. 두 벡터의 쌍 비교를 수행하고 비교 결과를 플래그 레지스터의 벡터 형식인 v.flags 레지스터에 저장합니다. v.flags 레지스터의 각 구성 요소에는 일반 프로세서의 플래그 레지스터와 유사한 E 및 GT 필드가 포함되어 있습니다.

cpp
v.cmp vr1, vr2

위의 명령문은 vr1과 vr2를 비교하고 결과를 v.flags 레지스터에 저장합니다. 이 명령어의 다른 형식을 사용하여 벡터를 스칼라와 비교할 수 있습니다.

cpp
v.cmp vr1, 10

이제 벡터 덧셈 명령어의 술어 형식을 정의해 보겠습니다. v.flags[i] 레지스터가 특정 속성을 충족하는 경우 이 명령어는 두 벡터의 i번째 요소를 추가하고 대상 벡터 레지스터의 i번째 요소를 업데이트합니다. 그렇지 않으면 대상 레지스터의 i번째 요소를 업데이트하지 않습니다. 판단 벡터 덧셈 명령의 일반적인 형태는 v.p.add이고, p는 판단 조건이라고 가정한다. 아래 표에는 p가 취할 수 있는 다양한 값이 나열되어 있습니다.

판단 조건 분석하다

lt <

gt

르 <=

=

eq

네 !=

이제 다음 코드 조각을 고려해보세요.

cpp
v.lt.add vr3, vr1, vr2

여기서 벡터 레지스터 vr3의 값은 vr1과 vr2가 표현하는 벡터의 합이고, 예측 조건은 (lt)보다 작다. 즉, v.flags 레지스터 i 요소의 E와 GT 플래그가 모두 false인 경우 i번째 요소에 대해서만 덧셈을 수행하고 vr3 레지스터에 그 값을 설정하고, 덧셈 명령어에 의해 설정되지 않은 vr3 레지스터의 요소들은 이전 값을 유지한다. 따라서 inc10(x) 함수를 구현하는 코드는 vr1에 입력 배열의 값이 포함되어 있다고 가정하면 다음과 같다.

cpp
v.cmp    vr1, 10
v.lt.add vr1, vr1, 10

마찬가지로 로드/저장 명령어와 기타 ALU 명령어의 예측 버전을 정의할 수 있습니다.

19.7.6 인터넷

19.7.6.1 인터넷 네트워크 개요

이제 서로 다른 처리 및 저장 요소를 상호 연결하는 문제를 고려하십시오. 일반적으로 멀티 코어 프로세서는 체커보드 디자인을 사용하지만 여기서는 프로세서 세트를 타일로 나눕니다. 타일은 일반적으로 공유된 마지막 레벨 캐시(L2 또는 L3)의 일부도 포함하는 자체 전용 캐시(L1 및 L2)가 있는 2~4개의 프로세서 그룹으로 구성됩니다. 특정 샤드의 일부인 공유된 마지막 레벨 캐시 부분을 슬라이스라고 하며, 슬라이스는 2~4개의 뱅크로 구성됩니다. 또한 최신 프로세서에서는 블록 또는 블록 그룹이 메모리 컨트롤러를 공유할 수 있으며, 이 컨트롤러의 역할은 온칩 캐시와 메인 메모리 간의 데이터 전송을 조정하는 것입니다. 아래 이미지는 캐시 뱅크에 비해 코어 색상이 더 어두운 32코어 멀티프로세서의 대표적인 레이아웃을 보여줍니다. 우리는 2개의 블록 크기(2개의 프로세서와 2개의 캐시 그룹)를 사용하고 공유 L2 캐시에는 블록 전체에 균등하게 분산된 32개의 캐시 그룹이 있다고 가정합니다. 또한 각 블록에는 전용 메모리 컨트롤러와 라우터라는 구조가 있습니다.

멀티 코어 프로세서 레이아웃.

밀접하게 결합된 멀티프로세서의 일반 아키텍처 다이어그램.

클러스터 구성.

라우터는 아래에 정의된 전용 장치입니다.

  1. 라우터는 타일의 프로세서 또는 캐시에서 발생한 메시지를 온칩 네트워크를 통해 다른 타일로 보냅니다.

  2. 라우터는 온칩 네트워크를 통해 서로 연결됩니다.

  3. 메시지는 일련의 라우터를 통해 소스 라우터에서 (원격 타일의) 대상 라우터로 전송됩니다. 도중에 있는 각 라우터는 메시지를 대상에 더 가까운 다른 라우터로 전달합니다.

  4. 마지막으로 대상 브릭과 연결된 라우터는 메시지를 원격 브릭의 프로세서나 캐시로 전달합니다.

  5. 인접한 라우터는 메시지를 전송하는 데 사용되는 수동 구리선 집합인 링크로 연결됩니다.

  6. 라우터에는 일반적으로 들어오는 링크와 나가는 링크가 많이 있습니다. 들어오고 나가는 링크 세트는 이를 프로세서에 연결하고 해당 타일에 캐시됩니다. 각 링크에는 고유 식별자가 있습니다.

  7. 라우터는 일반적으로 3~5단계의 파이프라인으로 구성된 다소 복잡한 구조를 가지고 있습니다. 대부분의 디자인은 일반적으로 메시지 버퍼링, 나가는 링크의 ID 계산, 링크 조정 및 나가는 링크를 통한 메시지 전송을 위해 파이프라인 단계를 사용합니다.

  8. 라우터와 링크의 배열을 네트워크 온 칩(Network-on-Chip) 또는 네트워크 온 칩(Network-On-Chip)이라고 하며, 줄여서 NOC라고 합니다.

  9. NOC에 연결된 각 라우터를 노드(node)라 하며, 노드들은 메시지를 보내 서로 통신한다.

프로그램 실행 중에 일관성 메시지, LLC(마지막 수준 캐시) 요청/응답 메시지, 캐시와 메모리 컨트롤러 간의 메시지를 전달하는 NOC를 통해 수십억 개의 메시지를 보냅니다. 운영 체제는 또한 스레드를 로드 및 언로드하기 위해 NOC를 사용하여 커널에 메시지를 보냅니다. 정보의 양이 많기 때문에 대부분의 NOC는 종종 상당한 혼잡을 경험합니다. 따라서 혼잡을 최대한 줄이고 설계 및 제조가 용이하며 정보가 목적지에 빠르게 도달할 수 있도록 NOC를 설계하는 것이 중요합니다. NOC의 두 가지 중요한 특성, 즉 이분 대역폭과 직경을 정의해 보겠습니다.

대칭형 다중 프로세서는 다음과 같이 구성됩니다.

19.7.6.2 이등분 대역폭 및 네트워크 직경

정점이 노드이고 버텍스 사이의 가장자리가 링크인 네트워크 토폴로지를 고려해 보겠습니다. 링크 오류가 있거나 정체 또는 기타 이유로 인해 링크를 사용할 수 없다고 가정하면 대체 경로를 통해 메시지를 라우팅하는 것이 가능해야 합니다. 예를 들어, 링으로 배열된 네트워크를 생각해 보세요. 한 링크가 실패하면 언제든지 링의 다른 쪽 끝을 통해 메시지를 보낼 수 있습니다. 메시지가 시계 방향으로 전송되면 시계 반대 방향으로도 전송될 수 있습니다. 그러나 두 개의 링크 오류가 발생하면 네트워크가 두 개의 동일한 부분으로 분할될 수 있습니다. 따라서 우리는 네트워크를 상당한(아마도 동일한) 부분으로 완전히 나누는 데 필요한 링크 실패 수를 최소화하려고 합니다. 이러한 실패 횟수를 이분 대역폭이라고 합니다. 이등분 대역폭은 네트워크 신뢰성의 척도이며 네트워크를 두 개의 동일한 부분으로 나누는 데 필요한 최소 링크 수로 정확하게 정의됩니다.

대역폭을 이등분하는 것에 대한 또 다른 해석이 제공될 수 있습니다. 네트워크 절반에 있는 노드가 나머지 절반에 있는 노드에 메시지를 보내려고 한다고 가정하면 동시에 보낼 수 있는 메시지 수는 적어도 이등분 대역폭과 같습니다. 따라서 이등분 대역폭은 네트워크 대역폭의 척도이기도 합니다.

이등분 대역폭은 네트워크를 두 개의 동일한 부분으로 분할하는 데 필요한 최소 링크 오류 수로 정의됩니다.

안정성과 대역폭에 대해 논의했으며 이제 대기 시간에 초점을 맞췄습니다. 네트워크의 노드 쌍을 고려하고 다음으로 각 노드 쌍 사이의 최단 경로를 고려하십시오. 이러한 최단 경로 중에서 최대 길이를 갖는 경로를 고려하십시오. 이 경로의 길이는 네트워크 직경이라고 하는 네트워크 내 노드 근접성의 상한입니다. 또는 네트워크의 직경은 노드 쌍 간의 최악의 지연에 대한 추정치로 해석될 수 있습니다.

모든 노드 쌍을 고려하고 각 노드 쌍 사이의 최단 경로를 계산해 보겠습니다. 가장 긴 경로의 길이를 네트워크 직경이라고 하며, 이는 네트워크의 최악의 대기 시간을 측정한 것입니다.

19.7.6.3 네트워크 토폴로지

이 섹션에서는 가장 일반적인 네트워크 토폴로지 중 일부를 검토하며 그 중 일부는 멀티코어 프로세서에 사용됩니다. 그러나 대부분의 복잡한 토폴로지는 프로세서를 연결하기 위해 일반 이더넷 링크를 사용하는 느슨하게 연결된 다중 프로세서에 사용됩니다. 각 토폴로지마다 N개의 노드가 있다고 가정하고 이등분 대역폭을 계산하기 위해 N을 2로 나눌 수 있다는 가정을 더욱 단순화할 수 있습니다. 이등분 대역폭 및 직경과 같은 측정값은 대략적인 것이며 광범위한 추세만 나타냅니다. 따라서 우리는 많은 코어에 적합한 단순한 토폴로지를 고려하는 것부터 시작하여 높은 이등분 대역폭과 낮은 네트워크 직경을 목표로 하는 간단한 가정을 할 여지가 있습니다.

체인 및 링

아래 그림의 왼쪽은 동일한 대역폭이 1이고 네트워크 직경이 N-1인 노드 체인을 보여 주며 이는 최악의 구성입니다. 두 지표 모두 이분 대역폭이 2이고 네트워크 직경이 N=2인 노드 링(아래 그림의 오른쪽)을 고려하여 개선될 수 있습니다. 두 토폴로지 모두 매우 간단하며 다른 토폴로지로 대체되었습니다. 이제 로컬 영역 네트워크로 연결된 여러 프로세서로 구성된 느슨하게 결합된 다중 프로세서인 클러스터 컴퓨터에서 일반적으로 사용되는 팻 트리(Fat Tree)라는 토폴로지를 생각해 보세요.

클러스터 컴퓨터는 LAN을 통해 연결된 여러 프로세서로 구성된 느슨하게 연결된 컴퓨터를 의미합니다.

왼쪽: 체인; 오른쪽: 반지.

뚱뚱한 나무

아래 그림은 뚱뚱한 나무를 보여줍니다. 모든 노드는 잎에 있고, 나무의 모든 내부 노드는 메시지 라우팅 전용 라우터이며, 이러한 내부 노드를 스위치라고 합니다. 노드 A에서 노드 b로의 메시지는 먼저 A와 b의 공통 조상인 가장 가까운 노드로 전파된 다음 b로 전파됩니다. 메시지 밀도는 루트 근처에서 가장 높으며 경합과 병목 현상을 피하기 위해 노드와 그 하위 노드를 연결하는 링크 수는 루트 노드로 갈수록 점차 증가합니다. 이 전략은 루트 노드의 메시지 정체를 줄입니다.

이 예에서는 두 개의 하위 트리가 루트 노드에 연결되어 있으며 각 하위 트리에는 4개의 노드가 있으며 루트는 각 하위 트리에서 최대 4개의 메시지를 수신할 수 있습니다. 둘째, 각 하위 트리에 최대 4개의 메시지를 보내야 합니다. 이중 링크를 가정하면 루트에는 각 하위 항목에 연결되는 4개의 링크가 있어야 합니다. 마찬가지로 다음 레벨 노드에는 해당 노드와 각 하위 노드 사이에 리프당 하나의 링크씩 2개의 링크가 필요합니다. 그러므로 뿌리쪽으로 갈수록 나무가 점점 굵어지는 것을 볼 수 있으므로 이를 살찐 나무라 부른다.

네트워크 직경은 2log(N)과 동일하고 이등분 대역폭은 루트 노드를 각 하위 노드에 연결하는 최소 링크 수와 같습니다. 트리가 루트의 링크에 전혀 경합이 없도록 설계되었다고 가정하면 루트를 각 하위 트리에 연결하는 데 N=2 링크가 필요하며 이 경우 이등분 대역폭은 N=2입니다. 대부분의 실제 사례에서는 하위 트리의 모든 노드가 동시에 메시지를 보낼 확률이 낮기 때문에 N = 2 링크가 루트와 하위 노드 사이에 할당되지 않으므로 실제로는 수준당 링크 수가 줄어들 수 있습니다.

그리드 및 링

왼쪽: 그리드; 오른쪽: 반지.

이제 멀티 코어에 더 적합한 토폴로지를 살펴보면 가장 일반적인 토폴로지 중 하나는 모든 노드가 매트릭스와 같은 방식으로 연결되는 메시입니다(왼쪽 위 그림). 모서리에 있는 노드에는 2개의 이웃이 있고, 가장자리에 있는 노드에는 3개의 이웃이 있으며, 나머지 노드에는 4개의 이웃이 있습니다. 이제 메시의 직경과 이등분 대역폭을 계산합니다. 가장 긴 경로는 두 모서리 노드 사이이며 직경은 (2N2)와 같습니다. 네트워크를 두 개의 동일한 부분으로 분할하려면 행이나 열에 N 노드가 있고 이등분 대역폭이 N와 같기 때문에 그리드를 중간(수평 또는 수직)으로 분할해야 합니다. 이러한 매개변수 측면에서 메시는 체인 및 링보다 우수합니다.

불행히도 그리드 토폴로지는 본질적으로 비대칭이며 그리드 가장자리의 노드는 서로 멀리 떨어져 있습니다. 각 행과 열의 끝 사이의 교차 연결을 통해 그리드를 확장할 수 있습니다. 위 이미지의 오른쪽에 표시된 것처럼 결과 구조를 토러스라고 합니다. 이제 원환체의 속성을 살펴보면 네트워크 가장자리의 양쪽에 있는 노드는 단 한 홉 떨어져 있고 가장 긴 경로는 모든 모서리 노드와 토러스 중심에 있는 노드 사이에 있으며 직경은 다시 동일합니다(작은 추가 상수 무시) N/2+N/2=N. 링의 각 변의 길이는 N과 같습니다.

이제 네트워크를 두 개의 동일한 부분으로 나누고 수평으로 나눕니다. 따라서 캡처해야 할 수직 링크는 N개이고, 교차 링크(각 열 끝 사이의 링크)는 N개입니다. 따라서 이등분 대역폭은 2N과 같습니다.

2N 교차 링크(동작 N, 열 N)를 추가하여 토러스의 직경을 절반으로 줄이고 이등분 대역폭을 두 배로 늘립니다. 그러나 이 솔루션에는 여전히 몇 가지 문제가 있으며 이에 대해서는 나중에 자세히 설명하겠습니다.

직경을 결정할 때 우리는 각 링크의 길이가 거의 같다거나 메시지가 링크를 통과하는 데 걸리는 시간이 네트워크의 모든 링크에 대해 거의 동일하다는 암묵적인 가정을 합니다. 따라서 직경은 메시지가 통과하는 링크 수에 따라 정의됩니다. 링크를 통한 전파 시간은 일반적으로 경로에 따른 라우터의 지연에 비해 짧기 때문에 이 가정은 그다지 비현실적이지는 않습니다. 그러나 링크 지연에는 한계가 있으며 링크가 너무 길면 링과 마찬가지로 직경 정의를 수정해야 합니다. 교차 링크는 인접 노드 간의 일반 링크보다 물리적으로 N배 더 깁니다. 따라서 그리드에 비해 실제로는 직경이 크게 줄어들지 않습니다. 행 끝의 노드가 여전히 멀리 떨어져 있기 때문입니다.

다행히 이 문제는 아래와 같이 Folded Torus라는 약간 수정된 구조를 사용하여 해결할 수 있습니다. 각 행과 열의 토폴로지는 링과 같습니다. 링의 절반은 원래 그리드 토폴로지의 일부인 일반 링크로 구성되고, 나머지 절반은 그리드를 토러스로 변환하는 데 사용되는 추가 교차 링크로 구성되어 일반 링크와 교차 링크에 노드를 교대로 배치합니다. 이 전략은 축소된 토러스의 인접 노드 사이의 거리가 일반 토러스의 인접 노드 사이 거리의 두 배임을 보장하지만 행이나 열의 끝 사이의 긴 교차 링크(N 홉 길이)를 방지합니다.

네트워크의 이등분 대역폭과 직경은 토러스와 동일하게 유지됩니다. 이 경우 가장 긴 경로 역할을 할 수 있는 경로가 여러 개 있지만 모퉁이에서 중앙까지의 경로는 가장 길지 않으며 가장 긴 경로 중 하나는 반대쪽 두 모퉁이 사이에 있습니다. 접힌 링은 긴 교차 링크를 방지하기 때문에 멀티 코어 프로세서에서 선호되는 구성인 경우가 많습니다.

하이퍼큐브

이제 직경이 O(log(N))인 네트워크를 생각해 보세요. 이러한 네트워크는 많은 수의 링크를 사용하므로 멀티 코어에는 적합하지 않으며 일반적으로 대규모 클러스터 컴퓨터에 사용됩니다. 이 네트워크를 하이퍼큐브라고 합니다. 하이퍼큐브는 실제로 네트워크 계열이며 각 네트워크에는 순서가 있으며 k-차 하이퍼큐브는 Hk라고 합니다. 아래 그림과 같이 여러 토폴로지가 있습니다.

나비

마지막 것은 버터플라이 네트워크(Butterfly Network)로, 직경도 O(log(N))이지만 멀티 코어에 적합합니다. 아래 그림은 8개의 노드가 있는 나비 네트워크를 보여줍니다. 각 노드는 원으로 표시됩니다. 노드 외에도 노드 간에 메시지를 라우팅하는 스위치 세트 또는 내부 노드(직사각형으로 표시)가 있습니다. 메시지는 왼쪽에서 시작하여 스위치를 거쳐 다이어그램의 오른쪽에 도달합니다. 그래프의 가장 왼쪽과 가장 오른쪽 노드는 실제로 동일한 노드 세트입니다. 두 노드의 집합을 보여주는 다이어그램을 복잡하게 만드는 것을 피하기 위해 왼쪽에서 오른쪽으로의 교차 링크는 추가되지 않습니다.

토폴로지 비교

다음 표에서는 내부 노드(또는 스위치) 수, 링크 수, 직경 및 이등분 대역폭의 네 가지 매개변수를 사용하여 토폴로지를 비교합니다. 모든 경우에 네트워크에 메시지를 보내고 받을 수 있는 N개의 노드가 있다고 가정합니다. N은 2의 거듭제곱입니다.

토폴로지 노드 수 링크 수 직경 이분법 네트워크

체인 0 N1N1 1

반지 0 NN/2 2

뚱뚱한 나무 N12N22log(N)N/2

그리드 0 2NN2N2N

반지 0 2NN2N

접힌 반지 0 2NN2N

하이퍼큐브 0 Nlog(N)/2\로(N)N/2

나비 Nlog(N)/2N+Nlog(N)log(N)+1N/2

이 외에도 다음과 같은 유형의 네트워크 토폴로지가 있습니다.

19.8 I/O 및 저장 장치

컴퓨터에는 I/O(입/출력) 시스템이 필요합니다. 다음 그림은 일반적인 컴퓨터의 구조를 보여줍니다.

전형적인 컴퓨터 시스템.

I/O 채널의 두 가지 아키텍처.

프로세서는 컴퓨터의 심장이며 일련의 I/O 장치에 연결되어 사용자 입력을 처리하고 결과를 표시합니다. 이러한 I/O 장치를 주변 장치라고 합니다. 가장 일반적인 사용자 입력 장치는 키보드와 마우스이고, 가장 일반적인 디스플레이 장치는 모니터와 프린터입니다. 컴퓨터는 또한 일련의 범용 I/O 포트를 통해 카메라, 스캐너, MP3 플레이어, 캠코더, 마이크, 스피커와 같은 다른 많은 장치와 통신할 수 있습니다. I/O 포트에는 다음이 포함됩니다.

  • 프로세서를 외부 장치에 연결하는 데 도움이 되는 금속 핀 세트입니다.

  • 주변 장치에 대한 연결을 관리하는 포트 컨트롤러입니다. 컴퓨터는 유선 또는 무선 연결을 통해 다른 컴퓨터와 통신하는 회로가 포함된 네트워크 카드라는 특수 주변 장치를 통해 외부 세계와도 통신할 수 있습니다.

I/O 포트는 외부 장치에서 제공하는 커넥터에 연결하는 데 사용되는 금속 핀 세트로 구성됩니다. 각 포트는 통신 링크에서 데이터 교환을 조정하는 포트 컨트롤러와 연결됩니다.

이 장에서는 특정 클래스의 장치, 즉 저장 장치에 특별한 우선순위를 부여합니다. 하드 드라이브 및 플래시 드라이브와 같은 저장 장치는 시스템 전원이 꺼진 후에도 데이터를 영구적으로 저장할 수 있도록 도와줍니다. 이 장에서 저장 장치를 강조하는 이유는 저장 장치가 컴퓨터 아키텍처의 필수적인 부분이고 주변 장치의 특성이 컴퓨터마다 다르기 때문입니다. 그러나 소형 휴대폰에서 대형 서버에 이르기까지 모든 컴퓨터에는 프로그램이 실행되는 동안 파일, 시스템 구성 데이터 및 스왑 공간을 보관하는 데 사용되는 일종의 영구 저장소가 있습니다. 따라서 건축가는 스토리지 시스템의 설계 및 최적화에 특별한 주의를 기울입니다.

19.8.1 I/O 시스템 개요

이제 I/O 장치의 정확한 세부 사항에서 벗어나 디자이너가 컴퓨터 시스템을 설계할 때 가능한 모든 유형의 I/O 장치를 고려하는 것은 불가능하며, 설사 그렇게 한다고 하더라도 2005년에는 존재하지 않았던 Apple iPad와 같은 태블릿과 같이 컴퓨터가 판매된 후에 새로운 종류의 장치가 나타날 가능성이 있습니다. 그럼에도 불구하고 iPad와 구형 컴퓨터 간의 데이터 전송은 대부분의 디자이너가 컴퓨터 시스템에 표준 인터페이스를 제공하기 때문에 여전히 가능합니다. 예를 들어, 일반적인 데스크탑이나 노트북 컴퓨터에는 USB 포트 세트가 있습니다. USB 사양을 준수하는 모든 장치는 USB 포트에 연결될 수 있으며 호스트 컴퓨터와 통신할 수 있습니다. 마찬가지로 노트북에는 모든 모니터에 연결할 수 있는 범용 DVI 포트가 있으므로 거의 모든 모니터나 프로젝터를 노트북에 연결할 수 있습니다. 노트북 회사는 프로세서와 포트 간에 데이터를 원활하게 전송하는 DVI 포트를 구현하여 DVI 사양을 준수합니다. 유사한 라인에서 모니터 회사는 모니터가 DVI 포트에서 전송된 모든 데이터를 원활하게 표시할 수 있도록 보장하여 DVI 사양의 해당 부분을 고수합니다. 따라서 컴퓨터가 주변 장치에 대한 고정된 인터페이스 세트를 지원하고 런타임 시 모든 주변 장치에 연결할 수 있는지 확인해야 합니다.

포트의 사양을 구현하여 범용 I/O 장치를 연결할 수 있다고 해서 반드시 해당 I/O 장치가 작동하는 것은 아닙니다. 예를 들어 프린터는 항상 USB 포트에 연결될 수 있지만 프린터를 작동하려면 소프트웨어 수준의 추가 지원이 필요하기 때문에 프린터에서 페이지를 인쇄하지 못할 수도 있습니다. 이 지원은 운영 체제의 프린터 장치 드라이버에 내장되어 있으며 사용자 프로그램에서 프린터로 데이터를 효율적으로 전송합니다.

따라서 소프트웨어와 하드웨어의 역할을 명확히 구분할 필요가 있습니다. 먼저 소프트웨어를 살펴보겠습니다. 대부분의 운영 체제에서는 I/O 장치에 액세스하기 위해 매우 간단한 사용자 인터페이스가 필요합니다. 예를 들어, Linux 운영 체제에는 다음과 같이 읽기와 쓰기라는 두 가지 시스템 호출이 있습니다.

cpp
read(int file_descriptor, void *buffer, int num_bytes)
write(int file_descriptor, void *buffer, int num_bytes)

Linux는 모든 장치를 파일로 취급하고 해당 장치에 파일 설명자를 할당합니다. 파일 설명자는 첫 번째 매개변수이며 장치의 ID를 지정합니다. 두 번째 매개변수는 데이터 소스나 대상이 포함된 메모리 영역을 가리키며, 마지막 매개변수는 전송해야 하는 바이트 수를 나타냅니다. 사용자의 관점에서 보면 이것이 완료되어야 할 전부입니다. 이는 운영 체제의 장치 드라이버와 나머지 프로세스를 조정하는 하드웨어의 작업이며, 이 방법은 I/O 장치에 액세스하는 매우 다양한 방법임이 입증되었습니다.

불행하게도 운영 체제는 더 많은 작업을 수행해야 합니다. 각 I/O 호출에 대해 적절한 장치 드라이버를 찾아 요청을 전달해야 합니다. 동일한 I/O 장치에 액세스하려고 하는 여러 프로세스가 있을 수 있으며, 이 경우 서로 다른 요청을 올바르게 예약해야 합니다.

장치 드라이버의 작업은 로컬 하드웨어와 인터페이스하고 필요한 작업을 수행하는 것입니다. 일반적으로 조립 지침을 사용하여 하드웨어 장치와 통신합니다. 먼저 자체 상태를 평가하고 유휴 상태인 경우 주변 장치에 필요한 작업을 수행하도록 요청하여 스토리지 시스템과 주변 장치 간의 데이터 전송 프로세스를 시작합니다.

아래 다이어그램은 위의 논의를 요약한 것입니다. 다이어그램의 위쪽 부분에는 소프트웨어 모듈(응용 프로그램, 운영 체제, 장치 드라이버)이 표시되고 다이어그램의 아래쪽 부분에는 하드웨어 모듈이 표시됩니다. 장치 드라이버는 I/O 명령을 사용하여 프로세서와 통신하고 프로세서는 명령을 적절한 I/O 장치로 라우팅합니다. I/O 장치에 프로세서로 보낼 데이터가 있으면 인터럽트를 보낸 다음 인터럽트 서비스 루틴이 데이터를 읽고 이를 애플리케이션에 전달합니다.

I/O 시스템(하드웨어 및 소프트웨어).

실제로 전체 I/O 프로세스는 매우 복잡한 프로세스입니다. 이 장은 장치 드라이버의 연구 및 설계에 전념하며 I/O 시스템의 하드웨어 부분에 대해서만 논의하고 필요한 소프트웨어 지원을 대략적으로 이해합니다. I/O 시스템의 소프트웨어와 하드웨어 구성 요소 간의 중요한 차이점은 다음과 같습니다.

  • I/O 시스템의 소프트웨어 구성요소는 응용 프로그램과 운영 체제로 구성됩니다. 응용 프로그램에는 일반적으로 I/O 장치에 액세스하기 위한 매우 간단한 인터페이스가 제공되며 운영 체제의 역할은 다양한 응용 프로그램의 I/O 요청을 조합하고 적절하게 예약한 다음 적절한 장치 드라이버에 전달하는 것입니다.

  • 장치 드라이버는 특수 조립 지침을 통해 하드웨어 장치와 통신합니다. 프로세서와 I/O 장치 간의 데이터 전송, 제어 및 상태 정보를 조정합니다.

  • 하드웨어(프로세서 및 관련 회로)의 역할은 단순히 운영 체제와 전용 I/O 포트에 연결된 I/O 장치 간의 메신저 역할을 하는 것입니다. 예를 들어, 디지털 카메라를 USB 포트에 연결하면 프로세서는 연결된 장치의 세부 사항을 알지 못하며 유일한 역할은 카메라의 장치 드라이버와 카메라에 연결된 USB 포트 간의 원활한 통신을 보장하는 것입니다.

  • I/O 장치는 현재 실행 중인 프로그램을 중단하고 인터럽트 서비스 루틴을 호출하는 인터럽트를 전송하여 프로세서와의 통신을 시작할 수 있습니다. 인터럽트 서비스 루틴은 해당 장치 드라이버에 제어권을 전달하고, 그러면 해당 장치 드라이버가 인터럽트를 처리하고 적절한 조치를 취합니다.

I/O 시스템의 하드웨어 구성 요소 아키텍처는 아래에서 자세히 설명합니다.

19.8.1.1 I/O 시스템 요구사항

이제 I/O 시스템의 아키텍처를 설계해 보십시오. 다음 표에는 지원하려는 모든 장치와 해당 대역폭 요구 사항이 나열되어 있습니다. 가장 많은 대역폭이 필요한 구성 요소는 그래픽 카드에 연결되고 이미지 및 비디오 데이터를 처리하는 그래픽 프로세서가 포함된 디스플레이 장치(모니터, 프로젝터, TV)입니다.

장비 버스 기술 대역폭 일반적인 값

디스플레이 장치 PCI 익스프레스(버전 4) 높은 1~10GB/초

하드 드라이브 ATA/SCSI/SAS 안으로 150-600MB/초

네트워크 카드(유/무선) PCI 익스프레스 버스 안으로 10-100MB/초

USB 장치 USB(범용 직렬 버스) 안으로 60-625MB/초

DVD 오디오/비디오 PCI(개인용 컴퓨터 인터페이스) 안으로 1~4MB/초

스피커/마이크 AC'97/인텔 하이. 데프. 오디오 낮음 100KB/초 ~ 3MB/초

키보드/마우스 USB, PCI 매우 낮음 10-100B/초

I/O 장치를 논의할 때 카드라는 용어가 자주 사용된다는 점에 유의하십시오. 카드는 특정 기능을 수행하기 위해 컴퓨터의 I/O 시스템에 연결할 수 있는 인쇄 회로 기판(PCB)입니다. 예를 들어, 그래픽 카드는 이미지와 비디오를 처리하는 데 도움이 되고, 사운드 카드는 HD 오디오를 처리하는 데 도움이 되며, 네트워크 카드는 네트워크 연결에 도움이 될 수 있습니다. 아래에는 네트워크 카드 사진이 나와 있는데, 여기서는 외부 장치를 카드에 연결하기 위한 포트 세트와 함께 인쇄 회로 기판에 상호 연결된 칩 세트를 볼 수 있습니다.

그래픽 카드 외에도 CPU에 연결해야 하는 또 다른 고대역폭 장치는 약 10~20GB/s의 대역폭을 갖는 메인 메모리입니다. 따라서 메인 메모리와 그래픽 카드에 대한 특수 처리를 갖춘 I/O 시스템을 설계해야 합니다.

나머지 장치는 대역폭 요구 사항이 상대적으로 낮습니다. 하드 드라이브, USB 장치 및 네트워크 카드는 500-600MB/s로 제한되며 키보드, 마우스, CD-DVD 드라이브 및 오디오 주변 장치는 대역폭 요구 사항이 매우 낮습니다(4MB/s 미만).

19.8.1.2 I/O 시스템 설계

위의 표를 종합해보면 USB, PCI Express, SATA 등 다양한 유형의 버스 기술이 있음을 알 수 있습니다. 버스는 I/O 시스템에 있는 두 개 이상의 구성 요소 사이의 링크로 정의됩니다. 우리는 다양한 유형의 I/O 장치를 연결하기 위해 다양한 유형의 버스를 사용합니다. 예를 들어 USB 버스를 사용하여 펜 드라이브 및 카메라와 같은 USB 장치를 연결하고, SATA 또는 SCSI 버스를 사용하여 하드 드라이브에 연결합니다. 다양한 유형의 버스를 사용하는 이유는 다음과 같습니다.

  • 다양한 I/O 장치에는 대역폭 요구 사항이 매우 다릅니다. 그래픽 카드에는 초고속 버스를 사용해야 하는 반면, 키보드와 마우스에는 전체 대역폭 요구 사항이 최소화되므로 더 간단한 버스 기술이 필요합니다.

  • 역사적 이유. 역사적으로 하드 디스크 공급업체는 항상 SATA 또는 IDE 버스를 사용해 왔고, 그래픽 카드 공급업체는 항상 AGP 버스를 사용해 왔습니다. 2010년 이후 그래픽 카드 공급업체는 PCI Express 버스로 전환했습니다.

여러 요인의 조합으로 인해 I/O 시스템 설계자는 여러 버스를 지원해야 합니다.

버스는 여러 장치를 병렬로 연결하는 데 사용되는 전선 그룹입니다. 장치는 버스를 사용하여 서로 간에 데이터를 전송하고 신호를 제어할 수 있습니다.

이제 버스의 구조에 대해 더 자세히 살펴보겠습니다. 버스는 단순히 두 끝점 사이의 구리선 세트가 아니라 실제로 수백 페이지에 달하는 사양을 갖춘 매우 복잡한 구조입니다. 전기적 특성, 오류 제어, 송신기 및 수신기 회로, 속도, 전력 및 대역폭에 중점을 두어야 합니다. 이번 장에서는 고속버스에 대해 논의할 수 있는 충분한 기회를 갖게 될 것입니다. 버스에 연결된 각 노드(소스 또는 대상)에는 데이터를 보내고 받기 위해 버스 컨트롤러가 필요합니다. 버스의 설계는 매우 복잡하지만 단일 소스에서 일련의 대상으로 바이트를 원활하고 안정적으로 전송할 수 있는 논리적 링크로 추상화할 수 있습니다.

컴퓨터의 I/O 시스템을 설계하려면 먼저 금속 핀이나 소켓 세트로 구성된 외부 I/O 포트를 제공해야 합니다. 이 I/O 포트는 외부 장치를 연결하는 데 사용할 수 있습니다. 각 포트에는 장치와 인터페이스하는 전용 포트 컨트롤러가 있습니다. 그런 다음 포트 컨트롤러는 위 표에 나열된 버스 중 하나를 사용하여 CPU에 데이터를 보내야 합니다.

여기서 주요 설계 문제는 다음과 같은 여러 가지 이유로 I/O 버스를 통해 CPU를 모든 I/O 포트에 연결할 수 없다는 것입니다.

  1. CPU가 각 I/O 포트에 연결된 경우 CPU에는 각 버스 유형에 대한 버스 컨트롤러가 장착되어야 하며 이로 인해 CPU의 복잡성, 면적 및 전력 소비가 증가합니다.

  2. CPU 출력 핀 수는 제한되어 있습니다. CPU가 I/O 장치 호스트에 연결된 경우 모든 I/O 버스를 지원하려면 많은 수의 추가 핀이 필요하며 대부분의 CPU에는 일반적으로 이 기능을 지원하는 데 충분한 핀이 없습니다.

  3. 비즈니스 관점에서는 CPU를 다양한 컴퓨터에서 사용할 수 있도록 CPU 설계와 I/O 시스템 설계를 분리하는 것이 좋습니다.

따라서 대부분의 프로세서는 하나의 버스에만 연결되거나 최대 2~3개의 버스에 연결됩니다. 프로세서를 다른 I/O 버스 마스터에 연결하려면 보조 칩을 사용해야 합니다. I/O 장치의 트래픽을 집계하고 CPU에서 생성된 데이터를 올바른 I/O 장치로 올바르게 라우팅해야 하며 그 반대의 경우도 마찬가지입니다. 이러한 추가 칩에는 해당 프로세서의 칩셋이 포함되며, 칩셋의 칩은 마더보드라고 하는 인쇄 회로 기판에 상호 연결됩니다.

칩셋: 메인 CPU가 메인 메모리, I/O 장치에 연결하고 시스템 관리 기능을 수행하는 데 필요한 칩 세트입니다.

마더보드: 칩셋의 모든 칩은 마더보드라고 불리는 인쇄 회로 기판에서 서로 연결됩니다.

대부분의 프로세서 칩셋에는 일반적으로 아래 그림과 같이 North BridgeSouth Bridge라는 두 가지 중요한 칩이 있습니다. CPU는 FSB(프런트 사이드 버스)를 사용하여 DRAM 메모리 모듈, 그래픽 카드 및 Southbridge 칩에 연결되는 Northbridge 칩에 연결됩니다. 이와 대조적으로 Southbridge 칩은 훨씬 느린 I/O 장치를 처리하고 키보드 및 마우스, 오디오 장치, 네트워크 카드 및 하드 드라이브를 포함한 모든 USB 장치에 연결되도록 설계되었습니다.

I/O 시스템 아키텍처.

완전성을 기하기 위해 컴퓨터 시스템의 다른 두 가지 일반적인 버스 유형부터 시작하겠습니다.

  • 뒷면 버스. CPU를 L2 캐시에 연결하는 데 사용됩니다. 초기 프로세서는 백엔드 버스를 통해 통신하는 오프칩 L2 캐시를 사용했습니다. 오늘날의 L2 캐시는 온칩으로 이동되었으므로 백엔드 버스도 온칩에 있습니다. 이는 일반적으로 코어 주파수에서 클럭되며 매우 빠른 버스입니다.

  • 백플레인 버스. 일반적으로 여러 개의 마더보드와 하드 드라이브와 같은 주변 장치가 있는 대형 컴퓨터나 저장 시스템에 사용됩니다. 이러한 모든 엔터티는 단일 백플레인 버스에 병렬로 연결되며, 백플레인 버스 자체는 여러 개의 병렬 구리선과 장치를 연결하는 데 사용할 수 있는 커넥터 세트로 구성됩니다.

전면 버스: CPU를 메모리 컨트롤러(Intel 시스템에서는 Northbridge 칩)에 연결하는 버스입니다.

후면 버스: CPU를 두 번째 수준 캐시에 연결하는 버스입니다.

백플레인 버스: 여러 마더보드, 스토리지 및 주변 장치를 연결하는 시스템 전체 버스입니다.

Northbridge와 Southbridge 칩 모두 연결된 모든 버스에 대한 버스 컨트롤러가 필요하며, 각 버스 컨트롤러는 관련 버스에 대한 액세스를 조정합니다. 패킷을 성공적으로 수신한 후 패킷을 대상(CPU 또는 I/O 장치 쪽으로)으로 보냅니다. 이러한 칩은 다양한 종류의 버스를 상호 연결하고 대상 버스가 사용 중일 때 데이터 값을 일시적으로 버퍼링하므로 브리지(버스 간 브리지)라고 합니다.

메모리 컨트롤러는 Northbridge 칩의 일부이며 주 메모리에 대한 읽기/쓰기 요청을 구현합니다. 지난 몇 년 동안 프로세서 공급업체에서는 메모리 컨트롤러를 메인 CPU 칩으로 옮기고 이를 더욱 복잡하게 만들기 시작했습니다. 메모리 컨트롤러에 대한 대부분의 개선 사항은 주 메모리 전력 감소, 새로 고침 주기 수 감소 및 성능 최적화에 중점을 두었습니다. Intel Sandybridge 프로세서부터 시작하여 그래픽 프로세서도 칩으로 이동했습니다. CPU 칩에 넣는 이유는 다음과 같습니다.

  • 추가 트랜지스터를 사용할 수 있습니다.

  • 온칩 통신은 오프칩 통신보다 훨씬 빠릅니다. 또한 많은 임베디드 프로세서는 대부분의 사우스브리지 칩, 포트 컨트롤러 및 CPU를 단일 칩에 통합하여 마더보드 크기를 줄이고 I/O 컨트롤러와 CPU 간의 보다 효율적인 통신을 가능하게 합니다. 이러한 유형의 시스템을 SOC(System on Chip)라고 합니다.

**SOC(System on Chip)는 일반적으로 메인 프로세서와 I/O 시스템의 대부분의 칩을 포함하여 컴퓨팅 시스템의 모든 관련 부품을 단일 칩에 패키지합니다.

19.8.1.3 I/O 시스템의 레이어

대부분의 복잡한 아키텍처는 일반적으로 인터넷 아키텍처와 마찬가지로 여러 레이어(레이어)로 나누어지며, 한 레이어는 기본적으로 다른 레이어와 독립적입니다. 따라서 표준 인터페이스를 준수하는 한 어떤 방식으로든 구현하도록 선택할 수 있습니다. 최신 컴퓨터의 I/O 아키텍처도 매우 복잡하므로 기능을 여러 계층으로 나누어야 합니다.

I/O 시스템의 기능을 대략적으로 네 가지 계층으로 나눌 수 있습니다. 주로 7계층 OSI 모델(WAN의 기능 계층을 나누는 데 사용됨)에서 영감을 받아 I/O 시스템의 기능을 여러 계층으로 나눕니다.

  • 물리적 계층: 버스의 물리적 계층은 주로 버스의 전기적 사양을 정의합니다. 이는 전송 하위 계층과 동기화 하위 계층이라는 두 개의 하위 계층으로 나뉩니다.

전송 하위 계층은 전송 비트의 사양을 정의합니다. 예를 들어, 한 버스는 액티브 하이(전압이 높으면 로직 1)가 되고 다른 버스는 액티브 로우(전압이 0이면 로직 1)가 될 수 있습니다. 오늘날의 고속 버스는 두 개의 구리 와이어를 사용하여 단일 비트를 전송할 수 있는 고속 차동 신호를 사용하며, 두 와이어 간의 전압 차이 신호를 모니터링하여 논리 0 또는 1을 추론합니다(SRAM 셀의 비트 라인 개념과 유사). 최신 버스는 이 아이디어를 확장하고 전기 신호의 조합을 사용하여 논리 비트를 인코딩합니다.

동기화 하위 계층은 신호 타이밍과 버스의 수신기가 보낸 데이터를 복구하는 방법을 지정합니다.

  • 데이터 링크 계층: 데이터 링크 계층은 주로 물리 계층에서 읽은 논리 비트를 처리하고, 비트 세트를 프레임으로 그룹화하고, 오류 검사를 수행하고, 버스에 대한 액세스를 제어하고, I/O 트랜잭션 구현을 돕는 데 사용됩니다. 특히, 특정 시점에 단 하나의 엔터티만 버스에서 신호를 전송할 수 있도록 보장하고 공통 메시징 패턴을 활용하는 특수 기능을 구현합니다.

  • 네트워크 계층: 이 계층은 주로 칩셋의 다양한 칩을 통해 프로세서에서 I/O 장치로 또는 그 반대로 프레임 세트를 성공적으로 전송하는 것과 관련됩니다. 우리는 I/O 장치의 주소를 고유하게 정의하고 I/O 명령에 I/O 장치 주소를 포함시키는 방법을 고려했습니다. 광범위하게 말하면 I/O 포트 기반 주소 지정과 메모리 매핑 주소 지정이라는 두 가지 접근 방식이 논의됩니다. 후자의 경우 I/O 장치에 대한 액세스는 지정된 메모리 위치에 대한 일반적인 액세스로 처리됩니다.

  • 프로토콜 레이어: 최상위 레이어는 프로토콜 레이어라고 하며 메시지 의미 측면에서 프로세서와 I/O 장치 간의 상위 수준 통신 방법을 포함하여 I/O 요청의 엔드투엔드 실행을 담당합니다. 예를 들어, I/O 장치는 프로세서를 중단할 수 있거나 프로세서는 각 I/O 장치의 상태를 명시적으로 요청할 수 있습니다. 둘째, 프로세서와 장치 사이에 데이터를 전송하기 위해서는 데이터를 직접 전송하거나 DMA 컨트롤러라고 불리는 칩셋 내 특수 칩에 데이터 전송 책임을 위임할 수 있다.

아래 그림은 일반적인 프로세서의 4계층 I/O 아키텍처를 요약합니다.

I/O 시스템의 4개 레이어.

19.8.2 물리 계층: 전송 하위 계층

물리 계층은 I/O 시스템의 가장 낮은 계층이며 소스와 수신기 간의 물리적 신호 전송을 포함합니다. 두 개의 하위 계층으로 나눌 수 있습니다. 첫 번째 하위 계층은 소스에서 대상으로 비트 전송을 처리하는 전송 하위 계층입니다. 이 하위 계층에는 링크의 전기적 특성(전압, 저항, 커패시턴스)과 전기 신호를 사용하여 논리 비트(0 또는 1)를 나타내는 방법이 포함됩니다.

동기화 하위 계층이라고 하는 두 번째 하위 계층에는 물리적 링크에서 전체 비트 프레임을 읽는 작업이 포함됩니다. 여기서 프레임은 특수 마커로 구분된 비트 집합으로 정의됩니다. I/O 채널은 지터(예측할 수 없는 신호 전파 시간)로 인해 어려움을 겪기 때문에 수신기에 도착하는 데이터를 올바르게 동기화하고 각 프레임을 올바르게 읽어야 합니다.

이 섹션에서는 전송 하위 계층에 대해 설명하고 다음 섹션에서는 동기화 하위 계층에 대해 설명합니다.

여러 레이어 대신 여러 하위 레이어를 만드는 이유는 하위 레이어가 서로 독립적일 필요가 없기 때문입니다. 이론적으로는 모든 물리 계층과 기타 데이터 링크 계층 프로토콜을 사용할 수 있으며 이상적으로는 서로를 완전히 잊어야 합니다. 그러나 전송 하위 계층과 동기화 하위 계층은 강력하게 연결되어 있으므로 별도의 계층으로 분리할 수 없습니다.

다음 그림은 I/O 링크의 일반적인 보기를 보여줍니다. 소스(송신기)는 일련의 비트를 대상(수신기)으로 보내고, 전송하는 동안 데이터는 항상 소스의 클럭과 동기화됩니다. 즉, 소스가 1GHz에서 실행 중이면 1GHZ의 속도로 비트를 전송합니다. 소스의 주파수는 데이터를 전송하는 프로세서 또는 I/O 요소의 주파수와 반드시 동일하지는 않습니다. 전송 회로는 일반적으로 자신이 속한 모듈의 클록에서 파생된 클록을 갖는 별도의 하위 모듈입니다. 예를 들어 프로세서의 전송 회로는 500MHz에서 데이터를 전송할 수 있지만 프로세서는 4GHz에서 실행될 수 있습니다. 어떤 경우든 송신기가 버스 주파수라고도 알려진 내부 클록 속도로 데이터를 전송한다고 가정합니다. 이 속도는 일반적으로 프로세서나 칩셋의 다른 칩의 클록 주파수보다 낮습니다. 수신기는 동일한 주파수에서 작동하거나 더 빠른 주파수를 사용할 수 있으며, 명시적으로 명시하지 않는 한 소스와 대상이 동일한 주파수에 있다고 가정하지 않습니다. 마지막으로, 송신자, 소스, 송신기라는 용어를 같은 의미로 사용하고 대상과 수신자라는 용어도 같은 의미로 사용합니다.

I/O 링크의 공통 보기.

19.8.2.1 단일 종단 신호

이제 소스에서 대상으로 일련의 펄스를 전송하여 1과 0의 시퀀스를 보내는 간단한 방법을 고려하십시오. 이 신호 방법을 단일 종단 신호라고 하며 가장 간단한 방법입니다.

특정 상황에서는 고전압 펄스를 1과 연관시키고 저전압 펄스를 0과 연관시킬 수 있습니다(액티브 하이라고 알려진 규칙). 또는 저전압 펄스를 로직 1과 연관시키고 고전압 펄스를 로직 0과 연관시킬 수 있습니다. 대신 이러한 방식을 액티브 로우(Active Low)라고 합니다. 이 두 가지 규칙은 아래 그림에 나와 있습니다.

고수준 및 저수준 능동 신호 방식.

안타깝게도 두 방법 모두 매우 느리고 구식입니다. 이전 장의 SRAM 셀에 대한 논의를 떠올려 보면, 고속 I/O 버스는 로직 0과 1 사이의 전압 차이를 가능한 가장 낮은 값으로 줄여야 합니다. 전압 차이는 내부 커패시터로 감지기를 충전한 후에 감지되기 ​​때문입니다. 필요한 전압이 높을수록 커패시터를 충전하는 데 시간이 더 오래 걸립니다. 전압 차이가 1V인 경우 0에서 1로의 전환을 감지하는 데 오랜 시간이 걸리므로 버스 속도가 제한됩니다. 그러나 전압 차이가 30mV이면 전압 천이를 더 빨리 감지할 수 있어 버스 속도가 빨라진다.

따라서 현대 버스 기술은 논리 0과 논리 1 사이의 전압 차이를 가능한 가장 낮은 값으로 줄이려고 시도합니다. 버스 속도를 높이기 위해 논리 0과 논리 1 사이의 전압 차이를 임의로 줄일 수는 없습니다. 예를 들어, 여러 요인으로 인해 시스템에 일정량의 전기적 노이즈가 발생하기 때문에 필요한 전압 차이를 0.001mV로 설정할 수 없습니다. 자동차나 컴퓨터 스피커를 켰을 때 휴대전화가 울리기 시작하면 스피커에서도 약간의 소음이 들릴 것입니다. 전자레인지가 작동하는 동안 휴대폰을 전자레인지 옆에 놓으면 전자기 간섭으로 인해 휴대폰의 음질이 저하됩니다. 마찬가지로 프로세서에 전자기 간섭이 존재할 수 있으며 전압 스파이크가 발생할 수 있습니다. 이 전압 스파이크의 최대 진폭이 20mV라고 가정하면 0과 1 사이의 전압 차이는 20mV보다 커야 합니다. 그렇지 않으면 간섭으로 인한 전압 스파이크로 인해 신호 값이 반전되어 오류가 발생할 수 있습니다. 다음 섹션에서는 온칩 시그널링의 가장 일반적인 기술 중 하나인 LVDS에 대해 간략하게 설명합니다.

19.8.2.2 저전압 차동 신호(LVDS)

LVDS는 두 개의 와이어를 사용하여 단일 신호를 전송합니다. 이러한 전선의 전압 차이를 모니터링하십시오. 전달된 값은 전압 차이의 부호를 통해 추론됩니다.

기본 LVDS 회로는 아래 그림에 나와 있습니다. 3.5mA의 고정 전류 소스가 있습니다. 입력 a의 값에 따라 전류는 라인 1 또는 라인 2를 통해 목적지로 흐른다. 예를 들어, a가 1이면 트랜지스터 T1이 도통되기 시작하고 T2가 꺼지기 때문에 라인 1을 통해 전류가 흐른다. 이 경우 전류는 목적지에 도달하여 저항 Rd를 통과한 다음 라인 2를 통해 다시 흐릅니다. 일반적으로 전류가 흐르지 않을 때 두 라인의 전압은 1.2V로 유지됩니다. 전류가 흐르면 전압 스윙이 발생합니다. 전압 스윙은 3.5mA x Rd와 같습니다. Rd는 일반적으로 100옴입니다. 따라서 총 차동 전압 스윙은 350mV입니다. 검출기의 기능은 전압차의 부호를 검출하는 것입니다. 양수이면 논리 1을 선언할 수 있습니다. 그렇지 않으면 논리 0을 선언할 수 있습니다. LVDS는 낮은 스윙 전압(350mV)으로 인해 매우 빠른 물리 계층 프로토콜입니다.

LVDS 회로.

19.8.2.3 다중 비트 전송

이제 여러 비트를 순차적으로 전송하는 문제를 고려하십시오. 대부분의 I/O 채널은 데이터를 전송할 때만 항상 사용 중이 아니므로 듀티 사이클(장치가 실행되는 시간의 비율)은 매우 가변적이며 대부분의 경우 그다지 높지 않은 경향이 있습니다. 그러나 감지기는 거의 항상 켜져 있어 버스의 전압을 지속적으로 감지하므로 잠재적으로 전력 소비와 정확도에 영향을 미칠 수 있습니다. 감지기가 매 사이클마다 로직 1 또는 0을 감지하고 데이터를 처리하려면 더 높은 레벨의 레이어가 필요하기 때문에 전력이 문제가 됩니다. 이를 방지하기 위해 대부분의 시스템에는 일반적으로 데이터 비트가 유효한지 또는 유효하지 않은지를 나타내는 추가 라인이 있습니다. 이 라인은 전통적으로 스트로브라고 불립니다. 송신기는 플래시 제어 값을 설정하여 수신기에 데이터의 유효 기간을 표시할 수 있습니다. 마찬가지로 데이터 라인과 플래시 제어도 동기화해야 합니다. 데이터 라인의 신호와 플래시 제어가 서로 다른 지연 양에 의해 영향을 받을 수 있기 때문에 고속 I/O 버스에서는 이것이 점점 더 어려워집니다. 따라서 두 라인이 동기화되지 않을 수 있으며 0, 1 및 유휴의 세 가지 유형의 신호를 정의하는 것이 좋습니다. 여기서 0과 1은 버스에서 논리 0과 1의 전송을 나타내는 반면 유휴 상태는 신호가 전송되지 않는다는 사실을 나타냅니다. 이 신호 모드는 세 가지 상태가 사용되므로 삼항 신호라고도 합니다.

LVDS를 사용하면 삼항 신호를 쉽게 구현할 수 있습니다. 각각 LVDS A와 B의 전선을 호출하면 VA는 A선의 전압이고 VB는 B선의 전압입니다. 다음과 같은 상황으로 나뉩니다.

  • \left|V_{A}-V_{B}\right|&lt;\tau(여기서 τ는 감지 임계값)인 경우 회선이 유휴 상태이고 아무것도 전송되지 않는 것으로 추론됩니다.

  • |VAVB|>τ인 경우 논리 1이 전송되고 있음을 의미합니다.

  • |VBVA|>τ인 경우 논리 0이 전송되고 있음을 의미합니다. 따라서 기본 LVDS 프로토콜을 변경할 필요가 없습니다.

다음은 물리 계층에서 여러 비트를 전송하는 데 최적화된 일련의 기술입니다.

19.8.2.4 제로(RZ) 프로토콜로 복귀

이 프로토콜은 펄스(양수 또는 음수)를 보낸 다음 1비트 기간 동안 일시 중지됩니다. 여기서 비트 주기는 비트를 전송하는 데 필요한 시간으로 정의할 수 있습니다. 대부분의 I/O 프로토콜은 비트 주기가 전송되는 비트 값(0 또는 1)과 무관하다고 가정합니다. 일반적으로 1비트 기간은 1개의 I/O 클럭 사이클 길이와 같습니다. I/O 클럭은 I/O 시스템 요소에서 사용되는 전용 클럭입니다. 우리는 용어 간의 차이를 강조하지 않고 클록 사이클과 비트 기간이라는 용어를 같은 의미로 사용할 것입니다.

RZ 프로토콜에서 로직 1을 전송하려는 경우 짧은 비트 기간 동안 링크에 양의 전압 펄스를 전송한 다음 펄스 전송을 중지하고 링크의 전압이 유휴 상태로 돌아가는지 확인합니다. 마찬가지로 논리 0을 전송할 때 짧은 주기 동안 라인을 따라 음의 전압 펄스를 보낸 다음 라인이 유휴 상태로 돌아올 때까지 기다립니다. 이는 커패시터가 방전되도록 허용하거나 역전압을 적용하여 라인을 유휴 상태로 전환함으로써 달성될 수 있습니다. 어떤 경우든 중요한 점은 전송할 때 비트 기간의 특정 부분에 대한 실제 값을 전송한 다음 회선이 유휴 상태로 간주될 수 있는 기본 상태로 돌아가도록 허용한다는 것입니다. 유휴 상태로 돌아가는 것은 수신기 회로가 송신기의 클록과 동기화하는 데 도움이 되므로 데이터를 올바르게 읽을 수 있습니다. 여기서 암시적인 가정은 송신기가 매 사이클(송신기 사이클)마다 1비트를 보낸다는 것입니다. 송신기와 수신기의 클럭 주기는 다를 수 있습니다.

아래 그림은 삼항 신호를 사용하는 RZ 프로토콜의 예를 보여줍니다. 바이너리 신호를 사용하려는 경우 다음과 같은 대안이 있습니다.

  • 로직 1의 경우 한 사이클에 짧은 펄스를 보낼 수 있습니다.

  • 논리 0의 경우 신호가 전송되지 않습니다.

가장 큰 문제는 로직 1을 보낸 후 휴지 시간을 보고 로직 0이 전송되고 있는지 여부를 결정하려면 수신기 끝 부분에 복잡한 회로가 필요하다는 것입니다.

Return to Zero(RZ) 프로토콜(예)

그러나 RZ(Return to Zero) 방법의 가장 큰 단점은 대역폭 낭비이며 논리 0 또는 1의 전송 후 짧은 일시 중지(유휴 기간)가 필요하다는 것입니다. 이러한 제한을 받지 않는 프로토콜을 설계할 수 있는 것으로 나타났습니다.

19.8.2.5 맨체스터 인코딩

맨체스터 인코딩을 논의하기 전에 물리적 비트와 논리적 비트를 구별해 보겠습니다. 지금까지 우리는 같은 의미라고 생각했지만, 이제부터는 더 이상 그렇지 않습니다. 물리적 비트(예: 물리적 1 또는 0)는 링크 전체의 전압을 나타냅니다. 예를 들어 액티브 하이 시그널링 방법에서 높은 전압은 비트 1이 전송되고 있음을 나타내고 낮은 전압(물리적 비트 0)은 0 비트가 전송되고 있음을 나타냅니다. 그러나 논리 비트(논리 0 또는 1)가 물리 비트 값의 함수라고 가정하면 더 이상 그렇지 않습니다. 즉, 현재 및 이전 물리 비트가 10이고 논리 0이 추론될 수 있다고 가정하면 논리 1을 추론하는 데에도 여러 가지 규칙이 있을 수 있습니다. 수신기의 작업은 물리 신호(또는 물리 비트)를 논리 비트로 변환하여 I/O 시스템의 상위 계층으로 전달하는 것입니다. 다음 계층(데이터 링크 계층)은 물리 계층의 논리 비트를 받아들이고 신호의 특성과 링크를 통해 전송되는 물리 비트의 의미를 무시합니다.

이제 맨체스터 인코딩의 메커니즘에 대해 논의합니다. 여기서 논리 비트의 변환은 물리 비트로 인코딩됩니다. 예가 아래 그림에 나와 있습니다. 물리적 비트의 01은 인코딩을 논리 1로 변환합니다. 반대로 물리적 비트의 10은 인코딩을 논리 0으로 변환합니다.

맨체스터 코드(예).

맨체스터 코드에는 항상 데이터를 인코딩하는 변환이 있습니다. 대부분의 경우 일정 기간의 중간에 전환이 발생합니다. 전환이 없으면 신호가 전송되지 않고 링크가 유휴 상태라고 결론을 내릴 수 있습니다. 맨체스터 인코딩의 한 가지 장점은 링크로 전송된 정보를 쉽게 디코딩할 수 있다는 것입니다. 전환의 성격만 감지하면 됩니다. 또한 데이터를 동기화하는 데 외부 플래시 신호가 필요하지 않습니다. 데이터는 자체 시계라고 하며, 이는 송신자의 시계가 데이터에서 추출될 수 있고 수신자가 송신자가 데이터를 보낸 것과 동일한 속도로 데이터를 읽도록 보장할 수 있음을 의미합니다.

맨체스터 인코딩은 오늘날의 근거리 통신망 이더넷 프로토콜의 기초를 형성하는 IEEE 802.3 통신 프로토콜에 사용됩니다. 비평가들은 논리의 모든 부분이 변환과 연관되어 있기 때문에 결국 불필요하게 많은 에너지를 사용하게 된다고 주장합니다. 각 변환에는 링크, 드라이버 및 관련 회로와 관련된 커패시터 세트를 충전/방전해야 합니다. 관련된 저항 손실은 열로 소산되므로 변환 횟수를 줄여 보겠습니다.

19.8.2.6 NRZ(Non-Return to Zero) 프로토콜

이 방법은 1과 0의 실행을 활용합니다. 전송 논리 1의 경우 링크 전압을 높게 설정합니다. 마찬가지로 논리 0을 전송하려면 링크의 전압을 낮게 설정하십시오. 이제 1비트를 두 번 실행하는 것을 고려해보세요. 두 번째 비트의 경우 링크에서 전환이 유도되지 않으며 링크의 전압이 높게 유지됩니다. 마찬가지로, n개의 0이 있는 경우. 그런 다음 마지막 (n-1)0에 대해서는 링크의 전압이 낮게 유지되므로 전환이 없습니다.

아래 그림은 전송해야 하는 논리 비트의 값이 변경되지 않은 상태에서 전압 전환을 완전히 피함으로써 전환 횟수가 최소화되었음을 관찰한 예를 보여줍니다. 이 프로토콜은 시간이 낭비되지 않기 때문에 빠르고(RZ 프로토콜과 같이) 동일한 비트 실행의 변환이 제거되므로(RZ 및 맨체스터 코드와 달리) 전력 효율적입니다.

Non-return to zero 프로토콜(예).

그러나 속도와 전력 효율성이 향상되면 복잡성이 증가합니다. 100개의 문자열을 전송하려고 한다고 가정합니다. 이 경우 첫 번째와 마지막 비트만 변환됩니다. 수신자는 송신자의 시계를 갖고 있지 않기 때문에 비트 기간의 길이를 알 수 없습니다. 송신기와 수신기가 동일한 클록을 공유하더라도 링크에서 발생하는 지연으로 인해 수신기는 확률이 0인 99 또는 101비트가 실행되고 있다고 결론을 내릴 수 있습니다. 따라서 수신자가 링크로 전송된 모든 데이터를 올바르게 읽을 수 있도록 추가적인 동기화 정보를 전송해야 합니다.

NRZI(Non-Return to Zero) 반전 프로토콜

NRZI(Non-Return to Zero) 반전 프로토콜은 NRZ 프로토콜의 변형입니다. 논리 1을 인코딩하려는 경우 0에서 1 또는 1에서 0으로의 전환이 있는 반면, 논리 0의 경우 전환이 없습니다. 아래 이미지는 예를 보여줍니다.

19.8.3 물리 계층: 동기화 하위 계층

전송 하위 계층은 펄스 시퀀스가 송신기에서 수신기 또는 수신기 그룹으로 성공적으로 전송되도록 보장합니다. 그러나 그것만으로는 충분하지 않습니다. 수신기는 적시에 신호를 읽어야 하며 올바른 비트 주기를 가정해야 합니다. 신호를 너무 일찍 읽거나 너무 늦게 읽으면 잘못된 신호 값을 얻을 수 있습니다. 둘째, NRZ 프로토콜은 잘못된 비트 주기 값을 가정하면 작동하지 않을 수 있으므로 소스와 대상 사이의 시간 개념을 유지해야 합니다. 타겟은 언제 값을 래치로 전송할지 정확히 알아야 합니다. 단일 소스 및 대상에 대한 솔루션을 고려해 보겠습니다.

요약하면, 동기화 하위 계층은 타이밍 보장 없이 전송 하위 계층으로부터 일련의 논리 비트를 수신합니다. 비트 주기의 값을 계산하고 송신자가 보낸 전체 데이터 프레임(고정 크기 블록)을 읽어 데이터 링크 계층으로 보내야 합니다. 프레임 경계를 찾고 프레임에 비트 세트를 배치하는 실제 작업은 데이터 링크 계층에서 수행됩니다.

19.8.3.1 동기 버스

먼저 송신자와 수신자가 동일한 클럭을 공유하고 송신자에서 수신자로 데이터를 전송하는 데는 몇 분의 1주기만 소요되는 동기식 시스템의 경우를 고려하고, 더 나아가 송신자가 계속 전송한다고 가정합니다. 이 시스템을 간단한 동기 버스라고 부르겠습니다.

이 경우 송신자와 수신자 간의 동기화 작업은 매우 간단합니다. 우리는 데이터가 클록의 네거티브 에지에서 전송되고 한 사이클 이내에 수신기에 도달한다는 것을 알고 있습니다. 피해야 할 가장 중요한 문제는 준안정성입니다. 클럭의 네거티브 에지 근처의 작은 시간 창 내에서 데이터가 전환되면 플립플롭은 준안정 상태로 들어갑니다. 구체적으로, 우리는 클럭 에지 이전에 설정된 시간 간격 동안 데이터가 안정적이기를 원하고, 클럭 에지 이후 홀드 시간 간격 동안 데이터가 안정적이어야 합니다. 설정 및 유지 간격으로 구성된 간격을 클록의 유지 영역이라고 합니다.

이 경우 데이터가 tclktsetup 시간 단위 미만으로 수신기에 도달하여 준안정성 문제가 없다고 가정하면 데이터를 수신기의 플립플롭으로 읽을 수 있습니다. 디지털 회로는 일반적으로 더 큰 청크(바이트 또는 단어)로 데이터를 처리하므로 수신기에서 직렬 입력이 사용됩니다. 즉, 레지스터 외부에서는 병렬로 n비트를 직렬로 읽고 n비트 청크를 한 번에 읽습니다. 송신기와 수신기의 클럭이 동일하므로 속도 불일치가 없습니다. 수신기의 회로는 아래 그림과 같습니다.

간단한 동기화 버스 수신기.

중간 시간 시스템에서 신호와 클록 간의 위상차는 일정합니다. 링크의 전파 지연과 송신기와 수신기 클록의 위상 차이로 인해 신호에 위상 차이가 발생할 수 있습니다. 이 경우 데이터가 수신기 클럭의 ​​금지된 임계 영역에 도착할 수 있으므로 준안정성 문제가 발생할 수 있으므로 수신기 클럭의 ​​금지된 영역에서 전환이 없도록 고정된 시간만큼 신호를 지연시키는 지연 요소를 추가해야 합니다. 회로의 나머지 부분은 단순 동기 버스에 사용된 것과 동일하게 유지됩니다. 회로 설계는 아래 그림과 같습니다.

중형 버스용 수신기.

지연 요소는 DLL(지연 잠금 루프)을 사용하여 구성할 수 있습니다. DLL은 다양한 디자인으로 제공될 수 있으며 그 중 일부는 상당히 복잡할 수 있습니다. 간단한 DLL은 일련의 인버터로 구성됩니다. 출력과 입력이 동일하도록 하려면 짝수 개의 인버터가 필요합니다. 조정 가능한 지연 요소를 생성하려면 각 인버터 쌍 다음에 신호를 탭하면 됩니다. 이러한 신호는 입력과 논리적으로 동일하지만 인버터의 전파 지연으로 인해 점진적인 위상 지연이 발생하므로 멀티플렉서를 사용하여 특정 양의 위상 지연이 있는 신호를 선택할 수 있습니다.

이제 좀 더 현실적인 시나리오를 생각해 보세요. 이 경우 송신자와 수신자 클록이 정확히 동일하지 않을 수 있으며, 소량의 클록 드리프트(드리프트)가 있을 수 있으며 이는 수십 또는 수백 사이클에 걸쳐 최소라고 가정할 수 있지만 수백만 사이클 중 몇 사이클의 드리프트가 있을 수 있습니다. 둘째, 송신자가 항상 데이터를 전송하는 것은 아니며 버스에 유휴 시간이 있다고 가정합니다. 이러한 유형의 버스는 이론적으로 동일한 주파수에서 실행되지만 공통 클럭을 공유하지 않는 여러 마더보드가 있는 서버 컴퓨터에서 발견됩니다. 수백만 주기 정도의 시간 규모를 고려할 때 프로세서 간에 약간의 클럭 드리프트가 있습니다.

이제 몇 가지 간단한 가정을 해보세요. 일반적으로 주어진 데이터 프레임은 100 또는 1000비트를 포함합니다. 몇 비트(<100)를 전송할 때 클럭 드리프트에 대해 걱정할 필요가 없습니다. 그러나 더 많은 비트(>100)의 경우 데이터가 손실되지 않도록 클럭을 주기적으로 재동기화해야 합니다. 더욱이, 수신기 클록의 금지 영역에 전이가 없는지 확인하는 것은 매우 중요한 문제입니다.

이 문제를 해결하기 위해 우리는 송신기의 시계와 동기화되는 플래시 제어라는 추가 신호를 사용합니다. 플래시 펄스는 프레임 전송 시작 ​​시(또는 첫 번째 데이터 비트가 전송되기 몇 사이클 전) 트리거되고, 그런 다음 플래시 펄스는 n 사이클마다 주기적으로 전환됩니다. 이 경우 수신기는 플래시 펄스가 수신되는 시간과 클록 전환 간격을 기반으로 지연을 조정하는 조정 가능한 지연 요소를 사용합니다. 여러 주기 동안 플래시 펄스를 보낸 후 데이터 전송이 시작됩니다. 클록이 표류할 수 있으므로 지연 요소의 재조정 또는 재조정이 필요하므로 주기적으로 플래시 펄스를 수신기에 보내야 합니다. 아래 그림은 데이터 및 플래시 제어의 타이밍 다이어그램을 보여줍니다.

준동기 버스의 타이밍 다이어그램.

중형 버스의 경우와 마찬가지로 n 사이클마다 수신기는 직렬 입출력 레지스터를 사용하여 n 비트를 모두 병렬로 읽을 수 있습니다. 수신기의 회로는 아래 그림과 같습니다. 플래시 펄스와 수신기 클럭(rclk)을 입력으로 사용하는 지연 계산기 회로가 있습니다. 위상 지연을 기반으로 소스의 데이터가 수신기 클록 사이클 중간에 도착하도록 지연 요소를 조정합니다. 이는 다음과 같은 이유로 필요합니다. 송신자와 수신자의 클럭 주기가 정확히 동일하지 않기 때문에 속도 불일치 문제가 있을 수 있습니다. 하나의 수신기 클럭 사이클에서 두 개의 유효한 데이터 비트를 얻을 수도 있고, 전혀 비트를 얻지 못할 수도 있습니다. 이는 비트가 클록 사이클의 시작 또는 끝에 도달할 때 발생합니다. 따라서 우리는 비트가 클록 사이클 중간에 도착하도록 하고 싶고, 추가적으로 준안정성 회피 문제도 있습니다.

준동기 버스용 수신기.

불행하게도 위상은 점진적으로 변하며 클록 주기가 시작될 때 비트가 수신기에 도달한 다음 동일한 주기에 두 비트가 수신될 수 있습니다. 이 경우 전용 회로는 이벤트를 예측하고 송신자에게 미리 메시지를 보내 비트 전송을 일시 중지해야 합니다. 동시에 비트가 사이클의 중간에 도달하도록 지연 요소를 다시 조정해야 합니다.

19.8.3.2 소스 동기 버스

안타깝게도 준동기식 버스조차 제조가 어렵습니다. 신호를 전송할 때 크고 예측할 수 없는 지연이 발생하는 경우가 많으며, 긴밀한 클럭 동기화를 보장하는 것도 어려울 수 있습니다. 예를 들어, 동일한 마더보드의 서로 다른 프로세서 간에 빠른 I/O 경로를 제공하는 데 사용되는 AMD HyperTransport 프로토콜은 동기 또는 준동기 클럭을 사용하지 않습니다. 둘째, 프로토콜은 최대 1사이클의 추가 지터(신호 전파 시간 예측 불가능)를 가정합니다.

이 경우에는 보다 복잡한 플래시 제어 신호를 사용해야 합니다. 소스 동기 버스에서 송신기 클록은 일반적으로 스트로브 신호로 전송되며, 신호 전파 시간에 지연이 발생하면 신호와 플래시 펄스가 동일하게 영향을 받습니다. 이는 매우 현실적인 가정이며, 2013년 현재 대부분의 고성능 I/O 버스는 소스 동기 버스를 사용합니다. 소스 동기 버스의 회로도 그다지 복잡하지 않습니다. 송신기의 클럭(플래시 제어 신호로 전송됨)을 사용하여 xclk라고 하는 직렬 입력-병렬 출력 레지스터에 데이터를 공급합니다. 아래 이미지와 같이 수신기의 시계를 사용하여 데이터를 읽습니다. 일반적으로 신호가 클록 경계를 넘을 때마다 금지 영역 외부로의 전환을 유지하기 위해 조정 가능한 지연 요소가 필요합니다. 그래서 플래시 펄스로 수신된 송신기 클럭(xclk)과 수신기 클럭(rclk) 사이의 위상차를 기반으로 지연 요소의 매개변수를 계산하는 지연 계산기 회로가 있습니다.

소스 동기 버스 수신기.

비트 세트가 동시에 전송될 수 있고 모든 데이터 라인이 동기화된 클록 신호를 전달하는 플래시 펄스를 공유할 수 있도록 여러 병렬 데이터 링크를 갖는 것이 가능합니다.

19.8.3.3 비동기 버스

이제 가장 일반적인 종류의 버스인 비동기식 버스를 생각해 보십시오. 여기서는 송신자와 수신자의 시계가 동기화된다는 보장이 없으며 송신자의 시계도 신호와 함께 전송되지 않습니다. 수신기의 임무는 신호에서 송신기의 클럭을 추출하고 데이터를 올바르게 읽는 것입니다. 아래 그림에 표시된 데이터 읽기 회로를 살펴보겠습니다.

비동기 버스의 수신기 회로.

설명의 편의를 위해 NRZ를 이용하여 비트를 인코딩하는 방법을 가정해보자. 디자인을 다른 유형의 인코딩으로 확장하는 것은 매우 쉽습니다. 전송 하위 계층에 의해 전달된 논리 비트 스트림은 첫 번째 D 플립플롭과 동시에 I/O 신호의 전이를 검사하고 송신기의 클럭을 추측하는 클럭 검출기 및 복구 회로로 전송됩니다. 특히, 클록 복구 회로에는 클록 신호를 생성하고 입력 신호의 전환 시퀀스와 최대한 일치하도록 위상 및 주파수를 조정하는 발진기인 PLL(위상 고정 루프)이 포함되어 있습니다. 이는 상당히 복잡한 작업입니다.

RZ 또는 맨체스터 인코딩의 경우 주기적인 전환이 있으므로 수신기에서 PLL 회로를 동기화하는 것이 더 쉽습니다. 그러나 NRZ 인코딩에서는 주기적인 전환이 없으므로 수신기의 PLL 회로가 동기화를 잃을 수 있습니다. NRZ 인코딩을 사용하는 많은 프로토콜(특히 USB 프로토콜)은 주기적인 전환이나 더미 비트를 신호에 삽입하여 수신기에서 PLL을 다시 동기화합니다. 둘째, 클럭 복구 회로의 PLL은 버스의 장기간 비활성 상태를 처리해야 하며, 그 동안 동기화가 끊어질 수 있습니다. 비동기 신호로부터 정확한 클럭 복구를 보장하기 위한 고급 방식이 있습니다. 이 섹션에서는 이에 대한 간략한 개요만 설명하고 클럭 복구 회로가 해당 작업을 올바르게 수행하고 있다고 가정합니다.

클록 감지 및 복구 회로의 출력을 첫 번째 D 플립플롭의 클록 입력에 연결하므로 데이터는 송신자의 클록을 기반으로 클록됩니다. 준안정성 문제를 피하기 위해 두 개의 D 플립플롭 사이에 지연 요소가 도입되었으며, 두 번째 D 플립플롭은 수신기의 클록 도메인에 있습니다. 회로의 이 부분은 소스 동기 버스 회로와 유사합니다.

삼진 신호의 경우 버스가 활성화된 시기(물리적 0 또는 1이 버스에 표시되는 경우)를 쉽게 알 수 있습니다. 그러나 바이너리 시그널링의 경우 원칙적으로 0 또는 1 비트가 항상 전송되기 때문에 버스가 언제 활성화되는지 알 수 없으므로 데이터 가용성을 나타내기 위해 추가 플래시 신호를 사용해야 합니다. 이제 플래시 신호를 사용하여 버스의 데이터 가용성을 나타내는 프로토콜을 살펴보겠습니다. 플래시 신호는 I/O 요청의 시작과 끝을 나타내기 위해 3항 버스에서 선택적으로 사용될 수도 있습니다. 어쨌든 플래시 제어 신호를 사용하여 제안된 두 가지 방법은 매우 기본적이며 보다 발전된 방법으로 대체되었습니다.

소스가 타겟으로 데이터를 전송하려고 한다고 가정합니다. 먼저 버스에 데이터를 배치하고 약간의 지연 후에 아래 타이밍 다이어그램에 표시된 대로 플래시 제어를 설정(1로 설정)합니다. 이는 수신기가 스트로브 설정을 감지하기 전에 버스에서 데이터가 안정적인지 확인하기 위해 수행됩니다. 수신기는 플래시 제어가 켜질 때까지 즉시 데이터 값 읽기를 시작하며, 이 시점에서 수신기는 계속해서 데이터를 읽고 레지스터에 넣은 다음 데이터 블록을 상위 계층으로 전송합니다. 소스가 데이터 전송을 중단하기로 결정하면 플래시 제어를 재설정(0으로 설정)합니다. 여기에서는 타이밍이 중요합니다. 플래시 제어는 일반적으로 데이터 전송을 중지하기 전에 재설정됩니다. 수신기는 스트로브 재설정 후 버스 콘텐츠를 마지막 비트로 처리해야 하기 때문에 완료를 기다리는 것이 필요합니다. 일반적으로 말하면, 데이터 신호는 판독된 후 일정 시간 동안 그 값을 유지할 것으로 예상됩니다(준안정성 제약 조건을 위해).

플래시 제어 기반 비동기 통신 시스템의 시퀀스 다이어그램.

플래시 신호를 사용하는 간단한 비동기 통신에서는 소스가 수신자가 데이터를 읽었는지 여부를 알 수 없습니다. 따라서 소스가 수신자가 모든 데이터를 읽었음을 명시적으로 알 수 있는 핸드셰이크 프로토콜이 도입되었습니다. 관련 타이밍 다이어그램은 아래 그림에 나와 있습니다.

플래시 제어 기반 비동기 통신 시스템의 시퀀스 다이어그램.

처음에 송신자는 버스에 데이터를 넣은 다음 플래시 제어를 설정합니다. 수신기는 설정된 플래시 펄스를 관찰하자마자 버스에서 데이터를 읽기 시작합니다. 데이터를 읽은 후 ack 라인을 1로 설정합니다. 송신기에서 ack 라인 설정이 1로 설정된 것을 관찰하면 수신기가 데이터를 읽었다고 판단할 수 있습니다. 따라서 송신기는 플래시 제어 펄스를 재설정하고 데이터 전송을 중지합니다. 수신기는 플래시 펄스가 재설정되었음을 확인하면 확인 라인을 재설정합니다. 그러면 송신기는 동일한 단계 순서를 사용하여 다시 전송할 준비가 됩니다.

이 일련의 단계를 통해 송신기는 수신기가 데이터를 읽었다는 사실을 인식할 수 있습니다. 이 다이어그램은 수신기가 송신기가 전송하려는 모든 데이터를 읽었음을 확신할 수 있을 때 의미가 있으므로 설계자는 대부분 이 프로토콜을 사용하여 단일 비트를 전송합니다. 이 경우 수신기는 비트를 읽은 후 ack 라인을 어설션할 수 있습니다. 둘째, 이 방법은 RZ 및 맨체스터 인코딩 방법과도 더 관련이 있습니다. 왜냐하면 송신기는 새 비트를 전송하기 전에 기본 상태로 돌아가야 하기 때문입니다. 확인을 받은 후 송신기는 위 그림과 같이 기본 상태로 돌아가는 프로세스를 시작할 수 있습니다.

여러 비트를 병렬로 전송하려면 각 데이터 라인에 대해 플래시 펄스를 설정해야 하지만 공통 승인 라인이 있을 수 있으며 모든 수신기가 해당 비트를 읽었을 때 승인 신호를 설정해야 하며 모든 플래시 라인이 재설정되면 승인 라인을 재설정해야 합니다. 마지막으로 이 프로토콜에는 4개의 독립적인 이벤트가 있습니다(그림 참조). 따라서 이 프로토콜을 4단계 핸드셰이크 프로토콜이라고 합니다.

NRZ 프로토콜을 사용하면 기본 상태로 돌아갈 필요가 없으며 승인을 받은 후 즉시 다음 비트 전송을 시작할 수 있습니다. 그러나 이 경우 플래시 및 승인 신호의 의미를 약간 변경해야 합니다. 아래 그림은 타이밍 다이어그램을 보여줍니다.

2단계 핸드셰이크를 사용하는 플래시 제어 기반 비동기 통신 시스템의 타이밍 다이어그램.

이 경우, 버스에 데이터를 넣은 후 송신기는 플래시 펄스의 값을 전환합니다. 나중에 데이터를 읽은 후 수신자는 행 값을 확인하도록 전환합니다. 송신기는 승인 라인이 전환되었음을 감지한 후 다음 비트 전송을 시작합니다. 잠시 후 플래시 펄스의 값을 전환하여 데이터가 있음을 나타냅니다. 다시 말하면, 비트를 읽은 후 수신기는 ack 라인을 전환하므로 프로토콜이 계속됩니다. 이 경우 ack 및 플래시 라인을 설정 및 재설정하는 것이 아니라 버스에서 추적해야 하는 이벤트 수를 줄이기 위해 이를 전환하는 것입니다. 그러나 발신자 측과 수신자 측에서 일부 추가 상태를 유지해야 합니다. 이는 무시할 수 있는 오버헤드이므로 4단계 프로토콜이 상당히 단순화되었습니다. NRZ 프로토콜은 중간 일시 중지 기간 없이 지속적인 데이터 전송을 제공하므로 이 접근 방식에 더 적합합니다.

단순 동기 버스: 송신기와 수신기가 동일한 클록을 공유하고 클록 간에 편차가 없다고 가정하는 단순 동기화 버스입니다.

메소크로노스 버스: 송신기와 수신기의 클럭 주파수는 동일하지만 클럭 사이에 위상 지연이 있을 수 있습니다.

Plesiochronous Bus: 송신기와 수신기의 클록 주파수 사이에 약간의 불일치가 있습니다.

소스 동기 버스: 송신기와 수신기의 클럭 사이에는 관계가 없습니다. 따라서 송신기의 시계는 메시지와 함께 수신기로 전송되어 메시지의 비트를 샘플링하는 데 사용할 수 있습니다.

비동기 버스: 비동기 버스는 송신기와 수신기의 클럭 사이에 어떤 관계도 가정하지 않으며 일반적으로 메시지의 전압 변화를 분석하여 송신기의 클럭을 복구하는 복잡한 회로를 가지고 있습니다.

19.8.4 데이터 링크 계층

데이터 링크 계층은 물리 계층으로부터 논리 비트 시퀀스를 얻습니다. 직렬 입출력 레지스터의 너비가 n 비트이면 한 번에 n 비트를 얻는 것이 보장됩니다. 데이터 링크 계층의 임무는 데이터를 프레임으로 나누고 다른 나가는 링크로 전송할 수 있도록 프레임을 버퍼링하는 것입니다. 둘째, 기본적인 오류 검사 및 수정 작업을 수행합니다. 전자기 간섭으로 인해 신호에 오류가 발생할 수 있습니다. 예를 들어 논리 1이 논리 0으로 바뀔 수 있고 그 반대일 수도 있습니다. 이러한 단일 비트 오류는 데이터 링크 계층에서 정정될 수 있습니다. 오류 수가 많고 오류를 수정할 수 없는 경우 이 단계에서 수신기는 재전송을 요청하는 메시지를 송신기에 보낼 수 있습니다. 오류 검사 후 필요한 경우 프레임을 다른 링크로 전달할 수 있습니다.

동시에 버스에 액세스하려는 여러 발신자가 있을 수 있습니다. 이 경우 요청을 중재하고 특정 시점에 한 명의 보낸 사람만 데이터를 보낼 수 있도록 해야 합니다. 이 프로세스를 중재라고 하며 일반적으로 데이터 링크 계층에서 수행됩니다. 마지막으로 중재 논리에는 요청을 트랜잭션의 일부로 처리하기 위한 특별한 지원이 필요합니다. 예를 들어 메모리 장치에 대한 버스에는 메모리 트랜잭션의 일부로 로드 요청이 포함될 수 있습니다. 이에 대한 응답으로, 메모리 유닛은 메모리 위치의 내용을 포함하는 응답 메시지를 보냅니다. 이러한 메시징 패턴을 지원하려면 버스 컨트롤러 수준에서 몇 가지 추가 지원이 필요합니다.

요약하면, 데이터 링크 계층은 물리 계층에서 수신한 데이터를 프레임으로 분해하고, 오류 검사를 수행하며, 단일 송신기를 동시에 사용할 수 있도록 하여 버스를 관리하고, 공통 메시지 패턴의 통신을 최적화합니다.

19.8.4.1 프레이밍 및 캐싱

데이터 링크 계층의 처리는 물리 계층에서 비트 세트를 읽는 것으로 시작됩니다. 비트를 동시에 전송하는 하나의 직렬 링크 또는 여러 개의 직렬 링크가 있을 수 있습니다. 여러 개의 직렬 링크 집합을 병렬 링크라고 합니다. 두 경우 모두 데이터를 읽고 병렬 출력 시프트 레지스터에 직렬로 저장한 다음 물리 계층에서 얻은 값을 기반으로 비트 프레임을 생성하는 역할을 하는 데이터 링크 계층으로 비트 블록을 보냅니다. 프레임은 키보드와 마우스에서 데이터를 전송하는 링크의 경우 1바이트일 수 있으며 프로세서와 주 메모리 또는 주 메모리와 그래픽 카드 간에 데이터를 전송하는 링크의 경우 최대 128바이트일 수 있습니다. 어떤 경우든, 각 버스 컨트롤러의 데이터 링크 계층은 프레임 크기를 인식하고 있으며, 주요 문제는 프레임 경계를 정의하는 것입니다. 프레임 분할 방법은 다음과 같습니다.

  • 긴 휴지를 삽입하여 나눕니다. 버스 컨트롤러는 두 개의 연속 프레임 사이에 긴 일시 중지를 삽입할 수 있습니다. 이러한 일시 중지 기간을 검사하여 수신기는 프레임 경계를 추론할 수 있습니다. 그러나 I/O 채널의 지터로 인해 이러한 일시 중지 기간이 변경될 수 있으며 새로운 일시 중지가 도입될 수 있습니다. 이 방법은 신뢰성이 낮고 귀중한 대역폭을 낭비합니다.

  • 비트 수. 먼저 프레임의 비트 수를 곱하고 전송된 비트 수를 간단히 계산한 다음 필요한 수의 비트가 수신기에 도달한 후 프레임의 끝을 선언할 수 있습니다. 주요 문제는 때때로 신호 왜곡으로 인해 펄스가 삭제되고 동기화가 쉽게 손실될 수 있다는 것입니다.

  • 비트/바이트 패딩. 가장 유연한 방법이며 대부분의 상용 I/O 버스 구현에 사용됩니다. 이 방법은 미리 지정된 비트 시퀀스를 사용하여 프레임의 시작과 끝을 지정합니다. 예를 들어 프레임의 시작을 나타내기 위해 0xDEADBEF 패턴을 사용하고 프레임의 끝을 나타내기 위해 0x12345678을 사용합니다. 프레임의 32비트 시퀀스가 ​​시작과 끝의 특수 시퀀스와 일치할 확률은 매우 낮으며 확률은 232 또는 2:5e10과 같습니다. 불행히도 확률은 여전히 ​​0이 아니므로 이 문제를 해결하기 위해 간단한 솔루션을 채택할 수 있습니다. 0xDEADBEF 시퀀스가 ​​프레임 내용에 나타나면 또 다른 32개의 더미 비트를 추가하고 패턴을 반복합니다. 비트 패턴 0xDEADBEF를 0xDEADEEFDEADBEF로 대체합니다. 수신기의 링크 계층은 짝수 번 반복되는 이 패턴을 감지할 수 있습니다. 패턴의 비트 중 절반은 프레임의 일부이고 나머지는 더미 비트이며 수신기는 더미 비트 제거를 진행할 수 있습니다. 이 접근 방식은 유연하며 지터 및 안정성 문제에 대한 복원력이 매우 뛰어납니다. 이러한 시퀀스를 쉼표라고도 합니다.

데이터 링크 계층이 프레임을 생성하면 이를 오류 검사 모듈로 보내 버퍼링됩니다.

19.8.4.2 오류 감지 및 수정

다양한 이유로 신호 전송에 오류가 발생할 수 있습니다. 근처에서 작동하는 다른 전자 장치로 인해 외부 전자파 간섭을 받을 수 있습니다. 예를 들어, 전자레인지와 같은 전자 장치를 켠 후 전자기파가 I/O 채널의 구리선과 결합되어 전류 펄스를 도입하기 때문에 전화기의 음질이 저하되는 것을 느낄 수 있습니다. 온도로 인해 전선 전송 지연이 변경될 뿐만 아니라 근처 전선으로부터 추가적인 간섭(누화라고 함)이 있을 수도 있습니다. 누적적으로 간섭은 지터를 발생시켜 신호 전파 시간에 변화를 가져오고 왜곡을 발생시켜 펄스의 모양을 변화시킵니다. 따라서 0을 1로 잘못 해석하거나 그 반대로 해석하는 것이 가능합니다. 따라서 올바른 값을 복구할 수 있도록 중복된 정보를 추가하는 것이 필요합니다.

실제로 오류 가능성은 매우 낮습니다. 마더보드의 상호 연결 전송 속도는 일반적으로 백만 분의 1 미만이지만 작은 숫자는 아닙니다. 초당 백만 개의 I/O 작업이 있는 경우 일반적으로 초당 하나의 오류가 발생하며 이는 실제로 매우 높은 오류율입니다. 오류를 감지하고 복구하려면 비트에 추가 정보를 추가해야 합니다. 이 방법을 순방향 오류 정정이라고 합니다. 이와 대조적으로 역방향 오류 수정에서는 오류를 감지하고 메시지를 삭제한 후 보낸 사람에게 메시지를 다시 보내도록 요청합니다. 널리 사용되는 오류 감지 및 복구 방식은 아래에 설명되어 있습니다.

단일 오류 감지

단일 비트 오류는 거의 발생하지 않으므로 동일한 프레임에서 두 개의 오류가 발생할 가능성은 극히 낮습니다. 따라서 단일 오류 감지에 중점을 두고 오류로 인해 단 하나의 비트만 상태를 뒤집는다고 가정해 보겠습니다.

문제를 단순화해 보겠습니다. 프레임에 8비트가 포함되어 있고 단일 비트 오류가 있는지 감지하려고 한다고 가정합니다. 프레임의 비트 번호를 D1;D2;::;D8로 지정하겠습니다. 이제 패리티 비트라는 추가 비트를 추가해 보겠습니다. 패리티 비트 P는 다음과 같습니다.

P=D1D2D8

여기서 연산은 XOR 연산자입니다. 즉, 패리티 비트는 모든 데이터 비트(D1...D8)의 XOR을 나타냅니다. 8비트마다 추가 비트인 패리티 비트를 전송하여 8비트 메시지를 동등한 9비트 메시지로 변환합니다. 이 경우 신뢰성이 높아지는 대신 사용 가능한 대역폭에 12.5%의 오버헤드가 효과적으로 추가됩니다. 다음 그림은 8비트 패리티 방식을 사용하는 프레임이나 메시지의 구조를 보여줍니다. 별도의 패리티 비트를 8개 데이터 비트의 각 시퀀스와 연결하여 더 큰 프레임 크기를 지원할 수도 있습니다.

패리티 비트가 있는 8비트 메시지.

수신자는 메시지를 수신하면 8개 데이터 비트의 XOR을 계산하여 패리티를 계산합니다. 값이 패리티 비트와 일치하면 오류가 없다고 결론을 내릴 수 있지만, 메시지의 패리티 비트가 계산된 패리티 비트 값과 일치하지 않으면 단일 비트 오류가 있다고 결론을 내릴 수 있습니다. 오류는 메시지의 모든 데이터 비트 또는 패리티 비트에서 발생할 수 있습니다. 이 경우 알 수 있는 방법은 없으며 감지할 수 있는 것은 비트 오류의 존재뿐입니다. 이제 오류를 수정해 보세요.

단일 오류 수정

단일 비트 오류를 수정하려면 오류가 있는 경우 버려진 비트의 인덱스를 알아야 합니다. 이제 가능한 결과를 계산해 보세요. n비트 블록의 경우 오류가 있는 비트의 인덱스를 알아야 합니다. 이 경우 오류 없이 n개의 가능한 인덱스가 있을 수 있으므로 단일 오류 정정(SEC) 회로의 경우 총 n + 1개의 가능한 결과가 있습니다(오류가 있는 n개 결과와 오류가 없는 1개 결과). 따라서 이론적 관점에서 [log(n+1)] 추가 비트가 필요합니다. 예를 들어, 8비트 프레임의 경우 [log(8+1)]=4 비트가 필요합니다. 모든 8비트 데이터 워드에 대해 4개의 추가 비트가 있는 (8,4) 코드를 설계해 보겠습니다.

확장 패리티 구성표부터 시작하겠습니다. 4개의 추가 비트 각각은 패리티 비트라고 가정하지만 전체 데이터 비트 세트의 패리티 함수는 아닙니다. 대신, 각 비트는 데이터 비트 하위 집합의 패리티 검사입니다. 4개의 패리티 비트는 각각 P1, P2, P3 및 P4로 명명됩니다. 또한, 아래 그림과 같이 8개의 데이터 비트와 4개의 패리티 비트가 배열되어 있습니다.

데이터 및 패리티 비트의 배열.

패리티 비트 P1, P2, P3, P4를 각각 위치 1, 2, 4, 8에 유지하고 데이터 비트 D1...D8을 각각 위치 3, 5, 6, 7, 9, 10, 11, 12에 배열합니다. 다음 단계는 각 패리티 비트에 데이터 비트 세트를 할당하는 것입니다. 각 데이터 비트의 위치를 ​​이진수로 나타냅니다. 이 경우 표시해야 하는 가장 큰 숫자는 12이므로 4개의 이진수 비트가 필요합니다. 이제 첫 번째 패리티 비트 P1을 해당 위치(이진수 표현에서)에서 LSB가 1인 모든 데이터 비트와 연결합니다. 이 경우 LSB가 1인 데이터 비트는 D1(3), D2(5), D4(7), D5(9) 및 D7(11)입니다. 따라서 패리티 비트 P1은 다음과 같이 계산됩니다.

P1=D1D2D4D5D7

마찬가지로, 세 번째 및 네 번째 패리티 비트에 대해 유사한 정의를 사용하여 두 번째 패리티 비트 P2를 두 번째 위치에 1이 있는 모든 데이터 비트(LSB가 첫 번째 위치에 있다고 가정)와 연결합니다.

다음 표는 데이터와 패리티 비트 간의 상관 관계를 보여줍니다. "X"는 주어진 패리티 비트가 데이터 비트의 함수임을 나타냅니다. 이 표를 바탕으로 패리티 비트를 계산하기 위해 다음 방정식을 유도합니다.

)

데이터와 패리티 비트의 관계.

메시지 전송 알고리즘은 다음과 같습니다. 패리티 비트는 아래 방정식에 따라 계산되며, 패리티 비트는 각각 위치 1, 2, 4 및 8에 삽입되고 위 다이어그램에 따라 데이터 비트를 추가하여 메시지가 형성됩니다. 수신기의 데이터 링크 계층이 메시지를 수신하면 먼저 패리티 비트를 추출하고 4개의 패리티 비트로 구성된 P=P4P3P2P1 형식의 숫자를 형성합니다. 예를 들어 P1=0, P2=0, P3=1, P4=1, P=1100인 경우입니다. 그런 다음 수신기의 오류 감지 회로는 수신된 데이터 비트에서 새로운 패리티 비트 세트(P1,P2,P3,P4)를 계산하고 P=P4P3P2P1 형식의 다른 숫자를 형성합니다. 이상적으로 P는 P0과 같아야 하지만 데이터 또는 패리티 비트에 오류가 있는 경우에는 그렇지 않습니다. PP를 계산해 보겠습니다. 이 값을 신드롬이라고도 합니다.

\begin{정렬됨} P_{1}=& D_{1} \oplus D_{2} \oplus D_{4} \oplus D_{5} \oplus D_{7} \\ P_{2}=& D_{1} \oplus D_{3} \oplus D_{4} \oplus D_{6} \oplus D_{7} \\ & P_{3}=D_{2} \oplus D_{3} \oplus D_{4} \oplus D_{8} \\ & P_{4}=D_{5} \oplus D_{6} \oplus D_{7} \oplus D_{8} \end{정렬됨}

이제 신드롬의 값을 오류 비트의 위치와 연관시켜 보십시오. 먼저 패리티 비트에 오류가 있다고 가정해 보겠습니다. 이 경우 아래 표의 처음 4개 항목은 메시지의 오류 비트 위치와 adjoint 값을 보여줍니다. Adjoint의 값은 메시지의 오류 비트 위치와 같습니다. 패리티 비트는 각각 1, 2, 4, 8번 위치에 있으므로 패리티 비트에 오류가 있으면 해당 신드롬의 해당 비트는 1로 설정되고 나머지 비트는 0으로 유지됩니다. 따라서 신드롬은 오류가 있는 비트의 위치와 일치합니다.

오류 위치와 수반 표현의 관계.

이제 데이터 비트의 단일 비트 오류의 경우를 고려하십시오. 다시 위의 표에서 데이터 비트에 오류가 발생하면 관련된 모든 패리티 비트가 뒤집히기 때문에 수반이 데이터 비트의 위치와 일치한다는 결론을 내릴 수 있습니다. 예를 들어, D5에 오류가 있으면 패리티 비트 P1과 P4가 뒤집힙니다. P1과 P4가 D5와 연관된 이유는 D5가 비트 번호 9(1001)이고 9의 이진수 표현에 있는 두 개의 1이 각각 위치 1과 4에 있기 때문입니다. 나중에 D5에 오류가 발생하면 증후군은 1001과 동일하며 이는 메시지의 비트 인덱스이기도 합니다. 마찬가지로 각 데이터 및 패리티 비트에는 고유한 신드롬이 있습니다(위 표 참조).

따라서 오류가 있는 경우 수반은 오류가 있는 비트(데이터 또는 패리티)의 인덱스를 가리킨다는 결론을 내릴 수 있습니다. 오류가 없으면 adjoint는 0이 됩니다. 따라서 개별 오류를 감지하고 수정하는 방법이 있습니다. 추가 패리티 비트를 사용하여 메시지를 인코딩하는 이 방법을 SEC(단일 오류 정정) 코드라고 합니다.

단일 오류 수정, 이중 오류 감지(SECDED)

이제 SEC 코드를 사용하여 이중 오류(2비트 오류)를 추가로 감지해 보십시오. Adjoint 기반 방법이 작동하지 않음을 증명하기 위해 반례를 제시하십시오. 비트 D2와 D3에 오류가 있다고 가정하면 수반은 0111이 되지만, D4에 오류가 있으면 수반도 0112가 된다. 따라서 단일 비트 오류(D4)인지 이중 비트 오류(D2, D3)인지 알 수 있는 방법이 없다.

이중 오류를 감지하려면 알고리즘을 약간 확장하세요. SEC 코드에 사용되는 모든 데이터 비트(D1…D8)와 4개의 패리티 비트(P1…P4)에 대한 패리티를 계산하는 추가 패리티 비트 P5를 추가한 다음 P5를 메시지에 추가합니다. 메시지의 비트 13에 저장하고 수반 계산에서 제외합니다. 새로운 알고리즘은 다음과 같습니다. Adjoint는 먼저 SEC(Single Error Correction) 코드와 동일한 프로세스를 사용하여 계산됩니다. 수반이 0이면 오류가 없습니다(단일 또는 이중). 단일 오류의 증명은 위의 표를 보면 쉽게 확인할 수 있습니다. 이중 오류의 경우 두 패리티 비트가 모두 뒤집혀 있다고 가정합니다. 이 경우 수반은 두 개의 1을 갖게 됩니다. 마찬가지로, 두 개의 데이터 비트가 뒤집히면 위 테이블의 두 데이터 비트가 동일한 열을 가지지 않기 때문에 신드롬은 적어도 하나의 1비트를 갖게 됩니다. 이제 데이터와 패리티 비트가 뒤집히면 하나의 데이터 비트가 여러 패리티 비트와 연결되므로 수반도 0이 아닙니다. 올바른 패리티 비트는 오류를 나타냅니다.

따라서 adjoint가 0이 아니면 오류가 의심됩니다. 그렇지 않으면 오류가 없는 것으로 간주됩니다. 오류가 있는 경우 메시지의 비트 P5를 보고 수신자에서 다시 계산합니다. 다시 계산된 패리티 비트를 P'5로 지정하겠습니다. 이제 P5=P'5이면 이중 비트 오류가 있다고 결론을 내릴 수 있습니다. 두 개의 단일 비트 오류는 최종 패리티를 계산할 때 본질적으로 서로 상쇄됩니다. 반대로 P5가 P'5와 같지 않으면 비트 오류가 있음을 의미합니다. 이 검사를 이용하면 2비트 또는 1비트에 오류가 있는지 감지할 수 있고, 1비트에 오류가 있으면 수정할 수 있습니다. 그러나 이중 비트 오류의 경우 감지만 가능하며 소스에 재전송을 요청할 수 있습니다. 이 코드를 SECDED 코드라고도 합니다.

해밍 코드

지금까지 설명된 모든 코드는 해밍 거리(Hamming distance)에 암묵적으로 의존하기 때문에 해밍 코드(Hamming code)라고 부릅니다. 해밍 거리(Hamming distance)는 두 이진 비트 시퀀스 간에 차이가 있는 해당 비트 수입니다. 예를 들어 0011과 1010 사이의 해밍 거리는 2입니다(MSB와 LSB는 다릅니다).

이제 4비트 패리티 코드를 고려해보세요. 메시지가 0001이면 패리티 비트는 1이고, MSB 위치에 패리티 비트와 함께 전송된 메시지는 10001입니다. 메시지 전송을 코드 워드라고 합시다. 00001은 유효한 코드워드가 아니며, 수신기는 이 사실에 의존하여 오류가 있는지 여부와 유효한 코드워드의 해밍 거리 1 내에 다른 유효한 코드워드가 없다는 사실에 의존합니다. 마찬가지로 코드워드 간의 최소 해밍 거리는 SEC 코드의 경우 2, SECDED 코드의 경우 3입니다. 이제 매우 인기 있는 다른 유형의 코드를 고려해 보겠습니다.

해밍 오류 수정 코드.

해밍 SEC-DEC 코드.

CRC(순환 중복 검사) 코드

CRC(yclic Redundancy Check, Cyclic Redundancy Check) 코드는 대부분의 경우 단일 비트 오류를 수정하는 데 사용할 수 있지만 주로 오류를 감지하는 데 사용됩니다. CRC 코드 사용을 유도하기 위해 실제 I/O 시스템의 오류 패턴을 살펴보겠습니다. 일반적으로 I/O 채널에서 글리치는 비트 기간보다 오래 지속됩니다. 예를 들어, 외부 전자기 간섭이 있는 경우 몇 사이클 동안 지속될 수 있으며 몇 비트가 반전될 수 있습니다. 이 오류 패턴을 버스트 오류라고 합니다. 예를 들어, 32비트 CRC 코드는 최대 32비트 길이의 버스트 오류를 ​​감지할 수 있으며 일반적으로 대부분의 2비트 오류와 모든 단일 비트 오류를 ​​감지할 수 있습니다.

CRC 코드의 수학은 매우 복잡합니다. 관심 있는 독자는 코딩 이론에 대한 텍스트를 참조할 수 있습니다. 아래에 작은 예가 나와 있습니다.

8비트 메시지에 대한 4비트 CRC 코드를 계산한다고 가정해 보겠습니다. 메시지를 이진수 101100112와 동일하게 만들기 위한 첫 번째 단계는 메시지를 CRC 코드의 길이인 4비트로 채워 새 메시지가 10110011 0000과 같도록 하는 것입니다(가독성을 위해 공백이 추가됨). CRC 코드에는 생성 다항식 또는 제수인 또 다른 5자리 숫자가 필요합니다. 원칙적으로 메시지가 표현하는 숫자는 제수로 표현되는 숫자로 나누어야 하며 나머지는 CRC 코드이다. 그러나 이 나눗셈은 일반 나눗셈과 다르며 모듈로 2 나눗셈이라고 합니다. 이 경우 제수는 110012라고 가정합니다. n비트 CRC 코드의 경우 제수 길이는 n+1비트입니다.

이제 알고리즘이 설명됩니다. 먼저 제수의 MSB를 메시지의 MSB와 정렬합니다. 메시지의 MSB가 1이면 처음 n+1비트와 제수를 XOR하여 메시지의 해당 비트를 결과로 바꿉니다. 그렇지 않고 MSB가 0이면 아무 작업도 수행하지 않습니다. 다음 단계에서는 제수를 한 단계 오른쪽으로 이동하고 제수의 MSB와 일치하는 메시지의 비트를 메시지의 MSB로 처리하고 동일한 프로세스를 반복합니다. 제수의 LSB가 메시지의 LSB와 일치할 때까지 이 일련의 단계를 계속합니다. 마지막으로 최하위 n(4비트)에는 CRC 코드가 포함됩니다. 메시지를 보내기 위해 CRC 코드가 메시지에 추가됩니다. 수신자는 CRC 코드를 다시 계산하여 메시지에 첨부된 코드와 일치시킵니다.

메시지가 10110011이고 제수가 11001인 4자리 CRC 코드를 계산하는 단계를 보여주는 구체적인 예입니다. 알고리즘 프로세스의 개략도는 다음과 같습니다.

이 다이어그램에서는 메시지 관련 부분의 MSB가 0인 단계를 무시합니다. 이러한 경우에는 아무 작업도 수행할 필요가 없기 때문입니다.

리드-솔로몬

해밍 코드는 오류가 드물다고 합리적으로 예상할 수 있는 상황에서 잘 작동합니다. 고정 디스크 드라이브의 오류율은 약 1억분의 1입니다. 3비트 해밍 코드는 이러한 오류를 쉽게 수정할 수 있지만 여러 인접 비트가 손상될 수 있는 상황(예: 버스트 오류)에서는 해밍 코드가 쓸모가 없습니다. 버스트 오류는 잘못된 취급 및 환경적 스트레스로 인해 테이프 및 광 디스크와 같은 이동식 미디어에서 흔히 발생합니다.

블록 내에서 오류가 발생할 것으로 예상되는 경우 비트 수준에서 동작하는 해밍 코드 대신 블록 수준 동작 기반의 오류 정정 코드를 사용해야 합니다. RS(Reed-Solomon) 코드는 단지 몇 비트가 아닌 전체 문자에 대해 작동하는 CRC로 생각할 수 있습니다. CRC와 마찬가지로 RS 코드는 체계적입니다. 즉, 패리티 바이트가 정보 바이트 블록에 추가됩니다. 다음 매개변수를 사용하여 RS(n,k) 코드를 정의합니다.

  • s = 문자(또는 "기호")의 자릿수입니다.

  • k = 데이터 블록을 구성하는 s-비트 문자의 수입니다.

  • n = 코드워드의 비트 수.

RS(n,k)는 k 정보 바이트의 nk2 오류를 정정할 수 있습니다. 따라서 널리 사용되는 RS(255,223) 코드는 223개의 8비트 정보 바이트와 32개의 동반 바이트를 사용하여 255바이트 코드워드를 형성하며, 이는 정보 블록에서 최대 16개의 잘못된 바이트를 수정합니다. RS 코드의 생성 다항식은 추상적인 수학적 구조(갈루아 필드라고 함)에 정의된 다항식으로 제공됩니다. RS 생성 다항식은 다음과 같습니다.

g(x)=\왼(xai\오)\왼(xai+1\오)\왼(xai+2t\오)

여기서 t=nkx는 전체 바이트(또는 기호)이고 g(x)GF(2S) 필드에서 작동합니다. 이 다항식은 일반 대수학에서 사용되는 정수 필드와 매우 다른 갈루아 필드로 확장됩니다. 다음 방정식을 사용하여 nbyteRS 코드워드를 계산합니다.

c(x)=g(x)×i(x)

여기서 i(x)는 정보 블록입니다. RS 오류 정정 알고리즘 뒤에는 어려운 대수가 있지만 컴퓨터 하드웨어, 메인프레임 컴퓨터용 고성능 디스크 드라이브, 음악 및 데이터 저장에 사용되는 광 디스크의 구현에 매우 적합합니다.

19.8.4.3 중재

이제 버스 중재에 대해 이야기하겠습니다. '중재'라는 말은 말 그대로 '분쟁 해결'을 의미합니다. 여러 송신기가 있을 수 있는 다중 지점 버스를 고려하십시오. 여러 송신기가 버스를 통해 값을 전송하는 데 관심이 있는 경우 언제든지 하나의 송신기만 버스에서 값을 보낼 수 있는지 확인해야 합니다. 따라서 버스를 통해 데이터를 전송할 수 있는 장치를 선택하려면 중재 전략이 필요합니다. 송신기와 수신기가 있는 지점 간 버스가 있는 경우 중재가 필요하지 않습니다. 전송 대기 중인 다양한 유형의 메시지가 있는 경우 일부 최적성 기준에 따라 링크를 통한 메시지 전송을 예약해야 합니다.

버스 중재 작업을 수행하는 중재자(arbiter)라는 특수 구조가 고려됩니다. 모든 장치는 버스와 중재자에 연결되어 있으며 중재자에게 메시지를 보내 데이터 전송 의지를 나타냅니다. 중재자는 장치 중 하나를 선택하며 장치를 중재자에 연결하기 위한 두 가지 토폴로지가 있습니다. 스타형 토폴로지나 데이지 체인 토폴로지를 사용할 수 있습니다. 두 옵션 모두 다음 하위 섹션에서 설명됩니다.

스타 토폴로지

이 중앙 집중식 프로토콜에는 버스에서 전송하려는 모든 장치의 버스 요청을 수락하는 전용 회로인 중재자(arbiter)라는 중앙 엔터티가 있습니다. 우선순위 및 공정성 정책을 시행하고 개별 장치에 버스에서 데이터를 보낼 수 있는 권한을 부여합니다. 특히, 요청이 완료된 후 중재자는 모든 현재 요청을 살펴보고 데이터를 전송하기 위해 선택된 장치에 대한 버스 승인 신호를 확인합니다. 선택한 장치는 버스 마스터가 되어 버스에 대한 독점적인 제어권을 갖게 되며, 이를 적절하게 구성하고 데이터를 전송할 수 있습니다. 시스템 개요는 아래 그림과 같습니다.

중앙 집중식 중재자 기반 아키텍처.

현재 요청이 완료되는 시점을 결정하기 위해 취할 수 있는 두 가지 접근 방식이 있습니다. 첫 번째 방법은 버스에 연결된 각 장치가 주어진 사이클 수 n 동안 전송하는 것입니다. 이 경우 n 사이클 후에 중재자는 자동으로 버스가 비어 있다고 가정하고 다른 요청을 예약할 수 있습니다. 그러나 항상 그런 것은 아니며 전송 속도와 메시지 크기가 다를 수 있으며, 이 경우 중재자에게 이것이 완료되었음을 알리는 것은 각 전송 장치의 책임입니다. 우리는 추가 신호 버스 릴리스를 구상하고 있으며, 각 장치에는 버스 릴리스 신호를 보내기 위해 중재자에 대한 전용 라인이 있습니다. 전송 프로세스가 완료되면 이 줄을 어설션합니다(1로 설정). 그런 다음 중재자는 버스를 다른 장치에 할당합니다. 일반적으로 라운드 로빈 또는 FIFO와 같은 표준 전략을 따릅니다.

데이지 체인 기반 중재

버스에 여러 장치가 연결된 경우 중재자는 모든 장치와 상대적 우선순위를 알아야 합니다. 또한 버스에 연결된 장치 수를 늘리면 중재자는 높은 경합을 경험하기 시작하고 속도가 느려집니다. 따라서 우선순위를 쉽게 적용하고 일정 수준의 공정성을 보장하며 연결된 장치 수가 증가함에 따라 버스 할당 결정이 느려지지 않는 방식이 바람직합니다. 데이지 체인 버스는 이러한 모든 요구 사항을 염두에 두고 개발되었습니다.

다음 그림은 데이지 체인 기반 버스의 토폴로지를 보여줍니다. 토폴로지는 한쪽 끝에 중재자가 있는 선형 체인과 유사합니다. 마지막 장치를 제외한 모든 장치에는 두 개의 연결이 있습니다. 프로토콜은 다음과 같이 시작됩니다. 장치는 버스 요청 라인을 어설션하여 시작하고 모든 장치의 버스 요청 라인은 유선 OR 방식으로 연결되며 중재자에 대한 요청 라인은 기본적으로 모든 버스 요청 라인의 논리적 OR을 계산합니다. 이후에 중재자가 토큰을 가지고 있는 경우 중재자는 연결된 장치에 토큰을 전달하고, 그렇지 않으면 중재자가 해제 신호를 받을 때까지 기다려야 합니다. 장치가 토큰을 획득하면 버스 마스터가 되며 필요한 경우 버스에서 데이터를 전송할 수 있습니다. 메시지를 보낸 후 각 장치는 동일한 프로토콜을 따르는 체인의 다음 장치에 토큰을 전달합니다. 필요한 경우 데이터를 전송하고, 그렇지 않으면 토큰만 전송합니다. 마지막으로 토큰은 체인의 끝에 도달합니다. 체인의 마지막 장치는 모든 버스 해제 신호의 논리적 OR인 버스 해제 신호를 주장하고 토큰을 파괴합니다. 중재자가 주장할 릴리스 신호를 관찰하면 토큰을 생성합니다. 1로 설정된 요청 라인을 확인한 후 이 토큰을 데이지 체인에 다시 삽입합니다.

데이지 체인 아키텍처.

이 솔루션에는 몇 가지 미묘한 장점이 있습니다. 첫째, 중재자에 연결된 장치가 가장 높은 우선순위를 갖는다는 암묵적인 우선순위 개념이 있습니다. 점차적으로 중재자를 떠나면 우선순위가 감소합니다. 둘째, 이 프로토콜은 어느 정도 공정성을 갖고 있습니다. 우선 순위가 높은 장치가 토큰을 포기한 후에는 우선 순위가 낮은 모든 장치가 토큰을 얻을 때까지 토큰을 다시 가져올 수 없기 때문에 장치가 혼자 기다릴 수 없기 때문입니다. 둘째, 버스에 장치를 삽입하고 제거하는 것이 쉽고 장치의 개별 상태를 유지하지 않으며 중재자에 대한 모든 통신이 집계되며 버스 요청 및 버스 해제 라인의 OR 함수만 계산합니다. 장치가 유지해야 하는 유일한 상태는 데이지 체인에서의 상대적 위치와 바로 이웃의 주소에 대한 정보입니다.

중앙 중재자를 완전히 피하는 순수 분산형 솔루션도 있을 수 있습니다. 이 방식에서는 모든 노드가 독립적으로 결정을 내리지만 이 방식은 거의 사용되지 않습니다.

19.8.4.4 트랜잭션 지향 버스

지금까지 우리는 단방향 통신에만 중점을 두었습니다. 즉, 특정 시점에 하나의 노드만 다른 노드에 전송할 수 있습니다. 보다 현실적인 버스를 고려하면 대부분의 고성능 I/O 버스는 실제로 멀티드롭 버스가 아닙니다. 다중 지점 버스는 동일한 시점은 아니지만 여러 송신기를 허용할 수 있으며 최신 I/O 버스는 일반적으로 두 개의 끝점이 있는 지점 간 버스입니다. 둘째, I/O 버스는 일반적으로 두 개의 물리적 버스로 구성되므로 양방향 통신이 가능합니다. 예를 들어 노드 A와 B를 연결하는 I/O 버스가 있으면 서로 동시에 메시지를 보낼 수 있습니다.

일부 초기 시스템에는 프로세서를 메모리에 직접 연결하는 버스가 있었습니다. 이 경우 프로세서는 버스 메시지 전송만 시작할 수 있으므로 마스터 프로세서로 지정됩니다. 메모리는 슬레이브 메모리라고 하며 요청에만 응답할 수 있습니다. 오늘날 마스터와 슬레이브의 개념은 퇴색되었지만 동시 양방향 통신의 개념은 여전히 ​​일반적입니다. 양방향 버스를 이중 버스 또는 전이중 버스라고 합니다. 이와 대조적으로 반이중 버스를 사용하면 어느 시점에서나 한쪽 면만 전송할 수 있습니다.

아래 그림은 메모리 컨트롤러 칩과 DRAM 모듈 간의 이중 통신의 일반적인 시나리오를 보여주며, 메모리 읽기 작업의 메시지 순서와 타이밍을 보여줍니다. 실제로 버스가 두 대 있습니다. 첫 번째 버스는 메모리 컨트롤러를 DRAM 모듈에 연결하며 주소 라인(메모리 주소를 전달하는 라인)과 작업 타이밍 및 DRAM 어레이에서 수행해야 하는 작업의 특성을 나타내는 전용 제어 신호를 전달하는 라인으로 구성됩니다. 두 번째 버스는 DRAM 모듈을 메모리 컨트롤러에 연결하고 데이터 라인(DRAM에서 읽은 데이터를 전달하는 라인)과 타이밍 라인(타이밍 정보를 전달하는 라인)을 포함합니다.

DRAM 읽기 타이밍.

계약은 다음과 같습니다. 메모리 컨트롤러는 워드 라인 값을 설정하는 디코더를 활성화하는 RAS(행 주소 스트로브) 신호를 확인하여 시작합니다. 동시에 메모리 컨트롤러는 행의 주소를 주소 라인에 배치하고 DRAM 모듈이 행 주소(trow)를 읽는 데 필요한 시간을 추정합니다. trow 시간 단위 후에 CAS 신호(열 주소 스트로브)를 확인하고 해당 열의 주소를 버스의 DRAM 어레이에 배치합니다. 또한 읽기 액세스를 수행해야 함을 DRAM 모듈에 나타내는 읽기 신호를 활성화합니다. 그런 다음 DRAM 모듈은 메모리 위치의 내용을 읽고 이를 출력 버퍼로 전송합니다. 그런 다음 준비 신호를 확인하고 데이터를 버스에 넣습니다. 그러나 메모리 컨트롤러는 현재 유휴 상태가 아니며 버스에 다음 요청 행 주소를 배치하기 시작합니다. DRAM 액세스 타이밍은 매우 복잡하며 연속적인 메시지 처리가 종종 겹치는 경우가 있습니다. 예를 들어, n번째 요청이 데이터를 전송할 때 (n+1)번째 요청의 행 주소를 계속 디코딩하여 DRAM 대기 시간을 줄일 수 있지만 이 기능을 지원하려면 이중 버스와 복잡한 메시지 시퀀스가 ​​필요합니다.

위 그림에 표시된 기본 DRAM 액세스 프로토콜의 두드러진 특징 중 하나에 주목해 보겠습니다. 요청과 응답은 서로 매우 강력하게 연결되어 있고 소스(메모리 컨트롤러)는 대상(DRAM 모듈)의 복잡성을 알고 있으며 소스와 대상이 보낸 메시지의 성격과 타이밍 사이에는 강한 상호 관계가 있습니다. 둘째, 요청 중에 메모리 컨트롤러와 DRAM 모듈 사이의 I/O 링크가 잠기므로 원래 요청과 응답 사이의 중간 요청을 서비스할 수 없습니다. 이러한 일련의 메시지를 버스 트랜잭션이라고 합니다.

거래 지향형 버스에는 장단점이 있습니다. 첫 번째는 복잡성입니다. 수신자의 타이밍에 대해 많은 가정을 하기 때문에 메시지 전송 프로토콜은 각 수신자 유형에 대해 매우 구체적입니다. 이는 이식성에 좋지 않으며 다른 메시지 의미를 가진 장치를 연결하는 것이 매우 어려워집니다. 또한 버스가 오랜 시간 동안 잠겨 있고 유휴 기간이 있어 대역폭이 낭비될 수 있습니다. 그러나 우리가 보여준 예와 같은 일부 시나리오에서는 트랜잭션 지향 버스가 매우 잘 작동하고 다른 유형의 버스보다 성능이 뛰어납니다.

19.8.4.5 분할 트랜잭션 버스

이제 트랜잭션 지향 버스의 단점을 수정하려고 시도하는 분할 트랜잭션 버스를 살펴보면 DRAM 및 메모리 컨트롤러의 예와 같이 서로 다른 노드 간의 메시지 순서가 엄격하다고 가정하지 않습니다. 메시지 전송은 두 개의 작은 트랜잭션으로 분할됩니다. 먼저, 메모리 컨트롤러가 DRAM에 메모리 요청을 보내고, DRAM 모듈은 메시지를 버퍼링한 후 메모리 액세스를 진행합니다. 그런 다음 메모리의 데이터를 포함하는 별도의 메시지를 메모리 컨트롤러에 보냅니다. 이때 두 메시지 시퀀스 사이의 간격은 임의로 커집니다. 이러한 유형의 버스를 분할 트랜잭션 버스라고 하며 더 큰 트랜잭션을 더 작고 짧은 개별 메시지 시퀀스로 분할합니다.

여기서의 장점은 단순성과 이식성입니다. 모든 전송은 기본적으로 단방향입니다. 우리는 메시지를 보낸 다음 버스를 잠가 응답을 기다리는 대신 보낸 사람이 다른 메시지를 계속 처리합니다. 수신자가 응답할 준비가 될 때마다 별도의 메시지를 보냅니다. 이 접근 방식은 단순할 뿐만 아니라 간단한 메시지 의미 체계를 정의하여 다양한 수신기를 버스에 연결할 수 있으며 이 의미 체계를 준수하는 모든 수신기 회로를 버스에 연결할 수 있습니다. 이 버스를 사용하여 여러 요청과 응답을 겹치거나 세밀한 타이밍 제어와 같은 복잡한 작업을 수행할 수 없습니다. 이러한 요구에 따라 언제든지 트랜잭션을 지원하는 버스를 사용할 수 있습니다.

19.8.5 네트워크 계층

이전 섹션에서는 전이중 버스를 설계하는 방법을 살펴보았습니다. 특히, 시그널링, 신호 인코딩, 타이밍, 프레이밍, 오류 검사 및 트랜잭션과 관련된 문제를 조사했습니다. 이제 I/O 버스가 끝점 간에 메시지를 올바르게 전달하고 적시에 올바른 전달을 보장한다고 가정할 수 있습니다. 이제 전체 칩셋을 살펴보면 본질적으로 대규모 I/O 버스 네트워크입니다.

이 섹션에서 다루는 문제는 I/O 주소 지정과 관련이 있습니다. 예를 들어, 프로세서가 USB 포트에 메시지를 보내려면 USB 포트의 주소를 고유하게 지정하는 방법이 있어야 합니다. 그러면 칩셋은 메시지가 적절한 I/O 장치로 올바르게 라우팅되는지 확인해야 합니다. 마찬가지로, 키보드와 같은 장치가 키 누르기의 ASCII 코드를 프로세서로 보내야 하는 경우 프로세서의 주소를 지정하는 방법이 필요합니다. 이 섹션에서는 칩셋의 라우팅 메시지를 살펴보겠습니다.

19.8.5.1 I/O 포트 주소 지정

하드웨어 I/O 포트는 연결된 외부 장치에 대한 연결 끝점이라고 합니다. 이제 소프트웨어에 있어서는 레지스터 또는 레지스터 세트인 추상 엔터티로 정의된 소프트웨어 포트를 생각해 보십시오. 예를 들어, USB 포트에는 물리적으로 금속 핀 세트와 USB 프로토콜을 실행하는 포트 컨트롤러가 포함되어 있습니다. 그러나 USB 포트의 소프트웨어 버전은 주소 지정이 가능한 레지스터 세트입니다. USB 장치에 쓰려면 USB 포트를 통해 소프트웨어에 노출된 레지스터 세트에 쓰게 됩니다. USB 포트 컨트롤러는 프로세서가 전송한 데이터를 연결된 I/O 장치에 물리적으로 기록하여 소프트웨어 추상화를 구현합니다. 마찬가지로, USB 포트를 통해 I/O 장치에서 보낸 값을 읽으려면 프로세서는 I/O 장치의 출력을 프로세서로 전달하는 해당 포트 컨트롤러에 읽기를 발행합니다.

아래 이미지는 이 개념을 보여줍니다. 다이어그램은 금속 핀 세트가 있는 물리적 하드웨어 포트와 물리적 및 데이터 링크 계층을 구현하는 관련 회로를 보여줍니다. 포트 컨트롤러는 프로세서가 보낸 완전한 요청을 통해 네트워크 계층을 구현합니다. 또한 읽기 전용, 읽기 전용 또는 읽기-쓰기가 가능한 8~32비트 레지스터 세트를 노출합니다. 예를 들어, 모니터와 같은 디스플레이 장치용 포트에는 입력을 가져올 수 없기 때문에 읽기 전용 레지스터가 포함되어 있습니다. 마찬가지로 마우스의 포트 컨트롤러에는 읽기 전용 레지스터가 포함되어 있는 반면, 스캐너의 포트 컨트롤러에는 읽기-쓰기 레지스터가 포함되어 있습니다. 이는 일반적으로 구성 데이터와 명령이 스캐너로 전송되고 문서 이미지가 스캐너에서 읽히기 때문입니다.

I/O 포트용 소프트웨어 인터페이스.

예를 들어, Intel 프로세서에는 4개의 연속 포트를 32비트 포트로 통합할 수 있는 64K(216) 8비트 I/O 포트가 필요합니다. 이러한 포트는 어셈블리 코드에 액세스할 수 있는 레지스터와 동일합니다. 둘째, 이더넷 포트 또는 USB 포트와 같은 특정 물리적 포트에는 이러한 소프트웨어 포트가 여러 개 할당될 수 있습니다. 예를 들어 한 번에 많은 양의 데이터를 이더넷에 쓰려는 경우 수백 개의 포트를 사용할 수 있습니다. Intel 프로세서의 각 포트는 0~0xFFFF 범위의 16비트 숫자를 사용하여 주소가 지정됩니다. 마찬가지로 다른 아키텍처에서는 실제 하드웨어 포트에 대한 소프트웨어 인터페이스 역할을 하는 I/O 포트 집합을 정의합니다.

I/O 주소 공간이라는 용어를 운영 체제와 사용자 프로그램에 액세스할 수 있는 모든 I/O 포트 주소 집합으로 정의하겠습니다. I/O 주소 공간의 각 위치는 물리적 I/O 포트 컨트롤러에 대한 소프트웨어 인터페이스인 I/O 포트에 해당합니다.

대부분의 명령어 세트 아키텍처에는 입력과 출력이라는 두 가지 명령어가 있습니다. 명령어의 의미는 다음과 같습니다.

지침 의미론

r1, <I/O 포트> I/O 포트의 내용은 r1 레지스터로 전송됩니다.

out r1, <I/O 포트> r1 레지스터의 내용이 I/O 포트로 전송됩니다.

in 명령어는 I/O 포트에서 레지스터로 데이터를 전송합니다. 대조적으로, out 명령은 레지스터에서 I/O 포트로 데이터를 전송하며 I/O 장치를 프로그래밍하기 위한 범용 메커니즘입니다. 예를 들어 페이지를 인쇄하려는 경우 전체 페이지의 내용을 프린터의 I/O 포트로 전송하고 마지막으로 프린터 명령을 받아들이는 I/O 포트에 인쇄 명령을 쓰면 프린터가 인쇄를 시작할 수 있습니다.

이제 입력 및 출력 명령을 실행해 보겠습니다. 첫 번째 작업은 메시지가 적절한 포트 컨트롤러에 도달하는지 확인하는 것이고, 두 번째 작업은 출력 명령의 경우 응답을 프로세서로 다시 라우팅하는 것입니다.

이전 장에서 다룬 마더보드 아키텍처를 다시 살펴보겠습니다. CPU는 전면 버스를 통해 Northbridge 칩에 연결됩니다. DRAM 메모리 모듈과 그래픽 카드도 Northbridge 칩에 연결됩니다. Northbridge 칩은 느린 장치를 처리하는 Southbridge 칩에 연결됩니다. Southbridge 칩은 USB 포트, PCI Express 버스(및 이에 연결된 모든 장치), 하드 드라이브, 마우스, 키보드, 스피커 및 네트워크 카드에 연결됩니다. 이러한 각 장치에는 연관된 I/O 포트 세트와 I/O 포트 번호가 있습니다.

일반적으로 마더보드 설계자는 I/O 포트 할당 계획을 가지고 있습니다. Intel 프로세서와 마찬가지로 64K 8비트 I/O 포트가 있고 I/O 포트 주소 범위는 0에서 0xFFFF까지라는 가정 하에 이러한 체계를 구축해 보겠습니다. 먼저 Northbridge 칩에 연결된 고대역폭 장치에 I/O 포트를 할당하고 0~0x00FF 범위의 포트 주소를 제공하고 Southbridge 칩에 연결된 장치의 나머지 주소를 나누고 하드 드라이브의 포트 범위가 0x0100~0x0800이라고 가정하고 USB 포트 범위를 0x0801~0x0FFF로 두고 네트워크 카드에 다음 범위를 할당합니다. 0x4000이면 나머지 몇 개의 포트를 다른 장치에 할당하고 나중에 연결하려는 새 장치를 위해 해당 부분을 비워 두십시오.

이제 프로세서가 I/O 명령(입력 또는 출력)을 발행하면 프로세서는 이것이 I/O 명령임을 인식하고 FSB(Front Side Bus)를 통해 I/O 포트 주소와 명령 유형을 노스브리지 칩에 보냅니다. Northbridge 칩은 각 I/O 포트 유형과 위치에 대한 범위 테이블을 유지합니다. 프로세서의 메시지를 확인한 후 이 테이블에 액세스하여 대상의 상대적 위치를 알아냅니다. 대상이 직접 연결된 장치인 경우 Northbridge 칩은 메시지를 대상으로 전달합니다. 그렇지 않은 경우 유사한 I/O 포트 범위 및 장치 위치 테이블을 유지 관리하는 사우스브리지 칩으로 요청을 전달합니다. 이 테이블에서 조회를 수행한 후 수신된 메시지를 적절한 장치로 전달합니다. 이러한 테이블을 I/O 라우팅 테이블이라고 합니다. I/O 라우팅 테이블은 개념적으로 대규모 네트워크 및 인터넷에서 사용되는 네트워크 라우팅 테이블과 유사합니다.

역방향 경로의 경우 일반적으로 응답이 프로세서로 전송됩니다. 우리는 프로세서에 고유 식별자를 할당하고 메시지는 Northbridge 및 Southbridge 칩에 의해 적절하게 라우팅됩니다. 때로는 유사한 주소 지정 체계를 사용하여 메시지를 메모리 모듈로 라우팅해야 하는 경우도 있습니다.

이 체계는 기본적으로 물리적 I/O 포트 세트를 I/O 주소 공간의 위치에 매핑하고 특수 I/O 명령어는 포트 주소를 사용하여 통신합니다. I/O 장치에 액세스하고 주소를 지정하는 이러한 방법을 종종 I/O 매핑 I/O라고 합니다.

19.8.5.2 메모리 매핑된 주소 지정

이제 입력 및 출력 I/O 명령어를 다시 살펴보세요. 실행 프로그램은 I/O 포트 명명 체계를 이해해야 합니다. 다양한 칩셋과 마더보드는 서로 다른 I/O 포트 주소를 사용할 수 있습니다. 예를 들어, 한 마더보드는 USB 포트에 대해 I/O 포트 주소 범위 0xFF80 ~ 0xFFC0을 할당할 수 있고 다른 마더보드는 0xFEA0 ~ 0xFFB0 범위를 할당할 수 있습니다. 따라서 첫 번째 마더보드에서 실행되는 프로그램이 두 번째 마더보드에서는 작동하지 않을 수 있습니다.

이 문제를 해결하려면 I/O 포트와 소프트웨어 사이에 추가 레이어를 추가해야 하며 가상 메모리와 유사한 솔루션을 제안합니다. 실제로 가상화는 컴퓨터 아키텍처의 다양한 문제를 해결하고 사용자 프로그램과 I/O 주소 공간 사이의 가상 계층을 지속적으로 설계하는 표준 기술입니다.

칩셋과 마더보드에 특정한 운영 체제에 전용 장치 드라이버가 있다고 가정합니다. I/O 포트의 의미와 실제 장치에 대한 매핑을 이해해야 합니다. USB 포트에 액세스하려는 프로그램(사용자 프로그램 또는 운영 체제)을 고려하십시오. 처음에는 USB 포트의 I/O 포트 주소를 모르므로 먼저 운영 체제의 관련 모듈에 가상 주소 공간의 메모리 영역을 I/O 주소 공간의 해당 부분에 매핑하도록 요청해야 합니다. 예를 들어, USB 장치의 I/O 포트가 0xF000과 0xFFFF 사이에 있는 경우 I/O 주소 공간의 이 4KB 영역은 프로그램의 가상 주소 공간의 페이지에 매핑될 수 있습니다. 페이지가 실제로 I/O 포트에 매핑되었음을 나타내기 위해 TLB 및 페이지 테이블 항목에 특수 비트를 추가해야 합니다. 둘째, 물리적 프레임의 주소 대신 I/O 포트 주소를 저장해야 합니다. 마더보드 드라이버의 역할은 이 매핑을 생성하는 운영 체제의 일부입니다. 운영 체제가 I/O 주소 공간을 프로세스의 가상 주소 공간에 매핑한 후 프로세스는 계속해서 I/O 액세스를 수행할 수 있습니다. 매핑을 생성하기 전에 프로그램에 I/O 장치에 액세스할 수 있는 충분한 권한이 있는지 확인해야 합니다.

매핑이 생성된 후 프로그램은 I/O 포트에 자유롭게 액세스할 수 있습니다. in 및 out과 같은 I/O 명령어를 사용하는 대신 일반 로드 및 저장 명령어를 사용하여 가상 주소 공간의 위치에 씁니다. 이러한 명령어가 파이프라인의 메모리 액세스(MA) 단계에 도달한 후 유효 주소는 변환을 위해 TLB로 전송됩니다. TLB 적중이 있는 경우 파이프라인은 가상 주소가 물리적 주소 공간이 아닌 I/O 주소 공간에 매핑된다는 사실도 인식합니다. 둘째, TLB는 가상 주소를 I/O 포트 주소로 변환합니다. 이 단계에서는 TLB를 사용할 필요가 없습니다. 또 다른 전용 모듈을 사용하여 주소를 변환할 수 있습니다. 어떤 경우든 프로세서는 MA 단계에서 동등한 I/O 포트 주소를 받습니다. 그런 다음 I/O 요청을 생성하고 해당 요청을 I/O 포트로 전달합니다.

메모리 매핑된 I/O는 I/O 주소 공간의 각 주소를 프로세스의 가상 주소 공간의 고유한 주소에 할당하여 I/O 장치의 주소를 지정하고 액세스하는 방식입니다. I/O 포트에 액세스하기 위해 프로세스는 일반 로드 및 저장 명령어를 사용합니다.

이 방식을 메모리 매핑된 I/O라고 하며 이 방식의 주요 장점은 특수한 I/O 명령이 아닌 일반 로드 및 저장 명령을 사용하여 I/O 장치에 액세스한다는 것입니다. 둘째, 프로그래머는 I/O 주소 공간에서 I/O 포트의 실제 주소를 알 필요가 없습니다. 운영 체제와 메모리 시스템의 특수 모듈이 I/O 주소 공간과 프로세스의 가상 주소 공간 간의 매핑을 설정하기 때문에 프로그램은 I/O 포트 주소 지정의 의미를 완전히 무시할 수 있습니다.

19.8.6 프로토콜 계층

이제 I/O 시스템의 마지막 계층에 대해 살펴보겠습니다. 처음 세 개의 계층은 I/O 시스템의 한 장치에서 다른 장치로 메시지가 올바르게 전달되도록 합니다. 이제 전체 페이지 인쇄, 전체 문서 스캔, 하드 드라이브에서 대량의 데이터 읽기 등 전체 I/O 요청 수준을 살펴보세요. 문서 인쇄를 예로 들어 보겠습니다.

프린터가 USB 포트에 연결되어 있다고 가정합니다. 프린터 장치 드라이버는 먼저 문서의 내용을 USB 포트와 연결된 버퍼로 보내도록 프로세서에 지시합니다. 각 버퍼에는 고유한 포트 주소가 할당되어 있고 전체 문서는 버퍼 세트에 있다고 가정하고 장치 드라이버는 버퍼가 비어 있다는 것을 알고 있다고 가정합니다. 문서 콘텐츠를 보내기 위해 장치 드라이버는 일련의 출력 명령을 사용하거나 메모리 매핑된 I/O를 사용할 수 있습니다. 문서 내용을 전송한 후 마지막 단계는 미리 지정된 I/O 포트에 PRINT 명령을 쓰는 것입니다. USB 컨트롤러는 연결된 모든 I/O 포트를 관리하고 이러한 포트로 전송된 메시지가 연결된 프린터로 전송되도록 보장합니다. 연결된 프린터는 USB 컨트롤러에서 PRINT 명령을 받은 후 인쇄 작업을 시작합니다.

사용자가 다른 문서의 인쇄 버튼을 클릭했다고 가정해 보겠습니다. 새 문서를 프린터로 보내기 전에 드라이버는 프린터가 이전 문서의 인쇄를 완료했는지 확인해야 합니다. 여기서는 한 번에 하나의 문서만 처리할 수 있는 간단한 프린터가 있다고 가정합니다. 따라서 프린터가 유휴 상태인지 드라이버에 알릴 수 있는 방법이 있어야 합니다.

프린터가 드라이버와 통신하는 다양한 메커니즘을 살펴보기 전에 소피아가 편지가 도착하기를 기다리는 비유 시나리오를 생각해 보세요. 편지가 소피아의 친구 중 한 명을 통해 전송된 경우 소피아는 친구에게 전화를 걸어 언제 돌아올지 물어볼 수 있으며, 일단 돌아오면 소피아는 집으로 가서 편지를 받을 수 있습니다. 또는 보낸 사람이 택배 서비스를 통해 편지를 보낼 수도 있습니다. 이 경우 소피아는 택배가 편지를 배달할 때까지 기다려야 합니다. 메시지를 수신하는 전자 메커니즘을 폴링이라고 하고 후자를 인터럽트라고 합니다. 이에 대해서는 후속 섹션에서 자세히 설명합니다.

19.8.6.1 폴링

프린터에 프린터의 상태를 유지하는 데 사용되는 상태 레지스터라는 특수 레지스터가 있다고 가정합니다. 프린터의 상태가 변경될 때마다 상태 레지스터 값이 업데이트됩니다. 상태 레지스터에는 0(유휴)과 1(사용 중)의 두 값이 포함될 수 있다고 가정합니다. 프린터가 문서를 인쇄할 때 상태 레지스터의 값은 1(사용 중)입니다. 프린터가 문서 인쇄를 완료하면 상태 레지스터 값을 0(유휴)으로 설정합니다.

이제 프린터 드라이버가 프린터의 상태 레지스터 값을 읽으려고 한다고 가정합니다. 상태 레지스터의 값을 가져오도록 요청하는 메시지를 프린터에 보냅니다. 메시지 전송의 첫 번째 단계는 일련의 바이트를 USB 포트 컨트롤러의 관련 I/O 포트로 보내는 것입니다. 그러면 해당 바이트가 프린터로 전송됩니다. 분할 트랜잭션 버스를 사용하는 경우 응답이 도착할 때까지 기다립니다. 동시에 프린터는 메시지를 해석하고 상태 레지스터의 값을 응답으로 전송하며, USB 포트 컨트롤러는 이를 I/O 시스템을 통해 프로세서에 전달합니다.

프린터가 유휴 상태이면 장치 드라이버는 다음 문서를 계속 인쇄할 수 있습니다. 그렇지 않으면 프린터가 완료될 때까지 기다려야 합니다. 프린터가 유휴 상태가 될 때까지 프린터에서 상태를 계속 요청할 수 있습니다. 장치의 상태가 특정 값을 가질 때까지 장치의 상태를 반복적으로 쿼리하는 이러한 방법을 폴링이라고 합니다.

폴링은 루프에서 장치의 상태를 반복적으로 쿼리하여 I/O 장치가 특정 상태에 도달할 때까지 기다리는 방법입니다.

다음은 가상 시스템에서 폴링을 구현하는 어셈블리 코드의 일부를 보여줍니다. 프린터 상태를 얻기 위한 메시지가 0xDEADBEEF라고 가정합니다. 먼저 I/O 포트 0xFF00으로 메시지를 보낸 다음 I/O 포트 0xFF04에서 응답을 읽어야 합니다.

cpp
/* 로드 DEADBEEF까지 r0 */
movh r0, 0xDEAD
addu r0, r0, 0xBEEF

/* 의 루프 */
.loop:
    out r0, 0xFF00
    in r1, 0xFF04
    cmp r1, 1
    beq .loop /* 루프 ,까지 status = 1 */

19.8.6.2 인터럽트

폴링 기반 접근 방식에는 몇 가지 단점이 있습니다. 이는 프로세서를 계속 사용하게 하고 전력을 낭비하며 I/O 트래픽을 증가시키며 대신 인터럽트를 사용할 수 있습니다. 아이디어는 프로세서가 유휴 상태일 때 프린터에 알리는 메시지를 프린터에 보내는 것입니다. 프린터가 유휴 상태가 되거나 프린터가 이미 유휴 상태인 경우 프린터는 프로세서에 인터럽트를 보냅니다. I/O 시스템은 일반적으로 인터럽트를 일반 메시지로 처리한 다음 프로세서나 전용 인터럽트 컨트롤러에 인터럽트를 전달하며 이러한 엔터티는 인터럽트가 I/O 시스템에서 발생한다는 것을 인식합니다. 그런 다음 프로세서는 현재 프로그램 실행을 중지하고 인터럽트 핸들러로 점프합니다.

각 인터럽트는 자체 또는 이를 생성한 장치를 식별해야 하며 마더보드의 각 장치에는 일반적으로 인터럽트의 일부인 고유 코드가 있습니다. 어떤 경우에는 장치를 범용 포트(예: USB 포트)에 연결할 때 인터럽트 코드에 두 부분이 포함됩니다. 한 부분은 외부 장치에 연결된 마더보드의 포트 주소이고 다른 부분은 마더보드의 I/O 포트에 의해 장치에 할당된 ID입니다. 고유한 코드를 포함하는 이 인터럽트를 벡터 인터럽트라고 합니다.

x86 시스템과 같은 일부 시스템에서 인터럽트 처리의 첫 번째 단계는 x86 프로세서의 APIC(Advanced Programmable Interrupt Controller)라고 불리는 PIC(Programmable Interrupt Controller)에 의해 수행됩니다. APIC의 역할은 인터럽트 메시지를 버퍼링하고 일련의 규칙에 따라 프로세서에 보내는 것입니다.

이제 PIC가 따르는 일련의 규칙을 살펴보십시오. 대부분의 프로세서는 특정 중요한 계산 단계에서 인터럽트 처리를 비활성화합니다. 예를 들어 인터럽트 핸들러가 원래 프로그램의 상태를 저장할 때 프로세서 인터럽트를 허용할 수 없습니다. 상태를 성공적으로 저장한 후 인터럽트 핸들러는 인터럽트를 다시 활성화할 수 있습니다. 일부 시스템에서는 인터럽트 핸들러가 실행될 때마다 인터럽트가 완전히 비활성화됩니다. 밀접하게 관련된 개념은 일부 인터럽트를 선택적으로 활성화하고 다른 일부 인터럽트를 비활성화하는 인터럽트 마스킹입니다. 예를 들어, 인터럽트 핸들러를 처리하는 동안 온도 컨트롤러에서 우선순위가 높은 인터럽트를 허용하고 하드 디스크에서 우선순위가 낮은 인터럽트를 일시적으로 무시하도록 선택할 수 있습니다. PIC에는 일반적으로 각 인터럽트 유형에 대해 하나의 항목이 있는 벡터가 있습니다. 이를 인터럽트 마스크 벡터라고 합니다. 인터럽트의 경우 인터럽트 마스크 벡터의 해당 비트가 1이면 인터럽트가 활성화되고 그렇지 않으면 비활성화됩니다.

마지막으로, 동일한 시간 창 내에 여러 인터럽트가 도착하는 경우 PIC는 인터럽트의 우선순위를 존중해야 합니다. 즉, 실시간 제약이 있는 장치(예: 연결된 고속 통신 장치)의 인터럽트는 더 높은 우선순위를 갖고 키보드 및 마우스 인터럽트는 더 낮은 우선순위를 갖도록 해야 합니다. PIC는 우선순위와 도착 시간을 고려한 휴리스틱을 사용하여 인터럽트를 정렬하고 해당 순서대로 프로세서에 제공합니다. 그런 다음 프로세서는 이전 섹션에서 설명한 방법에 따라 인터럽트를 처리합니다.

벡터 인터럽트: 인터럽트를 생성한 장치 ID 또는 외부 장치에 연결된 I/O 포트 주소가 포함된 인터럽트입니다.

PIC(Programmable Interrupt Controller): 프로세서로 전송된 인터럽트를 버퍼링, 교체 및 관리하는 데 사용됩니다.

인터럽트 마스킹: 사용자 또는 운영 체제는 프로그램의 특정 중요한 단계(예: 장치 드라이버 및 인터럽트 핸들러 실행 시) 동안 일련의 인터럽트를 선택적으로 비활성화하도록 선택할 수 있습니다. 이 메커니즘을 인터럽트 마스킹이라고 합니다. PIC의 인터럽트 마스크 벡터는 일반적으로 비트 벡터(인터럽트 유형당 1비트)입니다. 비트가 1로 설정되면 인터럽트가 활성화되고, 그렇지 않으면 비활성화되고 인터럽트가 무시되거나 PIC에 버퍼링되어 나중에 처리됩니다.

19.8.6.3 DMA

I/O 장치에 액세스하기 위해 폴링과 인터럽트를 모두 사용할 수 있습니다. 어떤 경우든 각 I/O 명령어에 대해 일반적으로 한 번에 4바이트가 전송됩니다. 즉, 4KB 블록이 I/O 장치로 전송되어야 하는 경우 1024개의 출력 명령어가 실행되어야 함을 의미합니다. 마찬가지로 4KB의 데이터를 읽으려면 1024개의 명령어를 실행해야 합니다. 각 I/O 명령은 여러 수준의 간접 참조를 거친 후 I/O 포트에 도달하므로 일반적으로 10사이클 이상이 소요됩니다. 둘째, I/O 버스의 주파수는 일반적으로 프로세서 주파수의 1/3~1/4입니다. 따라서 대규모 데이터 블록의 I/O는 다소 느린 프로세스이며 프로세서를 오랫동안 바쁘게 유지하게 됩니다. 목표는 장치 드라이버 및 인터럽트 핸들러와 같은 민감한 코드를 가능한 한 짧게 유지하는 것입니다.

따라서 프로세서 작업의 일부를 로드할 수 있는 솔루션을 설계해 보십시오. 비유 시나리오를 생각해 보면, 한 교수가 100명이 넘는 학생이 있는 수업을 가르치고 시험 후에 100개 이상의 대본을 채점해야 한다고 가정해 보겠습니다. 이러한 상황으로 인해 그녀는 적어도 일주일 동안 바쁘게 지내게 될 것이며, 대본을 채점하는 과정은 매우 피곤하고 시간이 많이 걸리는 과정입니다. 결과적으로 그녀는 시험 스크립트 채점을 조교에게 맡기고 교수들이 최신 연구 문제를 해결하는 데 집중할 수 있는 자유 시간을 확보할 수 있습니다. 우리는 이 예에서 단서를 얻어 프로세서에 대해 유사한 체계를 설계할 수 있습니다.

프로세서를 대신하여 일부 작업을 수행하는 DMA(직접 메모리 액세스) 엔진이라는 특수 장치를 상상해 보십시오. 특히, 프로세서가 대량의 데이터를 메모리에서 I/O 장치로 또는 그 반대로 전송하려는 경우 대신에 많은 수의 I/O 명령을 발행하여 DMA 엔진이 책임을 대신할 수 있습니다. DMA 엔진을 사용하는 과정은 다음과 같습니다.

  • 장치 드라이버가 메모리와 I/O 장치 사이에 많은 양의 데이터를 전송해야 한다고 판단합니다.

  • 메모리 영역의 세부사항(바이트 범위)과 I/O 장치의 세부사항(I/O 포트 주소)을 DMA 엔진으로 전송하며, DMA 엔진은 데이터 전송이 메모리에서 I/O로 또는 그 반대로 이루어지는지 여부를 추가로 지정합니다.

  • 장치 드라이버가 스스로 정지하고 프로세서가 다른 프로그램을 자유롭게 실행할 수 있습니다. 동시에 DMA 엔진 또는 DMA 컨트롤러는 주 메모리와 I/O 장치 간에 데이터를 전송하는 프로세스를 시작합니다. 전송 방향에 따라 데이터를 읽어 임시로 저장한 뒤 목적지로 보낸다.

  • 전송이 완료되면 전송 종료를 알리는 인터럽트를 프로세서에 보냅니다.

  • I/O 장치의 장치 드라이버가 복구 작업 준비가 되어 있으며 나머지 모든 단계를 완료합니다.

최신 프로세서는 DMA 기반 방법을 사용하여 주 메모리, 하드 디스크 또는 네트워크 카드 간에 대량의 데이터를 전송하는 경우가 많습니다. 데이터 전송은 백그라운드에서 이루어지며 프로세서는 기본적으로 프로세스를 인식하지 못합니다. 둘째, 대부분의 운영 체제에는 데이터 전송을 수행하도록 DMA 엔진을 프로그래밍하는 라이브러리가 있습니다.

DMA 엔진과 관련하여 두 가지 미묘한 점을 논의해야 합니다.

  • 첫 번째는 DMA 컨트롤러가 때때로 버스 마스터가 되어야 한다는 것입니다. 대부분의 설계에서 DMA 엔진은 일반적으로 Northbridge 칩의 일부로, 메모리 버스의 버스 마스터가 되고 필요할 경우 Southbridge 칩 버스가 됩니다. 모든 데이터를 한 번에 전송할 수 있는 버스트 모드라는 방법이 있고, 버스의 유휴 주기를 기다렸다가 해당 주기를 사용하여 자체 전송을 예약할 수 있는 방법(사이클 도용 모드라고 함)이 있습니다.

  • 두 번째 미묘한 문제는 주의하지 않으면 정확성 문제가 발생할 수 있다는 것입니다. 예를 들어, DMA 엔진이 주 메모리의 위치에 데이터를 쓰는 동안 캐시에 특정 위치가 있을 수 있습니다. 이 경우 캐시의 값은 유효하지 않게 되며 불행하게도 프로세서는 이 사실을 알 수 없습니다. 따라서 DMA 컨트롤러가 액세스하는 위치가 캐시에 있지 않은지 확인하는 것이 중요합니다. 이는 일반적으로 DMA 엔진이 캐시에 기록하는 경우 캐시에 존재하는 위치를 동적으로 제거하는 DMA 스누프 회로라는 전용 논리 블록을 통해 구현됩니다.

DMA 메커니즘은 다양한 방법으로 구성할 수 있으며 아래 이미지는 몇 가지 가능성을 보여줍니다. 첫 번째 예에서는 모든 모듈이 동일한 시스템 버스를 공유하고 DMA 모듈은 프로그래밍된 I/O를 사용하여 DMA 모듈을 통해 메모리와 I/O 모듈 간에 데이터를 교환하는 프록시 프로세서 역할을 합니다. 이 구성은 잠재적으로 저렴하기는 하지만 분명히 비효율적입니다. 프로세서 제어 프로그래밍 I/O와 마찬가지로 각 단어 전송은 두 개의 버스 사이클을 소비합니다.

19.8.7 I/O 프로토콜

이 섹션에서는 여러 가지 최첨단 I/O 프로토콜의 작동을 설명하고 이에 대한 간략한 개요를 제공합니다. 자세한 연구를 위해 또는 의심스러운 경우 공식 사양이 온라인에 게시됩니다. 공식 사양은 일반적으로 I/O 프로토콜을 지원하는 회사 컨소시엄에서 게시되며 이 섹션에 제시된 대부분의 자료는 여기에서 나온 것입니다.

19.8.7.1 PCI 익스프레스

대부분의 마더보드에는 전용 사운드 카드, 네트워크 카드, 그래픽 카드와 같은 장치를 노스브리지나 사우스브리지 칩에 연결하는 데 사용할 수 있는 로컬 버스가 필요합니다. 이러한 요구에 부응하여 1993년 기업 컨소시엄은 PCI(Peripheral Component Interconnect) 버스 사양을 만들었습니다.

1996년에 Intel은 그래픽 카드 연결을 위한 AGP(Accelerated Graphics Port) 버스를 만들었습니다. 1990년대 후반에는 다양한 하드웨어 장치를 노스브리지 및 사우스브리지 칩에 연결하기 위한 새로운 버스 유형이 많이 제안되었습니다. 설계자들은 다양한 버스 프로토콜이 표준화 노력을 방해하고 장비 공급업체가 여러 버스 프로토콜을 지원해야 한다는 사실을 빨리 깨달았습니다. 따라서 기업 컨소시엄은 표준화 노력을 시작했고 2004년에 PCI Express 버스 표준을 만들었습니다. 이 기술은 대부분의 이전 기술을 대체했으며 마더보드에서 가장 널리 사용되는 버스입니다.

PCI 익스프레스 버스의 기본 개념은 고속 지점 간 직렬(유닛) 상호 연결이라는 것입니다. 지점 간 상호 연결에는 두 개의 끝점만 있으며 여러 장치를 사우스브리지 칩에 연결하기 위해 PCI Express 장치 트리가 생성됩니다. 트리의 내부 노드는 여러 장치의 트래픽을 다중화하는 PCI Express 스위치입니다. 둘째, 이전 프로토콜과 비교하여 각 PCI Express 버스는 단일 비트 라인에서 비트를 직렬로 전송합니다. 고속 버스는 일반적으로 여러 비트를 병렬로 전송하기 위해 여러 구리선을 사용하지 않습니다. 링크마다 지터 및 신호 왜곡 정도가 다르기 때문입니다. 서로 다른 와이어의 모든 신호를 서로 동기화하는 것이 매우 어려워지므로 최신 버스는 대부분 직렬입니다.

단일 PCI Express 버스는 실제로 여러 개의 개별 직렬 버스(레인이라고 함)로 구성되며 각 레인에는 자체 별도의 물리적 계층이 있으며 PCI Express 패킷은 레인 전체에 걸쳐 스트라이프됩니다. 스트라이핑은 데이터 블록(패킷)을 더 작은 블록으로 나누고 이를 레인 전체에 분산시키는 것을 의미합니다. 예를 들어 8레인과 8비트 패킷이 있는 버스에서 패킷의 각 비트는 별도의 레인에서 전송될 수 있습니다. 서로 다른 레인에서 여러 비트를 병렬로 보내는 것은 데이터를 보낼 여러 와이어가 있는 병렬 버스와 다릅니다. 병렬 버스에는 모든 구리 와이어에 대한 물리적 계층 회로가 있는 반면, 이 경우 각 레인에는 자체 별도의 동기화 및 타이밍이 있기 때문입니다. 데이터 링크 계층은 서로 다른 채널에서 수집된 각 패킷의 하위 부분을 모아 프레이밍을 수행합니다.

스트라이핑 프로세스는 데이터 블록을 더 작은 데이터 블록으로 나누고 이를 일련의 엔터티에 배포하는 것입니다.

채널은 전이중 신호 전달을 위한 두 개의 LVDS 기반 와이어로 구성됩니다. 하나의 와이어는 첫 번째 끝점에서 두 번째 끝점으로 메시지를 보내는 데 사용되고, 두 번째 와이어는 반대 방향으로 신호를 보내는 데 사용됩니다. 채널 세트는 함께 그룹화되어 완전한 패킷(또는 프레임)을 전송하는 것으로 가정되는 I/O 링크를 형성합니다. 그런 다음 물리 계층은 오류 수정, 흐름 제어 및 구현 트랜잭션을 수행하는 데이터 링크 계층으로 패킷을 전송합니다. PCI Express 프로토콜은 각 계층의 기능이 우리가 정의하는 I/O 계층과 대략 유사한 계층형 프로토콜입니다. 트랜잭션을 데이터 링크 계층의 일부로 처리하는 대신 별도의 트랜잭션 계층을 갖습니다. 그러나 달리 명시하지 않는 한 이 장에서 정의된 용어를 사용하여 모든 I/O 프로토콜을 설명합니다.

PCI Express 프로토콜의 사양은 아래 표에 요약되어 있습니다. 1~32개 차선이 있습니다. 각 레인은 비동기 버스입니다. 8비트/10비트 인코딩이라는 복잡한 데이터 인코딩을 사용합니다. 8비트/10비트 인코딩은 개념적으로 NRZ 프로토콜의 확장으로 간주될 수 있습니다. 이 프로토콜은 8개의 논리 비트 시퀀스를 10개의 물리적 비트 시퀀스로 매핑하여 클록을 효과적으로 복구하는 데 연속 5개 이상의 1 또는 0을 사용할 수 없도록 보장합니다. 수신기는 데이터의 전환을 분석하여 송신기의 클록을 복구한다는 점을 기억하세요. 둘째, 인코딩은 전송된 신호에서 거의 동일한 수의 물리적 1과 0을 갖도록 보장합니다. 데이터 링크 계층에서 PCI Express 프로토콜은 1-128바이트 프레임과 32비트 CRC 기반 오류 수정 기능을 갖춘 분할 트랜잭션 버스를 구현합니다.

PCI Express 버스는 일반적으로 범용 I/O 장치를 연결하는 데 사용됩니다. 때때로 일부 슬롯은 사용자가 나중에 특정 응용 프로그램을 위해 카드를 연결할 수 있도록 사용되지 않은 채 남아 있습니다. 예를 들어 사용자가 특수 의료 장치 작업에 관심이 있는 경우 외부 또는 내부적으로 의료 장치에 연결할 수 있는 I/O 카드를 PCI Express 버스에 연결할 수 있습니다. 이 무료 PCI Express 슬롯을 확장 슬롯이라고 합니다.

QPI와 유사하게 PCIe는 지점 간 아키텍처이며 각 PCIe 포트는 여러 양방향 채널로 구성됩니다(QPI 채널은 단방향 전송만 나타냄). PCI 포트는 레인의 각 방향으로 이동하는 한 쌍의 전선에서 차동 신호를 통해 1, 4, 6, 16 또는 32개의 레인을 제공할 수 있습니다.

QPI와 마찬가지로 PCIe는 다중 레인 분배 기술을 사용합니다. 아래 이미지는 4개의 레인으로 구성된 PCIe 포트의 예를 보여줍니다. 간단한 라운드 로빈 방식을 사용하여 한 번에 1바이트씩 4개의 채널에 데이터를 할당하고, 16바이트(128비트)의 데이터가 한 번에 각 물리적 채널에서 버퍼링되고 처리됩니다. 128비트의 각 블록은 128b/130b 인코딩이라고 알려진 전송을 위한 고유한 130비트 코드워드로 인코딩됩니다. 따라서 단일 채널의 유효 데이터 속도는 128/130배로 감소됩니다.

PCIe 다층 배선.

아래 그림은 스크램블링과 인코딩의 사용을 보여줍니다. 전송될 데이터는 스크램블러에 공급되고, 스크램블링된 출력은 128b/130b 인코더에 공급됩니다. 이 인코더는 128비트를 버퍼링한 다음 128비트 블록을 130비트 블록으로 매핑합니다. 그런 다음 블록은 병렬-직렬 변환기를 통과하고 차동 신호를 사용하여 한 번에 한 비트씩 전송됩니다.

19.8.7.2 SATA

이제 버스를 살펴보면 주로 하드 드라이브, 광 디스크와 같은 저장 장치를 연결하기 위해 개발되었습니다. 1980년대 중반부터 설계자와 스토리지 공급업체가 이 버스를 설계하기 시작했습니다. 시간이 지나면서 IDE(Integrated Drive Electronics) 및 PATA(Parallel Advanced Technology Attachment) 버스와 같은 여러 버스가 개발되었습니다. 이러한 버스는 주로 병렬 버스이며, 버스가 구성하는 통신 링크는 지터와 왜곡의 정도가 다양합니다. 따라서 이러한 기술은 PCI Express와 같은 지점 간 링크인 SATA(Serial ATA)라는 직렬 표준으로 대체되었습니다.

저장 장치에 액세스하기 위한 SATA 프로토콜은 이제 대부분의 노트북 및 데스크탑 프로세서에서 사용되며 사실상의 표준이 되었습니다. SATA 프로토콜에는 물리 계층, 데이터 링크 계층 및 전송 계층의 세 가지 계층이 있습니다. SATA 프로토콜의 전송 계층을 프로토콜 계층에 매핑하고 각 SATA 링크에는 LVDS 신호를 사용하는 한 쌍의 장치 링크가 포함됩니다. PCI Express와 달리 SATA 프로토콜의 엔드포인트는 동시에 데이터를 읽고 쓰는 것이 불가능합니다. 언제든지 이러한 작업 중 하나만 수행할 수 있으므로 반이중 버스이고 8b/10b 인코딩을 사용하며 비동기 버스입니다. 데이터 링크 계층은 프레이밍 작업을 완료합니다. 이제 네트워크 계층에 대해 살펴보겠습니다. SATA는 지점 간 프로토콜이므로 SATA 장치 그룹을 트리 구조로 연결할 수 있습니다. 트리의 각 내부 노드는 승수라고 불리며, 요청을 부모에서 자식으로, 또는 자식에서 부모로 라우팅합니다. 마지막으로 프로토콜 계층은 프레임에 작용하여 프레임이 올바른 순서로 전송되는지 확인하고 SATA 명령을 구현합니다. 구체적으로 말하면 DMA 요청을 구현하고, 저장 장치에 액세스하고, 데이터를 버퍼링한 후 미리 정해진 순서에 따라 프로세서에 보냅니다.

다음 표는 SATA 프로토콜의 사양을 보여줍니다. SATA 프로토콜은 매우 풍부한 프로토콜 계층을 갖고 있으며 스토리지 기반 장치에 대한 다양한 명령을 제공한다는 점에 유의해야 합니다. 예를 들어 DMA 액세스 수행, 하드 디스크 직접 액세스 수행, 데이터 인코딩 및 암호화, 저장 장치 내부 제어 등을 수행하는 전용 명령이 있습니다. SATA 버스는 별도의 트랜잭션 버스이며 데이터 링크 계층은 명령과 응답을 구별합니다. 프로토콜 계층은 모든 명령의 의미를 구현합니다.

19.8.7.3 SCSI 및 SAS

SCSI 개요

이제 주변 장치를 위한 또 다른 I/O 프로토콜인 SCSI("scuzzy"로 발음) 프로토콜에 대해 살펴보겠습니다. SCSI는 PCI의 경쟁자로 시작되었지만 시간이 지나면서 저장 장치 연결을 위한 프로토콜로 변모했습니다.

원래의 SCSI 버스는 8~16개의 연결을 가질 수 있는 멀티드롭 병렬 버스였습니다. SCSI 프로토콜은 호스트와 주변 장치를 구별합니다. 예를 들어 Southbridge 칩은 호스트이고 CD 드라이브의 컨트롤러는 주변 장치입니다. 모든 노드 쌍(호스트 또는 주변 장치)은 서로 통신할 수 있습니다. 원래의 SCSI 버스는 동기식이었고 오늘날의 고속 버스에 비해 상대적으로 낮은 주파수에서 실행되었습니다. SCSI는 오늘날에도 여전히 존재하며, 가장 진보된 SCSI 버스는 80~160MHz 클럭을 사용하여 16비트를 병렬로 전송하므로 이론적 최대 대역폭은 320~640MB/s입니다. 직렬 버스는 최대 1GHz까지 올라갈 수 있고 더 다양하며 더 큰 대역폭을 지원할 수 있습니다.

다중 지점 병렬 버스의 문제를 고려하여 설계자는 SCSI 프로토콜을 지점 간 직렬 버스로 재배치하기 시작했습니다. PCI Express와 SATA 버스도 같은 이유로 만들어졌다는 점을 기억하세요. 따라서 설계자들은 원래 SCSI 프로토콜을 확장했지만 본질적으로 지점 간 직렬 버스인 일련의 버스를 고안했습니다. 이러한 중요한 기술 두 가지는 SAS(Serial Attached SCSI)와 FC(이중 채널) 버스입니다. FC 버스는 주로 슈퍼컴퓨터와 같은 최고급 시스템에 사용되며 SAS 버스는 기업 및 과학 응용 프로그램에서 더 일반적으로 사용됩니다.

따라서 오늘날 사용되는 SCSI 프로토콜의 가장 널리 사용되는 변형인 SAS 프로토콜에 주로 초점을 맞춰 보겠습니다. SAS는 이전 버전의 SATA 기반 장치와도 호환되는 직렬 지점 간 기술이며 해당 사양은 SATA 사양과 매우 유사합니다.

SAS 개요

SAS는 SATA와 역호환되도록 설계되었기 때문에 두 프로토콜은 물리 계층과 데이터 링크 계층에서 크게 다르지 않지만 여전히 약간의 차이점이 있습니다. 가장 큰 차이점은 SAS는 전이중 전송을 허용하는 반면 SATA는 반이중 전송만 허용한다는 것입니다. 둘째, SAS는 일반적으로 더 큰 랙 크기를 지원할 수 있으며 SATA보다 엔드포인트 간 더 긴 케이블 길이를 지원합니다(SAS의 경우 8미터, SATA의 경우 1미터).

네트워크 계층은 SATA와 다릅니다. SAS는 승수(SATA에서 사용됨)를 사용하는 대신 여러 SAS 대상을 연결하기 위해 확장기라는 더 복잡한 구조를 사용합니다. 전통적으로 SAS 버스의 버스 마스터 노드를 개시자(initiator)라고 부르고, 다른 노드를 대상 노드(target node)라고 부릅니다. 확장기에는 에지 확장기와 팬아웃 확장기의 두 가지 유형이 있습니다. 에지 확장기는 최대 255개의 SAS 장치를 연결하는 데 사용할 수 있으며 팬아웃 확장기는 최대 255개의 에지 확장기를 연결하는 데 사용할 수 있습니다. 루트 노드와 확장기 세트를 사용하여 트리 기반 토폴로지에 많은 수의 장치를 추가할 수 있습니다. 각 장치에는 부팅 시 고유한 SCSI ID가 할당됩니다. 장치는 여러 논리 파티션으로 더 세분화될 수 있습니다. 예를 들어, 작성자는 현재 각각 논리 장치 번호(LUN)가 있는 두 개의 논리 파티션으로 나누어진 스토리지 시스템을 다루고 있습니다. 라우팅 알고리즘은 다음과 같습니다. 개시자는 장치에 직접 명령을 보내거나 직접 연결이 있는 경우 확장기로 명령을 보냅니다. 확장기에는 SCSI ID를 기반으로 장치의 위치를 ​​유지하는 상세한 라우팅 테이블이 있으며, 이 라우팅 테이블을 조회하고 패킷을 장치 또는 에지 확장기로 전달합니다. 이 에지 확장기에는 명령을 적절한 SCSI 장치로 전달한 다음 명령을 적절한 LUN으로 전달하는 또 다른 라우팅 테이블이 있습니다. 다른 SCSI 장치나 프로세서에 메시지를 보내기 위해 요청은 반대 경로를 따릅니다.

마지막으로, 프로토콜 계층은 SAS 버스에 대해 매우 유연합니다. 세 가지 프로토콜을 지원하며 SATA 명령, SCSI 명령 또는 SMP(SAS 관리 프로토콜) 명령을 사용할 수 있습니다. SMP 명령은 SAS 장치 네트워크를 구성하고 유지 관리하는 데 사용되는 특수 명령입니다. SCSI 명령 세트는 매우 광범위하며 다양한 장치(주로 저장 장치)를 제어하도록 설계되었습니다. 장치에 SCSI 명령을 보내기 전에 장치가 SCSI 프로토콜 계층과 호환되어야 한다는 점에 유의하십시오. 장치가 특정 명령을 이해하지 못하면 재앙적인 일이 발생할 수 있습니다. 예를 들어 CD를 읽고 싶지만 CD 드라이버가 명령을 이해하지 못하면 CD를 꺼낼 수 있습니다. 더 나쁜 것은 꺼내기 명령을 이해하지 못하기 때문에 CD를 꺼내지 못할 수도 있다는 것입니다. SATA의 경우에도 동일한 주장이 적용됩니다. SATA 명령을 사용하려면 SATA 호환 하드 드라이브, SATA 호환 광학 드라이브 등 SATA 호환 장치가 필요합니다. 프로토콜 계층의 유연성으로 인해 SAS 버스는 SATA 장치 및 SAS/SSCSI 장치와 호환되도록 설계되었습니다. 프로토콜 계층의 경우 SAS 개시자는 SCSI 명령을 SAS/SSCSI 장치로 보내고 SATA 명령을 SATA 장치로 보냅니다.

Nearline SAS(NL-SAS) 드라이브는 기본적으로 SATA 드라이브이지만 SCSI 명령을 SATA 명령으로 변환하는 SCSI 인터페이스가 있습니다. 따라서 NL-SAS 드라이브는 SAS 버스에서 원활하게 사용될 수 있습니다. 보다 표현력이 풍부하고 효율적인 SCSI 명령 세트로 인해 NL-SAS 드라이브는 순수 SATA 드라이브보다 10-20% 더 빠릅니다.

이제 4개의 문장을 사용하여 SCSI 명령 세트를 간략하게 설명하십시오. 개시자는 먼저 대상에 명령을 보냅니다. 각 명령에는 1바이트 헤더와 가변 길이 페이로드가 있습니다. 그런 다음 대상은 명령 실행 상태가 포함된 응답을 보냅니다. SCSI 사양은 장치 제어 및 데이터 전송을 위해 최소 60가지의 다양한 명령을 제공합니다.

19.8.7.4 USB

개요

USB 프로토콜은 주로 키보드, 마우스, 스피커, 웹캠, 프린터와 같은 외부 장치를 노트북이나 데스크톱 컴퓨터에 연결하는 데 사용됩니다. 1990년대 중반에 공급업체는 다중 I/O 버스 프로토콜과 커넥터를 사용하면 마더보드 설계자와 장치 드라이버 작성자가 많은 수의 장치를 지원하기 어렵다는 것을 깨달았습니다. 따라서 표준화가 필요했고 회사 컨소시엄(DEC, IBM, Intel, Nortel, NEC 및 Microsoft)이 USB 프로토콜(범용 직렬 버스)을 구상했습니다.

USB 프로토콜의 주요 목적은 다양한 장치에 대한 표준 인터페이스를 설계하는 것입니다. 설계자들은 처음에 장치를 저속(키보드, 마우스), 전속(고속 오디오), 고속(스캐너 및 카메라)이라는 세 가지 유형으로 분류했습니다. 2012년 현재 USB 프로토콜의 세 가지 버전, 즉 버전 1.0, 2.0, 3.0이 제안되었습니다. 기본 USB 프로토콜은 거의 동일하며 프로토콜은 이전 버전과 호환됩니다. 즉, USB 3.0 포트가 있는 최신 컴퓨터는 USB 1.0 장치를 지원합니다. 특정 하드웨어 세트용으로 설계된 SAS 또는 SATA 프로토콜과 달리 USB 프로토콜은 매우 일반적으로 설계되었으므로 대상 장치의 동작에 대해 많은 가정을 할 수 있습니다. 따라서 설계자는 장치 유형과 요구 사항을 파악하고 적절하게 구성할 수 있도록 운영 체제에 대한 광범위한 지원을 제공해야 합니다. 둘째, 키보드나 마우스와 같이 전원이 없는 USB 장치가 많습니다. USB 케이블로 연결된 장치를 실행하려면 전원 코드가 포함되어 있어야 합니다. USB 프로토콜 설계자는 이러한 모든 요구 사항을 염두에 두었습니다.

설계자들은 처음부터 USB가 향후 고화질 비디오와 같은 고속 장치를 지원할 수 있는 빠른 프로토콜이 되기를 원했습니다. 따라서 그들은 지점 간 직렬 버스(PCI Express, SATA 및 SAS와 유사)를 사용하기로 결정했습니다. 모든 노트북, 데스크탑 및 중급 서버의 전면 또는 후면 패널에는 일련의 USB 포트가 있으며, 각 USB 포트는 일련의 USB 장치를 연결할 수 있는 호스트로 간주됩니다. 직렬 링크를 사용하므로 PCI Express 및 SAS 장치 트리와 유사한 USB 장치 트리를 생성할 수 있습니다. 대부분의 경우 하나의 장치만 USB 포트에 연결됩니다. 그러나 유일한 구성은 아니고 트리의 내부 노드 역할을 하는 USB 허브를 연결하는 것도 가능합니다. USB 허브는 원칙적으로 SATA 승수 및 SAS 확장기와 유사합니다.

USB 허브는 대부분의 경우 수동 장치이며 일반적으로 다른 장치와 허브 다운스트림에 연결되는 4개의 포트가 있습니다. 허브의 가장 일반적인 구성에는 업스트림 포트 1개(상위 노드에 연결됨)와 다운스트림 포트 4개가 포함됩니다. 이런 방식으로 USB 허브 트리를 생성하고 여러 장치를 마더보드의 단일 USB 호스트에 연결할 수 있습니다. USB 프로토콜은 호스트당 127개의 장치를 지원하며 최대 5개의 허브를 직렬로 연결할 수 있습니다. 허브는 호스트에 의해 전원을 공급받을 수도 있고 자체 전원을 공급받을 수도 있습니다. 허브가 자체 전원을 공급받는 경우 USB 프로토콜은 단일 장치에 전송할 수 있는 전류량에 제한이 있으므로 더 많은 장치를 연결할 수 있습니다. 현재는 500mA로 제한되어 있으며 전력은 100mA 블록으로 분배됩니다. 따라서 호스트로 구동되는 허브는 각 장치에 100mA를 제공하고 100mA를 유지할 수 있으므로 최대 4개의 포트를 가질 수 있습니다. 때로는 허브가 활성 장치여야 하며, USB 장치가 허브에서 연결 해제될 때마다 허브는 이 이벤트를 감지하고 프로세서에 메시지를 보냅니다.

USB 프로토콜 계층

  • 물리적 레이어

이제 물리 계층부터 시작하여 프로토콜에 대해 더 자세히 논의합니다. 표준 USB 커넥터에는 4개의 핀이 있으며, 첫 번째 핀은 Vbus의 경우 Vcc라고도 하는 고정 5V DC 전압을 제공하는 전력선입니다. 차동 신호용 핀은 D+와 D- 두 개가 있으며 기본 전압 설정은 3.3V입니다. 네 번째 핀은 접지 핀(GND)이며 미니 및 마이크로 USB 커넥터에는 호스트와 장치에 대한 연결을 구별하는 데 도움이 되는 ID라는 추가 핀이 있습니다.

USB 프로토콜은 NRZI 프로토콜의 변형을 사용하는 차동 신호를 사용합니다. 논리 비트를 인코딩하는 경우 논리 0은 물리적 비트의 전환으로 표시되고 논리 1은 전환 없음(기존 NRZI 프로토콜과 반대)으로 표시된다고 가정합니다. USB 버스는 클럭을 복구하는 비동기 버스입니다. 클럭 복구를 돕기 위해 동기화 하위 계층은 데이터에 전환이 없는 경우 가상 전환을 도입합니다. 예를 들어, 1의 연속 실행이 있는 경우 전송된 신호에는 전환이 없습니다. 이 경우 USB 프로토콜은 6개의 1이 실행될 때마다 0비트를 도입합니다. 이 전략은 신호의 일부 전환이 보장되고 수신기가 동기화 손실 없이 송신기의 클럭을 복구할 수 있도록 보장합니다. USB 커넥터에는 차동 신호용 전선이 한 쌍만 있으므로 전이중 신호가 불가능하고 대신 USB 링크가 반이중 신호를 사용합니다.

  • 데이터 링크 계층

데이터 링크 계층의 경우 USB 프로토콜은 CRC 기반 오류 검사와 가변 프레임 길이를 사용합니다. 이는 비트 스터핑(개인 프레임 시작 및 끝 기호)을 사용하여 프레임 경계를 구분합니다. 중재는 많은 종류의 트래픽과 많은 종류의 장치가 있기 때문에 USB 허브에서 상당히 복잡한 문제입니다. USB 프로토콜은 네 가지 유형의 트래픽을 정의합니다.

  • 제어: 장치를 구성하는 데 사용되는 제어 메시지입니다.

  • 중단: 소량의 데이터를 긴급하게 장치에 전송해야 합니다.

  • 대량: 지연이나 대역폭 보장이 없는 대량의 데이터입니다. 스캐너의 이미지 데이터 등.

  • 등시성: 대기 시간 및 대역폭이 보장되는 고정 속도 데이터 전송입니다. 네트워크 카메라의 오디오/비디오 등.

트래픽 흐름이 다르기 때문에 저속 장치(192KB/s), 전속 장치(1.5MB/s), 고속 장치(60MB/s) 등 다양한 종류의 USB 장치가 있습니다. USB 3.0 프로토콜에는 384MB/s가 필요한 고속 장치가 도입되었습니다.

이제 동일한 허브에 고속 및 저속 장치를 연결할 수 있습니다. 고속 장치가 대량 전송을 수행하고 저속 장치가 인터럽트를 전송한다고 가정합니다. 이 경우 허브의 업스트림 링크에 대한 액세스가 우선되어야 합니다. 각 트래픽 유형과 각 장치 유형에 대한 사양을 준수해야 하기 때문에 중재가 어렵고, 대량 전송 수행과 인터럽트 전송 사이에 딜레마가 발생합니다. 이상적으로는 서로 다른 트래픽 우선순위 휴리스틱을 사용하여 충돌하는 요구 사항 간의 균형을 이루고 싶습니다. 중재 메커니즘에 대한 자세한 설명은 USB 사양을 참조하세요.

이제 거래 문제를 고려해보세요. 고속 허브가 호스트에 연결되어 있다고 가정합니다. 고속 허브(허브)는 다운스트림 전속 및 저속 장치에도 연결됩니다. 이 경우 호스트가 고속 허브를 통해 저속 장치에 대한 트랜잭션을 시작하면 고속 허브와 장치 간의 링크가 느리므로 장치로부터 응답을 기다려야 합니다. 이 경우 호스트와 허브 사이의 버스를 잠글 이유가 없습니다. 대신 분할 트랜잭션의 첫 번째 부분이 저속 장치에 명령을 보내고 분할 트랜잭션의 두 번째 부분이 저속 장치에서 호스트로 메시지를 포함하는 분할 트랜잭션을 구현할 수 있습니다. 분할 트랜잭션 사이의 간격 동안 호스트는 다른 장치와 통신할 수 있습니다. USB 버스는 다른 많은 유형의 시나리오에 대해 유사한 분할 트랜잭션을 구현합니다(USB 사양 참조).

  • 네트워크 계층

이제 네트워크 계층을 고려하십시오. 허브를 포함한 각 USB 장치에는 호스트가 고유한 ID를 할당합니다. 각 호스트는 최대 127개의 장치를 지원할 수 있으므로 7자리 장치 ID가 필요합니다. 둘째, 모든 장치에는 여러 개의 I/O 포트가 있으며 이러한 각 I/O 포트를 엔드포인트라고 합니다. 데이터 엔드포인트(인터럽트, 배치 또는 동기화)가 있을 수 있고 제어 엔드포인트가 있을 수 있습니다. 또한 끝점은 IN 또는 OUT으로 분류될 수 있으며, IN 끝점은 프로세서에만 데이터를 보낼 수 있는 I/O 포트를 나타내고 OUT 끝점은 프로세서의 데이터를 수락합니다. 각 USB 장치는 최대 16개의 IN 끝점과 16개의 OUT 끝점을 가질 수 있으며 모든 USB 요청은 액세스해야 하는 끝점 유형(IN 또는 OUT)을 명확하게 지정합니다. 엔드포인트의 유형이 요청에 의해 고정된다는 점을 고려하면 엔드포인트의 주소를 지정하는 데는 4비트만 필요합니다.

모든 USB 장치에는 ID가 0인 기본 IN 및 OUT 끝점 세트가 있습니다. 이러한 끝점은 장치를 활성화하고 장치와 통신을 설정하는 데 사용됩니다. 이후 각 장치는 사용자 지정 엔드포인트 집합을 정의합니다. 마우스나 키보드와 같은 간단한 장치에는 일반적으로 프로세서에 데이터를 보내는 데 IN 엔드포인트만 필요합니다. 그러나 웹캠과 같은 더 복잡한 장치에는 여러 개의 엔드포인트가 필요합니다. 하나의 엔드포인트는 비디오 피드용이고 다른 하나는 오디오 피드용이며 제어 및 상태 데이터 교환을 위한 여러 엔드포인트가 있을 수 있습니다.

허브는 메시지를 올바른 USB 장치로 라우팅하는 역할을 합니다. 허브는 USB 장치를 로컬 포트 ​​ID와 연결하는 라우팅 테이블을 유지 관리합니다. 메시지가 장치에 도달하면 이를 올바른 엔드포인트로 라우팅합니다.

  • 프로토콜 계층

USB 프로토콜 계층은 매우 복잡합니다. 첫째, 파이프라고 하는 끝점 사이에 두 개의 연결이 정의됩니다. 파이프는 특정 메시지 구조 없이 스트림 파이프라인을 데이터 흐름으로 정의합니다. 이와 대조적으로 메시지 파이프라인은 더 구조화되어 있으며 발신자와 수신자가 모두 따라야 하는 메시지 시퀀스를 정의합니다. 메시지 파이프라인의 일반적인 메시지는 세 가지 유형의 패킷으로 구성됩니다. 통신은 장치 ID, 엔드포인트 ID, 통신 특성 및 연결에 대한 추가 정보가 포함된 토큰 패킷으로 시작됩니다. 경로를 따라 있는 허브는 토큰 패킷을 대상으로 라우팅하여 연결을 설정합니다. 그런 다음 전송 방향(호스트에서 장치로 또는 장치에서 호스트로)에 따라 호스트나 장치는 일련의 패킷을 보냅니다. 마지막으로, 데이터 패킷 시퀀스의 끝에서 패킷 수신자는 I/O 요청의 성공적인 완료를 나타내기 위해 핸드셰이크 패킷을 보냅니다.

요약

다음 표에는 USB에 대한 설명이 요약되어 있습니다. 자세한 내용은 USB 프로토콜 사양을 참조하세요.

19.8.8 스토리지

일반적으로 프로세서에 연결되는 모든 주변 장치 중에서 저장 장치는 주로 컴퓨터 시스템 기능에 필수적이기 때문에 특별한 위치를 차지합니다.

저장 장치는 지속적인 상태를 유지합니다. 지속 상태는 전원이 켜져 있는 동안에도 컴퓨터 시스템에 저장된 모든 데이터를 나타냅니다. 특히 스토리지 시스템에는 운영 체제, 모든 프로그램 및 모든 문서, 노래, 이미지 및 비디오를 포함한 관련 데이터가 저장됩니다. 컴퓨터 설계자의 관점에서 스토리지 시스템은 부팅 프로세스에서 파일과 데이터는 물론 가상 메모리까지 보관하는 적극적인 역할을 합니다. 이러한 역할을 하나씩 논의해 보겠습니다.

프로세서가 시작되면(부팅이라는 프로세스) 운영 체제 코드를 로드해야 합니다. 일반적으로 운영 체제의 코드는 주 하드 디스크의 주소 공간 시작 부분에서 사용할 수 있으며, 프로세서는 운영 체제의 코드를 주 메모리에 로드하고 실행을 시작합니다. 부팅 프로세스 후 사용자는 운영 체제를 사용하여 프로그램을 실행하고 데이터에 액세스할 수 있습니다. 프로그램은 스토리지 시스템에 일반 파일로 저장되며, 데이터도 파일로 저장됩니다. 파일은 본질적으로 프로세서가 액세스할 수 있도록 주 메모리로 읽어야 하는 하드 드라이브 또는 유사한 저장 장치의 데이터 블록입니다.

마지막으로 저장 장치는 가상 메모리 구현에 매우 중요한 역할을 하며, 주 메모리에 담을 수 없는 모든 프레임을 포함하는 스왑 공간을 저장하여 가상 주소 공간의 크기에 맞게 물리적 주소 공간을 효과적으로 확장하는 데 도움을 줍니다. 프레임의 일부는 메인 메모리에 저장되고 나머지 프레임은 스왑 공간에 저장됩니다. 페이지 폴트가 발생할 때 도입(스왑)됩니다.

거의 모든 유형의 컴퓨터에는 저장 장치가 연결되어 있지만 일부 예외가 있으며 특히 연구실 환경의 일부 컴퓨터는 네트워크를 통해 하드 드라이브에 액세스할 수 있습니다. 일반적으로 네트워크 부팅 프로토콜을 사용하여 원격 하드 드라이브에서 부팅하고 네트워크를 통해 스왑 공간을 포함한 모든 파일에 액세스합니다. 개념적으로는 여전히 연결된 저장 장치가 있습니다. 마더보드에 물리적으로 연결되어 있지는 않지만 네트워크를 통해 액세스할 수 있습니다.

이제 주요 스토리지 기술을 살펴보겠습니다. 전통적으로는 대형 강자성 디스크의 작은 영역에 비트 값을 기록하는 자기 스토리지가 지배적인 기술이었다. 자화 상태에 따라 논리값 0 또는 1이 유추될 수 있습니다. 자기 디스크 기술 대신 CD/DVD/Blu-ray 드라이브와 같은 광 디스크 기술을 사용할 수 있습니다. CD/DVD/블루레이 디스크에는 일련의 이진 값을 인코딩하는 일련의 피트(표면 수차)가 포함되어 있으며, 디스크 드라이브는 레이저를 사용하여 디스크에 저장된 값을 읽습니다. 컴퓨터에서의 대부분의 작업은 일반적으로 하드 드라이브에 액세스하며 광 디스크는 주로 비디오와 음악을 보관하는 데 사용되지만 광 드라이브에서 부팅하는 것은 드문 일이 아닙니다.

솔리드 스테이트 드라이브는 자기 디스크 및 광 디스크에 대한 빠른 대안입니다. 움직이는 부품이 있는 자기 및 광학 드라이브와 달리 솔리드 스테이트 드라이브는 반도체로 만들어집니다. SSD에 사용되는 가장 일반적인 기술은 플래시 메모리입니다. 플래시 메모리 장치는 반도체에 저장된 전하를 사용하여 논리 0 또는 1을 나타냅니다. 이는 기존 하드 드라이브보다 훨씬 빠르지만 훨씬 적은 양의 데이터를 저장할 수 있으며 2012년 현재 가격은 5~6배 더 높습니다. 따라서 고급 서버는 더 큰 하드 드라이브의 캐시 역할을 하는 빠른 SSD 드라이브를 갖춘 하이브리드 솔루션을 선택합니다.

19.8.8.1 하드 드라이브

하드 드라이브는 노트북에서 서버에 이르기까지 대부분의 컴퓨터 시스템에서 필수적인 부분입니다. 강자성체 재료와 기계 부품으로 만들어진 저장 장치로, 저렴한 비용으로 대용량 저장 용량을 제공할 수 있습니다. 결과적으로 하드 드라이브는 지난 30년 동안 개인용 컴퓨터, 서버 및 기업 수준 시스템의 영구 상태를 저장하는 데에만 사용되었습니다.

놀랍게도 데이터 저장의 기본 물리학은 매우 간단합니다. 일련의 자석에 0과 1을 유지하는 것입니다. 이제 하드 드라이브의 데이터 저장에 대한 기본 물리학을 빠르게 검토합니다.

하드 디스크 데이터 저장 물리학

북극과 남극이 있고 같은 극은 서로 밀어내고 반대 극은 서로 끌어당기는 일반적인 자석을 생각해 보세요. 기계적 특성 외에도 자석에는 전기적 특성도 있습니다. 예를 들어 와이어 코일을 통과하는 자기장이 자석과 코일 사이의 상대 운동으로 인해 변경되면 패러데이 법칙에 따라 와이어에 EMF(전압)가 유도됩니다. 하드 드라이브는 패러데이의 법칙을 작동의 기초로 사용합니다.

하드 드라이브의 기본 요소는 작은 자석입니다. 하드 드라이브에 사용되는 자석은 일반적으로 산화철로 만들어지며 영구 자석이므로 자기 특성이 일정하게 유지됩니다. 그것들은 영구 자석 또는 강자성체(산화철 때문에)라고 불립니다. 대조적으로, 철 막대를 감싼 전류 운반선 코일로 구성된 전자석을 가질 수 있는데, 전류가 차단되면 자성을 잃습니다.

이제 아래 그림과 같이 직렬로 연결된 자석 세트를 고려하십시오. 상대 방향에는 N-S(북-남) 또는 S-N(남-북)의 두 가지 옵션이 있습니다. 이제 자석 배열 위로 작은 원의 와이어를 움직이면 반대 방향을 가리키는 두 자석의 경계를 넘을 때마다 자기장이 변경됩니다. 따라서 패러데이 법칙의 직접적인 결과로 EMF가 코일 전체에 유도됩니다. 그러나 자기장의 방향에 변화가 없으면 코일에 유도되는 EMF는 무시할 수 있습니다. 작은 자석 방향의 전환은 논리 1비트에 해당하는 반면, 전환이 없으면 논리 0비트를 나타냅니다. 따라서 다이어그램의 자석은 I/O 채널의 NRZI 인코딩과 유사한 비트 패턴 0101을 나타냅니다.

하드 드라이브 표면에 있는 일련의 작은 자석입니다.

변환 시 데이터가 인코딩되므로 데이터 블록을 저장해야 하며 하드 디스크는 데이터 블록을 섹터에 저장합니다. 하드 디스크 섹터의 ​​크기는 512바이트 사이이며 원자 블록으로 처리되며 일반적으로 전체 섹터를 한 번에 읽거나 씁니다. 작은 코일과 통과하는 자석을 포함하는 구조를 읽기 헤드라고 합니다.

이제 하드 드라이브에 데이터를 쓰는 방법을 살펴보겠습니다. 이 경우 자석의 방향을 설정하는 것이 과제인데, 작은 전자석이 들어 있는 쓰기 헤드라는 구조가 있습니다. 전자석이 영구자석을 통과하면 영구자석이 자화됩니다. 둘째, 자화의 방향은 전류의 방향에 따라 달라집니다. 전류의 방향이 바뀌면 자화의 방향도 바뀐다.

단순화를 위해 읽기 헤드와 쓰기 헤드가 결합된 어셈블리를 자기 헤드라고 합니다.

디스크 구조

하드 드라이브는 일반적으로 플래터 세트로 구성됩니다. 플래터는 중앙에 구멍이 있는 둥근 원반이며, 중앙의 구멍을 통해 스핀들이 플래터에 연결됩니다. 플래터는 아래 그림과 같이 트랙이라고 불리는 일련의 동심원 링으로 나누어지며, 트랙은 다시 고정 길이 섹터로 나뉩니다.

하드 드라이브는 여러 개의 플래터로 구성됩니다. 플래터는 스핀들에 고정된 디스크입니다. 플래터에는 트랙이라고 불리는 일련의 동심 링도 포함되어 있으며, 각 트랙은 일련의 섹터로 구성됩니다. 섹터에는 일반적으로 트랙에 관계없이 고정된 수의 바이트가 포함됩니다.

이제 하드 드라이브의 기본 작동에 대한 개요를 설명합니다. 플래터는 스핀들에 부착됩니다. 하드 드라이브 작동 중에는 스핀들과 스핀들에 부착된 플래터가 계속 회전합니다. 단순화를 위해 단일 디스크를 가정합니다. 첫 번째 단계는 필요한 데이터가 포함된 트랙에 자기 헤드를 배치하는 것입니다. 다음으로, 헤드는 원하는 섹터가 헤드 아래에 도달할 때까지 이 위치에서 기다려야 합니다. 플래터는 일정한 속도로 회전하기 때문에 헤드의 현재 위치를 기준으로 대기 시간을 계산할 수 있습니다. 원하는 섹터가 헤더 아래에 도달하면 계속해서 데이터를 읽거나 쓸 수 있습니다.

여기서 고려해야 할 중요한 문제가 있습니다. 트랙당 섹터 수가 동일한가요, 아니면 다른가요? 트랙당 저장할 수 있는 비트 수에는 기술적 제한이 있습니다. 따라서 트랙당 동일한 수의 섹터가 있는 경우 중앙에 가장 가까운 트랙에 저장할 수 있는 비트 수에 의해 제한되기 때문에 실제로 트랙의 저장 용량을 주변쪽으로 낭비하게 됩니다. 따라서 최신 하드 드라이브에서는 이러한 접근 방식을 피합니다.

트랙당 다양한 수의 섹터를 저장해 보세요. 중앙으로 향하는 트랙에는 더 적은 섹터가 포함되고, 주변으로 향하는 트랙에는 더 많은 섹터가 포함됩니다. 이 계획에는 문제가 있습니다. 가장 안쪽 트랙과 가장 바깥쪽 트랙을 비교하고 가장 안쪽 트랙에 N 섹터가 포함되어 있고 가장 바깥쪽 트랙에 2N 섹터가 포함되어 있다고 가정합니다. 분당 회전 수가 일정하다고 가정하면 가장 안쪽 궤도에서보다 가장 바깥쪽 궤도에서 데이터를 두 배 더 빠르게 읽어야 합니다. 실제로 트랙마다 데이터 검색 속도가 다르기 때문에 디스크의 전자 회로가 복잡해집니다. 탐색할 수 있는 또 다른 옵션은 데이터 전송 속도가 일정하게 유지되도록 각 트랙마다 다른 속도로 디스크를 회전시키는 것입니다. 이 경우 전자 회로는 더 간단하지만 스핀들 모터를 다양한 속도로 작동하는 데 필요한 복잡성 수준은 엄청납니다. 따라서 두 솔루션 모두 실용적이지 않습니다.

실용적이지 않은 두 가지 솔루션을 결합하여 실용적으로 만들어 보는 것은 어떨까요? 이 궤적 세트는 지역 세트로 나뉘며, 각 지역은 m개의 연속 트랙 세트로 구성됩니다. 디스크에 n개의 트랙이 있으면 n=m개의 영역이 있습니다. 트랙당 섹터 수는 각 구역에서 동일하며, 플래터는 구역의 모든 트랙에 대해 일정한 각속도로 회전합니다. 영역 내에서 중앙에 가까운 트랙은 플래터 주변에 있는 트랙보다 데이터 밀도가 높습니다. 즉, 섹터는 한 지역의 트랙마다 물리적으로 다른 크기를 갖습니다. 디스크 드라이브는 한 영역의 각 섹터를 통과하는 데 동일한 시간이 걸린다고 가정하고 일정한 각속도로 회전하는 것이 이를 보장하기 때문에 이는 문제가 되지 않습니다.

아래 그림은 플래터를 영역으로 나누는 개념도를 보여줍니다. 트랙당 섹터 수는 지역마다 다릅니다. 이 방법을 ZBR(Zoned-Bit Recording)이라고 합니다. 우리가 고려하지 않은 두 가지 비현실적인 디자인은 ZBR의 특별한 경우입니다. 첫 번째 디자인은 하나의 지역이 있다고 가정하고 두 번째 디자인은 각 트랙이 다른 지역에 속한다고 가정합니다.

파티션 비트 레코드.

디스크 레이아웃 방법 비교: (a) 일정한 각속도, (b) 다중 영역 기록.

이제 이 솔루션이 작동하는 이유를 살펴보세요. 파티션이 여러 개 있기 때문에 파티션 하나만 사용하는 디자인만큼 낭비되는 저장 공간이 높지 않습니다. 둘째, 영역의 수가 일반적으로 그리 크지 않기 때문에 스핀들 모터의 속도를 자주 재조정할 필요가 없습니다. 실제로 공간적 위치로 인해 같은 공간에 머물 확률은 상당히 높다.

하드디스크 구조

이제 모든 부품을 조립하고 아래 두 그림의 하드 드라이브 구조를 살펴보세요. 단일 회전 스핀들에 부착된 플래터 세트와 끝에 헤드가 포함된 디스크 암 세트(플래터의 각 측면에 하나씩)가 있습니다. 일반적으로 모든 팔은 함께 움직이며 모든 헤드는 동일한 실린더에 수직으로 정렬됩니다. 여기서 실린더는 동일한 반경을 갖는 여러 플래터의 트랙 세트로 정의됩니다. 대부분의 하드 드라이브에서는 한 번에 하나의 헤드만 활성화되어 특정 섹터에 대한 읽기 또는 쓰기 액세스를 수행합니다. 읽기 액세스의 경우 데이터는 포스트 프로세스(프레이밍, 오류 수정)를 위해 드라이브 전자 장치로 다시 전송된 다음 버스 인터페이스를 통해 버스에서 프로세서로 전송됩니다.

하드 드라이브 구조.

이제 하드 드라이브 설계의 몇 가지 미묘한 부분을 고려하십시오(아래 이미지 참조). 스핀들에 부착된 두 개의 플래터를 보여 주며, 각각 두 개의 기록 표면이 있습니다. 스핀들은 우리가 접근하려는 영역에 따라 속도를 조정하는 스핀들 모터라고 불리는 모터에 연결됩니다. 모든 암 세트는 함께 움직이며 스핀들을 사용하여 액추에이터에 연결됩니다. 액추에이터는 팔을 시계 방향 또는 시계 반대 방향으로 움직이는 작은 모터입니다. 액추에이터의 기능은 팔의 머리를 주어진 각도만큼 시계 방향 또는 시계 반대 방향으로 회전시켜 지정된 트랙에 팔의 머리를 위치시키는 것입니다.

하드디스크의 내부 구조.

데스크탑 프로세서의 일반적인 디스크 드라이브의 트랙 밀도는 인치당 약 10,000개 트랙입니다. 즉, 트랙이 2.5미크론 떨어져 있으므로 액추에이터는 매우 정확해야 합니다. 일반적으로 섹터에는 트랙 번호를 나타내는 일부 표시가 있으며 일반적으로 액추에이터는 정확한 지점에 도달하기 위해 약간의 조정이 필요합니다. 이 제어 메커니즘을 서보 제어라고 합니다. 액추에이터와 스핀들 모터는 모두 하드 드라이브 섀시 내의 전자 회로에 의해 제어됩니다. 액추에이터가 헤드를 올바른 트랙에 배치하면 원하는 섹터가 헤드 아래에 도착할 때까지 기다려야 합니다. 트랙에는 섹터 번호를 나타내는 표시가 있습니다. 헤드가 트랙에 배치되면 계속해서 표시를 읽습니다. 이러한 마커를 기반으로 원하는 섹터가 언제 헤드 아래에 있을지 정확하게 예측할 수 있습니다.

하드 드라이브에는 기계 구성 요소 외에도 소형 프로세서를 포함한 전자 구성 요소도 있습니다. 버스에서 데이터를 수신 및 전송하고, 하드 드라이브에서 요청을 예약하고, 오류 수정을 수행합니다. 하드 드라이브는 인간 공학의 놀라운 성과입니다. 하드 드라이브는 대부분의 경우 오류를 원활하게 허용하여 불량 섹터(결함 섹터)를 동적으로 무효화하고 데이터를 양호한 섹터에 다시 매핑할 수 있습니다.

다음 다이어그램은 모든 SSD 시스템과 관련된 일반적인 아키텍처 시스템 구성 요소의 일반적인 보기를 보여줍니다. 호스트 시스템에서 운영 체제는 파일 시스템 소프트웨어를 호출하여 디스크의 데이터에 액세스하고, 파일 시스템은 차례로 I/O 드라이버 소프트웨어를 호출하여 특정 SSD 제품에 대한 호스트 액세스를 제공합니다. 다이어그램의 인터페이스 구성 요소는 호스트 프로세서와 SSD 주변 장치 사이의 물리적, 전기적 인터페이스를 나타냅니다. 장치가 내부 하드 드라이브인 경우 공통 인터페이스는 PCIe입니다. 외부 장치의 경우 일반적인 인터페이스는 USB입니다.

솔리드 스테이트 드라이브 아키텍처.

하드 디스크 액세스의 수학적 모델

이제 요청이 하드 디스크에 대한 액세스를 완료하는 데 걸리는 시간에 대한 빠른 수학적 모델을 구축해 보겠습니다. 소요된 시간은 세 부분으로 나눌 수 있습니다.

  • 첫 번째는 탐색 시간으로, 헤드가 올바른 트랙에 도달하는 데 걸리는 시간으로 정의됩니다.

  • 헤드는 필요한 섹터가 그 아래에 도착할 때까지 기다려야 하며, 이 시간 간격을 회전 지연이라고 합니다.

  • 자기 헤드는 데이터를 읽고 오류와 중복 정보를 제거하기 위해 데이터를 처리한 다음 버스에서 데이터를 전송해야 하는데 이를 전송 시간이라고 합니다.

따라서 이를 설명하는 간단한 방정식이 있습니다.

T_{\text {디스크\_액세스 }=T_{\text {탐색 }+T_{\text {rot\_latency }+T_{\text {전송 }

그 밖에도 RAID 어레이, 광디스크, 플래시 디스크 등의 미디어가 있습니다. 자세한 내용은 18.11 파일 및 I/O를 참조하세요.

19.9 GPU

19.9.1 개요

고강도 그래픽은 현대 컴퓨터 시스템의 특징입니다. 스마트폰부터 고급 데스크탑까지 오늘날의 컴퓨터는 사용자 경험을 향상시키기 위해 다양하고 정교한 시각 효과를 사용합니다. 또한 사용자는 컴퓨터를 사용하여 그래픽 집약적인 게임을 플레이하고, 고화질 비디오를 시청하고, 컴퓨터 지원 엔지니어링 설계를 수행하는 등 광범위한 그래픽 처리가 필요한 모든 응용 프로그램을 수행할 수 있습니다.

초기에는 컴퓨터의 그래픽 지원이 매우 초보적이어서 프로그래머가 화면에 그려지는 각 모양의 좌표를 지정해야 했습니다. 예를 들어 선을 그리려면 프로그래머가 선의 좌표를 명시적으로 제공하고 색상을 지정해야 했습니다. 색상 범위는 매우 제한적이며 그래픽 집약적인 작업을 오프로드할 하드웨어도 거의 없습니다. 화면에 그려지는 선이나 원마다 여러 개의 어셈블리 문이 필요했기 때문에 컴퓨터 그래픽을 만들고 사용하는 과정은 느렸습니다. 점차적으로 하드웨어에 그래픽에 대한 일부 지원이 필요해졌습니다.

GPU와 CPU는 매우 다른 두 가지 애플리케이션을 위해 설계되고 최적화되었기 때문에 아키텍처에 상당한 차이가 있습니다. 이는 두 프로세서 기술(아래)에서 캐시, 제어 로직 및 처리 로직에 할당된 다이 영역(트랜지스터 수)의 상대적인 양을 비교하면 알 수 있습니다.

CPU와 GPU 간 캐시, ALU, 컨트롤러 등 하드웨어 유닛 비교.

19.9.1.1 그래픽 애플리케이션

최신 그래픽 애플리케이션을 두 가지 유형으로 나눌 수 있습니다. 첫 번째 카테고리는 자동 이미지 합성입니다. 예를 들어, 달밤에 캐릭터가 기관총을 들고 달리는 게임의 복잡한 장면을 생각해 보세요. 이 경우 프로그래머는 각 픽셀의 값을 주어진 색상으로 수동으로 설정하지 않습니다. 이는 너무 느리고 시간이 많이 걸리는 프로세스입니다. 이 방법을 사용하면 어떤 대화형 게임도 작동하지 않습니다. 대신 프로그래머는 높은 수준의 개체 수준에서 프로그램을 작성합니다. 예를 들어 도로, 식물, 장애물 등의 개체 집합으로 장면을 정의할 수 있습니다. 그는 캐릭터를 형성할 수 있을 뿐만 아니라 기관총, 칼, 망토와 같은 유물을 운반할 수도 있습니다. 프로그래머는 이러한 개체를 기반으로 프로그램을 작성합니다. 또한 그는 이러한 객체의 상호 작용을 정의하는 일련의 규칙을 지정할 수 있습니다. 예를 들어 캐릭터가 벽과 충돌하면 캐릭터가 돌아서 다른 방향으로 달립니다. 객체와 그 의미를 정의하는 것 외에도 장면의 조명도 정의해야 합니다. 이 경우 프로그래머는 달빛 아래 밤의 빛 강도를 지정해야 합니다. 그런 다음 특수 그래픽 소프트웨어와 하드웨어를 통해 캐릭터와 배경의 조명이 자동으로 계산됩니다.

불행하게도 그래픽 하드웨어는 복잡한 개체와 문자의 언어를 이해하지 못합니다. 따라서 대부분의 그래픽 툴킷에는 복잡한 구조를 기본 모양 집합으로 분해하는 그래픽 라이브러리가 있으며, 컴퓨터 그래픽 응용 프로그램의 대부분 모양은 삼각형 집합으로 분해되며 모든 작업(예: 개체 충돌, 이동, 조명, 그림자 및 조명)은 삼각형에 대한 기본 작업으로 변환됩니다. 그러나 그래픽 라이브러리는 이러한 삼각형을 처리하고 궁극적으로 컴퓨터 화면에 표시할 픽셀 배열을 생성하기 위해 기존 프로세서를 사용하지 않습니다. 프로그래머의 의도가 기본 모양에 대한 작업으로 변환되면 그래픽 라이브러리는 나머지 작업을 수행하는 전용 그래픽 프로세서에 코드를 보냅니다. 그래픽 프로세서는 사용자가 제공한 데이터와 규칙을 기반으로 복잡한 장면을 생성하고 가장자리와 꼭짓점으로 지정된 모양에서 작동합니다. 대부분의 경우 이러한 모양은 2차원의 삼각형이거나 3차원의 사면체입니다. 그래픽 프로세서는 최종 이미지를 생성할 때 조명, 개체 위치, 깊이 및 원근의 효과도 계산합니다. 그래픽 프로세서가 최종 이미지를 생성하면 이를 디스플레이 장치로 보냅니다. 컴퓨터 게임을 하는 경우 이 프로세스는 초당 최소 50~100회 수행되어야 합니다.

요약하자면, 복잡한 그래픽 장면을 생성하는 것은 어렵고 느리기 때문에 프로그래머는 개체에 대한 높은 수준의 설명을 개발합니다. 그런 다음 그래픽 라이브러리는 프로그래머의 명령을 기본 모양에 대한 작업으로 변환하고 이에 대한 작업을 위한 일련의 모양과 규칙을 그래픽 프로세서로 보냅니다. 그래픽 프로세서는 기본 모양을 조작하고 이를 픽셀 배열로 변환하여 최종 장면을 생성합니다.

그래픽 프로세서의 두 번째 중요한 응용 분야는 비디오와 같은 애니메이션 콘텐츠를 표시하는 것입니다. 고화질 비디오는 장면당 수백만 개의 픽셀을 갖습니다. 저장 요구 사항을 줄이기 위해 대부분의 고화질 비디오는 크게 압축(인코딩)됩니다. 따라서 컴퓨터는 비디오를 디코딩하거나 압축을 풀고 초당 50~100회 픽셀 배열을 계산하여 화면에 표시해야 합니다. 이는 매우 계산 집약적인 프로세스이며 CPU 리소스를 소비할 수 있습니다. 따라서 비디오 디코딩은 일반적으로 비디오 처리 전용 장치가 포함된 그래픽 프로세서에도 로드됩니다.

거의 모든 현대 컴퓨터 시스템에는 GPU(그래픽 처리 장치)라고 하는 그래픽 프로세서가 포함되어 있습니다. 최신 GPU에는 64~128개 이상의 코어가 포함되어 있으므로 광범위한 병렬 처리를 위해 설계되었습니다.

19.9.1.2 그래픽 파이프라인

이제 아래 이미지에서 일반적인 그래픽 프로세서의 파이프라인을 살펴보겠습니다.

그래픽 파이프라인.

첫 번째 단계를 버텍스 처리라고 합니다. 이 단계에서는 버텍스, 모양 및 삼각형 집합이 처리됩니다. GPU는 객체 회전 및 이동과 같은 복잡한 작업을 수행합니다. 프로그래머는 주어진 개체가 특정 속도로 다른 개체를 향해 이동하도록 지정할 수 있으므로 모양의 위치를 ​​주어진 속도로 변환해야 하며 이 작업도 이 단계에서 수행됩니다. 이 단계의 출력은 2D 평면의 간단한 삼각형 세트입니다.

두 번째 단계는 래스터라이제이션(rasterization)라고 합니다. 래스터라이제이션 프로세스는 각 삼각형을 조각(또는 조각)이라고 하는 픽셀 집합으로 변환합니다. 또한 나중에 색상 값을 보간하는 데 사용되는 매개변수 집합과 조각의 각 픽셀을 연결합니다.

세 번째 단계는 조각 처리입니다. 이 단계에서는 이전 단계에서 계산된 중간 결과를 사용하여 고정된 규칙 세트에 따라 조각의 픽셀을 음영 처리하거나 주어진 텍스처를 조각에 매핑합니다. 예를 들어 조각이 나무 테이블의 표면을 나타내는 경우 이 단계에서는 나무의 질감을 픽셀 색상에 매핑합니다. 이 단계는 그림자나 조명과 같은 효과를 통합하는 데에도 사용됩니다.

지금까지 우리는 장면에 있는 모든 객체의 조각 색상을 계산했습니다. 그러나 한 개체가 다른 개체 앞에 있을 수 있으므로 두 번째 개체의 일부가 숨겨질 수 있습니다.

네 번째 단계에서는 세 번째 단계의 모든 조각을 집계하고 프레임 버퍼 처리라는 작업을 수행합니다. 프레임 버퍼는 각 픽셀의 색상 값을 포함하는 대규모 배열이며, 그래픽 카드는 초당 50~100회 프레임 버퍼를 디스플레이 장치로 전송합니다. 이 단계에서 수행되는 작업 중 하나는 깊이 버퍼링이라고 하는데, 이는 객체의 일부를 숨겨서 3D 공간의 2D 뷰를 특정 각도로 계산하는 것입니다. 최종 장면이 생성된 후 그래픽 파이프라인은 이미지를 프레임 버퍼로 전송합니다.

이는 그래픽 프로세서가 복잡한 게임은 물론 창 최소화 또는 최대화와 같은 표준 작업까지 렌더링하는 방법입니다. 렌더링은 객체, 규칙 및 시각 효과 측면에서 장면에 대한 높은 수준의 설명을 처리하여 픽셀 단위로 장면을 생성하는 프로세스로 정의됩니다. 렌더링 프로세스에는 본질적으로 객체 회전이나 이동과 같은 행렬 연산을 포함한 많은 선형 대수 연산이 포함됩니다. 이러한 작업은 많은 수의 부동 소수점 값을 처리하며 본질적으로 병렬입니다.

19.9.1.3 고성능 컴퓨팅과 그래픽 컴퓨팅의 통합

1990년대 후반에는 컴퓨터 그래픽 분야가 급속도로 발전했습니다. 컴퓨터 게임, 데스크탑 시각 효과 및 고급 엔지니어링 소프트웨어의 확산에는 정교한 컴퓨터 그래픽 하드웨어 가속기가 필요합니다. 따라서 디자이너들은 보다 생생한 장면과 보다 사실적인 사물을 창조해야 한다는 요구가 점점 더 커지고 있습니다. 1980년대 후반에 제작된 애니메이션 영화와 오늘날의 헐리우드 영화를 비교할 수 있다. 오늘날의 애니메이션 영화에는 매우 현실적인 캐릭터와 매우 세밀한 표정이 담겨 있습니다. 이 모든 것은 그래픽 하드웨어 덕분에 가능합니다. 이러한 몰입형 경험을 만들려면 다양한 유형의 시각 효과를 결합할 수 있도록 그래픽 프로세서에 상당한 유연성을 추가해야 합니다. 결과적으로 그래픽 프로세서 설계자는 프로세서의 내부 구성 요소 중 많은 부분을 낮은 수준의 소프트웨어에 노출하고 프로그래머가 프로세서를 보다 유연하게 사용할 수 있도록 합니다. 프로그래머가 유연한 조각 및 픽셀 처리 루틴을 만들 수 있도록 하는 셰이더라는 프로그램 그룹이 2000년대 초반에 만들어졌습니다.

2006년까지 주요 GPU 공급업체는 그래픽 파이프라인이 조각 또는 픽셀 처리 작업과 개념적으로 유사한 수치가 많은 과학 코드와 같은 범용 계산에 사용될 수 있다는 사실을 인식했습니다. 일반 사용자 프로그램이 그래픽 프로세서에 액세스하여 작업을 수행하도록 허용하면 그래픽 프로세서에서 많은 과학 프로그램을 실행할 수 있습니다. 이러한 요청에 응답하여 NVIDIA는 C 프로그래머가 C 언어로 코드를 작성하고 이를 그래픽 프로세서에서 실행할 수 있도록 하는 CUDA API를 출시했으며 GPGPU(General Purpose GPU)라는 용어가 탄생했습니다.

GPGPU는 General Purpose Graphics Processor Unit의 약자로 기본적으로 일반 사용자가 코드를 작성하고 실행할 수 있는 그래픽 프로세서입니다. 사용자는 일반적으로 특수 언어 또는 표준 언어에 대한 확장을 사용하여 GPGPU 호환 코드를 생성합니다.

NVIDIA Tesla GPU 아키텍처의 설계는 나중에 논의됩니다. 특히 GeForce 8800 GPU의 설계에 대해 논의합니다. GPU의 가장 빠른 부분(코어)은 일반적으로 1.5GHz 이상에서 작동하며 다른 구성 요소는 600MHz, 750MHz 이상에서 작동합니다.

19.9.2 GPU 시스템 아키텍처

오늘날 일반적으로 사용되는 GPU 시스템 아키텍처는 여러 가지가 있습니다. 시스템 구성, GPU 기능 및 서비스, 표준 프로그래밍 인터페이스, 기본 GPU 내부 아키텍처는 아래에 설명되어 있습니다.

19.9.2.1 이기종 CPU-GPU 시스템 아키텍처

GPU와 CPU를 사용하는 이기종 컴퓨터 시스템 아키텍처는 두 가지 주요 특성으로 높은 수준에서 설명할 수 있습니다. 첫째, 사용되는 기능 하위 시스템 및/또는 칩의 수와 상호 연결 기술 및 토폴로지입니다. 둘째, 이러한 기능적 하위 시스템에 어떤 메모리 하위 시스템을 사용할 수 있는지입니다.

아래 이미지는 1990년경 레거시 PC의 상위 수준 블록 다이어그램을 보여줍니다. 노스 브리지에는 CPU, 메모리 및 PCI 버스를 연결하는 고대역폭 인터페이스가 포함되어 있으며 사우스 브리지에는 ISA 버스(오디오, LAN), 인터럽트 컨트롤러와 같은 기존 인터페이스 및 장치가 포함되어 있습니다. DMA 컨트롤러; 시간/카운터. 이 시스템에서 디스플레이는 PCI 버스에 연결된 VGA(Video Graphics Array)라는 간단한 프레임 버퍼 하위 시스템에 의해 구동됩니다. 처리 요소(GPU)가 내장된 그래픽 하위 시스템은 1990년 PC 환경에는 존재하지 않았습니다.

아래 그림은 오늘날 일반적으로 사용되는 두 가지 구성을 보여줍니다. 이 제품은 독립적인 GPU(개별 GPU)와 해당 메모리 하위 시스템을 갖춘 CPU를 갖추고 있습니다. 그림 a에서 Intel CPU의 경우 GPU는 16레인 PCI Express 2.0 링크를 통해 연결되어 16GB/s의 최대 전송 속도(각 방향에서 최대 8GB/s)를 제공합니다. 마찬가지로 그림 b에서 AMD CPU의 경우 GPU도 동일한 사용 가능한 대역폭으로 PCI Express를 통해 칩셋에 연결됩니다. 두 경우 모두 GPU와 CPU는 서로의 메모리에 액세스할 수 있지만 사용 가능한 대역폭은 직접 연결된 메모리에 액세스하는 경우보다 적습니다. AMD 시스템의 경우 Northbridge 또는 메모리 컨트롤러가 CPU와 동일한 칩에 통합되어 있습니다.

PCI Express(PCIe): 구성 가능한 레인 수와 대역폭을 갖춘 지점 간 링크를 사용하는 표준 시스템 I/O 상호 연결입니다.

UMA(통합 메모리 아키텍처): CPU와 GPU가 공통 시스템 메모리를 공유하는 시스템 아키텍처입니다.

이러한 시스템의 저렴한 변형인 통합 메모리 아키텍처 시스템은 CPU 시스템 메모리만 사용하고 시스템에서 GPU 메모리를 생략합니다. 이러한 시스템은 사용 가능한 시스템 메모리 대역폭과 증가된 메모리 액세스 대기 시간에 의해 달성되는 성능이 제한되는 반면, 전용 GPU 메모리는 높은 대역폭과 낮은 대기 시간을 제공하므로 GPU 성능이 상대적으로 낮습니다.

고성능 시스템 변형은 고성능 게임 및 워크스테이션용으로 설계된 NVIDIA SLI(Scalable Link Interconnect) 다중 GPU 시스템과 같이 데이지 체인으로 연결된 모니터와 함께 일반적으로 2~4개가 병렬로 작동하는 여러 연결된 GPU를 사용합니다.

다음 시스템 범주는 전용 그래픽 메모리 유무에 관계없이 GPU를 노스브리지(Intel) 또는 칩셋(AMD)과 통합합니다.

이전 섹션에서는 캐시가 공유 주소 공간 내에서 일관성을 유지하는 방법을 설명했습니다. CPU와 GPU에는 여러 개의 주소 공간이 있으며, GPU는 GPU의 MMU에 의해 변환된 가상 주소를 사용하여 자체 물리적 로컬 메모리와 CPU 시스템의 물리적 메모리에 액세스할 수 있습니다. 운영 체제 커널은 GPU의 페이지 테이블을 관리하며, GPU 페이지 테이블의 속성에 따라 일관된 또는 비일관적인 PCI Express 트랜잭션을 사용하여 시스템 물리적 페이지에 액세스할 수 있습니다. CPU는 PCI Express 주소 공간의 주소 범위(간극이라고도 함)를 통해 GPU의 로컬 메모리에 액세스할 수 있습니다.

Sony PlayStation 3 및 Microsoft Xbox 360과 같은 콘솔 시스템은 앞에서 설명한 PC 시스템 아키텍처와 유사하며, 콘솔 시스템은 5년 이상의 유효 수명 동안 동일한 성능과 기능을 제공하도록 설계되었습니다. 이 기간 동안 시스템을 여러 번 다시 구현하여 저렴한 비용으로 지속적인 기능을 제공하는 고급 실리콘 제조 공정을 개발할 수 있습니다. 콘솔 시스템은 PC 시스템처럼 하위 시스템을 확장하고 업그레이드할 필요가 없으므로 주요 내부 시스템 버스는 표준화되기보다는 맞춤화되는 경향이 있습니다.

오늘날의 PC에서는 GPU가 PCI Express를 통해 CPU에 연결되며, 이전 세대에서는 AGP를 사용했습니다. 그래픽 응용 프로그램은 GPU를 보조 프로세서로 사용하여 OpenGL 또는 Direct3D API 기능을 호출합니다. API는 특정 GPU에 최적화된 그래픽 장치 드라이버를 통해 명령, 프로그램 및 데이터를 GPU로 보냅니다.

AGP: 원래 PCI I/O 버스의 확장 버전으로, 단일 카드 슬롯에 원래 PCI 버스 대역폭의 최대 8배를 제공합니다. 주요 목적은 그래픽 하위 시스템을 PC 시스템에 연결하는 것입니다.

19.9.2.2 기본 통합 GPU 아키텍처

통합 GPU 아키텍처는 프로그래밍 가능한 여러 프로세서의 병렬 배열을 기반으로 합니다. 이는 각 처리 유형에 전용으로 별도의 프로세서를 사용했던 이전 GPU와 달리 동일한 프로세서에서 버텍스, 기하학 및 픽셀 셰이더 처리와 병렬 컴퓨팅을 통합합니다. 프로그래밍 가능 프로세서 어레이는 텍스처 필터링, 래스터라이제이션, 래스터 작업, 앤티앨리어싱, 압축, 압축 해제, 디스플레이, 비디오 디코딩 및 고화질 비디오 처리를 위한 고정 기능 프로세서와 긴밀하게 통합되어 있습니다. 고정 기능 프로세서는 면적, 비용 또는 전력 예산 제약에 따른 절대 성능 측면에서 보다 일반적인 프로그래밍 가능 프로세서보다 성능이 훨씬 뛰어나지만 이 하위 섹션에서는 프로그래밍 가능 프로세서에 중점을 둡니다.

멀티코어 CPU와 비교하여 멀티코어 GPU는 여러 프로세서 코어에서 여러 병렬 스레드를 효율적으로 실행하는 데 초점을 맞춘 다양한 아키텍처 설계 포인트를 가지고 있습니다. 더 간단한 코어를 많이 사용하고 스레드 그룹 간의 데이터 병렬 동작을 최적화함으로써 각 칩의 트랜지스터 예산은 온칩 캐시 및 오버헤드가 아닌 계산에 더 많이 사용됩니다.

통합 GPU 프로세서 배열에는 다중 스레드 다중 프로세서로 구성된 많은 프로세서 코어가 포함되어 있습니다. 아래 그림은 14개의 멀티스레드 스트리밍 멀티프로세서(SM)로 구성된 112개의 스트림 프로세서(SP) 코어 배열을 갖춘 GPU를 보여줍니다. 각 SP 코어는 다중 스레드로 구성되어 96개의 동시 스레드와 하드웨어 상태를 관리합니다. 프로세서는 상호 연결 네트워크를 통해 4개의 64비트 폭 DRAM 파티션에 연결되며 각 SM에는 8개의 SP 코어, 2개의 SFU(특수 기능 장치), 명령 및 상수 캐시, 멀티스레드 명령 장치 및 공유 메모리가 있습니다. 이는 NVIDIA GeForce 8800이 구현한 기본 Tesla 아키텍처로, 버텍스, 기하학, 픽셀 셰이딩을 위한 기존 그래픽 프로그램이 통합 SM 및 해당 SP 코어에서 실행되고 계산 프로그램이 동일한 프로세서에서 실행되는 통합 아키텍처를 갖습니다.

프로세서 어레이 아키텍처는 다중 프로세서 수와 메모리 파티션 수를 확장하여 더 작고 더 큰 GPU 구성으로 확장됩니다. 위 이미지는 텍스처 유닛과 텍스처 L1 캐시를 공유하는 2개의 SM으로 구성된 7개의 클러스터를 보여줍니다. 텍스처 유닛은 필터링된 결과를 SM에 전달하고 일련의 좌표를 텍스처 맵으로 변환합니다. 연속적인 텍스처 요청에 대한 지원 필터 영역은 종종 겹치기 때문에 작은 스트리밍 L1 텍스처 캐시는 메모리 시스템에 대한 요청 수를 효과적으로 줄입니다. 프로세서 어레이는 GPU 전체 상호 연결 네트워크를 통해 ROP(Raster Operation Processor), L2 텍스처 캐시, 외부 DRAM 메모리 및 시스템 메모리에 연결됩니다. 프로세서 수와 메모리 양을 확장하여 다양한 성능과 시장 부문에 맞게 균형 잡힌 GPU 시스템을 설계할 수 있습니다.

아래 그림은 NVIDIA Fermi 아키텍처 GPU의 전체 레이아웃을 보여줍니다. 그림에서 볼 수 있듯이 L2 캐시는 16개의 SM(상하 8개의 SM)의 중앙에 위치하며, 각 SM은 2개의 인접한 열과 16개의 직사각형(GPU 프로세서 코어) 행, 16개의 로드/저장 장치로 구성된 1열과 4개의 특수 기능 장치(SFU)로 구성된 1열로 표시됩니다. SM 모듈에 대한 보다 자세한 그림은 아래와 같습니다. 아래 그림에서 SM의 머리 부분과 아래쪽에 있는 직사각형은 레지스터와 L1/공유 메모리가 있는 곳이며, 6개의 DRAM I/O 인터페이스는 각각 64비트 메모리 인터페이스를 가지고 있습니다(DRAM 인터페이스 회로는 가장 바깥쪽 왼쪽과 오른쪽에 진한 파란색 직사각형으로 표시되어 있습니다). 따라서 전체적으로 GPU의 GDDR5(Graphics Double Data Rate, 그래픽 처리용으로 설계된 DDR 메모리) DRAM은 384비트 인터페이스를 갖추고 있어 총 6GB의 SM 오프칩 메모리(예: 글로벌, 고정, 텍스처 및 로컬)를 지원할 수 있습니다. 또한 아래에는 GPU 레이아웃 다이어그램의 왼쪽에 있는 호스트 인터페이스가 나와 있습니다. 호스트 인터페이스는 GPU와 CPU 간의 PCIe 연결을 허용합니다. 마지막으로 GigaThread 전역 스케줄러(호스트 인터페이스 옆에 위치)는 모든 SM의 워프 스케줄러에 스레드 블록을 할당하는 역할을 합니다.

19.9.3 다중 스레드 다중 프로세서 아키텍처

다양한 시장 부문을 다루기 위해 GPU는 확장 가능한 수의 다중 프로세서를 구현합니다. 실제로 GPU는 다중 프로세서로 구성된 다중 프로세서입니다. 또한 각 다중 프로세서는 고도로 다중 스레드로 구성되어 있어 세밀한 여러 버텍스 및 픽셀 셰이더 스레드를 효율적으로 실행할 수 있습니다. 고품질 기본 GPU에는 2~4개의 멀티프로세서가 있는 반면, 게임 매니아 GPU나 컴퓨팅 플랫폼에는 수십 개가 있습니다. 이 섹션에서는 앞에서 설명한 NVIDIA Tesla 스트리밍 멀티 프로세서(SM)의 단순화된 버전인 멀티 스레드 멀티 프로세서 아키텍처를 소개합니다.

여러 개의 독립 프로세서 대신 다중 프로세서를 사용하는 이유는 무엇입니까? 각 멀티프로세서 내의 병렬성은 국지적인 고성능을 제공하고 스레드 블록의 개별 스레드가 멀티프로세서 내에서 함께 실행되어 데이터를 공유하는 세분화된 병렬 프로그래밍 모델의 광범위한 멀티스레딩을 지원합니다. 여기에 설명된 멀티스레드 다중 프로세서 설계에는 긴밀하게 결합된 아키텍처에서 최대 512개의 스레드를 실행하는 8개의 스칼라 프로세서 코어가 있습니다. 영역 및 전력 효율성을 향상시키기 위해 멀티프로세서는 명령 캐시, 멀티스레드 명령 장치 및 공유 메모리 RAM을 포함하여 8개의 프로세서 코어 간에 크고 복잡한 장치를 공유합니다.

GPU 프로세서는 고도로 멀티스레드되어 있으며 다음과 같은 목표를 달성할 수 있습니다.

  • DRAM 메모리 로딩 및 텍스처 가져오기 지연 무시

  • 세분화된 병렬 그래픽 셰이더 프로그래밍 모델 지원

  • 세분화된 병렬 컴퓨팅 프로그래밍 모델 지원

  • 물리적 프로세서를 스레드 및 스레드 블록으로 가상화하여 투명한 확장성을 제공합니다.

  • 하나의 스레드에 대한 직렬 프로그램 작성으로 병렬 프로그래밍 모델을 단순화합니다.

메모리 및 텍스처 가져오기 대기 시간에는 수백 개의 프로세서 클럭이 필요할 수 있으며, CPU와 같은 대규모 작업 세트 캐시와 달리 GPU에는 일반적으로 작은 스트리밍 캐시가 있으므로 가져오기 요청에는 일반적으로 전체 DRAM 액세스 대기 시간과 상호 연결 및 버퍼링 대기 시간이 필요합니다. 하나의 스레드가 로드 또는 텍스처 가져오기가 완료되기를 기다리는 동안 멀티스레딩은 유용한 계산으로 지연을 처리하는 데 도움이 되며 프로세서는 다른 스레드를 실행할 수 있습니다. 세분화된 병렬 프로그래밍 모델은 단일 스레드의 긴 메모리 대기 시간에도 불구하고 많은 프로세서를 바쁘게 유지할 수 있는 수천 개의 독립 스레드를 제공합니다.

그래픽 버텍스 또는 픽셀 셰이더 프로그램은 버텍스 또는 픽셀을 처리하기 위한 단일 스레드 프로그램이며, 마찬가지로 CUDA 프로그램은 결과 계산을 위한 단일 스레드 C 프로그램입니다. 그래픽 및 컴퓨팅 프로그램은 많은 병렬 스레드를 인스턴스화하여 복잡한 이미지를 렌더링하고 대규모 결과 배열을 계산합니다. 이동하는 버텍스 및 픽셀 셰이더 스레드 작업 부하의 균형을 동적으로 맞추기 위해 각 다중 프로세서는 여러 개의 서로 다른 스레드 프로그램과 서로 다른 유형의 셰이더 프로그램을 동시에 실행합니다.

그래픽 셰이딩 언어의 독립적인 버텍스, 기본 및 픽셀 프로그래밍 모델과 CUDA C/C++의 단일 스레드 프로그래밍 모델을 지원하기 위해 각 GPU 스레드에는 자체 전용 레지스터, 스레드별 전용 메모리, 프로그램 카운터 및 스레드 실행 상태가 있으며 독립적인 코드 경로를 실행할 수 있습니다. 수백 개의 동시 경량 스레드를 효율적으로 실행하기 위해 GPU 멀티프로세서는 하드웨어 다중 스레드로 구성되어 오버헤드를 예약하지 않고도 하드웨어에서 수백 개의 병렬 스레드를 관리하고 실행합니다. 스레드 블록의 동시 스레드는 장벽에서 단일 명령으로 동기화될 수 있으며 경량 스레드 생성, 오버헤드 없는 스레드 스케줄링 및 빠른 장벽 동기화는 매우 세분화된 병렬성을 효과적으로 지원합니다.

19.9.3.1 대규모 스레드

GPU 프로세서는 고도로 멀티스레드되어 있으며 다음과 같은 목표를 달성할 수 있습니다.

  • DRAM 메모리 로딩 및 텍스처 가져오기 지연을 무시합니다.

  • 세분화된 병렬 그래픽 셰이더 프로그래밍 모델을 지원합니다.

  • 세분화된 병렬 컴퓨팅 프로그래밍 모델을 지원합니다.

  • 물리적 프로세서를 스레드 및 스레드 블록으로 가상화하여 투명한 확장성을 제공합니다.

  • 병렬 프로그래밍 모델을 하나의 스레드에 대한 직렬 프로그램 작성으로 단순화합니다.

CPU의 대규모 작업 세트 캐시와 달리 GPU에는 일반적으로 작은 스트리밍 캐시가 있기 때문에 메모리 및 텍스처 가져오기 대기 시간에는 수백 개의 프로세서 클럭이 필요할 수 있습니다. 가져오기 요청에는 일반적으로 전체 DRAM 액세스 대기 시간과 상호 연결 및 버퍼링 대기 시간이 필요합니다. 하나의 스레드가 로드 또는 텍스처 가져오기가 완료되기를 기다리는 동안 멀티스레딩은 유용한 계산으로 대기 시간을 오버레이하는 데 도움이 되며 프로세서는 다른 스레드를 실행할 수 있습니다(아래 이미지). 세분화된 병렬 프로그래밍 모델은 단일 스레드의 긴 메모리 대기 시간에도 불구하고 많은 프로세서를 바쁘게 유지할 수 있는 수천 개의 독립 스레드를 제공합니다.

GPU는 메모리 액세스 대기 시간을 처리하기 위해 여러 컨텍스트 스위치를 활용합니다.

그래픽 버텍스 또는 픽셀 셰이더 프로그램은 버텍스 또는 픽셀을 처리하기 위한 단일 스레드가 있는 프로그램입니다. 마찬가지로 CUDA 프로그램은 결과 계산을 위한 단일 스레드가 있는 C 프로그램입니다. 그래픽 및 컴퓨팅 프로그램은 많은 병렬 스레드를 인스턴스화하여 복잡한 이미지를 렌더링하고 대규모 결과 배열을 계산합니다. 이동하는 버텍스 및 픽셀 셰이더 스레드 작업 부하의 균형을 동적으로 맞추기 위해 각 다중 프로세서는 여러 개의 서로 다른 스레드 프로그램과 서로 다른 유형의 셰이더 프로그램을 동시에 실행합니다.

그래픽 셰이딩 언어의 독립적인 버텍스, 기본 및 픽셀 프로그래밍 모델과 CUDA C/C++의 단일 스레드 프로그래밍 모델을 지원하기 위해 각 GPU 스레드에는 자체 전용 레지스터, 스레드별 전용 메모리, 프로그램 카운터 및 스레드 실행 상태가 있으며 독립적인 코드 경로를 실행할 수 있습니다. 수백 개의 동시 경량 스레드를 효율적으로 실행하기 위해 GPU 멀티프로세서는 하드웨어 멀티스레드 방식으로, 예약 오버헤드 없이 하드웨어에서 수백 개의 병렬 스레드를 관리하고 실행합니다. 스레드 블록의 동시 스레드는 장벽에서 단일 명령으로 동기화될 수 있으며 경량 스레드 생성, 오버헤드 없는 스레드 스케줄링 및 빠른 장벽 동기화는 매우 세분화된 병렬성을 효과적으로 지원합니다.

19.9.3.2 다중 프로세서 아키텍처

통합 그래픽 및 컴퓨팅 멀티프로세서는 버텍스, 기하학, 픽셀 조각 셰이더 프로그램은 물론 병렬 컴퓨팅 프로그램도 실행합니다. 아래 그림에 표시된 것처럼 예제 멀티프로세서는 8개의 스칼라 프로세서(SP) 코어로 구성되며, 각 코어에는 대형 멀티스레드 레지스터 파일(RF), 2개의 특수 기능 장치(SFU), 멀티스레드 명령 장치, 명령 캐시, 읽기 전용 상수 캐시 및 공유 메모리가 있습니다.

8개의 스칼라 프로세서(SP) 코어를 갖춘 멀티스레드 멀티프로세서. 8개의 SP 코어는 각각 대규모 멀티스레드 레지스터 파일(RF)을 가지며 명령어 캐시, 멀티스레드 명령어 발행 장치, 상수 캐시, 2개의 특수 기능 유닛(SFU), 상호 연결 네트워크 및 멀티뱅크 공유 메모리를 공유합니다.

16KB 공유 메모리에는 그래픽 데이터 버퍼와 공유 계산 데이터가 저장되며, __shared__로 선언된 CUDA 변수는 공유 메모리에 있습니다. 여러 프로세서에 걸쳐 논리적 그래픽 파이프라인 작업 부하를 여러 번 매핑하기 위해 버텍스, 기하 도형 및 픽셀 스레드에는 독립적인 입력 및 출력 버퍼가 있고 작업 부하가 스레드 실행과 독립적으로 도착하고 출발합니다.

각 SP 코어에는 대부분의 명령을 실행하는 스칼라 정수 및 부동 소수점 산술 장치가 포함되어 있습니다. SP는 하드웨어 다중 스레드이며 최대 64개의 스레드를 지원합니다. 각 파이프라인 SP 코어는 클럭당 스레드당 하나의 스칼라 명령을 실행하며, 이는 다양한 GPU 제품에서 1.2GHz~1.6GHz 범위입니다. 각 SP 코어에는 할당된 스레드 간에 분할된 1024개의 범용 32비트 레지스터로 구성된 대규모 RF가 있습니다. 프로그램은 레지스터 요구 사항(일반적으로 스레드당 16~64개의 스칼라 32비트 레지스터)을 선언합니다. SP는 적은 수의 레지스터를 사용하여 많은 스레드를 동시에 실행하거나 더 많은 레지스터를 사용하여 더 적은 스레드를 실행할 수 있습니다. 컴파일러는 유출된 레지스터 비용과 더 적은 스레드 비용의 균형을 맞추기 위해 레지스터 할당을 최적화합니다. 픽셀 셰이더 프로그램은 일반적으로 16개 이하의 레지스터를 사용하므로 각 SP가 최대 64개의 픽셀 셰이더 스레드를 실행하여 대기 시간이 긴 텍스처 가져오기를 처리할 수 있습니다. 컴파일된 CUDA 프로그램에는 일반적으로 스레드당 32개의 레지스터가 필요하므로 각 SP는 32개의 스레드로 제한되며, 이 예제 다중 프로세서의 커널 프로그램은 최대 512개의 스레드 대신 스레드 블록당 256개의 스레드로 제한됩니다.

파이프라인 SFU는 특수 기능을 계산하고 원시 버텍스 어트리뷰트에서 픽셀 속성을 보간하는 스레드 명령어를 실행하며 SP의 명령어와 동시에 실행될 수 있습니다.

멀티프로세서는 텍스처 인터페이스를 통해 텍스처 유닛에 텍스처 페치 명령을 실행하고, 메모리 인터페이스를 이용하여 SP의 명령과 동시에 실행될 수 있는 외부 메모리 로드, 저장, 원자 액세스 명령을 실행한다. 공유 메모리 액세스는 SP 프로세서와 공유 메모리 뱅크 사이에 지연 시간이 짧은 상호 연결 네트워크를 사용합니다.

19.9.3.3 단일 명령 다중 스레딩(SIMT)

여러 다른 프로그램을 실행하는 수백 개의 스레드를 효율적으로 관리하고 실행하기 위해 멀티프로세서는 워프라고 하는 병렬 스레드 그룹에서 동시 스레드를 생성, 관리, 예약 및 실행하는 SIMT(단일 명령 다중 스레딩) 아키텍처를 사용합니다. "워프(warp)"라는 단어는 최초의 평행선 기술인 직조(weaving)에서 유래되었습니다. 아래 사진은 직기에 나타난 평행선의 뒤틀림을 보여줍니다. 이 예제 멀티프로세서는 32개 스레드의 SIMT 워프 크기를 사용하여 4개 클록의 8개 SP 코어 각각에서 4개 스레드를 실행합니다. Tesla SM 멀티프로세서는 또한 32개 병렬 스레드의 워프 크기를 사용하며, 각 SP 코어는 4개의 스레드를 실행하여 다수의 픽셀 스레드와 컴퓨팅 스레드의 효율성을 높입니다. 스레드 블록은 하나 이상의 날실로 구성됩니다.

SIMT 다중 스레드 워프 스케줄링. 스케줄러는 준비된 워프를 선택하고 워프를 구성하는 병렬 스레드에 동기적으로 명령을 내립니다. 워프는 독립적이므로 스케줄러는 매번 다른 워프를 선택할 수 있습니다.

SIMT(단일 명령 다중 스레드): 단일 명령을 여러 독립 스레드에 병렬로 적용하는 프로세서 아키텍처입니다.

워프: SIMT 아키텍처에서 동일한 명령어를 함께 실행하는 병렬 스레드 그룹입니다.

이 예시 SIMT 다중 프로세서는 총 512개의 스레드에 대해 16개의 워프 풀을 관리합니다. 워프를 구성하는 개별 병렬 스레드는 동일한 유형이고 동일한 프로그램 주소에서 함께 시작되지만 그렇지 않으면 독립적으로 자유롭게 분기하고 실행할 수 있습니다. 각 명령어 발행 시 SIMT 다중 스레드 명령어 장치는 다음 명령어를 실행할 준비가 된 워프를 선택한 다음 해당 명령어를 해당 워프의 활성 스레드에 발행합니다. SIMT 명령어는 워프의 활성 병렬 스레드에 동기적으로 브로드캐스트되며 개별 스레드는 독립적인 분기 또는 예측으로 인해 비활성화될 수 있습니다. 이 멀티프로세서에서 각 SP 스칼라 프로세서 코어는 4개의 클럭을 사용하여 워프의 개별 스레드 4개에 대해 하나의 명령을 실행합니다. 이는 워프 스레드 대 코어 비율이 4:1임을 반영합니다.

SIMT 프로세서 아키텍처는 단일 명령을 여러 데이터 레인에 적용하는 SIMD(단일 명령 다중 데이터) 설계와 유사하지만 차이점은 SIMT가 여러 데이터 레인이 아닌 여러 독립 스레드에 병렬로 명령을 적용한다는 것입니다. SIMD 프로세서용 명령어는 여러 데이터 레인의 벡터를 함께 제어하는 ​​반면 SIMT 프로세서용 명령어는 단일 스레드를 제어하며 SIMT 명령어 장치는 효율성을 높이기 위해 독립적인 병렬 스레드를 워프하는 명령을 발행합니다. SIMT 프로세서는 수퍼스칼라 프로세서가 런타임 시 명령어 간의 명령어 수준 병렬성을 발견하는 방식과 유사하게 런타임 시 스레드 간의 데이터 수준 병렬성을 발견합니다.

SIMT 프로세서는 워프의 모든 스레드가 동일한 실행 경로를 사용할 때 완전한 효율성과 성능을 달성합니다. 워프의 스레드가 데이터 종속 조건 분기를 통해 분기되는 경우 각 분기 경로에 대해 실행이 직렬화되고 모든 경로가 완료되면 스레드가 동일한 실행 경로로 수렴됩니다. 길이가 동일한 경로의 경우 분기형 if-else 블록의 효율성은 50%이며, 멀티프로세서는 분기 동기화 스택을 사용하여 분기 및 수렴하는 독립적인 스레드를 관리합니다. 서로 다른 워프는 공통 또는 분리된 코드 경로를 실행하는지 여부에 관계없이 최대 속도로 독립적으로 실행됩니다. 결과적으로 SIMT GPU는 워프가 기존 GPU의 SIMD 너비보다 훨씬 좁기 때문에 이전 GPU보다 코드 분기에서 훨씬 더 효율적이고 유연합니다.

4개 요소 예측 벡터 코어에서 분기 및 비분기 실행. 각 요소는 결정 p에서 분기되는 셰이더 A의 10가지 작업을 수행합니다. B의 경우 4개 요소 모두 분기가 없고 분기도 없으며 6개의 실행 단계만 필요합니다. C의 경우 요소 1은 no 분기를 사용하지만 다른 세 요소는 yes 분기를 사용합니다. 판단은 아니요와 예 작업을 별도로 수행하여 이러한 차이를 처리하므로 10개의 실행 단계가 모두 필요합니다.

SIMD 벡터 아키텍처와 비교하여 SIMT를 사용하면 프로그래머는 단일 독립 스레드에 대한 스레드 수준 병렬 코드는 물론 여러 조정 스레드에 대한 데이터 병렬 코드를 작성할 수 있습니다. 프로그램 정확성을 위해 프로그래머는 본질적으로 워프의 SIMT 실행 속성을 무시할 수 있지만 코드에서 워프의 스레드가 분기되는 경우가 거의 없다는 점을 지적하면 상당한 성능 향상을 얻을 수 있습니다. 실제로 이는 기존 코드에서 캐시 라인의 역할과 유사합니다. 정확성을 위해 설계할 때는 캐시 라인 크기를 무시해도 안전하지만, 최고 성능을 위해 설계할 때는 코드 구조에서 캐시 라인 크기를 고려해야 합니다.

19.9.3.4 SIMT Warp 실행 및 발산

독립적인 워프를 스케줄링하는 SIMT 방법은 이전 GPU 아키텍처의 스케줄링보다 더 유연합니다. 워프에는 버텍스, 형상, 픽셀 또는 계산과 같은 동일한 유형의 병렬 스레드가 포함되어 있습니다. 픽셀 조각 셰이더 처리의 기본 단위는 4개의 픽셀 셰이더 스레드로 구현된 2*2 픽셀 쿼드입니다. 멀티프로세서 컨트롤러는 픽셀 쿼드를 워프로 압축합니다. 마찬가지로 정점과 기본 요소를 워프로 그룹화하고 계산 스레드를 워프로 압축합니다. 스레드 블록에는 하나 이상의 날실이 포함되어 있습니다. SIMT 설계는 워프의 병렬 스레드 간에 명령어 가져오기 및 실행 단위를 효율적으로 공유하지만, 전체 성능 효율성을 얻으려면 전체 활성 스레드 워프가 필요합니다.

이 통합 멀티프로세서는 여러 워프 유형을 동시에 예약하고 실행하므로 버텍스 및 픽셀 워프를 동시에 실행할 수 있습니다. 워프 스케줄러는 프로세서 코어당 4개의 스레드 채널이 있기 때문에 프로세서 클럭 속도보다 낮은 속도로 실행됩니다. 각 스케줄링 주기에서는 위 그림과 같이 SIMT 워프 명령을 실행할 워프를 선택합니다. 발행된 워프 명령어는 4개의 프로세서 처리량 주기에서 8개의 스레드로 구성된 4개의 그룹으로 실행되며, 프로세서 파이프라인은 여러 대기 시간 클럭을 사용하여 각 명령어를 완료합니다. 활성 워프 수에 워프당 클럭 수를 곱한 값이 파이프라인 지연을 초과하는 경우 프로그래머는 파이프라인 지연을 무시할 수 있습니다. 이 다중 프로세서의 경우 8개 워프의 라운드 로빈 스케줄링은 동일한 워프의 연속 명령어 간에 32사이클을 갖습니다. 프로그램이 다중 프로세서당 256개의 스레드를 활성 상태로 유지할 수 있는 경우 단일 연속 스레드는 최대 32주기의 명령 대기 시간을 숨길 수 있습니다. 그러나 활성 워프가 거의 없으면 프로세서 파이프라인 깊이가 표시되어 잠재적으로 프로세서 정지가 발생할 수 있습니다.

어려운 설계 문제는 다양한 워프 프로그램과 프로그램 유형을 동적으로 혼합하여 오버헤드가 없는 워프 스케줄링을 달성하는 것입니다. 명령어 스케줄러는 각 스레드가 프로세서 코어당 1.0의 IPC에 해당하는 클럭당 하나의 명령어를 발행하도록 4개의 클럭마다 워프를 선택해야 합니다. 워프는 독립적이기 때문에 유일한 종속성은 동일한 워프의 순차적 명령입니다. 스케줄러는 레지스터 종속성 점수판을 사용하여 활성 스레드가 명령을 실행할 준비가 된 워프를 한정합니다. 준비된 워프 모두에 우선순위를 부여하고 문제에 대해 우선순위가 가장 높은 워프를 선택합니다. 우선순위는 워프 유형, 명령 유형, 모든 활성 워프에 대한 공정성을 고려하여 결정해야 합니다.

19.9.3.5 스레드 및 스레드 블록 관리

다중 프로세서 컨트롤러와 명령 장치는 스레드와 스레드 블록을 관리합니다. 컨트롤러는 작업 요청과 입력 데이터를 받아들이고 텍스처 유닛, 메모리 액세스 경로, I/O 경로를 포함한 공유 리소스에 대한 액세스를 중재합니다. 그래픽 워크로드의 경우 버텍스, 지오메트리, 픽셀의 세 가지 유형의 그래픽 스레드를 동시에 생성하고 관리합니다. 각 유형의 그래픽 작업에는 독립적인 입력 및 출력 경로가 있습니다. 이는 이러한 각 입력 작업 유형을 동일한 스레드 프로그램을 실행하는 병렬 스레드의 SIMT 워프로 누적 및 패키징하고, 프리 워프를 할당하고, 워프 스레드에 대한 레지스터를 할당하고, 멀티프로세서에서 워프 실행을 시작합니다. 각 프로그램은 스레드별 레지스터 요구 사항을 선언하고 컨트롤러는 요청된 레지스터 수를 워프에 할당할 수 있는 경우에만 워프를 시작합니다. 워프의 모든 스레드가 종료되면 컨트롤러는 결과를 압축 해제하고 워프 레지스터와 리소스를 해제합니다.

컨트롤러는 CUDA 스레드 블록을 하나 이상의 병렬 스레드 워프로 구현하는 CTA(협력 스레드 배열)를 생성합니다. 모든 CTA 워프를 생성하고 모든 CTA 리소스를 할당할 수 있을 때 CTA를 생성합니다. 스레드와 레지스터 외에도 CTA는 공유 메모리와 장벽을 할당해야 합니다. 프로그램은 필요한 용량을 선언하고 컨트롤러는 이러한 용량이 할당될 수 있을 때까지 기다린 다음 CTA를 시작합니다. 그런 다음 워프 예약 속도로 CTA 워프를 생성하여 CTA 프로그램이 즉시 전체 다중 프로세서 성능으로 실행을 시작할 수 있도록 합니다. 컨트롤러는 CTA의 모든 스레드가 종료되는 시기를 모니터링하고 CTA 공유 리소스와 해당 워프 리소스를 해제합니다.

CTA(협력 스레드 배열): 동일한 스레드 프로그램을 실행하고 협력하여 결과를 계산할 수 있는 동시 스레드 그룹입니다. GPU CTA는 CUDA 스레드 블록을 구현합니다.

19.9.3.6 스레드 명령

SP 스레드 프로세서는 각 버텍스 또는 픽셀 셰이더 프로그램에 대해 4개의 구성 요소 벡터 명령을 실행했던 이전 GPU 벡터 명령 아키텍처와 달리 단일 스레드에 대해 스칼라 명령을 실행합니다. 버텍스 프로그램은 일반적으로 (x, y, z, w) 위치 벡터를 계산하는 반면 픽셀 셰이더 프로그램은 (빨간색, 녹색, 파란색, 알파) 색상 벡터를 계산합니다. 그러나 셰이더 프로그램은 점점 길어지고 스칼라화되어 기존 GPU 4개 구성 요소 벡터 아키텍처의 두 구성 요소조차 완전히 점유하기 어렵습니다. 실제로 SIMT 아키텍처는 픽셀 내의 4개 벡터 구성 요소를 병렬화하는 대신 32개의 독립적인 픽셀 스레드를 통해 병렬화합니다. CUDA C/C++ 프로그램은 주로 스레드당 스칼라 코드를 갖고 있으며 이전 GPU는 벡터 패킹(예: 효율성을 위해 작업의 하위 벡터 결합)을 사용했지만 이로 인해 스케줄링 하드웨어와 컴파일러가 복잡해졌습니다. 스칼라 명령어는 더 간단하고 컴파일러 친화적이며 텍스처 명령어는 여전히 벡터 기반이며 소스 좌표 벡터를 사용하고 필터링된 색상 벡터를 반환합니다.

다양한 이진 마이크로 명령어 형식을 사용하는 여러 GPU를 지원하기 위해 고급 그래픽 및 컴퓨팅 언어 컴파일러는 중간 어셈블러 수준 명령어(예: Direct3D 벡터 명령어 또는 PTX 스칼라 명령어)를 생성한 다음 최적화하여 이진 GPU 마이크로 명령어로 변환합니다. NVIDIA PTX(병렬 스레드 실행) 명령어 세트 정의는 컴파일러에 안정적인 대상 ISA를 제공하고 GPU 세대 전반에 걸쳐 진화하는 바이너리 마이크로 명령어와의 호환성을 제공하므로 최적화 프로그램이 Direct3D 벡터 명령어를 여러 스칼라 바이너리 마이크로 명령어로 쉽게 확장할 수 있습니다. 일부 PTX 명령어는 여러 이진 마이크로 명령어로 확장되고 여러 PTX 명령어는 단일 이진 마이크로 명령어로 축소될 수 있지만 PTX 스칼라 명령어는 스칼라 이진 마이크로 명령어로 거의 일대일로 변환될 수 있습니다. 중간 어셈블러 수준 명령어는 가상 레지스터를 사용하므로 최적화 프로그램은 데이터 종속성을 분석하고 실제 레지스터를 할당합니다. 최적화 프로그램은 데드 코드를 제거하고, 가능한 경우 명령어를 함께 접고, SIMT 분기의 분기점과 수렴점을 최적화합니다.

명령어 세트 아키텍처(ISA)

여기에 설명된 스레드 ISA는 부동 소수점, 정수, 논리, 변환, 특수 기능, 흐름 제어, 메모리 액세스 및 텍스처 작업을 포함하는 레지스터 기반 스칼라 명령어 세트인 Tesla 아키텍처 PTX ISA의 단순화된 버전입니다. 아래 이미지에는 기본 PTX GPU 스레드 지침이 나열되어 있습니다. 자세한 내용은 NVIDIA PTX 사양을 참조하세요.

명령 형식은 다음과 같습니다.

cpp
opcode.type d, a, b, c;

여기서 d는 대상 피연산자이고, a, b, c는 소스 피연산자이고, .type은 다음 중 하나입니다.

유형 .특정 값 입력

유형이 지정되지 않은 비트 8, 16, 32 및 64비트 .b8, .b16, .b32, .b64

부호 없는 정수 8, 16, 32 및 64비트 .u8, .u16, .u22, .u64

부호 있는 정수 8, 16, 32 및 64비트 .s8, .s16, .s32, .s64

부동 소수점 16, 32 및 64비트 .16, .f32, .f64

소스 피연산자는 레지스터 내의 스칼라 32비트 또는 64비트 값, 즉치값, 상수이고, 판단 피연산자는 1비트 불리언 값이다. 대상은 메모리에 저장하는 것을 제외하고 레지스터입니다. 명령 앞에는 @p 또는 @!p가 붙습니다. 여기서 p는 판단 레지스터입니다. 메모리 및 텍스처 명령어는 2~4개의 구성 요소 스칼라 또는 벡터(총 128비트)를 전송합니다. PTX 명령어는 스레드의 동작을 지정합니다.

PTX 산술 명령어는 32비트 및 64비트 부동 소수점, 부호 있는 정수 및 부호 없는 정수 유형에서 작동합니다. 현재 GPU는 64비트 배정밀도 부동 소수점을 지원합니다. PTX 64비트 정수 및 논리 명령어는 32비트 작업을 수행하는 두 개 이상의 이진 마이크로 명령어로 변환됩니다. GPU 특수 기능 명령어는 32비트 부동 소수점으로 제한됩니다. 스레드 제어 흐름 명령에는 조건부 분기, 함수 호출 및 반환, 스레드 종료 및 bar.sync(장벽 동기화)가 포함됩니다. 조건 분기 명령어 @p bra target은 판단 레지스터 p(또는 !p)를 사용하여 스레드가 분기를 실행하는지 여부를 결정합니다. 판단 레지스터 p는 이전에 비교 및 ​​판단 설정 setp 명령에 의해 설정되었습니다. 다른 명령도 판단 기록에 따라 참 또는 거짓일 수 있습니다.

19.9.3.7 메모리 액세스 지침

tex 명령은 텍스처 하위 시스템을 통해 메모리의 1D, 2D 및 3D 텍스처 배열에서 텍스처 샘플을 추출하고 필터링합니다. 텍스처 추출은 일반적으로 보간된 부동 소수점 좌표를 사용하여 텍스처를 처리합니다. 그래픽 픽셀 셰이더 스레드가 픽셀 조각 색상을 계산하면 래스터 작업 프로세서는 이를 지정된 (x,y) 픽셀 위치의 픽셀 색상과 혼합하고 최종 색상을 메모리에 씁니다.

컴퓨팅 및 C/C++ 언어 요구 사항을 지원하기 위해 Tesla PTX ISA는 메모리 로드/저장 지침을 구현합니다. 이는 일반 컴파일러 코드 최적화를 용이하게 하기 위해 정수 바이트 주소 지정 및 레지스터 + 오프셋 주소 지정 알고리즘을 사용합니다. 메모리 로드/저장 명령은 프로세서에서 일반적이지만 이전 GPU는 그래픽 API에 필요한 텍스처 및 픽셀 액세스만 제공했기 때문에 Tesla 아키텍처 GPU의 중요한 새로운 기능입니다.

계산을 위해 로드/저장 명령어는 섹션 B.3의 해당 CUDA 메모리 공간을 구현하는 세 개의 읽기/쓰기 메모리 공간에 액세스합니다.

  • 주소 지정이 가능한 임시 데이터를 위한 스레드별 전용 로컬 메모리(외부 DRAM에 구현됨)

  • 동일한 CTA/스레드 블록의 협력 스레드가 공유하는 데이터에 대한 낮은 대기 시간 액세스를 위한 공유 메모리(온칩 SRAM에서 구현됨)

  • 컴퓨팅 애플리케이션의 모든 스레드가 공유하는 대규모 데이터 세트를 위한 글로벌 메모리(외부 DRAM에 구현됨)

메모리 로드/저장 명령어 ld.global, st.global, ld.shared, st.shared, ld.local 및 st.local은 각각 전역, 공유 및 로컬 메모리 공간에 액세스합니다. 컴퓨팅 프로그램은 공유 및 전역 메모리를 통해 서로 통신하는 CTA/스레드 블록 내의 스레드를 동기화하기 위해 빠른 장벽 동기화 명령 bar.sync를 사용합니다.

메모리 대역폭을 늘리고 오버헤드를 줄이기 위해 로컬 및 전역 로드/저장 명령어는 주소가 동일한 블록에 속하고 정렬 기준을 충족할 때 동일한 SIMT 워프의 개별 병렬 스레드 요청을 단일 메모리 블록 요청으로 결합합니다. 메모리 요청을 결합하면 단일 스레드의 개별 요청에 비해 성능이 크게 향상됩니다. 다수의 뛰어난 로드 요청에 대한 지원과 결합된 멀티프로세서의 대규모 스레드 수는 로드를 처리하는 데 도움이 되며 외부 DRAM에 구현된 로컬 및 글로벌 메모리 사용의 대기 시간을 줄여줍니다.

Tesla 아키텍처 GPU는 또한 병렬 축소 및 병렬 데이터 구조 관리를 용이하게 하는 정수 연산 add, min, max, and/or, xor, exchange 및 cas(비교 및 스왑) 연산을 포함하여atom.op.u32 명령을 통해 메모리에 대한 효율적인 원자 메모리 연산을 제공합니다.

19.9.3.8 스레드 통신을 위한 장벽 동기화

빠른 장벽 동기화를 사용하면 CUDA 프로그램이 단순히 __syncthreads()를 호출하여 각 스레드 간 통신 단계의 일부로 공유 메모리 및 전역 메모리를 통해 자주 통신할 수 있습니다. 동기화 내장 함수는 단일 bar.sync 명령어를 생성하지만 CUDA 스레드 블록당 최대 512개 스레드 사이에서 빠른 장벽 동기화를 달성하는 것은 어려운 일입니다.

스레드를 32개의 스레드로 그룹화하는 SIMT 워프는 동기화 난이도를 32배로 줄여줍니다. 스레드는 SIMT 스레드 스케줄러의 장벽에서 기다리므로 기다리는 동안 프로세서 사이클을 소비하지 않습니다. 스레드가 bar.sync 명령을 실행하면 장벽의 스레드 도착 카운터가 증가하고 스케줄러는 스레드를 장벽에서 대기 중인 것으로 표시합니다. 모든 CTA 스레드가 도착하고 장벽 카운터가 예상 터미널 수와 일치하면 스케줄러는 장벽에서 대기 중인 모든 스레드를 해제하고 스레드 실행을 재개합니다.

19.9.3.9 스트리밍 멀티프로세서(SM)

텍스처/프로세서 클러스터.

위 그림은 두 개의 SM이 있는 TPC의 구조를 보여줍니다. 지오메트리 컨트롤러는 단일 코어에서 버텍스 및 모양 처리를 조정하고, 메모리 계층에서 버텍스 데이터를 수집하고, 코어가 이를 처리하도록 지시한 다음, 메모리 계층에 대한 출력 저장을 조정합니다. 또한 출력을 다음 처리 단계로 전달하는 데 도움이 됩니다. SMC(SM 컨트롤러)는 외부 리소스에 대한 요청을 예약합니다. 예를 들어 SM의 여러 코어는 DRAM 메모리에 쓰거나 텍스처 장치에 액세스하려고 할 수 있습니다. 이 경우 SMC가 요청을 중재합니다.

이제 SM의 구조를 살펴보자. 각 SM에는 멀티 스레드 작업 부하를 위한 I 캐시(명령어 캐시), C 캐시(상수 캐시) 및 내장 스레드 스케줄러(MT Issue Unit)가 있습니다. 8개의 SP 코어는 상호 통신을 위해 SM에 내장된 공유 메모리 장치에 액세스할 수 있습니다. SP 코어는 덧셈, 뺄셈, 곱셈과 같은 일반적인 부동 소수점 연산을 수행할 수 있는 IEEE 754 호환 부동 소수점 ALU를 갖추고 있습니다. 또한 그래픽 컴퓨팅에서 매우 일반적으로 사용되는 곱셈-덧셈이라는 특수 명령어도 지원합니다. 이 명령어는 a*b+c 표현식을 평가합니다. FP ALU와 함께 각 SP에는 일반 정수 명령어와 논리 명령어를 실행할 수 있는 정수 ALU가 있습니다. 또한 SP 코어는 메모리 명령과 분기 명령을 실행할 수 있습니다. 벡터 프로세서와 마찬가지로 SP 코어는 추측 명령어를 구현합니다. 즉, nop 명령어로 대체되더라도 실행 슬롯을 잘못된 경로의 명령어에 전용으로 할당한다는 의미입니다. SP는 속도에 최적화되어 있으며 주로 기본 명령어로 구성된 매우 간단한 RISC와 유사한 명령어 세트를 구현하기 때문에 전체 GPU에서 가장 빠른 장치입니다.

초월 또는 삼각 함수와 같은 더 복잡한 수학 함수를 계산하기 위해 각 SM에는 두 개의 특수 기능 장치(SFU)가 있습니다. SFU에는 조각 내에서 색상 값을 보간하기 위한 전용 장치도 있으며, GPU는 이 기능을 사용하여 각 삼각형 조각의 내부에 색상을 지정합니다. 특수 유닛 외에도 SFU에는 범용 코드를 실행하기 위한 일반 정수/부동 소수점 ALU도 있습니다.

TPC의 두 SM은 동시에 4개의 스레드를 처리할 수 있고 삼각형과 관련된 표면 텍스처로 래스터라이제이션 후 생성된 모든 삼각형을 처리할 수 있는 텍스처 단위를 공유합니다. 텍스처 정보는 텍스처 유닛 내의 작은 캐시에 저장되며, 캐시 미스가 발생하면 텍스처 유닛은 관련 L2 캐시 또는 메인 DRAM 메모리에서 데이터를 가져올 수 있습니다.

이제 GPU에서 계산을 수행하는 방법을 살펴보겠습니다. SM의 각 스레드(SP에 매핑됨)는 스레드별 로컬 메모리(외부 DRAM에 저장됨), 공유 메모리(SM의 모든 스레드 간에 공유되고 온칩에 저장됨) 또는 글로벌 DRAM 메모리에 액세스할 수 있습니다. 프로그래머는 특정 종류의 메모리를 사용하도록 GPU에 명시적으로 지시할 수 있습니다.

단일 SM의 보다 자세한 구조는 아래 그림에 나와 있습니다.

단일 SM 아키텍처.

위 그림의 오른쪽은 NVIDIA Fermi 아키텍처를 단일 SM의 기본 구성 요소로 분해합니다. 이러한 구성 요소는 다음과 같습니다.

  • GPU 프로세서 코어(총 32개의 CUDA 코어).

  • 워프 스케줄러 및 스케줄링 포트.

  • 16개의 로드/저장 장치.

  • SFU 4개.

  • 32k*32비트 레지스터.

  • 공유 메모리 및 L1 캐시(총 64kB).

SM 내의 각 구성 요소는 아래에 자세히 설명되어 있습니다. 먼저 듀얼 워프 스케줄러에 대해 설명하겠습니다.

앞에서 언급했듯이 GPU 칩의 GigaThread 전역 스케줄러 장치는 스레드 블록을 SM에 할당한 다음 듀얼 워프 스케줄러가 처리하는 각 스레드 블록을 워프로 분해합니다. 여기서 워프는 32개의 스레드로 구성된 워프입니다. 이러한 스레드는 동일한 시작 주소에서 시작하며 해당 스레드 ID는 연속적입니다. 워프가 실행되면 각 스레드는 SM에서 각 스레드의 독립적인 분기 및 실행을 허용하도록 자체 명령 주소 카운터와 레지스터 세트를 갖게 됩니다.

GPU는 CUDA 코어의 활용도를 극대화하기 위해 최대한 많은 워프를 처리할 때 가장 효율적입니다. 아래 그림에서 볼 수 있듯이 듀얼 워프 스케줄러와 명령어 스케줄링 장치가 두 클록 사이클마다 두 개의 워프를 발행할 수 있는 경우(Fermi 아키텍처) SM 하드웨어 활용도는 최대에 도달합니다. 아래에서 설명하는 것처럼 구조 충돌은 SM이 최대 처리 속도에 도달할 수 없는 주된 이유인 반면 오프칩 메모리 액세스 지연은 숨기기가 더 쉽습니다.

구성 요소 열에 구조적 충돌이 없는 경우 분할된 각 열은 16개의 CUDA 코어(*2), 16개의 로드/저장 유닛 및 4개의 SFU(위)로 구성되며, 각 클록 주기는 처리를 위해 2개의 워프 스케줄러/스케줄링 단위 각각에서 절반의 워프(16스레드)를 할당할 수 있습니다. 구조 충돌은 제한된 SFU, 배정밀도 곱셈 및 분기로 인해 발생합니다. 그러나 워프 스케줄러에는 실행 및 구조 충돌에 사용할 수 있는 워프를 추적하는 스코어보드가 내장되어 있어 SM이 구조 충돌을 피하고 칩 외부 메모리 액세스 지연을 최대한 숨길 수 있습니다.

듀얼 워프 스케줄러 및 명령 발송 장치 실행 예시.

따라서 프로그래머는 스레드 블록 크기를 SM의 총 CUDA 코어 수보다 크지만 블록당 허용되는 최대 스레드 수보다 작게 설정해야 하며, SM의 거의 최적 활용을 달성하기 위해 스레드 블록 크기(x 및/또는 y 차원)가 32의 배수(워프 크기)인지 확인해야 합니다.

듀얼 워프 스케줄러를 설명한 후 CUDA 코어에 대해 설명하겠습니다.

NVIDIA GPU 프로세서 코어는 CUDA 코어라고도 합니다. Fermi 아키텍처에는 각 SM 전용으로 총 32개의 CUDA 코어가 있습니다. 각 CUDA 코어에는 정수(INT) 단위 파이프라인과 부동 소수점(FP) 단위 파이프라인(위 그림 참조)이라는 두 개의 독립적인 파이프라인 또는 데이터 경로가 있으며, 클록 주기 동안 이러한 데이터 경로 중 하나만 사용할 수 있습니다. INT 장치는 32비트, 64비트 및 확장 정밀도 정수 및 논리/비트 연산이 가능하고, FP 장치는 단정밀도 FP 연산을 수행할 수 있으며, 배정밀도 FP 연산에는 2개의 CUDA 코어가 필요합니다. 따라서 배정밀도 FP 작업만 수행하는 스레드는 단정밀도 FP 스레드에 비해 실행 시간이 두 배 더 오래 걸립니다. Kepler 아키텍처는 대부분의 단정밀도 장치와 함께 각 SM에 전용 배정밀도 장치를 포함하여 배정밀도 FP 알고리즘이 성능에 미치는 영향을 해결합니다. 다행스럽게도 스레드 수준 FP 단정밀도 및 배정밀도 작업 관리는 CUDA 프로그래머에게 숨겨져 있지만 프로그래머는 사용된 GPU에 따라 두 정밀도 유형 간에 발생할 수 있는 잠재적인 성능 영향을 알고 있어야 합니다.

Fermi 아키텍처는 IEEE 754-1985 부동 소수점 연산 표준을 IEEE 754-2008 표준으로 업그레이드하여 CUDA 코어의 FP 장치에 개선 사항을 추가합니다. 이는 융합된 곱셈 덧셈(FMA) 명령어를 사용하여 곱셈 덧셈 명령어(MAD)의 정밀도를 향상함으로써 달성됩니다. FMA 명령어는 단정밀도 및 배정밀도 산술 모두에 유효합니다. Fermi 아키텍처는 FMA 명령어 끝에서 한 번의 반올림만 수행합니다. 이는 결과의 정확성을 향상시킬 뿐만 아니라 FMA 명령 실행을 단일 프로세서 클럭 주기로 압축합니다. 따라서 각 SM은 하나의 프로세서 클록 사이클에서 32개의 단정밀도 또는 16개의 배정밀도 FMA 작업을 수행할 수 있습니다.

다른 부분은 다음과 같이 설명됩니다.

  • 특수 기능 유닛: 각 SM에는 4개의 SFU가 있습니다. SFU는 하나의 클록 사이클에서 코사인, 사인, 역수, 제곱근과 같은 초월 연산을 수행합니다. SM에는 SFU가 4개만 있고 워프의 한 명령에 대해 병렬 스레드가 32개만 있으므로 SFU가 필요한 워프를 완료하는 데 8클럭 사이클이 걸리지만 CUDA 프로세서와 로드 및 저장 장치는 여전히 동시에 사용할 수 있습니다.

  • 로드 및 저장 장치: SM의 16개 로드 및 저장 장치 각각은 스레드가 데이터를 쓰거나 데이터를 읽으려는 캐시 또는 DRAM에 대한 단일 스레드의 소스 및 대상 주소를 각각의 클록 주기로 계산합니다.

  • 레지스터, 공유 메모리 및 L1 캐시: 각 SM에는 자체(온칩) 전용 레지스터 세트와 공유 메모리/L1 캐시 블록이 있습니다. 지연 시간이 짧은 온칩 메모리에 대한 자세한 내용과 이점은 아래 표에 나와 있습니다.

메모리 유형 상대 액세스 시간 액세스 유형 범위 데이터 수명

등록하다 가장 빠른 온칩 읽기/쓰기 단일 스레드 실

공유 빠르게, 칩 내에서 읽기/쓰기 블록의 모든 스레드 블록

지역 공유 및 레지스터, 오프칩보다 100~150배 느림 읽기/쓰기 단일 스레드 실

전반적인 상황 공유 및 레지스터, 오프칩보다 100~150배 느림 읽기/쓰기 모든 스레드 및 호스트 신청

고정 공유 및 레지스터, 오프칩보다 100~150배 느림 R 모든 스레드 및 호스트 신청

질감 공유 및 레지스터, 오프칩보다 100~150배 느림 R 모든 스레드 및 호스트 신청

Fermi 아키텍처에는 SM당 인상적인 32k x 32비트 레지스터가 있지만 각 스레드에는 SM당 허용되는 최대 활성 워프 수와 SM당 레지스터 수의 함수인 CUDA 컴퓨팅 기능 버전 2.x에 정의된 대로 최대 64x32비트 레지스터가 할당됩니다. 위 표에서 볼 수 있듯이 레지스터와 공유 메모리에 대한 가장 빠른 액세스 시간은 불과 몇 나노초(ns)에 불과합니다. 임시 레지스터가 오버플로되면 데이터는 먼저 L1 캐시로 이동한 다음 L2 캐시로 전송된 다음 장기 액세스 대기 시간 로컬 메모리로 전송됩니다(아래 그림 a 참조). 첫 번째 수준 캐시를 사용하면 데이터 읽기/쓰기 충돌이 발생하는 것을 방지할 수 있으므로 스레드에 할당된 레지스터의 데이터는 스레드가 지속되는 동안에만 유지됩니다.

페르미 메모리 아키텍처.

SM의 GPU 프로세서 코어 전용 주소 지정이 가능한 온칩 공유 메모리는 CPU와 같은 최신 멀티 코어 마이크로프로세서에 비해 독특한 구성입니다. 이러한 최신 아키텍처에는 전용 온칩 L1 캐시와 코어당 레지스터 세트가 있지만 일반적으로 온칩 주소 지정 가능 메모리는 없습니다. 대조적으로, 특수 메모리 관리 하드웨어는 프로그래머의 제어 없이 캐시와 메인 메모리 사이의 데이터 이동을 조절하는데, 이는 GPU 아키텍처와 크게 다릅니다.

특히 GPGPU 애플리케이션을 지원하기 위해 공유 메모리가 GPU 아키텍처에 추가되었습니다. 공유 메모리 사용을 최적화하면 오프칩 메모리에 대한 불필요한 장기 지연 액세스를 제거하여 GPGPU 애플리케이션의 속도와 성능을 크게 향상시킬 수 있습니다. 각 SM의 공유 메모리 크기는 작지만(최대 구성의 경우 48kB) 액세스 지연 시간은 전역 메모리보다 100~150배 적습니다(위 표 참조). 따라서 공유 메모리는 세 가지 주요 방법으로 병렬 처리 작업 속도를 높일 수 있습니다.

  • 공유 메모리 데이터(예: 행렬-행렬 곱셈을 위한 데이터 블록)는 블록의 모든 스레드에서 여러 번 재사용됩니다.

  • 블록의 선택 스레드(특정 ID 기반)를 사용하여 전역 메모리에서 공유 메모리로 데이터를 전송함으로써 동일한 메모리 위치에 대한 중복 읽기 및 쓰기를 제거합니다.

  • 가능하다면 사용자는 액세스가 통합되도록 하여 전역 메모리에 대한 데이터 액세스를 최적화할 수 있습니다.

이러한 모든 사항은 오프칩 메모리 대역폭 제한 문제를 줄이는 데도 도움이 됩니다. SM 공유 메모리의 데이터는 스레드 블록이 처리되는 동안 지속됩니다. 따라서 블록의 모든 스레드가 완료되면 SM 공유 메모리의 데이터는 더 이상 유효하지 않습니다.

공유 메모리를 사용하면 최상의 런타임을 제공할 수 있지만 프로그래밍 단계에서 메모리 액세스를 알 수 없는 일부 애플리케이션에서는 사용 가능한 L1 캐시를 더 많이(최대 설정은 48kB) 두는 것이 최상의 결과를 제공합니다. 또한 L1 캐시는 로컬(오프칩) DRAM 메모리로 직접 이동하는 대신 레지스터 오버플로를 방지하는 데 도움이 됩니다. SM당 하나의 L1 캐시와 칩 및 SM 간에 공유되는 L2 캐시를 갖춘 2단계 캐시 계층 구조는 기존 멀티 코어 마이크로프로세서와 동일한 이점을 제공합니다.

우리는 GPU 프로그래밍에서 메모리 유형을 이해하는 것이 중요한 역할을 한다는 것을 깨달아야 합니다.

프로그래머는 CUDA를 사용하여 정확하고 효율적인 코드 개발을 위해 다양한 GPU 메모리의 미묘한 차이, 특히 사용 가능한 크기, 상대적 액세스 시간 및 각 메모리 유형의 접근성 제한을 이해해야 합니다. GPGPU 프로그래밍에는 사용된 특정 데이터 저장 하드웨어(파일 I/O 제외)가 프로그래머에게 숨겨져 있는 CPU용 프로그램 개발과는 훨씬 다른 접근 방식이 필요합니다.

예를 들어, GPU 아키텍처에서 CUDA 커널에 할당된 각 스레드에는 고유한 레지스터 세트가 있으므로 동일한 SM에 있든 없든 한 스레드는 다른 스레드의 레지스터에 액세스할 수 없습니다. 특정 SM의 스레드가 (데이터 공유를 통해) 서로 협력할 수 있는 유일한 방법은 공유 메모리(아래)를 통해서입니다. 일반적으로 프로그래머는 SM의 특정 스레드만 공유 메모리의 특정 위치에 쓰도록 할당하여 쓰기 충돌이나 낭비되는 사이클(예: 많은 스레드가 전역 메모리에서 동일한 데이터를 읽고 동일한 공유 메모리 주소에 쓰는 경우)을 방지합니다. 특정 SM의 모든 스레드가 방금 작성된 공유 메모리에서 읽을 수 있도록 허용되기 전에 해당 SM의 모든 스레드를 동기화하여 RAW(읽기 후 쓰기) 데이터 충돌을 방지해야 합니다.

기본 GPU 아키텍처의 CUDA 표현입니다.

19.9.3.10 스트림 프로세서(SP)

멀티스레드 SP(스트림 프로세서) 코어는 멀티프로세서의 기본 스레드 명령 프로세서이며, 해당 RF(레지스터 파일)는 최대 64개 스레드에 대해 1024개의 스칼라 32비트 레지스터를 제공합니다. add.f32, mul.f32, mad.f32(부동 곱셈 및 덧셈), min.f32, max.f32 및 setp.f32(부동 비교 및 ​​설정 판단)를 포함한 모든 기본 부동 소수점 연산을 수행합니다. 부동 소수점 덧셈 및 곱셈 연산은 IEEE 754 표준과 호환되며 정수가 아닌 값(NaN) 및 무한대 값을 포함한 단정밀도 FP 숫자에서 작동합니다. SP 코어는 또한 모든 32비트 및 64비트 정수 연산, 비교, 변환 및 논리 PTX 명령을 구현합니다.

부동 소수점 더하기 및 곱하기 연산은 기본 반올림 모드인 경우에도 IEEE 반올림을 사용합니다. mad.f32 부동 소수점 곱셈-덧셈 연산은 잘림으로 곱셈을 수행한 다음 가장 가까운 짝수로 반올림하여 덧셈을 수행합니다. SP는 입력 비정규 피연산자를 부호 보존 0으로 플러시하고, 반올림 후에 대상 출력 지수 범위 언더플로의 결과를 부호 보존 0으로 플러시합니다.

19.9.3.11 특수 기능 유닛(SFU)

특정 스레드 명령어는 SP에서 실행되는 다른 스레드 명령어와 동시에 SFU에서 실행될 수 있습니다. SFU는 32비트 부동 소수점 근사치의 역수, 역수 제곱근 및 주요 초월 함수를 계산하는 특수 함수 명령을 구현합니다. 또한 픽셀 셰이더에 대한 32비트 부동 소수점 평면 속성 보간을 구현하여 색상, 깊이, 텍스처 좌표와 같은 속성의 정확한 보간을 제공합니다.

각 파이프라인 SFU는 사이클당 하나의 32비트 부동 소수점 특수 기능 결과를 생성하고, 멀티프로세서당 2개의 SFU는 8개 SP의 단순 명령어 속도의 1/4로 특수 기능 명령어를 실행합니다. SFU는 또한 8개의 SP와 동시에 mul.f32 곱셈 명령어를 실행하여 적절한 명령어 혼합을 통해 스레드의 최고 컴퓨팅 속도를 50%로 높입니다.

기능 평가를 위해 Tesla 아키텍처 SFU는 향상된 최소-최대 근사법을 기반으로 하는 2차 보간법을 사용하여 역수, 역수 제곱, log2x, 2x 및 sin/cos 함수를 근사화합니다. 함수 계산의 정확도 범위는 22~24개의 가수 자리입니다.

19.9.3.12 다른 멀티프로세서와 비교

x86 SSE와 같은 SIMD 벡터 아키텍처와 달리 SIMT 다중 프로세서는 개별 스레드를 항상 동기화된 그룹에서 함께 실행하는 대신 독립적으로 실행할 수 있습니다. SIMT 하드웨어는 독립 스레드 간의 데이터 병렬성을 찾는 반면, SIMD 하드웨어는 모든 벡터 명령에서 데이터 병렬성을 명시적으로 표현하는 소프트웨어가 필요합니다. SIMT 머신은 스레드가 동일한 실행 경로를 사용할 때 32개의 스레드 워프를 동기적으로 실행하지만, 스레드가 분리되면 각 스레드가 독립적으로 실행될 수 있습니다. SIMT 프로그램과 명령어는 4개 이상의 데이터 채널로 구성된 SIMD 데이터 벡터가 아닌 단일 독립 스레드의 동작만 설명하기 때문에 이러한 이점은 중요합니다. 그러나 SIMT 멀티프로세서는 SIMD와 유사한 효율성을 제공하여 명령 단위 하나의 영역과 비용을 워프 스레드 32개와 스트림 프로세서 코어 8개로 확장합니다. SIMT는 멀티스레딩의 생산성과 함께 SIMD 성능을 제공하므로 가장자리 조건 및 부분 발산에 대해 SIMD 벡터를 명시적으로 코딩할 필요가 없습니다.

SIMT 멀티프로세서는 하드웨어 장벽 동기화를 갖춘 하드웨어 멀티스레딩이므로 오버헤드가 거의 없으며 그래픽 셰이더와 CUDA 스레드가 매우 세밀한 병렬 처리를 표현할 수 있습니다. 그래픽 및 CUDA 프로그램은 프로그래머가 이를 SIMD 벡터 명령으로 표시하도록 강요하는 대신 스레드를 사용하여 스레드별 프로그램에서 세분화된 데이터 병렬성을 나타냅니다. 스칼라 단일 스레드 코드 개발은 벡터 코드보다 더 간단하고 효율적이며 SIMT 멀티프로세서는 SIMD와 유사한 효율성으로 코드를 실행합니다.

8개의 스트림 프로세서 코어가 멀티프로세서로 긴밀하게 결합된 후 확장 가능한 수의 멀티프로세서가 구현되어 멀티프로세서로 구성된 2레벨 멀티프로세서가 됩니다. CUDA 프로그래밍 모델은 세분화된 병렬 계산을 위한 단일 스레드와 대략적인 병렬 작업을 위한 스레드 블록 그리드를 제공하여 2단계 계층 구조를 활용합니다. 동일한 스레드 프로그램은 세분화된 작업과 거친 작업을 모두 제공할 수 있습니다. 대신 SIMD 벡터 명령어가 있는 CPU는 세밀하고 거친 작업을 제공하기 위해 서로 다른 두 가지 프로그래밍 모델, 즉 서로 다른 코어의 대략적인 병렬 스레드와 세밀한 데이터 병렬 처리를 위한 SIMD 벡터를 사용해야 합니다.

19.9.3.13 다중 스레드 다중 프로세서 결론

Tesla 아키텍처를 기반으로 한 예제 GPU 멀티프로세서는 고도로 멀티스레드되어 최대 512개의 경량 스레드를 동시에 실행하여 세밀한 픽셀 셰이더 및 CUDA 스레드를 지원합니다. SIMD 아키텍처와 SIMT(Single Instruction Multi-Threading)라는 멀티스레딩 변형을 사용하여 단일 명령어를 32개의 병렬 스레드 워프로 효과적으로 브로드캐스트하는 동시에 각 스레드가 독립적으로 분기하고 실행할 수 있도록 합니다. 각 스레드는 최대 64개의 스레드를 포함하는 8개의 SP(스트림 프로세서) 코어 중 하나에서 명령 스트림을 실행합니다.

PTX ISA는 단일 스레드의 실행을 설명하는 데 사용되는 레지스터 기반 로드/저장 스칼라 ISA입니다. PTX 명령어는 최적화되어 특정 GPU에 대한 이진 마이크로 명령어로 변환되므로 하드웨어 명령어는 PTX 명령어를 생성하는 컴파일러 및 소프트웨어 도구를 중단하지 않고 빠르게 발전할 수 있습니다.

19.9.3.14 비닝된 렌더링

우리는 일반적으로 래스터라이제이션를 화면 좌표 기하 구조를 픽셀 조각으로 직접 변환하는 프로세스로 정의하지만, n×n 픽셀 블록과 같은 더 큰 화면 영역으로 래스터라이제이션하는 것도 가능합니다. 예를 들어 텍스처 재매핑 계산을 단순화하기 위해 2×2 쿼드 조각을 출력하는 GeForce 9800 GTX 래스터라이저가 있습니다. 비닝 렌더링(비닝 렌더링이라고도 함)은 래스터라이제이션를 두 단계로 나눕니다. 첫 번째 단계에서는 중간 크기의 비닝된 조각을 출력합니다. 각 조각은 화면 좌표의 8×8, 16×16 또는 32×32 픽셀 격자에 해당하며, 두 번째 단계에서는 비닝된 각 조각을 픽셀 조각으로 줄입니다. 물론, 타일 조각에는 화면 좌표 프리미티브에서 파생된 정보가 포함되어 있어 2단계 래스터라이제이션에서 올바른 픽셀 조각을 생성할 수 있습니다.

타일 ​​렌더링은 실제로 전체 렌더링 프로세스를 래스터라이제이션의 두 단계에 해당하는 두 단계로 나눕니다. 첫 번째 단계에서는 장면이 타일 래스터라이제이션를 통해 처리되고 결과 타일 조각이 각 화면 타일당 하나씩 저장소로 정렬됩니다. 첫 번째 단계가 완료된 후에만(즉, 전체 장면의 청크 조각이 생성되어 저장소로 분류된 후) 두 번째 단계가 시작됩니다. 두 번째 단계에서는 각 빈이 완료될 때까지 개별적으로 처리되어 n × n 픽셀 블록이 생성되어 프레임 버퍼에 저장됩니다.

타일 ​​렌더링에는 몇 가지 매력적인 속성이 있습니다.

  • 로컬 메모리: 프레임 버퍼 데이터 일관성을 절대적으로 보장합니다. 타일의 픽셀에만 액세스하여 픽셀을 메인 메모리에서 캐시하는 대신 로컬 메모리에서 처리할 수 있습니다. 전력 소비와 메인 메모리 주기를 모두 절약할 수 있으므로 타일 렌더링은 모바일 장치에 매력적인 솔루션이 됩니다.

  • 전체 화면 앤티앨리어싱: 다중 샘플 앤티앨리어싱을 사용하려면 픽셀당 여러 색상 및 깊이 샘플을 저장해야 합니다. 샘플 수를 늘려 품질이 향상되므로 전체 프레임 버퍼로 렌더링할 때 저장 공간과 대역폭 모두 매우 비싸지지만 렌더링이 작은 픽셀 블록으로 제한되면 여전히 경제적입니다. 투명한 표면의 순서 독립적 렌더링과 같은 훨씬 더 고급 렌더링 알고리즘은 로컬 메모리를 현명하게 사용하여 지원할 수 있습니다.

  • 디퍼드 렌더링: 렌더링을 작은 픽셀 패치로 제한하면 디퍼드 렌더링의 주요 제한 사항, 즉 과도한 메모리 저장 공간과 대역폭이 필요하고 다중 샘플 앤티앨리어싱과 호환되지 않는 문제가 해결됩니다.

타일 ​​렌더링의 장점은 강력하지만 현재 이를 구현하는 PC급 GPU는 없습니다. 가장 근본적인 이유는 블록 렌더링과 파이프라인 Direct3D 및 OpenGL 아키텍처의 차이가 너무 크기 때문입니다. 추상화 거리가 너무 크다고 말합니다. 종종 과도한 추상 거리로 인해 제품의 성능 특성이 혼합되거나(빠를 것으로 예상되는 작업은 느리고 느릴 것으로 예상되는 작업은 빠름) 지정된 작업에서 미묘한 편차가 발생하는 경우가 많습니다. 직면한 실제 문제는 다음과 같습니다.

  • 과도한 대기 시간: Chapel Hill의 노스캐롤라이나 대학에서 개발된 PixelPlanes 5 시스템과 같은 이전 타일 렌더링 시스템에는 전체 프레임 대기 시간이 추가되었습니다.

  • 불량한 멀티패스 작업: Direct3D 및 OpenGL은 각 최종 프레임에 대해 여러 개의 2패스 작업이 필요한 타일 구현에서 고급 멀티패스 렌더링 기술을 권장합니다. 예를 들어 표면의 반사는 1) 반사에 표시되는 장면을 렌더링하고, 2) 해당 이미지를 텍스처로 로드하고, 3) 적절하게 왜곡된 텍스처 이미지로 표면을 렌더링하여 렌더링됩니다. 일부 타일 렌더링 시스템은 이러한 작업을 지원하지 않으며 다른 시스템은 지원하지만 제대로 수행되지 않습니다.

  • 무제한 메모리 요구 사항: 타일 렌더링은 픽셀 저장을 단일 타일에 필요한 정도로 제한하지만 타일 자체에 필요한 메모리는 장면 복잡성에 따라 증가합니다. OpenGL이나 Direct3D에는 장면 복잡도 제한이 없으므로 완전히 확인된 구현에는 무한한 메모리가 필요하거나(분명히 불가능함) 유한 블록의 부족한 저장 공간을 처리하기 위해 복잡성이 도입되어야 합니다.

이러한 복잡성은 주류 PC GPU에서 비닝된 렌더링을 제외하기에 충분했습니다. 그러나 최근 구현 추세, 특히 모든 파이프라인 셰이딩 단계에 대해 시간 공유 단일 컴퓨팅 엔진을 사용하면 몇 가지 어려움을 극복할 수 있습니다.

19.9.4 병렬 메모리 시스템

GPU 자체 외에 메모리 하위 시스템은 매우 높은 메모리 전송 속도가 필요한 그래픽 작업 부하와 함께 그래픽 시스템 성능을 결정하는 가장 중요한 요소입니다. 픽셀 쓰기 및 혼합(읽기-수정-쓰기) 작업, 깊이 버퍼 읽기 및 쓰기, 텍스처 맵 읽기, 명령 및 개체 꼭짓점 및 속성 데이터 읽기가 메모리 트래픽의 대부분을 차지합니다.

최신 GPU는 병렬성이 매우 높습니다. 예를 들어 GeForce 8800은 600MHz에서 클록당 32픽셀을 처리할 수 있으며 각 픽셀에는 일반적으로 4바이트의 픽셀 색상 읽기 및 쓰기와 깊이 읽기 및 쓰기가 필요합니다. 일반적으로 평균 2~3개의 4바이트 텍셀을 읽어 픽셀 색상을 생성합니다. 일반적인 경우에는 28바이트 x 32픽셀 = 클록당 896바이트가 필요하므로 메모리 시스템의 대역폭 요구 사항은 엄청납니다.

이러한 요구 사항을 충족하기 위해 GPU 메모리 시스템에는 다음과 같은 기능이 있습니다.

  • 넓습니다. 즉, GPU와 메모리 장치 사이에 데이터 전송을 위한 많은 핀이 있고, 메모리 어레이 자체에는 전체 데이터 버스 폭을 제공하기 위한 많은 DRAM 칩이 포함되어 있습니다.

  • 속도가 빠르므로 공격적인 신호 기술을 사용하여 핀당 데이터 속도(비트/초)를 최대화합니다.

  • GPU는 사용 가능한 모든 주기를 사용하여 메모리 배열과 데이터를 주고 받습니다. 이를 달성하기 위해 GPU는 특히 메모리 시스템의 대기 시간을 최소화하는 것을 목표로 하지 않습니다. 높은 처리량(활용 효율성)과 짧은 대기 시간은 근본적으로 충돌합니다.

  • 사용되는 압축 기술은 프로그래머가 알아야 하는 손실 압축 기술부터 응용 프로그램에 보이지 않는 무손실 압축 기술까지 다양합니다.

  • 캐시 및 작업 병합 구조는 필요한 오프칩 트래픽을 줄이고 데이터 이동에 소요되는 주기를 최대한 활용하는 데 사용됩니다.

19.9.4.1 비디오 메모리 구조

DRAM은 일반적으로 플랫 바이트 어레이로 간주되지만 내부 구조는 훨씬 더 복잡합니다. GPU와 같은 고성능 애플리케이션의 경우 이를 깊이 있게 이해하는 것이 매우 필요합니다. 아래에서 위로 대략 살펴보면 VRAM은 다음과 같은 부분으로 구성됩니다.

  • 행 R과 열 C의 메모리 평면, 셀당 1비트.

imgimg

  • 병렬로 사용되는 32개, 64개 또는 128개의 메모리 플레인으로 구성된 메모리 뱅크 - 이러한 플레인은 일반적으로 여러 칩에 분산되며, 하나의 칩에는 16개 또는 32개의 메모리 플레인이 포함됩니다. 뱅크의 모든 페이지는 행 주소 지정 시스템(열도 마찬가지)에 연결되어 있으며 이러한 페이지는 각 행/열에 대한 명령 신호 및 주소에 의해 제어됩니다. 뱅크에 행과 열이 많을수록 주소에 더 많은 비트를 사용해야 합니다.

imgimg

  • 함께 연결되고 주소 비트에 의해 선택되는 여러 [2, 4 또는 8]개의 메모리 뱅크로 구성된 메모리 랭크 - 지정된 메모리 플레인의 모든 메모리 뱅크는 동일한 칩에 위치합니다.

  • 함께 연결되고 칩 선택 라인에 의해 선택되는 하나 또는 두 개의 메모리 랭크로 구성된 메모리 하위 파티션입니다. 랭크는 뱅크처럼 동작하지만 균일한 기하학적 구조를 가질 필요는 없고 별도의 칩에 있습니다.

  • **메모리 파티션(메모리 파티션)**은 약간 독립적인 하나 또는 두 개의 메모리 하위 파티션으로 구성됩니다.

  • 전체 VRAM은 여러 개의 [1-8] 메모리 파티션으로 구성됩니다.

위의 수치는 다양한 GPU 아키텍처 및 제품군에 따라 달라집니다.

GDDR3 메모리 회로의 단순화된 블록 다이어그램. 명확성을 높이기 위해 실제 저장 용량(10억 비트)은 16개의 16비트 블록(행이라고도 함) 배열로 구현된 256비트로 축소되었습니다. 블록의 왼쪽 가장자리에 도달하는 빨간색 화살표는 제어 경로를 나타내고, 블록의 상단과 하단에 도달하는 파란색 화살표는 데이터 경로를 나타냅니다.

DRAM의 가장 기본적인 단위는 소위 열과 행으로 구성된 2차원 비트 배열인 메모리 플레인입니다.

cpp
column
row  0  1  2  3  4  5  6  7
0    X  X  X  X  X  X  X  X
1    X  X  X  X  X  X  X  X
2    X  X  X  X  X  X  X  X
3    X  X  X  X  X  X  X  X
4    X  X  X  X  X  X  X  X
5    X  X  X  X  X  X  X  X
6    X  X  X  X  X  X  X  X
7    X  X  X  X  X  X  X  X

buf  X  X  X  X  X  X  X  X

메모리 플레인에는 전체 행을 보유하는 버퍼가 포함되어 있습니다. 내부적으로 DRAM은 버퍼를 통해 행 단위로 읽고 쓰입니다. 이로 인해 다음과 같은 여러 가지 결과가 발생합니다.

  • 비트 작업을 수행하기 전에 해당 행을 버퍼에 로드해야 하므로 속도가 느려질 수 있습니다.

  • 행을 처리한 후 메모리 배열에 다시 기록해야 하는데 이 역시 속도가 느립니다.

  • 따라서 새로운 행에 대한 접근이 느리며, 이미 활성화된 행이 있는 경우에는 접근이 더욱 느려집니다.

  • 일정 기간 동안 활동이 없을 경우 은행을 선제적으로 폐쇄하는 것이 유용한 경우가 많습니다. 이 작업을 은행 선충전이라고 합니다.

  • 그러나 동일한 행 내의 다른 열에 빠르게 액세스할 수 있습니다.

열 주소 자체를 로드하는 것은 활성 버퍼의 비트에 실제로 액세스하는 것보다 더 많은 시간이 걸리기 때문에 DRAM은 버스트 방식으로 액세스됩니다. 이는 활성 행의 1~8개 인접 비트에 대한 일련의 액세스이며 일반적으로 버스트의 모든 비트는 정렬된 단일 옥텟에 있어야 합니다. 메모리 플레인의 행과 열 수는 항상 2의 거듭제곱이며 행 선택 및 열 선택 비트 수로 측정됩니다. 행/열 개수의 log2], 일반적으로 8~10열 비트 및 10~14행 비트입니다. 메모리 평면은 두 개의 메모리 평면의 거듭제곱으로 구성된 뱅크로 구성됩니다. 메모리 평면은 병렬로 연결되어 주소와 제어 라인을 공유하며 데이터/데이터 활성화 라인만 분리됩니다. 이는 효과적으로 메모리 뱅크를 개별 비트가 아닌 32비트/64비트/128비트 메모리 셀로 구성된 메모리 플레인과 유사하게 만듭니다. 플레인에 적용되는 모든 규칙은 여전히 ​​뱅크에 적용되지만 작동되는 셀은 비트보다 큽니다. 단일 메모리 칩에는 일반적으로 단일 뱅크에 대해 16개 또는 32개의 메모리 플레인이 포함되어 있으므로 여러 칩이 함께 연결되어 더 넓은 뱅크를 형성하는 경우가 많습니다.

메모리 칩에는 동일한 데이터 라인을 사용하고 뱅크 선택 라인에 의해 다중화되는 여러 [2, 4 또는 8] 뱅크가 포함되어 있습니다. 뱅크 간 전환은 행의 열 간 전환보다 느리지만, 동일한 뱅크의 행 간 전환보다는 훨씬 빠릅니다. 따라서 메모리 뱅크는 (MEMORY_CELL_SIZE / MEMORY_CELL_SIZE_PER_CHIP)개의 메모리 칩으로 구성됩니다. 칩 선택 라인을 제외하고 공통 라인(데이터 포함)으로 연결된 하나 또는 두 개의 메모리 열이 메모리 하위 파티션을 구성합니다. 순위 간 전환은 기본적으로 뱅크의 열 그룹 간 전환과 동일한 성능 결과를 가져옵니다. 유일한 차이점은 물리적 구현과 각 순위에 대해 다른 수의 행 선택 비트를 사용할 수 있다는 것입니다(열 수와 열 수는 일치해야 함). 여러 은행/순위의 결과:

  • 함께 액세스되는 데이터가 동일한 행에 속하거나 다른 뱅크에 속하는지 확인하는 것이 중요합니다(행 전환을 방지하기 위해).

  • 블록 메모리 레이아웃은 블록이 대략 행에 해당하고 인접한 블록이 뱅크를 공유하지 않도록 설계되었습니다.

메모리 하위 파티션에는 GPU에 자체 DRAM 컨트롤러가 있습니다. 1개 또는 2개의 하위 파티션이 메모리 파티션을 구성합니다. 이는 자체 메모리 액세스 큐, 자체 ZROP 및 CROP 장치, 이후 카드의 L2 캐시를 갖춘 상당히 독립적인 개체입니다. 모든 메모리 파티션은 크로스바 로직과 함께 GPU의 전체 VRAM 로직을 구성합니다. 파티션 내의 모든 하위 파티션은 동일하게 구성되어야 합니다. GPU의 파티션은 일반적으로 동일하게 구성되지만 최신 카드에서는 이것이 필요하지 않습니다. 하위 파티션/파티션 존재의 결과:

  • 뱅크와 마찬가지로 관련 데이터에 대한 행 충돌을 피하기 위해 다른 파티션을 사용할 수 있습니다.

  • 뱅크와 달리 (하위)파티션이 동일하게 활용되지 않으면 대역폭에 영향을 미칩니다. 따라서 로드 밸런싱이 매우 중요합니다.

메모리 주소 지정은 GPU 제품군에 크게 의존하지만 기본 접근 방식은 여기에 설명되어 있습니다. 메모리 주소의 비트는 다음에 순차적으로 할당됩니다.

  • 어쨌든 전체 장치에 액세스해야 하므로 메모리 장치의 바이트를 식별합니다.

  • 버스트를 허용하는 다중 열 선택 비트.

  • 파티션/하위 파티션 선택 - 좋은 로드 밸런싱을 보장하려면 낮게 설정하세요. 그러나 ROP를 용이하게 하기 위해 상대적으로 큰 타일을 단일 파티션에 유지하기에는 너무 낮지 않게 설정하세요.

  • 남은 열 선택 비트.

  • 인접 주소가 행 충돌을 일으키지 않도록 모든/대부분의 뱅크 선택 비트 및 때로는 순위 선택 비트.

  • 행 비트.

  • 나머지 뱅크 비트 또는 순위 비트를 사용하면 VRAM을 두 영역으로 효과적으로 분할할 수 있습니다. 한 영역에는 컬러 버퍼가 배치되고 다른 영역에는 제타 버퍼가 배치되어 두 영역 사이에 행 충돌이 발생하지 않습니다.

GPU는 DRAM의 고유한 특성을 고려해야 합니다. DRAM 칩은 내부적으로 여러 개의(보통 4~8개) 메모리 뱅크로 배열됩니다. 각 뱅크에는 2의 거듭제곱 수의 행(일반적으로 16384)이 포함되고 각 행에는 2의 거듭제곱 비트 수(일반적으로 8192)가 포함됩니다. DRAM은 제어 프로세서에 다양한 타이밍 요구 사항을 부과합니다. 예를 들어 행을 활성화하려면 수십 사이클이 걸리지만 일단 활성화되면 행 내의 비트는 4클럭마다 새로운 열 주소에 무작위로 액세스할 수 있습니다. DDR(Double Data Rate) 동기 DRAM은 인터페이스 클록의 상승 및 하강 에지(아래 두 그림)에서 데이터를 전송하므로 1GHz 클록 DDR DRAM은 데이터 핀당 초당 2기가비트의 속도로 데이터를 전송합니다. 그래픽 DDR DRAM에는 일반적으로 32개의 양방향 데이터 핀이 있으므로 클록당 DRAM에서 8바이트를 읽거나 쓸 수 있습니다.

imgimg

싱글 탱크와 더블 랭크의 비교입니다.

imgimg

싱글레이트, 듀얼레이트, 쿼드레이트 비교표입니다.

GPU 내부에는 많은 메모리 트래픽 생성기가 있습니다. 논리적 그래픽 파이프라인의 다양한 단계에는 명령 및 버텍스 어트리뷰트 추출, 셰이더 텍스처 추출 및 로드/저장, 픽셀 깊이 및 색상 읽기 및 쓰기 등 고유한 요청 흐름이 있습니다. 각 논리 단계 내에는 일반적으로 병렬 처리량을 제공하는 여러 개의 독립 장치(모두 독립 메모리 요청자)가 있습니다. 메모리 시스템을 살펴보면 관련되지 않은 요청이 많이 실행되고 있으며 이는 DRAM이 선호하는 참조 패턴과 자연스럽게 일치하지 않습니다. 한 가지 솔루션은 GPU의 메모리 컨트롤러가 다양한 DRAM 뱅크에 대해 별도의 트래픽 힙을 유지하고 특정 DRAM 행에 충분한 트래픽이 있을 때까지 기다린 다음 해당 행을 활성화하고 모든 트래픽을 동시에 전송하는 것입니다. 보류 중인 요청을 누적하면 DRAM 행 위치를 파악하여 데이터 버스를 효율적으로 사용하는 데 도움이 되지만 다른 요청을 기다리는 요청자가 볼 수 있듯이 평균 대기 시간이 길어집니다. 특정 요청을 너무 오래 기다리지 않도록 설계에 주의해야 합니다. 그렇지 않으면 일부 처리 장치가 데이터를 기다리게 되어 결국 인접한 프로세서가 유휴 상태가 될 수 있습니다.

GPU 메모리 하위 시스템은 여러 개의 메모리 파티션으로 배열되어 있으며, 각 파티션에는 완전히 독립적인 메모리 컨트롤러와 해당 파티션이 완전히 독점적으로 소유하는 하나 또는 두 개의 DRAM 장치가 포함되어 있습니다. 최적의 로드 밸런싱을 달성하고 n 파티션의 이론적 성능에 근접하기 위해 주소는 일반적으로 몇 백 바이트의 블록 단위로 파티션 인터리빙 단계를 사용하여 모든 메모리 파티션에 균등하고 정밀하게 인터리브됩니다. 메모리 파티션 수는 프로세서 수와 기타 메모리 요청자 수의 균형을 맞추도록 설계되었습니다.

19.9.4.2 캐시

GPU 워크로드에는 일반적으로 단일 그래픽 프레임을 생성하기 위해 수백 메가바이트 정도의 매우 큰 작업 세트가 있습니다. CPU와 달리 그래픽 애플리케이션의 전체 작업 세트에 가까운 모든 것을 담을 수 있을 만큼 큰 칩에 캐시를 구축하는 것은 비현실적입니다. CPU는 매우 높은 캐시 적중률(99.9% 이상)을 가정할 수 있지만 GPU는 90%에 가까운 적중률을 가지므로 즉석에서 많은 누락을 처리해야 합니다. CPU는 드물게 발생하는 캐시 미스를 기다리는 동안 정지하도록 합리적으로 설계할 수 있지만 GPU는 혼합된 미스와 적중을 처리해야 합니다. 우리는 이를 스트리밍 캐시 아키텍처라고 부릅니다.

GPU 캐시는 클라이언트에 매우 높은 대역폭을 제공해야 합니다. 텍스처 캐시의 경우를 생각해 보면 일반적인 텍스처 단위는 클록 주기당 4개 픽셀 각각에 대해 두 개의 이중선형 보간을 수행할 수 있으며 GPU는 이러한 텍스처 단위를 많이 가질 수 있으며 모두 독립적으로 작동합니다. 각각의 쌍선형 보간에는 4개의 개별 텍셀이 필요하며, 각 텍셀은 64비트 값일 수 있고, 4개의 16비트 구성 요소가 일반적이므로 총 대역폭은 2×4×4×64=2048비트/클럭입니다. 각각의 개별 64비트 텍셀은 독립적으로 주소가 지정되므로 캐시는 클록당 32개의 고유 주소를 처리해야 합니다. 이는 자연스럽게 SRAM 어레이의 다중 뱅크 및/또는 다중 포트 배열을 용이하게 합니다.

19.9.4.3 MMU

최신 GPU는 가상 주소를 물리적 주소로 변환할 수 있습니다. GeForce 8800에서 모든 처리 장치는 40비트 가상 주소 공간에서 메모리 주소를 생성합니다. 계산을 위해 스레드 로드 및 저장 명령어는 32비트 바이트 주소를 사용하며, 이는 40비트 오프셋을 추가하여 40비트 가상 주소로 확장됩니다. 메모리 관리 장치는 가상-물리적 주소 변환을 수행합니다. 하드웨어는 프로세서와 렌더링 엔진 사이에 분산된 변환 참조 버퍼를 나타내는 계층 구조를 사용하여 누락에 응답하기 위해 로컬 메모리에서 페이지 테이블을 읽습니다. 물리적 페이지 비트 외에도 GPU 페이지 테이블 항목은 페이지 크기 범위가 4~128KB인 각 페이지의 압축 알고리즘도 지정합니다.

CUDA는 프로그래머가 성능을 최대화하는 방식으로 데이터 값을 저장할 수 있도록 다양한 메모리 공간을 노출합니다. 다음 그림은 CPU 및 GPU 메모리 요청 경로를 보여줍니다.

imgimg

GTT/GART는 통신을 위한 CPU-GPU 공유 버퍼로 사용됩니다.

다음 섹션의 논의는 NVIDIA Tesla 아키텍처 GPU를 기반으로 합니다.

19.9.4.4 전역 메모리

글로벌 메모리는 외부 DRAM에 저장되며 서로 다른 그리드에 있는 서로 다른 CTA(스레드 블록) 간의 통신에 사용되므로 하나의 물리적 스트리밍 멀티프로세서(SM)에 로컬이 아닙니다. 실제로 전역 메모리의 위치를 ​​참조하는 많은 CTA는 GPU에서 동시에 실행되지 않을 수 있으며 CUDA에서는 설계상 프로그래머가 CTA가 실행되는 상대적 순서를 알 수 없습니다. 주소 공간은 모든 메모리 파티션에 고르게 분산되므로 스트리밍 멀티프로세서에서 DRAM 파티션으로의 읽기/쓰기 경로가 있어야 합니다.

서로 다른 스레드(및 서로 다른 프로세서)에 의한 전역 메모리 액세스는 순차적 일관성이 보장되지 않습니다. 스레드 프로그램은 완화된(완화된) 메모리 순서 모델을 참조합니다. 스레드에서는 동일한 주소에 대한 메모리 읽기 및 쓰기 순서가 유지되지만 다른 주소에 대한 액세스 순서는 유지되지 않을 수 있습니다. 다른 스레드에서 요청한 메모리 읽기 및 쓰기가 순서가 잘못되었습니다. CTA에서는 장벽 동기화 명령 bar.sync를 사용하여 CTA의 스레드 간 엄격한 메모리 순서를 얻을 수 있습니다. membar 스레드 명령어는 이전 메모리 액세스를 커밋하고 계속하기 전에 다른 스레드에 표시되도록 하는 메모리 장벽/펜스 작업을 제공합니다. 스레드는 원자 메모리 작업을 사용하여 공유 메모리에 대한 작업을 조정할 수도 있습니다.

19.9.4.5 공유 메모리

CTA별 공유 메모리는 해당 CTA에 속한 스레드에만 표시되며, 공유 메모리는 CTA 생성부터 CTA 종료까지 저장 공간만 차지하므로 공유 메모리가 칩에 상주할 수 있습니다. 이 접근 방식에는 다음과 같은 이점이 있습니다.

  • 공유 메모리 트래픽은 글로벌 메모리 참조에 필요한 제한된 오프칩 대역폭과 경쟁할 필요가 없습니다.

  • 각 스트리밍 멀티프로세서의 읽기/쓰기 요구 사항을 지원하기 위해 칩에 매우 높은 대역폭의 메모리 구조를 구축하는 것이 가능합니다. 실제로 공유 메모리는 스트리밍 멀티프로세서와 긴밀하게 결합되어 있습니다.

각 스트리밍 다중 프로세서에는 8개의 물리적 스레드 프로세서가 포함되어 있습니다. 하나의 공유 메모리 클록 주기에서 각 스레드 프로세서는 두 스레드의 명령을 처리할 수 있으므로 각 클록은 16개 스레드의 공유 메모리 요청을 처리해야 합니다. 각 스레드는 자체 주소를 생성할 수 있고 주소는 일반적으로 고유하므로 공유 메모리는 독립적으로 주소를 지정할 수 있는 SRAM 뱅크 16개를 사용하여 구축됩니다. 일반적인 액세스 패턴의 경우 16개 뱅크이면 처리량을 유지하기에 충분하지만 16개 스레드 모두 정확히 하나의 SRAM 뱅크에서 서로 다른 주소에 액세스할 수 있는 등의 특수한 경우가 있을 수 있습니다. 모든 스레드 채널의 요청을 SRAM 뱅크로 라우팅할 수 있어야 하므로 16*16 상호 연결 네트워크가 필요합니다.

19.9.4.6 로컬 메모리

스레드별 로컬 메모리는 단일 스레드에만 표시되는 전용 메모리입니다. 로컬 메모리는 구조적으로 스레드의 레지스터 파일보다 크며 프로그램은 로컬 메모리에 대한 주소를 계산할 수 있습니다. 로컬 메모리의 대규모 할당을 지원하기 위해(총 할당은 스레드당 할당에 활성 스레드 수를 곱한 것임을 기억하세요) 로컬 메모리는 외부 DRAM에 할당됩니다. 글로벌 및 스레드별 로컬 메모리는 칩 외부에 있지만 칩 내 캐시에 이상적으로 적합합니다.

19.9.4.7 상수 메모리

상수 메모리는 SM에서 실행되는 프로그램에 대해 읽기 전용이며(명령을 통해 GPU에 쓸 수 있음) 외부 DRAM에 저장되고 SM에 캐시됩니다. 일반적으로 SIMT 워프의 대부분 또는 모든 스레드는 상수 메모리의 동일한 주소에서 읽기 때문에 클록당 단일 주소 조회로 충분합니다. 상수 캐시는 각 워프의 스레드에 스칼라 값을 브로드캐스트하도록 설계되었습니다.

19.9.4.8 텍스처 메모리

텍스처 메모리는 대규모 읽기 전용 데이터 배열을 보유하며, 계산에 사용되는 텍스처는 3D 그래픽에 사용되는 텍스처와 동일한 속성 및 기능을 갖습니다. 텍스처는 일반적으로 2차원 이미지(픽셀 값의 2D 배열)이지만 1D(선형) 및 3D(체적) 텍스처도 사용할 수 있습니다.

컴퓨팅 프로그램은 텍스처 이름을 지정하는 데 사용되는 식별자와 텍스처 크기에 따른 1개, 2개 또는 3개의 좌표를 피연산자에 포함하는 tex 명령어를 사용하여 텍스처를 참조합니다. 부동 소수점 좌표는 일반적으로 텍셀 위치 사이에서 지정된 샘플 위치의 소수 부분으로 구성됩니다. 정수가 아닌 좌표는 결과를 프로그램에 반환하기 전에 가장 가까운 4개 값(2D 텍스처의 경우)의 이중선형 가중치 보간을 호출합니다.

텍스처 가져오기는 수천 개의 동시 스레드에 대한 텍스처 가져오기 처리량을 최적화하도록 설계된 스트림 캐시 계층 구조에 캐시됩니다. 일부 프로그램은 전역 메모리를 캐싱하는 방법으로 텍스처 가져오기를 사용합니다.

19.9.4.9 표면

표면은 픽셀 값의 1차원, 2차원 또는 3차원 배열과 관련 형식에 대한 일반적인 용어로, 4개의 8비트 RGBA 정수 구성 요소 또는 4개의 16비트 부동 소수점 구성 요소와 같은 다양한 형식을 정의합니다. 프로그램 커널은 표면 유형을 알 필요가 없습니다. tex 명령어는 표면 형식에 따라 결과 값을 부동 소수점으로 다시 변환합니다.

19.9.4.10 로드/저장 액세스

정수 바이트 주소 지정이 포함된 로드/저장 명령어를 사용하면 C 및 C++와 같은 기존 언어로 프로그램을 작성하고 컴파일할 수 있습니다. CUDA 프로그램은 로드/저장 명령어를 사용하여 메모리에 액세스합니다.

메모리 대역폭을 늘리고 오버헤드를 줄이기 위해 로컬 및 전역 로드/저장 명령은 주소가 동일한 블록에 있고 정렬 기준이 충족될 때 동일한 워프의 개별 병렬 스레드 요청을 단일 메모리 블록 요청으로 결합합니다. 개별적인 작은 메모리 요청을 큰 데이터 블록 요청으로 통합하면 개별 요청의 성능이 크게 향상될 수 있으며, 많은 스레드 수는 뛰어난 로드 요청에 대한 지원과 결합되어 외부 DRAM에 구현된 로컬 및 글로벌 메모리의 로드 사용 대기 시간을 처리하는 데 도움이 됩니다.

19.9.4.11 ROP

NVIDIA Tesla 아키텍처 GPU에는 확장 가능한 SPA(스트림 처리 어레이)와 확장 가능한 메모리 시스템이 포함되어 있습니다. 확장 가능한 스트림 처리 배열은 GPU의 모든 프로그래밍 가능한 계산을 수행합니다. 확장 가능한 메모리 시스템에는 외부 DRAM 제어 기능과 메모리에서 직접 색상 및 깊이 프레임 버퍼 작업을 수행할 수 있는 고정 기능 ROP(래스터 작업 프로세서)가 포함되어 있습니다. 각 ROP 장치는 특정 메모리 파티션과 쌍을 이루고 ROP 파티션은 상호 연결 네트워크를 통해 SM에 의해 데이터로 채워집니다. 각 ROP는 깊이와 스텐실 테스트, 업데이트는 물론 색상 혼합도 담당합니다. ROP와 메모리 컨트롤러는 협력하여 무손실 색상 및 깊이 압축(최대 8:1)을 달성하여 외부 대역폭 요구 사항을 줄이고 ROP 장치는 메모리에서 원자적 작업도 수행합니다.

19.9.5 부동 소수점 연산

오늘날의 GPU는 IEEE 754 호환 단정밀도 32비트 부동 소수점을 사용하여 프로그래밍 가능 프로세서 코어에서 대부분의 산술 연산을 수행합니다. 초기 GPU의 고정 소수점 연산은 16비트, 24비트, 32비트 부동 소수점, 그리고 IEEE 754 호환 32비트 부동 소수점으로 이어졌습니다. 텍스처 필터링 하드웨어와 같은 GPU의 일부 고정 기능 논리는 독점 숫자 형식을 계속 사용하며 일부 GPU는 IEEE 754 호환 배정밀도 64비트 부동 소수점 명령어도 제공합니다.

19.9.5.1 지원 형식

IEEE 754 부동 소수점 산술 표준은 기본 형식과 저장 형식을 지정합니다. GPU는 일반적으로 단정밀도와 배정밀도로 알려진 32비트 및 64비트 이진 부동 소수점이라는 두 가지 기본 컴퓨팅 형식을 사용합니다. 표준은 또한 16비트 이진 저장소 부동 소수점 형식, 절반 정밀도를 지정합니다. GPU 및 Cg 셰이딩 언어는 좁은 16비트 절반 데이터 형식을 사용하여 높은 동적 범위를 유지하면서 효율적인 데이터 저장 및 이동을 가능하게 합니다. GPU는 텍스처 필터링 장치 및 래스터 작업 장치 내에서 절반 정밀도로 많은 텍스처 필터링 및 픽셀 혼합 계산을 수행합니다. Industrial Light and Magic[2003]에서 개발한 OpenEXR HDR 이미지 파일 형식은 컴퓨터 이미징 및 모션 그래픽 응용 프로그램에서 동일한 절반 형식 색상 구성 요소 값을 사용합니다.

반정밀도: 부호 비트 1개, 지수 비트 5개, 소수 10개 및 암시적 정수 비트를 포함하는 16비트 이진 부동 소수점 형식입니다.

19.9.5.2 기본 산술

GPU 프로그래밍 가능 코어의 일반적인 단정밀도 부동 소수점 연산에는 덧셈, 곱셈, 곱셈, 최소값, 최대값, 비교, 판단 설정 및 정수와 부동 소수점 숫자 간의 변환이 포함됩니다. 부동 소수점 명령어는 일반적으로 부정 및 절대값에 대한 소스 피연산자 수정자를 제공합니다.

다중 덧셈(MAD): 곱셈과 덧셈의 복합 연산을 수행하는 단일 부동 소수점 명령입니다.

오늘날 대부분의 GPU의 부동 소수점 덧셈 및 곱셈 연산은 NaN(Not-a-Number) 및 무한대 값을 포함한 단정밀도 FP 숫자에 대한 IEEE 754 표준을 준수합니다. FP 덧셈 및 곱셈 연산에서는 기본 반올림 모드와 마찬가지로 IEEE 반올림을 가장 가까운 값으로 사용합니다. 부동 소수점 명령어 처리량을 향상시키기 위해 GPU는 종종 복합 곱셈-누산 명령어(mad)를 사용합니다. 여기서 미친 연산은 잘림을 사용하여 FP 곱셈을 수행한 다음 가장 가까운 짝수로 반올림하여 FP 덧셈을 수행합니다. 이는 명령어 스케줄러가 두 개의 별도 명령어를 예약할 필요 없이 하나의 실행 주기에 두 개의 부동 소수점 연산을 제공하지만, 계산이 융합되지 않고 추가 전에 제품이 잘려서 나중에 설명하는 융합된 곱셈-덧셈 명령어와 다릅니다. GPU는 일반적으로 비정규화된 소스 피연산자를 부호 보존 0으로 플러시하고, 대상 출력 지수 범위 언더플로의 결과를 반올림 후 부호 보존 0으로 플러시합니다.

19.9.5.3 특수 산술

GPU는 특수 함수 계산, 속성 보간 및 텍스처 필터링을 가속화하는 하드웨어를 제공합니다. 특수 함수 명령에는 코사인, 사인, 이진 지수, 이진 로그, 역수 및 제곱근 역수가 포함됩니다. 속성 보간 명령은 평면 방정식의 평가에서 파생된 픽셀 속성을 효율적으로 생성합니다. 앞서 소개한 SFU(Special Function Unit)는 특수 기능을 계산하고 평면 속성을 보간합니다.

특수 기능 유닛(SFU): 특수 기능 및 보간 평면 속성을 계산하는 하드웨어 유닛입니다.

하드웨어에서 특수 기능을 수행하는 방법에는 여러 가지가 있습니다. 향상된 Minimax 근사를 기반으로 한 2차 보간법은 역수, 역수 제곱근, log2x, 2x, sin 및 cos를 포함한 하드웨어 기능을 근사화하는 매우 효과적인 방법인 것으로 나타났습니다.

SFU 2차 보간 방법을 요약할 수 있습니다. n 유효 비트가 있는 이진 입력 피연산자 X의 경우 유효 비트는 두 부분으로 나뉩니다. Xu는 m 비트를 포함하는 위쪽 부분이고 Xl은 n-m 비트를 포함하는 아래쪽 부분입니다. 상위 m 비트 Xu는 세 개의 유한 필드 계수 C0, C1 및 C2를 반환하기 위해 세 개의 조회 테이블 세트를 쿼리하는 데 사용됩니다. 근사화할 각 함수에는 다음 표현식을 평가하여 X_u ≤ X &lt; X_u+2^{−m} 범위에서 주어진 함수 f(X)를 근사화하기 위한 고유한 계수 세트가 필요합니다.

f(X)=C0+C1X1+C2X12

각 함수 계산의 정확도 범위는 유효 숫자 22~24개이며 샘플 함수 통계는 아래 그림에 나와 있습니다.

IEEE 754 표준은 나눗셈 및 제곱근에 대한 정확한 반올림 요구 사항을 지정하지만 많은 GPU 응용 프로그램의 경우 엄격한 준수가 필요하지 않으며 대신 마지막 정밀도보다 더 높은 계산 처리량이 더 중요합니다. SFU 특수 기능의 경우 CUDA 수학 라이브러리는 SFU 명령 정밀도를 갖춘 완전 정밀도 기능과 빠른 기능을 제공합니다.

GPU의 또 다른 특수 산술 연산은 속성 보간입니다. 이는 일반적으로 렌더링되는 장면의 프리미티브를 구성하는 정점에 대한 색상, 깊이 및 텍스처 좌표와 같은 키포인트 속성을 지정합니다. 이러한 속성은 각 픽셀 위치의 속성 값을 결정하는 데 필요에 따라 (x, y) 화면 공간 내에서 보간되어야 합니다. (x, y) 평면에서 주어진 속성 U의 값은 다음 형식의 평면 방정식을 사용하여 표현될 수 있습니다.

U(x,y)=Aux+BuY+Cu

여기서 A, B, C는 각 속성 U와 연관된 보간 매개변수이고, 보간 매개변수 A, B, C는 모두 단정밀도 부동 소수점 숫자로 표현됩니다.

픽셀 쉐이더 프로세서에는 함수 평가기와 속성 보간기가 모두 필요하다는 점을 고려하면 이 두 기능을 실행하는 SFU를 설계하면 효율성이 향상됩니다. 두 함수 모두 곱셈 연산을 사용하여 결과를 보간하며, 합산되는 항의 수는 두 함수 모두 매우 유사합니다.

텍스처 매핑 및 필터링은 GPU의 특수 부동 소수점 산술 연산의 또 다른 중요한 세트입니다. 텍스처 매핑에 사용되는 작업은 다음과 같습니다.

  1. 현재 화면 픽셀(x, y)의 텍스처 주소(s, t)를 수신합니다. 여기서 s와 t는 단정밀도 부동 소수점 숫자입니다.

  2. 정확한 텍스처 MIP맵 수준을 식별하기 위해 세부 수준을 계산합니다.

  3. 삼선형 보간 점수를 계산합니다.

  4. 선택한 MIP 매핑 수준의 텍스처 주소(s, t)를 조정합니다.

  5. 메모리에 액세스하고 원하는 텍셀(텍스처 요소)을 검색합니다.

  6. 텍셀에 대한 필터링 작업을 수행합니다.

MIP 맵: 렌더링 속도를 높이고 아티팩트를 줄이기 위해 다양한 해상도의 미리 계산된 이미지를 포함합니다.

텍스처 매핑에는 최대 속도 작업을 위한 많은 부동 소수점 계산이 필요하며 대부분은 16비트 반 정밀도로 수행됩니다. 기존의 IEEE 단정밀도 부동 소수점 명령 외에도 GeForce 8800 Ultra는 텍스처 매핑 명령을 위한 약 500GFLOPS의 독점 형식 부동 소수점 계산도 제공합니다.

부동 소수점 덧셈 및 곱셈 하드웨어는 완전히 파이프라인화되었으며 대기 시간은 대기 시간과 영역의 균형을 맞추도록 최적화되었습니다. 파이프라인 방식이지만 특수 함수의 처리량은 부동 소수점 덧셈 및 곱셈 연산보다 적습니다. 특수 기능의 1/4 속도 처리량은 최신 GPU의 일반적인 성능입니다. 하나의 SFU는 4개의 SP 코어에 의해 공유됩니다. 이에 비해 CPU는 일반적으로 나눗셈 및 제곱근과 같은 유사한 기능에 대해 처리량이 훨씬 낮지만 결과는 더 정확합니다. 속성 보간 하드웨어는 일반적으로 전체 속도 픽셀 셰이더를 활성화하도록 완전히 파이프라인됩니다.

19.9.5.4 더블

Tesla T10P와 같은 GPU는 하드웨어에서 IEEE 754 64비트 배정밀도 작업도 지원합니다. 배정밀도 표준 부동 소수점 산술 연산에는 덧셈, 곱셈, 다양한 부동 소수점과 정수 형식 간의 변환이 포함됩니다. 2008 IEEE 754 부동 소수점 표준에는 FMA(fused-multiply-add) 연산에 대한 사양이 포함되어 있습니다. FMA 연산은 부동 소수점 곱셈, 덧셈, 반올림을 수행합니다. 융합된 곱셈과 덧셈 연산은 중간 계산에서 완전한 정확성을 유지합니다. 이 동작을 통해 내적, 행렬 곱셈 및 다항식 계산을 포함한 곱의 누적을 포함하여 보다 정확한 부동 소수점 계산이 가능해집니다. FMA 명령을 사용하면 하드웨어 나누기나 제곱근 단위 없이도 정확한 반올림 나누기 및 제곱근의 효율적인 소프트웨어 구현이 가능합니다.

배정밀도 하드웨어 FMA 장치는 64비트 덧셈, 곱셈, 변환 및 FMA 연산 자체를 구현합니다. 배정밀도 FMA 장치의 아키텍처는 입력 및 출력에 대한 전속 비정규화 숫자 지원을 가능하게 합니다. 아래 그림은 FMA 장치의 구조를 보여줍니다.

배정밀도 FMA(퓨즈 곱셈 누산) 장치, 하드웨어는 배정밀도 부동 소수점 A×B+C를 구현합니다.

위 그림에서 볼 수 있듯이 A와 B의 유효 비트를 곱하여 106비트 곱을 형성하고 그 결과가 캐리 형식으로 보존됩니다. 병렬로 53비트 가수 C는 조건부로 반전되어 106비트 곱과 정렬됩니다. 106비트 곱의 합계 및 캐리 결과는 161비트 폭의 캐리 저장 가산기(CSA)를 통해 정렬된 가수와 함께 추가됩니다. 그런 다음 캐리 저장 출력은 캐리 전파 가산기에 추가되어 반올림되지 않은 결과의 중복되지 않는 2의 보수 형식을 생성합니다. 결과는 조건부로 다시 계산되어 결과가 부호 크기 형식으로 반환되고, 2의 보수 결과가 정규화된 다음, 대상 형식에 맞게 반올림됩니다.

19.9.6 프로그래밍 가능 GPU

멀티프로세서 GPU 프로그래밍은 멀티코어 CPU와 같은 다른 멀티프로세서 프로그래밍과 근본적으로 다릅니다. GPU는 CPU보다 2~3배 더 많은 스레드 및 데이터 병렬성을 제공하며 수백 개의 프로세서 코어와 수만 개의 동시 스레드로 확장 가능합니다. GPU는 병렬성을 지속적으로 증가시켜 약 12~18개월마다 두 배로 증가합니다. 이는 무어의 법칙이 집적 회로 밀도를 높이고 아키텍처 효율성을 높이는 결과입니다. 다양한 시장 부문에 걸쳐 광범위한 가격과 성능을 제공하기 위해 다양한 GPU 제품은 서로 다른 수의 프로세서와 스레드를 구현합니다. 그러나 사용자는 실행하는 병렬 스레드 수나 병렬 프로세서 코어 수에 관계없이 모든 GPU에서 게임, 그래픽, 이미징 및 컴퓨팅 응용 프로그램이 실행되기를 원하며, 응용 프로그램을 더 빠르게 실행하기 위해 더 비싼 GPU(더 많은 스레드와 코어 포함)를 원합니다. 따라서 GPU 프로그래밍 모델과 애플리케이션은 광범위한 병렬 처리로 투명하게 확장되도록 설계되었습니다.

GPU의 수많은 병렬 스레드와 코어 뒤에 숨은 원동력은 실시간 그래픽 성능입니다. 즉, 초당 최소 60프레임의 대화형 프레임 속도로 복잡한 3D 장면을 고해상도로 렌더링해야 합니다. 따라서 그래픽 셰이딩 언어(예: Cg, HLSL, GLSL)의 확장 가능한 프로그래밍 모델은 많은 독립적인 병렬 스레드를 통해 대규모 병렬 처리를 활용하도록 설계되었으며 원하는 수의 프로세서 코어로 확장될 수 있습니다. 마찬가지로 CUDA 확장 가능한 병렬 프로그래밍 모델을 사용하면 범용 병렬 컴퓨팅 응용 프로그램이 많은 수의 병렬 스레드를 활용하고 응용 프로그램에 투명하게 임의 개수의 병렬 프로세서 코어로 확장할 수 있습니다.

이러한 확장 가능한 프로그래밍 모델에서 프로그래머는 단일 스레드에 대한 코드를 작성하고 GPU는 수많은 스레드 인스턴스를 병렬로 실행하므로 프로그램은 광범위한 하드웨어 병렬 처리에 걸쳐 투명하게 확장될 수 있습니다. 꼭짓점이나 픽셀을 음영 처리하는 방법을 설명하는 그래픽 API와 음영 언어에서 파생된 이 간단한 패러다임은 GPU가 병렬성과 성능을 급속히 향상시키면서 1990년대 후반부터 유효한 패러다임이었습니다.

이 섹션에서는 그래픽 API와 프로그래밍 언어를 사용하여 실시간 그래픽 애플리케이션용 GPU를 프로그래밍하는 방법을 간략하게 소개한 다음, C 언어와 CUDA 프로그래밍 모델을 사용하여 비주얼 컴퓨팅 및 일반 병렬 컴퓨팅 애플리케이션용 GPU를 프로그래밍하는 방법을 소개합니다.

API는 GPU 및 프로세서의 빠르고 성공적인 개발에 중요한 역할을 합니다. 두 가지 주요 표준 그래픽 API가 있습니다: OpenGL과 Direct3D. OpenGL은 원래 Silicon Graphics Incorporated에서 제안하고 정의한 개방형 표준입니다. OpenGL 표준의 지속적인 개발 및 확장은 업계 협회인 Khronos에서 관리합니다. Direct3D는 Microsoft와 파트너가 정의하고 개발한 사실상의 표준입니다. OpenGL과 Direct3D는 유사한 구조를 가지고 있으며 GPU 하드웨어의 발전과 함께 계속해서 빠르게 발전하고 있습니다. 이는 GPU 하드웨어 및 프로세서에 매핑된 논리적 그래픽 처리 파이프라인은 물론 프로그래밍 가능한 파이프라인 단계를 위한 프로그래밍 모델 및 언어를 정의합니다.

다음 그림은 Direct3D 10 논리 그래픽 파이프라인을 보여줍니다. OpenGL은 비슷한 그래픽 파이프라인 구조를 가지고 있습니다. API 및 논리 파이프라인은 파란색으로 표시된 프로그래밍 가능한 셰이더 단계를 위한 스트리밍 데이터 흐름 인프라와 파이프라인을 제공합니다. 3D 애플리케이션은 기하학적 기본 요소인 점, 선, 삼각형 및 다각형으로 그룹화된 꼭지점 시퀀스를 GPU로 보내고 입력 어셈블리 프로그램은 꼭지점과 기본 요소를 수집합니다. 꼭지점 셰이더 프로그램은 꼭지점 3D 위치를 화면 위치로 변환하고 꼭지점에 조명을 공급하여 색상을 결정하는 등 꼭지점별 처리를 수행합니다. 기하 셰이더 프로그램은 기본형별 처리를 수행하고 기본형을 추가하거나 제거할 수 있습니다. 설정 및 래스터라이제이션 유닛은 기하 기본 요소로 덮힌 픽셀 조각을 생성합니다(조각은 픽셀에 대한 잠재적인 기여입니다).

픽셀 셰이더 프로그램은 조각별 매개변수 보간, 텍스처링 및 음영 처리를 포함하여 조각별 처리를 수행합니다. 픽셀 셰이더는 보간된 부동 소수점 좌표를 사용하여 대규모 1D, 2D 또는 3D 배열(텍스처라고 함)에 대한 샘플링 및 필터링 조회를 광범위하게 사용합니다. 셰이더는 맵, 함수, 데칼, 이미지 및 데이터의 텍스처 액세스를 사용합니다. 래스터 작업 처리(또는 출력 비닝) 단계에서는 숨겨진 픽셀 조각을 삭제하거나 픽셀 깊이를 조각 깊이로 대체할 수 있는 Z 버퍼 깊이 테스트 및 스텐실 테스트를 수행하고 조각 색상을 픽셀 색상과 결합하고 혼합된 색상으로 픽셀을 쓰는 색상 혼합 작업을 수행합니다.

그래픽 API 및 그래픽 파이프라인은 각 버텍스, 기본 요소 및 픽셀 조각을 처리하는 셰이더 프로그램을 위한 입력, 출력, 메모리 개체 및 인프라를 제공합니다.

19.9.6.1 병렬 컴퓨팅 애플리케이션 프로그래밍

그래픽 처리 모델은 실제로 멀티스레딩, 멀티프로그래밍 및 SIMD 실행의 조합이며 NVIDIA는 해당 모델을 SIMT(Single Instruction, Multi-Threading)라고 부릅니다. NVIDIA의 SIMT 실행 모델을 살펴보겠습니다.

프로그래머는 먼저 CUDA 프로그래밍 언어로 코드를 작성합니다. CUDA는 Compute Unified Device Architecture의 약자이며, CPU의 ISA(CPU용) 및 PTX 명령어 세트(GPU용)에서 코드를 생성하기 위해 NVIDIA의 nvcc 컴파일러로 컴파일된 C/C++에 대한 사용자 정의 확장입니다. CUDA 프로그램은 GPU에서 실행되는 커널 세트와 호스트 CPU에서 실행되는 함수 세트로 구성됩니다. 호스트 CPU의 함수는 GPU와 데이터를 주고받고, 변수를 초기화하고, GPU에서 병렬로 실행되는 함수로 정의되는 GPU에서의 커널 실행을 조정합니다. 그래픽 하드웨어는 각각 별도의 스레드에서 실행되는 각 CUDA 코어의 여러 복사본을 생성합니다.

GPU는 이러한 각 스레드를 SP 코어에 매핑합니다. 단일 CUDA 코어에 대해 수백 개의 스레드를 원활하게 생성하고 실행할 수 있습니다. 어떤 사람들은 동일한 코드가 있으면 여러 복사본을 실행하는 것이 무슨 의미가 있다고 생각할 수도 있습니다. 대답은 코드가 정확히 동일하지 않다는 것입니다. 코드는 암시적으로 스레드 ID를 입력으로 사용합니다. 예를 들어 각 CUDA 커널에 대해 100개의 스레드를 생성하면 각 스레드는 집합 [0...99]에서 고유 ID를 가지며 CUDA 커널의 코드는 스레드 ID를 기반으로 적절한 처리를 수행합니다. 여러 개별 애플리케이션의 스레드는 스레드를 예약하고 실행을 조정하는 각 SM의 MT 게시 논리를 통해 동시에 실행될 수 있습니다. 이 아키텍처의 SM은 최대 768개의 스레드를 처리할 수 있습니다.

여러 애플리케이션을 병렬로 실행하면 GPU 전체가 수천 개의 스레드를 예약해야 하며 예약 오버헤드가 너무 높아집니다. 따라서 스케줄링 작업을 단순화하기 위해 GeForce 8800 GPU는 32개의 스레드 세트를 하나의 워프로 결합합니다. 각 SM은 24개의 워프를 관리할 수 있습니다. 워프는 스레드의 원자 단위입니다. 워프의 모든 스레드가 예약되거나 워프의 스레드가 예약되지 않습니다. 게다가 워프의 모든 스레드는 동일한 코어에 속하며 정확히 동일한 주소에서 시작됩니다. 그러나 시작된 후에는 다른 프로그램 카운터가 있을 수 있습니다.

각 SM은 워프 스레드를 SP 코어에 매핑하고 여러 데이터 스트림에서 명령을 실행한 후 다음 명령으로 이동하는 기존 SIMD 실행과 유사하게 명령별로 워프 명령을 실행합니다. SM은 워프의 각 스레드에 대해 하나의 명령을 실행하고 모든 스레드가 명령을 완료한 후 다음 명령을 실행합니다. 커널에 데이터 또는 스레드 종속 분기가 있는 경우 SM은 올바른 분기 경로에 명령이 있는 스레드에 대해서만 명령을 실행합니다. GeForce GPU는 예측 명령을 사용합니다. 잘못된 경로에 대한 명령의 경우 판단 조건이 거짓이므로 이러한 명령은 nop 명령으로 동적으로 대체됩니다. 분기 경로(실행되고 실행되지 않음)가 다시 병합되면 워프의 모든 스레드가 다시 활성화됩니다. SIMD 모델과의 주요 차이점은 SIMD 프로세서에서는 동일한 스레드가 동일한 명령어의 여러 데이터 스트림을 처리한다는 것입니다. 그러나 이 경우 동일한 명령이 여러 스레드에서 실행되며 각각은 서로 다른 데이터 스트림에서 작동합니다. MT 실행부는 워프 내의 명령어를 실행한 후 동일한 워프, 동일한 애플리케이션에서 다른 워프, 또는 다른 애플리케이션에서 워프를 스케줄링할 수 있다. GPU는 기본적으로 워프 수준의 세분화된 멀티스레딩을 구현하며, 그 예가 아래 그림에 나와 있습니다.

워프 일정.

32스레드 실행의 경우 SM은 일반적으로 4사이클을 사용합니다. 첫 번째 주기에서는 8개의 SP 코어 각각에 8개의 스레드를 발급합니다. 두 번째 주기에서는 SFU에 8개의 스레드를 더 발행합니다. 두 SFU에는 각각 4개의 기능 단위가 있으므로 구조적 충돌 없이 8개의 명령을 병렬로 처리할 수 있습니다. 세 번째 주기에는 8개의 스레드가 SP 코어에 추가로 전송되고, 마지막으로 네 번째 주기에는 8개의 스레드가 2개의 SFU 코어에 전송됩니다. SFU 코어와 SP 코어 사용 간을 전환하는 이 전략은 두 장치가 모두 사용 상태를 유지하도록 보장합니다. 워프는 원자 단위이므로 SM 간에 분할될 수 없으며 워프의 다음 명령어가 실행되기 전에 워프의 각 명령어가 모든 활성 스레드에서 실행되어야 합니다. 개념적으로 워프의 개념은 동일한 애플리케이션의 여러 워프가 독립적으로 실행될 수 있는 32채널 폭의 SIMD 머신과 동일시할 수 있습니다. 워프 간 동기화를 위해서는 글로벌 메모리나 최신 GPU에서 사용할 수 있는 복잡한 동기화 프리미티브를 사용해야 합니다.

CUDA, Brook 및 CAL은 그래픽보다는 데이터 병렬 컴퓨팅에 중점을 둔 GPU용 프로그래밍 인터페이스입니다. CAL(Computational Abstraction Layer)은 AMD GPU를 위한 저수준 어셈블리 언어 인터페이스이고, Brook은 Buck 등의 GPU에 적합한 스트리밍 언어이며, NVIDIA가 개발한 CUDA는 멀티 코어 GPU 및 멀티 코어 CPU의 확장 가능한 병렬 프로그래밍을 위한 C 및 C++ 언어의 확장입니다.

새로운 모델을 통해 GPU는 고성능 컴퓨팅 애플리케이션과 그래픽 애플리케이션을 실행하기 위한 데이터 병렬성과 처리량 컴퓨팅 측면에서 탁월합니다.

대규모 계산 문제를 고도의 병렬 처리 아키텍처에 효과적으로 매핑하기 위해 프로그래머나 컴파일러는 문제를 병렬로 해결할 수 있는 여러 개의 작은 문제로 나눕니다. 예를 들어, 프로그래머는 대규모 결과 데이터 배열을 청크로 나누고 각 청크를 요소로 더 나누어 결과 청크를 독립적으로 병렬로 계산하고 각 청크 내의 요소를 병렬로 계산할 수 있습니다. 아래 이미지는 3×2 블록 그리드로 분할된 결과 데이터 배열을 보여줍니다. 여기서 각 블록은 5×3 요소 배열로 추가로 분할됩니다. 2단계 병렬 분해는 자연스럽게 GPU 아키텍처에 매핑됩니다. 즉, 병렬 멀티프로세서는 결과 블록을 계산하고 병렬 스레드는 결과 요소를 계산합니다.

결과 데이터를 요소 블록의 그리드로 나누어 병렬로 계산합니다.

프로그래머는 일련의 결과 데이터 그리드를 계산하는 프로그램을 작성하고 각 결과 그리드를 병렬로 독립적으로 계산할 수 있는 대략적인 결과 블록으로 나눕니다. 프로그램은 세분화된 병렬 스레드 배열을 사용하여 각 결과 블록을 계산하고, 각 스레드가 하나 이상의 결과 요소를 계산하도록 스레드 간에 작업을 나눕니다.

19.9.6.2 CUDA 프로그래밍

CUDA 확장 가능한 병렬 프로그래밍 모델은 C 및 C++ 언어를 확장하여 고도의 병렬 멀티프로세서(특히 GPU)에서 범용 애플리케이션을 위한 대규모 병렬 처리를 활용합니다. 초기 경험을 통해 많은 복잡한 프로그램이 몇 가지 잘 이해된 추상화로 표현될 수 있음이 나타났습니다. NVIDIA가 2007년에 CUDA를 출시한 이후 개발자들은 지진 데이터 처리, 계산 화학, 선형 대수학, 희소 행렬 솔버, 정렬, 검색, 물리적 모델링 및 시각화 컴퓨팅을 포함한 광범위한 응용 프로그램을 위한 확장 가능한 병렬 프로그램을 빠르게 개발했습니다. 이러한 애플리케이션은 수백 개의 프로세서 코어와 수천 개의 동시 스레드로 투명하게 확장될 수 있습니다. Tesla의 통합 그래픽 및 컴퓨팅 아키텍처를 갖춘 NVIDIA GPU는 CUDA C 프로그램을 실행하며 노트북, PC, 워크스테이션 및 서버에 널리 사용됩니다. CUDA 모델은 멀티 코어 CPU를 포함한 다른 공유 메모리 병렬 처리 아키텍처에도 적용 가능합니다.

CUDA는 계층 구조의 한 스레드에 대해 기존 C 코드에 대한 명확한 병렬 구조를 제공하는 스레드 그룹 계층 구조, 공유 메모리 및 장벽 동기화라는 세 가지 주요 추상화를 제공합니다. 다단계 스레딩, 메모리 및 동기화는 세분화된 데이터 병렬 처리 및 작업 병렬 처리 내에 중첩된 세분화된 데이터 병렬 처리와 스레드 병렬 처리를 제공하며, 추상화는 프로그래머가 문제를 병렬로 독립적으로 해결할 수 있는 대략적인 하위 문제로 분할한 다음 병렬로 해결할 수 있는 더 미세한 부분으로 분할하도록 안내합니다. 프로그래밍 모델은 다수의 프로세서 코어로 투명하게 확장됩니다. 컴파일된 CUDA 프로그램은 원하는 수의 프로세서에서 실행될 수 있으며 런타임 시스템만이 물리적 프로세서의 수를 알아야 합니다.

CUDA는 프로그래머가 간단한 함수 또는 완전한 프로그램일 수 있는 병렬 커널을 호출하는 직렬 프로그램을 작성할 수 있도록 하는 C 및 C++ 프로그래밍 언어의 최소 확장입니다. 커널은 프로그래머가 스레드 블록 계층 구조와 스레드 블록 그리드로 구성한 병렬 스레드 집합에서 병렬로 실행됩니다. 스레드 블록은 장벽 동기화 및 블록 전용 메모리 공간에 대한 공유 액세스를 통해 서로 협력할 수 있는 동시 스레드 그룹입니다. 그리드는 스레드 블록의 집합으로, 각 블록은 독립적으로 실행될 수 있으므로 병렬로 실행됩니다.

커널: 여러 스레드에 의해 실행되도록 설계된 스레드 프로그램 또는 함수입니다.

스레드 블록: 동일한 스레드 프로그램을 실행하고 협업하여 결과를 계산할 수 있는 동시 스레드 그룹입니다.

그리드: 동일한 커널 프로그램을 실행하는 스레드 블록 그룹입니다.

스레드, 블록, 그리드 간의 관계.

CUDA 용어와 GPU 하드웨어 구성 요소 간의 동등한 매핑은 다음과 같습니다.

CUDA 용어 정의 동등한 GPU 하드웨어 구성 요소

커널 GPU에서 실행되는 기능적 병렬 코드 해당 없음

스레드 GPU의 커널 예 GPU/CUDA 프로세서 코어

차단 특정 SM에 할당된 스레드 집합 CUDA 멀티프로세서(SM)

그리드 GPU GPU

CUDA 프로그램은 자연스럽게 GPU의 구조에 매핑됩니다. 먼저 런타임에 할당된 스레드 ID를 기반으로 일련의 작업을 수행하는 커널을 CUDA에 작성합니다. 커널의 동적 인스턴스는 스레드입니다(CPU 컨텍스트의 스레드와 유사). 스레드 그룹을 블록 또는 CTA(협력 스레드 배열)로 그룹화합니다. 블록 또는 CTA는 워프에 해당합니다. 블록에는 1~512개의 스레드가 있을 수 있으며 각 SM은 언제든지 최대 8개 블록의 상태를 버퍼링할 수 있습니다. 청크의 각 스레드에는 고유한 스레드 ID가 있습니다. 마찬가지로 청크는 애플리케이션의 모든 스레드를 포함하는 그리드로 그룹화됩니다. 어떤 형태의 동기화를 명시적으로 구현하지 않는 한 서로 다른 청크(또는 워프)는 서로 독립적으로 실행될 수 있습니다. 간단한 예에서 블록은 스레드의 선형 배열로, 그리드는 블록의 선형 배열로 생각하십시오. 또한 블록은 스레드의 2D 또는 3D 배열로 정의하거나 그리드는 블록의 2D 또는 3D 배열로 정의할 수 있습니다.

이제 두 개의 n 요소 배열을 추가하는 작은 CUDA 프로그램을 살펴보면서 CUDA 스케줄링을 부분적으로 고려해 보겠습니다. 아래 코드 조각에서는 세 개의 배열 a, b 및 c가 초기화되고 a 및 b 요소를 추가하고 결과를 c에 저장하려고 합니다.

cpp
#define N 1024

void main() 
{
    // 선언 배열
    int a[N], b[N], c[N];

    // GPU 내에서 선언 의 배열
    int size = N * sizeof(int);
    int *gpu_a, *gpu_b, *gpu_c;

    // 로 GPU 내에서 의 배열 할당(Allocate)
    cudaMalloc((void**) &gpu_a, size);
    cudaMalloc((void**) &gpu_b, size);
    cudaMalloc((void**) &gpu_c, size);

    // 초기화(Initialize)배열
    (...)

    // 배열 까지 GPU
    cudaMemcpy (gpu_a, a, size, cudaMemcpyHostToDevice);
    cudaMemcpy (gpu_b, b, size, cudaMemcpyHostToDevice);
}

이 코드 조각에서는 N 요소를 포함하는 세 개의 배열(a, b, c)이 선언되고 GPU의 해당 저장 위치가 이후에 정의됩니다. 그런 다음 cudaMalloc 호출을 사용하여 GPU에 공간을 할당합니다. 다음으로, 배열 a와 b는 값(코드는 표시되지 않음)으로 초기화된 다음 이러한 배열은 호스트가 CPU이고 장치가 GPU인 cudaMemcpyHostToDevice라는 플래그를 사용하는 CUDA 함수 cudaMemcpy를 사용하여 GPU(gpu_a 및 gpu_b)의 해당 위치에 복사됩니다.

다음 작업은 GPU에 gpu_a 및 gpu_b 벡터를 추가하는 것입니다. 이렇게 하려면 벡터를 추가하는 vectorAdd 함수를 작성해야 합니다. 이 함수는 2개의 입력 벡터와 1개의 출력 벡터로 구성된 3개의 매개변수를 취해야 합니다. 이 함수를 호출하는 코드는 아래와 같습니다.

cpp
vectorAdd <<< N/32, 32 >>> (gpu_a, gpu_b, gpu_c);

gpu_a, gpu_b 및 gpu_c의 세 가지 매개변수를 사용하여 vectorAdd 함수를 호출합니다. <<< N/32, 32>>> 표현식은 GPU에 N=32개의 블록이 있고 각 블록에는 32개의 스레드가 포함되어 있음을 알려줍니다. GPU가 마술처럼 두 배열을 추가하고 결과를 물리적 메모리 공간의 gpu_c 배열에 저장한다고 가정합니다. main 함수의 마지막 단계는 GPU에서 결과를 얻어 GPU의 공간을 해제하는 것이며, 코드는 다음과 같습니다.

cpp
/* Copy from the GPU to the CPU */
cudaMemcpy(c, gpu_c, size, cudaMemcpyDeviceToHost);

/* free space in the GPU */
cudaFree(gpu_a);
cudaFree(gpu_b);
cudaFree(gpu_c);

/* end of the main function */

이제 GPU에서 실행되어야 하는 vectorAdd 함수를 정의해 보겠습니다.

cpp
/* The GPU kernel */
__global__ void vectorAdd( int *gpu a, int *gpu b, int *gpu c)
{
    /* compute the index */
    int idx = threadIdx.x + blockIdx.x * blockDim.x;

    /* perform the addition */
    gpu_c[idx] = gpu_a[idx] + gpu_b[idx];
}

위 코드에서는 CUDA 런타임에 의해 채워지는 일부 내장 변수에 액세스합니다. 일반적으로 그리드와 블록에는 세 개의 축(x, y, z)이 있습니다. 이 예에서는 블록과 그리드에서 하나의 축만 가정하므로 x축만 사용합니다. blockDim.x 변수는 블록의 스레드 수와 같습니다. 2D 그리드를 고려하면 블록 크기는 blockDim.xblockDim.y이고, blockIdx.x는 블록의 인덱스이고 threadIdx.x는 블록에 있는 스레드의 인덱스이므로 threadIdx.x+blockIdx.x blockDim.x 표현식은 스레드의 인덱스를 나타냅니다. 이 예에서 배열의 각 요소는 스레드와 연결되어 있습니다. 스레드를 생성하고 초기화하고 전환하는 데 드는 오버헤드가 작기 때문에 GPU의 경우 이 접근 방식을 채택할 수 있지만 CPU가 스레드를 생성하고 관리하는 데 오버헤드가 높으면 적합하지 않습니다. 스레드의 인덱스가 계산되면 추가 작업이 수행됩니다.

GPU는 이 커널의 N 복사본을 생성하여 N 스레드에 배포합니다. 각 코어는 서로 다른 인덱스를 계산한 후 추가 작업을 수행합니다. 그러나 C/C++로 확장된 CUDA를 사용하면 동기화된 명령문과 조건 분기 명령문을 포함하는 매우 복잡한 프로그램을 작성할 수 있습니다.

병렬 프로그래밍의 또 다른 간단한 예를 들어보겠습니다. n개의 부동 소수점 숫자로 구성된 두 개의 벡터 x와 y를 얻고 특정 스칼라 값 a에 대한 y=ax+y의 결과를 계산한다고 가정합니다. 이는 BLAS 선형 대수 라이브러리에 의해 정의된 소위 SAXPY 커널입니다. CUDA를 사용하여 직렬 및 병렬 프로세서에서 이 계산을 수행하는 C 코드는 아래와 같습니다.

cpp
// 을(를) 활용하여 직렬 루프 계산/산출(Calculate)y=ax+y
void saxpy_serial( int n, float alpha, float * x, float ) 
{
    for( int i=0; i<n; ++i) 
        y[i] = alpha * x[i] + y[i];
}
// 호출(Call)직렬 SAXPY  
saxpy_serial(n, 2.0, x, y); 

// 을(를) 활용하여 CUDA병렬 계산/산출(Calculate)y=ax+y
__global__
void saxpy_parallel( int n, float alpha, float *x, float *y) 
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if(i < n) 
        y[i] = alpha * x[i] + y[i];
}

// Invoke parallel SAXPY kernel (256 threads per block) 
int nblocks = (n + 255) / 256;
saxpy_parallel<<< nblocks, 256>>> ( n, 2.0, x, y);

global 선언 지정자는 프로세스가 커널 진입점이고 CUDA 프로그램이 확장 함수 호출 구문을 사용하여 병렬 커널을 시작함을 나타냅니다.

cpp
kernel<<<dimGrid, dimBlock>>>(… parameter list …);

그 중, DimGrid와 DimBlock은 Dim3 유형의 세 가지 요소 벡터로, 각각 블록 내 그리드 크기와 스레드 내 블록 크기를 지정합니다. 지정되지 않은 크기의 기본값은 1입니다.

위 코드는 n개의 스레드로 구성된 그리드를 시작하여 벡터의 각 요소에 하나의 스레드를 할당하고 각 블록에 256개의 스레드를 배치합니다. 각 개별 스레드는 스레드 및 블록 ID를 기반으로 요소 인덱스를 계산한 다음 해당 벡터 요소에 대해 필요한 계산을 수행합니다. 이 코드의 직렬 버전과 병렬 버전을 비교해 보면 두 버전이 매우 유사하고 상당히 일반적인 패턴이라는 것을 알 수 있습니다. 직렬 코드는 각 반복이 다른 모든 반복과 독립적인 루프로 구성됩니다. 이러한 루프는 기계적으로 병렬 커널로 변환될 수 있습니다. 각 루프 반복은 독립적인 스레드가 됩니다. 각 출력 요소에 스레드를 할당하면 결과를 메모리에 쓸 때 스레드 간의 동기화가 방지됩니다.

CUDA 커널은 문자 그대로 순차적으로 스레드된 C 함수이므로 일반적으로 작성하기 쉽고 벡터 연산을 위한 병렬 코드를 작성하는 것보다 간단합니다. 커널을 시작할 때 그리드의 크기와 해당 스레드 블록을 지정하여 병렬성을 명시적으로 결정할 수 있습니다.

병렬 실행 및 스레드 관리는 자동으로 이루어지며 모든 스레드 생성, 예약 및 종료는 프로그래머를 위한 기본 시스템에서 처리됩니다. 실제로 Tesla 아키텍처 GPU는 하드웨어에서 직접 모든 스레드 관리를 수행합니다. 블록의 스레드는 동시에 실행되며 __syncthreads() 내장 함수를 호출하여 동기화 장벽에서 동기화될 수 있습니다. 이를 통해 블록의 모든 스레드가 장벽에 도달할 때까지 블록의 스레드가 계속되지 않도록 할 수 있습니다. 장벽을 통과한 후 이러한 스레드는 장벽 이전 블록의 스레드가 수행한 메모리에 대한 모든 쓰기도 볼 수 있습니다. 따라서 블록 내의 스레드는 동기화 장벽에서 각 블록의 공유 메모리에 쓰고 읽음으로써 서로 통신할 수 있습니다.

동기화 장벽: 스레드는 스레드 블록의 모든 스레드가 장벽에 도달할 때까지 동기화 장벽에서 기다립니다.

블록의 스레드는 메모리를 공유하고 장벽을 통해 동기화될 수 있으므로 동일한 물리적 프로세서 또는 다중 프로세서에 함께 상주하지만 스레드 블록 수가 프로세서 수를 크게 초과할 수 있습니다. CUDA 스레드 프로그래밍 모델은 프로세서를 가상화하고 프로그래머에게 가장 편리한 세분성으로 병렬화할 수 있는 유연성을 제공합니다. 스레드 및 스레드 블록으로의 가상화를 사용하면 시스템의 프로세서 수가 아닌 처리되는 데이터의 크기에 따라 블록 수가 결정될 수 있으므로 문제를 직관적으로 분해할 수 있습니다. 또한 동일한 CUDA 프로그램을 다른 수의 프로세서 코어로 확장할 수 있습니다.

처리 요소의 가상화를 관리하고 확장성을 제공하기 위해 CUDA에서는 스레드 블록이 독립적으로 실행될 수 있어야 합니다. 블록을 어떤 순서로든 병렬 또는 직렬로 실행할 수 있어야 합니다. 큐 포인터를 원자적으로 증가시키는 등 모든 스레드에 표시되는 전역 메모리에 대한 원자 메모리 작업을 사용하여 활동을 조정할 수 있지만 여러 블록이 직접 통신할 수 있는 방법은 없습니다. 이러한 독립 요구 사항을 통해 스레드 블록을 원하는 수의 코어에서 원하는 순서로 예약할 수 있으므로 CUDA 모델을 원하는 수의 코어와 여러 병렬 아키텍처에서 확장할 수 있으며 교착 상태 가능성을 방지하는 데도 도움이 됩니다. 애플리케이션은 여러 그리드를 독립적으로 또는 독립적으로 실행할 수 있으며, 충분한 하드웨어 리소스가 제공되면 독립 그리드를 동시에 실행할 수 있습니다. 슬레이브 그리드는 그 사이에 암시적 코어 간 장벽을 두고 순차적으로 실행되므로 두 번째 슬레이브 그리드의 블록이 시작되기 전에 첫 번째 슬레이브 그리드의 모든 블록이 완료되도록 보장합니다.

원자 메모리 작업: 중간 액세스 없이 완료되는 일련의 메모리 읽기, 수정 및 쓰기 작업입니다.

스레드는 실행 중에 여러 메모리 공간의 데이터에 액세스할 수 있습니다. 각 스레드에는 전용 로컬 메모리가 있습니다. CUDA는 스레드 레지스터에 맞지 않는 스레드 특정 변수뿐만 아니라 스택 프레임 및 레지스터 오버플로에도 로컬 메모리를 사용합니다. 각 스레드 블록에는 블록의 모든 스레드에 표시되고 블록과 동일한 수명을 갖는 공유 메모리가 있습니다. 마지막으로 모든 스레드는 동일한 전역 메모리에 액세스할 수 있으며 프로그램은 shareddevice 유형 한정자를 사용하여 공유 및 전역 메모리에서 변수를 선언합니다. Tesla 기반 GPU에서 이러한 메모리 공간은 물리적으로 분리된 메모리에 해당합니다. 공유 메모리의 각 블록은 대기 시간이 짧은 온칩 RAM이고 전역 메모리는 그래픽 보드의 고속 DRAM에 있습니다.

로컬 메모리: 스레드별 스레드별 로컬 메모리입니다.

공유 메모리: 블록의 모든 스레드가 공유하는 블록별 메모리입니다.

전역 메모리: 모든 스레드가 공유하는 애플리케이션별 메모리입니다.

공유 메모리는 L1 캐시와 마찬가지로 각 프로세서 근처의 지연 시간이 짧은 메모리여야 스레드 블록의 스레드 간 고성능 통신 및 데이터 공유를 제공할 수 있습니다. 수명이 해당 스레드 블록과 동일하기 때문에 커널 코드는 일반적으로 공유 변수의 데이터를 초기화하고 공유 변수를 사용하여 계산을 수행하며 공유 메모리 결과를 전역 메모리에 복사합니다. 순차적으로 종속되는 그리드의 스레드 블록은 전역 메모리를 통해 통신하며 이를 사용하여 입력을 읽고 결과를 씁니다.

다음 그림은 스레드, 스레드 블록 및 스레드 블록 그리드의 중첩된 수준 다이어그램을 보여주며 해당 메모리 공유 수준(스레드별, 스레드 블록별 및 애플리케이션별 데이터 공유를 위한 로컬, 공유 및 전역 메모리)도 보여줍니다.

중첩된 세분성 수준 스레드, 스레드 블록 및 그리드에는 해당 로컬, 공유 및 전역 메모리 공유 수준이 있습니다. 스레드별 로컬 메모리는 스레드별로 다르며, 블록별 공유 메모리는 블록의 모든 스레드에서 공유되며, 애플리케이션별 전역 메모리는 모든 스레드에서 공유됩니다.

프로그램은 CUDA 런타임(예: cudaMalloc() 및 cudaFree())을 호출하여 커널에 표시되는 전역 메모리 공간을 관리합니다. 커널은 GPU에서 커널을 실행하는 것처럼 물리적으로 분리된 장치에서 실행될 수 있으므로 애플리케이션은 cudaMemcpy()를 사용하여 할당된 공간과 호스트 시스템 메모리 간에 데이터를 복사해야 합니다.

CUDA 프로그래밍 모델은 각 코어가 고정된 수의 스레드에서 실행되는 병렬성을 명시적으로 표현한다는 점에서 친숙한 SPMD(Single Program Multiple Data) 모델과 스타일이 유사합니다. 그러나 CUDA는 대부분의 SPMD 구현보다 더 유연합니다. 각 커널 호출이 애플리케이션 단계에 대한 올바른 수의 스레드 블록 및 스레드를 사용하여 새 그리드를 동적으로 생성하기 때문입니다. 프로그래머는 동일한 수의 스레드를 사용하도록 계산의 모든 단계를 설계할 필요 없이 코어당 편리한 수준의 병렬 처리를 사용할 수 있습니다. 아래 이미지는 SPMD와 유사한 CUDA 코드 시퀀스의 예를 보여줍니다. 먼저 3×2 블록의 2D 그리드에서 커널 F를 인스턴스화합니다. 여기서 각 2D 스레드 블록은 5×3 스레드로 구성됩니다. 그런 다음 각각 6개의 스레드가 있는 4개의 1D 스레드 블록으로 구성된 1D 그리드에서 커널 G를 인스턴스화합니다. kernelG는 kernelF의 결과에 의존하기 때문에 커널 간 동기화 장벽으로 분리됩니다.

SPMD(단일 프로그램 다중 데이터): 모든 스레드가 동일한 프로그램을 실행하는 병렬 프로그래밍 모델입니다. SPMD 스레드는 일반적으로 장벽 동기화와 함께 조정됩니다.

2D 스레드 블록의 2D 그리드에서 인스턴스화된 일련의 커널 F는 코어 간 동기화 장벽이고 그 뒤에는 1D 스레드 블록의 1D 그리드에 있는 커널 G가 있습니다.

스레드 블록의 동시 스레드는 세분화된 데이터 병렬성과 스레드 병렬성을 나타내고, 그리드의 독립 스레드 블록은 대략적인 데이터 병렬성을 나타내며, 독립 그리드는 대략적인 작업 병렬성을 나타냅니다. 커널은 계층 구조의 한 스레드에 대한 C 코드일 뿐입니다.

GPU 커널과 CPU에서 실행되는 코드를 단일 프로그램으로 병합했고 NVIDIA의 컴파일러는 단일 파일을 두 개의 바이너리로 분할했습니다. 하나는 CPU에서 실행되고 CPU의 명령어 세트를 사용하는 바이너리이고, 다른 하나는 GPU에서 실행되고 PTX 명령어 세트를 사용하는 바이너리입니다. 이는 다양한 명령어 세트와 여러 데이터 스트림의 다양한 프로그램을 사용하는 MPMD 실행의 일반적인 예입니다. 따라서 GPU의 병렬 프로그래밍 모델은 SIMD, MPMD 및 워프 수준의 Fine-grained 멀티스레딩(아래 그림)의 조합으로 볼 수 있습니다.

효율성을 높이고 구현을 단순화하기 위해 CUDA 프로그래밍 모델에는 몇 가지 제한 사항이 있습니다. 스레드와 스레드 블록은 병렬 커널이 아닌 병렬 커널을 호출해야만 생성할 수 있습니다. 이는 스레드 블록의 필수 독립성과 결합되어 런타임 오버헤드를 최소화하는 간단한 스케줄러를 사용하여 CUDA 프로그램을 실행할 수 있게 해줍니다. 실제로 Tesla GPU 아키텍처는 하드웨어 관리와 스레드 및 스레드 블록 예약을 구현합니다.

작업 병렬성은 스레드 블록 수준에서 표현할 수 있지만 스레드 동기화 장벽이 블록의 모든 스레드에서 실행되기 때문에 스레드 블록 내에서 표현하기가 어렵습니다. CUDA 프로그램이 여러 프로세서에서 실행되기 위해서는 동일한 커널 그리드 내의 스레드 블록 간의 종속성이 허용되지 않습니다. CUDA에서는 스레드 블록이 독립적이어야 하고 블록이 임의의 순서로 실행될 수 있도록 허용하므로 여러 블록에서 생성된 결과를 결합하려면 일반적으로 스레드 블록에 대한 새 그리드에서 두 번째 코어를 시작하여 수행해야 합니다(스레드 블록은 큐 포인터를 원자적으로 증가시키는 등 모든 스레드에 표시되는 전역 메모리에 대한 원자 메모리 작업을 사용하여 활동을 조정할 수 있지만).

재귀 함수 호출은 현재 CUDA 커널에서 허용되지 않으며, 수만 개의 활성 스레드에 스택 공간을 제공하는 데 필요한 많은 양의 메모리 때문에 대규모 병렬 커널에서는 재귀가 매력적이지 않습니다. 일반적으로 재귀를 사용하여 표현되는 직렬 알고리즘(예: 퀵 정렬)은 명시적 재귀보다는 중첩된 데이터 병렬 처리를 사용하여 가장 잘 구현되는 경우가 많습니다.

CPU와 GPU를 결합하는 이기종 시스템 아키텍처를 지원하려면 CUDA 프로그램은 호스트 메모리와 장치 메모리 간에 데이터와 결과를 복사해야 합니다. DMA 블록 전송 엔진과 빠른 상호 연결을 사용하면 CPU-GPU 상호 작용 및 데이터 전송의 오버헤드가 최소화되고, GPU 성능 향상이 필요할 만큼 큰 계산 집약적인 문제는 작은 문제보다 오버헤드를 더 잘 상각합니다.

그래픽 및 계산의 병렬 프로그래밍 모델은 GPU 아키텍처를 CPU 아키텍처와 다르게 만듭니다. GPU 프로세서 아키텍처를 구동하는 GPU 프로그램의 주요 측면은 다음과 같습니다.

  • 세밀한 데이터 병렬 처리의 광범위한 사용: 셰이더 프로그램은 개별 픽셀이나 정점을 처리하는 방법을 설명하고, CUDA 프로그램은 개별 결과를 계산하는 방법을 설명합니다.

  • 고스레드 프로그래밍 모델: 셰이더 스레드 프로그램은 단일 픽셀 또는 정점을 처리하고 CUDA 스레드 프로그램은 단일 결과를 생성할 수 있습니다. GPU는 초당 60프레임으로 프레임당 수백만 개의 스레드 프로그램을 생성하고 실행해야 합니다.

  • 확장성: 추가 프로세서가 제공되면 프로그램은 재컴파일 없이 자동으로 성능을 높여야 합니다.

  • 집중적인 부동 소수점(또는 정수) 계산.

  • 고처리량 컴퓨팅을 지원합니다.

19.9.6.3 NVIDIA GPU 메모리 구조

아래 그림은 NVIDIA GPU의 메모리 구조를 보여줍니다. 각 멀티스레드 SIMD 프로세서에 로컬인 온칩 메모리를 로컬 메모리라고 합니다. 멀티스레드 SIMD 프로세서 내의 SIMD 채널에서 공유되지만, 이 메모리는 멀티스레드 SIMC 프로세서 간에 공유되지 않으며, 전체 GPU와 모든 스레드 블록에서 공유되는 오프칩 DRAM을 GPU 메모리라고 합니다.

GPU 메모리 구조. GPU 메모리는 벡터화된 루프에 의해 공유되고, 로컬 메모리는 스레드 블록에 있는 SIMD 명령의 모든 스레드에 의해 공유됩니다.

GPU는 전통적으로 수백 MB에 달하는 애플리케이션의 전체 작업 세트를 포함하기 위해 대규모 캐시에 의존하는 대신 더 작은 스트리밍 캐시를 사용하고 DRAM의 긴 대기 시간을 숨기기 위해 광범위한 SIMD 명령어 스레드의 멀티스레딩에 의존해 왔습니다. 따라서 멀티 코어 마이크로프로세서의 마지막 레벨 캐시에는 적합하지 않습니다. DRAM 레이턴시를 숨기기 위해 하드웨어 멀티스레딩을 사용하는 것을 고려하면, 시스템 프로세서에서 캐시로 사용되는 칩 영역은 SIMD 명령의 많은 스레드 상태를 저장하기 위한 컴퓨팅 리소스와 많은 수의 레지스터로 사용됩니다.

메모리 대기 시간을 숨기는 것이 기본 원칙이지만 최신 GPU 및 벡터 프로세서에는 최근 Fermi 아키텍처와 같은 캐시가 추가되었지만 GPU 메모리 요구 사항을 줄이기 위한 대역폭 필터 또는 멀티스레딩이 대기 시간을 숨길 수 없는 몇 가지 변수에 대한 가속기로 간주됩니다. 함수를 호출할 때 대기 시간이 중요하기 때문에 스택 프레임, 함수 호출 및 레지스터 유출을 위한 로컬 메모리는 캐싱에 적합합니다. 또한 온칩 캐시 액세스는 여러 외부 DRAM 칩에 액세스하는 것보다 훨씬 적은 에너지를 소비하므로 캐싱은 에너지를 절약합니다.

높은 수준에서 SIMD 명령어 확장이 포함된 멀티 코어 컴퓨터는 GPU와 유사점이 있으며 다음 그림에는 유사점과 차이점이 요약되어 있습니다. 둘 다 MIMD이고 프로세서는 여러 SIMD 채널을 사용하지만 GPU에는 더 많은 프로세서와 채널이 있습니다. GPU는 더 많은 스레드에 대한 하드웨어 지원을 제공하지만 둘 다 하드웨어 멀티스레딩을 사용하여 프로세서 활용도를 향상시킵니다. 둘 다 캐시를 ​​사용하지만 GPU는 더 작은 스트리밍 캐시를 사용하는 반면 멀티 코어 컴퓨터는 전체 작업 세트를 완전히 포함하려고 하는 대규모 다중 레벨 캐시를 사용합니다. GPU의 물리적 주 메모리는 훨씬 작지만 둘 다 64비트 주소 공간을 사용합니다. GPU는 페이지 수준에서 메모리 보호를 지원하지만 아직 요구 페이징을 지원하지 않습니다.

특징 SIMD가 포함된 멀티 코어(CPU) GPU

SIMD 프로세서 4 ~ 8 8 ~ 16

프로세서당 SIMD 레인 수 2~4 8 ~ 16

SIMD 스레드에 대한 멀티스레딩 하드웨어 지원 2~4 16~32

최대 캐시 크기 8M 0.75M

메모리 주소 크기 64비트 64비트

메인 메모리 크기 8G~256G 4G~16G

페이지 수준 메모리 보호 예 예

요청 시 페이징 예 아니요

캐시 일관성 예 아니요

SIMD 프로세서는 벡터 프로세서와도 유사합니다. GPU의 여러 SIMD 프로세서는 많은 벡터 컴퓨터에 여러 벡터 프로세서가 있는 것처럼 독립적인 MIMD 코어로 작동합니다. 이 관점에서는 Fermi GTX 580이 멀티스레딩 하드웨어 지원과 코어당 16채널을 갖춘 16코어 시스템이라고 주장합니다. 가장 큰 차이점은 GPU의 기본이지만 대부분의 벡터 프로세서에는 없는 멀티스레딩입니다.

GPU와 CPU는 컴퓨터 아키텍처 계보에서 공유된 조상을 추적하지 않으며, 둘을 설명하는 누락된 링크가 없습니다. 이러한 특이한 전통으로 인해 GPU는 컴퓨터 아키텍처 커뮤니티에서 흔히 사용되는 용어를 사용하지 않아 GPU가 무엇인지, 어떻게 작동하는지에 대한 혼란을 야기합니다. 혼란을 해결하는 데 도움이 되도록 아래 이미지에는 이 기사의 일부에서 사용된 보다 설명적인 용어, 즉 주류 컴퓨팅에 가장 가까운 용어가 나열되어 있습니다.

GPU가 주류 컴퓨팅으로 나아가고 있지만 그래픽 분야에서 계속해서 탁월해야 한다는 책임을 포기할 수는 없습니다. 따라서 설계자가 그래픽을 잘 수행하는 데 투자한 하드웨어를 고려할 때 더 광범위한 애플리케이션의 성능을 향상시키기 위해 이를 어떻게 보완할 수 있는지 묻는다면 GPU 설계가 더 합리적일 수 있습니까?

GPU에 대한 자세한 기술 정보는 심층적인 GPU 하드웨어 아키텍처 및 작동 메커니즘을 참조하세요.

19.9.7 i7 960 및 Tesla GPU 성능

Intel 연구원들은 2010년에 쿼드 코어 Intel 코어 i7 960의 멀티미디어 SIMD 확장을 이전 세대 GPU인 NVIDIA Tesla GTX 280과 비교하는 논문을 발표했습니다. 아래 표에는 두 시스템의 특성이 나열되어 있습니다. Core i7은 Intel의 45nm 반도체 기술을 사용하고 GPU는 TSMC의 65nm 기술을 사용합니다. 중립 당사자나 이해 당사자 두 명이 비교를 하는 것이 더 공정할 수 있지만, 이 섹션의 목적은 한 제품이 다른 제품보다 얼마나 빠른지 판단하는 것이 아니라 서로 다른 두 아키텍처 스타일의 특징에 대한 상대적 가치를 이해하려고 노력하는 것입니다.

특징 코어 i7-960 GTX280 GTX480 280/i7 비율 480/i7 비율

처리된 요소(코어 또는 SM) 수 4 30 15 7.5 3.8

클록 주파수(GHz) 3.2 1.3 1.4 0.41 0.44

금형(다이) 크기 263 576 520 2.2 2.0

기술 인텔 45nm TSMC 65nm TSMC 40nm 1.6 1.0

전원(모듈이 아닌 칩) 130 130 167 1.0 1.3

트랜지스터 700M 1400M 3030M 2.0 4.4

메모리 대역폭(G/초) 32 141 177 4.4 5.5

단일 정밀도 SIMD 와이드 4 8 32 2.0 8.0

이중 정밀도 SIMD 와이드 2 1 16 0.5 8.0

피크 단정밀도 스칼라 FLOPS(GFLOP/초) 26 117 63 4.6 2.5

피크 단정밀도 SIMD FLOPS(GFLOP/초) 102 311-933 515-1344 3.0-9.1 6.6-13.1

SP 1 더하기 또는 곱하기 해당 없음 311 515 3.0 6.6

SP 1 명령어 융합 곱셈-덧셈 해당 없음 622 1344 6.1 13.1

특수 SP 이중 문제 융합 곱셈 및 곱셈 해당 없음 933 해당 없음 9.1

  • 피크 배정밀도 SIMD FLOPS(GFLOP/초) 51 78 515 1.5 10.1

아래 이미지의 Core i7 960과 GTX 280에 대한 곡선은 컴퓨터 간의 차이점을 보여줍니다. GTX280은 더 높은 메모리 대역폭과 배정밀도 부동 소수점 성능을 제공할 뿐만 아니라 배정밀도 척추도 왼쪽에 있습니다. GTX 280의 배정밀도 스파인 포인트는 0.6이고 Core i7의 스파인 포인트는 3.1입니다. 위에서 언급했듯이 곡선의 능선이 왼쪽에 가까울수록 컴퓨팅 성능이 최고 수준에 도달하기가 더 쉽습니다. 단정밀도 성능의 경우 두 컴퓨터의 척추가 오른쪽으로 이동하므로 단정밀도 성능의 최고 수준에 도달하기가 어렵습니다. 커널의 산술 능력은 캐시로 들어가는 바이트가 아니라 주 메모리로 들어가는 바이트를 기반으로 한다는 점에 유의하세요. 따라서 위에서 언급한 것처럼 캐시는 대부분의 참조가 실제로 캐시로 이동하는 경우 특정 시스템에서 코어의 산술 강도를 변경할 수 있습니다. 또한 두 아키텍처 모두의 단위 단계 액세스는 이 대역폭을 사용하므로 GTX 280 및 Core i7의 실제 집계 분산 주소는 느려질 수 있습니다.

곡선의 위쪽 행에는 배정밀도 부동 소수점 성능이, 아래쪽 행에는 단정밀도 성능이 표시됩니다. (DP FP 성능 한도는 관점을 제공하기 위해 맨 아래 줄에도 표시되어 있습니다.) 왼쪽의 Core i7 960은 최대 DP FP 성능이 51.2 GFLOP/초, 최대 SP FP 성능이 102.4 GFLOP/초, 최대 메모리 대역폭이 16.4 GBytes/초입니다. NVIDIA GTX 280의 DP FP 피크는 78GFLOP/초, SP FP 피크는 624GFLOP/초, 메모리 대역폭은 127GB/초입니다. 왼쪽의 수직 점선은 0.5 FLOP/바이트의 산술 강도를 나타내며 Core i7에서 메모리 대역폭은 8 DP GFLOP/초 또는 8 SP GFLOP/초 이하로 제한됩니다. 오른쪽의 수직 점선은 4 FLOP/바이트의 산술 강도를 갖습니다. Core i7에서는 컴퓨팅 속도가 51.2 DP GFLOP/초 및 102.4 SP GFLOP/초로 제한되며, GTX 280에서는 78 DP GFLOP/초 및 624 SP GFLOP/초로 제한됩니다. Core i8에서 최대 컴퓨팅 속도를 달성하려면 4개의 코어와 SSE 명령어를 모두 사용하고 동일한 수의 곱셈과 덧셈을 사용해야 합니다. GTX 280의 경우 모든 멀티스레드 SIMD 프로세서에 융합된 곱셈-덧셈 명령이 필요합니다.

연구원들은 최근 제안된 4가지 벤치마크 제품군의 컴퓨팅 및 메모리 특성을 분석하여 벤치마크 프로그램을 선택한 다음 "이러한 특성을 포착하는 처리량 컴퓨팅 커널 세트를 공식화했습니다." 아래 그래프는 성능 결과를 보여줍니다. 숫자가 높을수록 속도가 빨라지고, 곡선은 이 사례 연구의 상대적 성능을 설명하는 데 도움이 됩니다.

GTX 280의 원시 성능 사양이 2.5배 느린(클럭 속도) ~ 7.5배 빠른(칩당 코어) 범위인 반면, 성능은 2.0배 느린(Solv) ~ 15.2배 빠른(GJK) 범위라는 점을 고려하여 Intel 연구원들은 불일치의 이유를 찾기로 결정했습니다.

  • 메모리 대역폭. GPU의 메모리 대역폭은 4.4배로 LBM과 SAXPY가 각각 5.0배와 5.3배 더 빠르게 실행되는 이유를 설명하는 데 도움이 됩니다. 작업 세트는 수백 메가바이트이므로 Core i7 캐시에 맞지 않습니다. 메모리에 집중적으로 액세스하기 위해 의도적으로 캐시 차단을 사용하지 않습니다. 곡선의 기울기가 성능을 설명합니다. SpMV에는 대규모 작업 세트도 있지만 GTX 280의 배정밀도 부동 소수점 연산이 Core i7보다 1.5배 더 빠르기 때문에 1.9배 더 빠르게 실행될 뿐입니다.

  • 대역폭 계산. 나머지 5개 코어는 컴퓨팅 집약적입니다(SGEMM, Conv, FFT, MC 및 Bilat). GTX 속도는 각각 3.9, 2.8, 3.0, 1.8 및 5.7배입니다. 처음 3개는 단정밀도 부동소수점 연산을 사용하고, GTX 280 단정밀도 연산은 3~6배 빠르며, MC는 배정밀도를 사용하는데, 이는 DP 성능이 고작 1.5배 빨라서 1.8배만 빠른 이유를 설명합니다. Bilat은 GTX 280에서 직접 지원하는 초월 기능을 사용합니다. Core i7은 Bilat의 초월 기능을 계산하는 데 3분의 2의 시간을 소비하므로 GTX 280이 5.7배 더 빠릅니다. 이러한 관찰은 작업 부하에서 발생하는 작업(배정밀도 부동 소수점 작업 및 초월 작업 포함)을 지원하는 하드웨어의 가치를 지적하는 데 도움이 됩니다.

  • 캐싱 이점. Raycasting(RC)은 Core i7 캐시의 캐시 차단이 GPU에서와 마찬가지로 메모리 대역폭 제한을 방지하여 검색에도 도움이 되므로 GTX에서 1.6배 더 빠릅니다. 인덱스 트리가 캐시에 들어갈 만큼 작다면 Core i7은 두 배 더 빠릅니다. 인덱스 트리가 클수록 메모리 대역폭이 제한됩니다. 전반적으로 GTX 280은 검색 속도가 1.8배 빠릅니다. 캐시 차단은 정렬에도 도움이 됩니다. 대부분의 프로그래머는 SIMD 프로세서에서 정렬을 실행하지 않지만 분할이라는 1비트 정렬 기본 요소를 사용하여 작성할 수 있지만 분할 알고리즘은 스칼라 정렬보다 더 많은 명령을 실행하므로 Core i7은 GTX 280보다 1.25배 더 빠르게 실행됩니다. 캐시 차단을 통해 SGEMM, FFT 및 SpMV가 컴퓨팅 경계가 되므로 캐싱은 Core i7의 다른 코어에도 도움이 됩니다. 이 관찰은 캐시 차단 최적화의 중요성을 다시 한 번 강조합니다.

  • 흩어지고 모으기. 멀티미디어 SIMD 확장은 데이터가 메인 메모리에 분산되어 있는 경우 거의 도움이 되지 않으며, 데이터에 대한 액세스가 16바이트 경계에 정렬될 때만 최적의 성능이 달성됩니다. 따라서 GJK는 Core i7의 SIMD로부터 거의 이점을 얻지 못합니다. 위에서 언급했듯이 GPU는 벡터 아키텍처에서 수집-분산 주소 지정을 제공하지만 대부분의 SIMD 확장에서는 이 주소 지정이 무시되며 메모리 컨트롤러는 동일한 DRAM 페이지에 일괄적으로 함께 액세스하기도 합니다. 이 조합은 GTX 280이 Core i7보다 15.2배 더 빠르게 GJK를 실행한다는 것을 보여줍니다. 이는 위 그래프의 단일 물리적 매개변수보다 더 큽니다. 이러한 관찰은 벡터 및 GPU 아키텍처에 대한 SIMD 확장에서 누락된 군집 산란의 중요성을 강화합니다.

  • 동기화. 동기식 성능은 하드웨어 가져오기 및 델타 명령에도 불구하고 Core i7 전체 런타임의 28%를 차지하는 원자 업데이트로 인해 제한됩니다. 결과적으로 Hist는 GTX 280에서 1.7배 더 빠르며 Solv는 소수의 계산으로 일련의 독립적인 제약 조건을 해결한 후 장벽 동기화를 수행합니다. Core i7은 메모리 계층 구조에 대한 이전 액세스가 모두 완료되지 않은 경우에도 올바른 결과를 보장하는 원자 명령 및 메모리 일관성 모델의 이점을 제공합니다. 메모리 일관성 모델이 없으면 GTX 280 버전은 시스템 프로세서에서 일부 일괄 처리를 시작하여 GTX 280이 Core i7보다 0.5배 빠르게 실행됩니다. 이 관찰은 특정 데이터 병렬 문제에 대한 동기화 성능의 중요성을 지적합니다.

놀랍게도 Intel 연구원이 선택한 코어의 Tesla GTX 280에서 발견된 약점은 Tesla의 후속 아키텍처에서 해결되었습니다. Fermi는 더 빠른 배정밀도 부동 소수점 성능, 더 빠른 원자 연산 및 캐싱을 제공합니다. 또한 SIMD 명령어보다 수십 년 앞서 나온 벡터 아키텍처의 수집-분산 지원이 이러한 SIMD 확장의 효과적인 유용성에 중요했으며 일부는 비교 전에 예측했다는 점도 흥미롭습니다. Intel 연구원들은 14개 코어 중 6개가 SIMD를 더 잘 활용하여 Core i7에서 보다 효율적인 수집-분산 지원을 제공할 수 있다고 지적했습니다. 이 연구는 또한 캐시 차단의 중요성을 확인합니다.

19.9.8 NVidia Tesla 아키텍처

아래 다이어그램은 Tesla 아키텍처를 보여줍니다. 다이어그램 상단부터 설명을 시작하겠습니다. 호스트 CPU는 전용 버스를 통해 일련의 명령과 데이터를 그래픽 프로세서로 보냅니다. 그런 다음 전용 버스는 일련의 명령과 데이터를 GPU의 버퍼로 전송하고, 여기서 GPU 장치가 정보를 처리합니다. 아래 다이어그램에서는 작업 흐름이 위에서 아래로 진행됩니다. GPU는 본질적으로 순서가 지정된 매우 간단한 코어 세트이며 복잡한 작업의 실행을 조정하고 작업을 코어 세트에 배포하기 위한 많은 추가 하드웨어를 갖추고 있습니다. GPU는 또한 다단계 메모리 계층을 지원하며 소수의 그래픽 관련 작업을 수행하는 전용 장치를 갖추고 있습니다.

NVIDIA Tesla 아키텍처.

19.9.8.1 작업 할당

GPU는 버텍스 처리, 픽셀 처리, 일반 컴퓨팅 작업의 세 가지 유형의 작업을 분배할 수 있습니다. GPU는 PTX 및 SASS 명령어 세트를 사용하여 자체 어셈블리 코드를 정의합니다. 이러한 명령어 세트의 각 명령어는 GPU에서 기본 작업을 수행하며 레지스터 피연산자 또는 메모리 피연산자를 사용합니다. CPU와 달리 GPU의 레지스터 파일 구조는 일반적으로 소프트웨어에 노출되지 않으며 프로그래머는 가상 레지스터를 무제한으로 사용해야 하며 GPU 또는 장치 드라이버는 이를 실제 레지스터에 매핑합니다.

이제 버텍스 처리를 위해 하위 수준 그래픽 소프트웨어는 일련의 조립 명령을 GPU로 보냅니다. GPU에는 바이너리 코드를 생성하고 이를 GPU 코어 간에 작업을 조정하고 배포하는 전용 버텍스 처리 장치로 보내는 하드웨어 어셈블러가 있습니다. 또는 CPU가 래스터라이제이션, 프래그먼트 처리 및 깊이 버퍼링 프로세스를 수행하는 GPU에 픽셀 처리 작업을 보낼 수 있습니다. GPU의 전용 장치는 이러한 작업을 위한 코드 조각을 생성하고 이를 픽셀 처리 장치로 보냅니다. 픽셀 처리 장치는 작업 항목을 GPU 코어 세트에 배포합니다. 세 번째 단위는 두 개의 행렬을 추가하거나 두 벡터의 내적을 계산하는 등 CPU에서 일반적인 계산 작업을 받는 계산 작업 할당자입니다. 프로그래머는 하위 작업 세트를 지정하고, 계산 작업 분배 엔진의 역할은 이러한 하위 작업 세트를 GPU의 코어로 보내는 것입니다.

이 단계 이후 GPU는 명령의 소스를 어느 정도 무시합니다. 엔지니어링의 이 부분은 GPU 성공의 핵심 기여입니다. 설계자들은 GPU의 기능을 두 가지 계층으로 성공적으로 나누었습니다. 첫 번째 계층은 작업 유형(그래픽 또는 범용)에 따라 다릅니다. 이 단계에서 각 파이프라인의 역할은 상위 수준 작업의 성격에 관계없이 동일한 하드웨어 장치를 사용할 수 있도록 특정 작업 시퀀스를 공통 작업 집합으로 변환하는 것입니다. 이제 컴퓨팅 엔진을 포함하는 GPGPU의 후반부를 살펴보겠습니다.

19.9.8.2 GPU 컴퓨팅 엔진

GeForce 8800 GPU에는 128개의 코어가 있습니다. 코어 그룹은 8개의 그룹으로 나뉘며, 각 그룹은 TPC(Texture/Processor Cluster)라고 하며, 각 TPC에는 2개의 SM(Streaming Multiprocessor)이 포함됩니다. 또한 각 SM에는 스트림 프로세서(SP)라고 하는 8개의 코어가 포함되어 있으며, 각 SP는 IEEE 754 호환 부동 소수점 ALU, 분기 및 메모리 액세스 장치를 갖춘 간단한 순차 코어입니다. 간단한 코어 세트 외에도 각 SM에는 일부 전용 메모리 구조가 포함되어 있습니다. 이러한 메모리 구조에는 상수, 텍스처 데이터 및 GPU 명령이 포함되어 있습니다. 모든 SP는 일련의 명령을 병렬로 실행할 수 있으며 서로 긴밀하게 동기화됩니다.

19.9.8.3 상호 연결 네트워크, DRAM 모듈, L2 캐시 및 ROP

8개의 TPC는 상호 연결 네트워크를 통해 캐시, DRAM 모듈 및 ROP(Raster Operation Processor) 세트에 연결됩니다. SM에는 1차 캐시가 포함되어 있으며, 캐시 누락이 발생하면 SP 코어는 NOC를 통해 해당 2차 캐시 라이브러리에 액세스합니다. GPU의 경우 L2 캐시는 뱅크 레벨에서 분할된 공유 캐시이며, L2 캐시 아래에는 GPU에 대용량 외부 DRAM 메모리가 있습니다. GeForce 8800에는 외부 DRAM 모듈에 연결하기 위한 384개의 핀이 있으며, 각각 64개의 핀으로 구성된 6개 그룹으로 나뉩니다. 물리적 메모리 공간도 6개 그룹에 걸쳐 6개 부분으로 나누어집니다. 래스터라이제이션 작업에는 일반적으로 특수한 처리 루틴이 필요합니다. 안타깝게도 이러한 루틴은 TPC에서 비효율적으로 실행되므로 GeForce 8800 칩에는 6개의 ROP가 있습니다. 각 ROP 프로세서는 사이클당 최대 4개의 픽셀을 처리할 수 있습니다. 주로 픽셀의 색상을 보간하고 색상 혼합 작업을 수행합니다.

19.9.9 NVidia RTX 4090 아키텍처 및 기능

NVIDIA가 얼마 전 Ada Lovelace GeForce 세대를 발표했을 때 Ray Tracing 성능을 효과적으로 두 배로 늘리는 몇 가지 대담한 주장을 펼쳤으며 다양한 인기 렌더링 엔진을 테스트한 결과 이는 확실히 사실입니다.

RTX 4090 GPU 칩 구조.

Ada Lovelace 세대는 4세대 Tensor 코어와 향상된 광학 흐름을 제공합니다. 생성하는 동안 소음 감소와 같은 기능이 속도를 높이고 게임 응용 프로그램에서는 DLSS 3.0으로 업그레이드됩니다. 레이 트레이싱 코어 측면에서 Ada Lovelace는 3세대 구현을 출시했으며 Ampere 세대에 비해 크게 2배 향상된 성능을 제공합니다.

다른 주목할만한 기능으로는 게임을 포함한 레이 트레이싱 성능을 더욱 향상시키는 Shader Execution Reordering이 있으며, 한 예에서는 Cyberpunk 2077에서 44% 향상된 성능을 보여줍니다. 또한 Intel은 AV1 가속 GPU 인코더 출시에 앞장섰고 NVIDIA가 바짝 뒤따랐으며 Ada Lovelace도 이를 출시했습니다. 흥미롭게도 NVIDIA는 듀얼 인코더를 탑재하여 인코딩 시간을 절반으로 단축할 것이라고 주장합니다. 향후 전체 크리에이터 성과를 살펴보며 이에 대해 알아볼 예정입니다.

NVIDIA 최신 플래그십의 렌더링 성능을 이해하기 전에 NVIDIA의 공식 하드웨어 매개변수를 살펴보겠습니다.

GPU 모델 코어 수 최대 주파수 피크 FP32 기억 대역폭 총 전력

RTX 4090 16,384 2,520 82.6TFLOPS 24GB 1008GB/초 450W

RTX 4080 16GB 9,728 2,510 48.8TFLOPS 16GB 717GB/초 320W

RTX 3090Ti 10,752 1,860 40TFLOPS 24GB 1008GB/초 450W

RTX 3080Ti 10,240 1,670 34.1TFLOPS 12GB 912GB/초 350W

RTX 3070Ti 6,144 1,770 21.7TFLOPS 8GB 608GB/초 290W

RTX 3060Ti 4,864 1,670 16.2 테플롭스 8GB 448GB/초 200W

RTX 4090에는 이 세대 최초의 Ada Lovelace GPU인 AD102가 함께 제공됩니다. 하지만 이 플래그십 카드에 사용된 칩은 이미 방대한 사양 시트에도 불구하고 풀 코어가 아니라는 점은 주목할 가치가 있습니다. 코어에는 128개의 스트리밍 멀티프로세서(SM)에 분산된 16,384개의 CUDA 코어가 있으며 이는 RTX 3090 Ti의 GA102 GPU(그 자체가 전체 Ampere 코어임)보다 52% 증가한 수치입니다.

상단: RTX 4090 내의 AD102 구조; 하단: 완전한 AD102 GPU 구조.

GA102 및 AD102 아키텍처 비교 차트.

완전한 AD102 칩에는 18432개의 CUDA 코어와 144개의 SM이 포함되어 있습니다. 이는 또한 144개의 3세대 RT 코어와 576개의 4세대 Tensor 코어를 볼 수 있음을 의미합니다. Nvidia가 원한다면 RTX 4090 Ti 또는 심지어 Titan을 위한 공간이 충분합니다.

Ada Lovelace와 Ampere 아키텍처의 SM 비교 차트.

기억은 크게 변하지 않았습니다. 동일한 24GB GDDR6X는 21Gbps로 실행되어 1008GB/초 메모리 대역폭을 제공합니다. 다음 표는 GeForce RTX 4090과 GeForce RTX 3090 Ti의 일부 매개변수에 대한 비교 차트입니다.

지포스 RTX 4090 지포스 RTX 3090 Ti

건축 에이다 러브레이스 암페어

쿠다 코어 16,432 10,752

에스엠 128 84

RT 코어 128 84

텐서 코어 512 4세대 336 3세대

ROP 176 112

최대 주파수 2,520MHz 1,860MHz

기억 24GBGDDR6X 24GBGDDR6X

메모리 속도 21Gbps 21Gbps

메모리 대역폭 1,008GB/초 1,008GB/초

버스 폭 384 384

L1 | L2 캐시 16,384KB | 73,728KB 10,752KB | 6,144KB

생산과정 5nmTSMC 8nm 삼성

트랜지스터 763억 283억

칩 면적 608.5mm² 628.5mm²

총 전력 450W 450W

방정식의 원시 셰이더 측면에서도 실제로 Ampere 아키텍처와 크게 발전하지 않았습니다. 각 SM은 여전히 ​​동일한 64개의 전용 FP32 장치를 사용하지만 필요에 따라 부동 소수점 계산과 정수 계산 간에 분할할 수 있는 64개 단위 보조 스트림이 있습니다. 이는 Ampere에서 도입한 것과 동일합니다.

RTX 3090과 RTX 4090의 상대적인 성능 차이를 살펴보면 래스터라이제이션 관점에서 두 아키텍처가 얼마나 유사한지 알 수 있습니다.

레이 트레이싱 및 업스케일링을 무시하면 해당 성능 증가는 AD102 GPU의 추가 CUDA 코어 수보다 약간 더 큽니다. 성능 성장은 해당 수준보다 "약간 높지만" 이 수준에는 약간의 차이가 있음을 나타냅니다.

그 중 일부는 Ada Lovelace GPU를 위한 Nvidia의 새로운 4nm 생산 프로세스 때문입니다. Ampere의 8nm Samsung 프로세스와 비교하여 TSMC가 제조한 4N 프로세스는 동일한 전력에서 두 배의 성능을 제공하거나 동일한 성능에서 절반의 성능을 제공한다고 합니다.

이는 Nvidia가 RTX 4090의 부스트 클럭이 2520MHz인 등 클럭 속도 측면에서 매우 공격적일 수 있음을 의미합니다. 실제로 테스트에서 Founders Edition 카드의 평균 2716MHz를 확인했는데, 이는 이전 세대 RTX 3090보다 1GHz 더 빠른 속도입니다.

게다가 프로세스 감소로 인해 TSMC와 협력하는 Nvidia 엔지니어들은 AD102 코어에 무려 763억 개의 트랜지스터를 집어넣었습니다. 608.5mm² Ada GPU에 GA102 실리콘의 283억 개의 트랜지스터보다 더 많은 트랜지스터가 포함되어 있다는 점을 고려하면 628.4mm² 암페어 칩보다 훨씬 작을 가능성이 높습니다.

Nvidia가 점점 더 많은 수의 트랜지스터를 단일 칩에 계속해서 집어넣으면서도 실제 다이 크기를 줄일 수 있다는 사실은 이 분야에서 고급 프로세스 노드의 힘을 입증하는 것입니다. 참고로 RTX 2080 Ti의 TU102 칩은 754mm²이며 12nm 트랜지스터 186억 개만 수용할 수 있습니다. 그러나 이것이 단일 칩 GPU가 제한 없이 영원히 지속될 수 있다는 의미는 아닙니다. GPU 경쟁사인 AMD는 11월에 새로운 RDNA 3 칩을 출시하면서 그래픽 컴퓨팅 칩으로 전환하겠다고 약속했습니다. AD102 GPU의 복잡성이 최첨단 814mm² Nvidia Hopper 실리콘의 800억 개의 트랜지스터에 의해서만 능가된다는 점을 고려하면 확실히 값비싼 칩입니다. 그러나 더 작은 컴퓨팅 칩은 비용을 낮추고 수율을 높여야 합니다.

더 많은 매개변수 사양 및 기능은 다음과 같습니다.

그러나 적어도 현재로서는 무차별 접근 방식이 Nvidia에 여전히 좋은 성과를 내고 있습니다.

더 빠른 속도를 원하고 가능한 한 많은 고급 트랜지스터를 탑재했다면 또 무엇을 할 수 있을까요? 패키지에 더 많은 캐시를 추가하는 대답은 AMD가 Infinity Cache를 통해 큰 효과를 달성한 것이며, Nvidia가 반드시 멋진 새 브랜드 접근 방식을 취하는 것은 아니지만 Ada 코어에 엄청난 양의 L2 캐시를 추가하는 것입니다.

이전 세대 GA102에는 SM 중간에 6144KB의 공유 L2 캐시가 포함되어 있었으며 Ada는 이를 16배 늘려 AD102 SM에서 사용할 98304KB L2 캐시 풀을 만들었습니다. RTX 4090 버전의 칩의 경우 용량은 73728KB로 떨어지지만 여전히 캐시가 충분합니다. SM당 L1 수는 변경되지 않았지만 이제 칩 내에 총 SM이 더 많기 때문에 L1 캐시 수도 Ampere에 비해 더 많다는 의미이기도 합니다.

그러나 요즘에는 래스터라이제이션가 GPU의 전부는 아닙니다. Turing이 게임에 실시간 Ray Tracing을 처음 도입했을 때는 이런 느낌이었을 것입니다. 이제는 거의 PC 게임의 표준 구성 요소가 되었습니다. 업그레이드도 마찬가지입니다. 따라서 PC 게임의 이 두 가지 기둥에 아키텍처가 접근하는 방식은 전반적인 디자인을 이해하는 데 매우 중요합니다.

세 그래픽 카드 제조업체 모두 이제 레이 트레이싱 성능과 기술 업그레이드의 정교함에 중점을 두고 있으며, 이들 간의 새로운 전쟁을 일으키고 있습니다.

RTX 4090은 450W 사양을 갖추고 전력 소모가 많은 GPU이므로 PSU가 클수록 좋습니다. NVIDIA가 지정한 최소 전력은 850W이며, 집중적인 3DMark 테스트에서 최고 650W에 도달했습니다. RTX 4090에는 3개의 8핀 전원 커넥터가 필요하거나 새로운 PCIe 5 지원 PSU가 있는 1개가 필요합니다. 테스트 플랫폼의 PSU는 최소 요구 사항만 충족하므로 향후 더 큰 PSU로 이동할 예정입니다.

RTX 4090의 쿨러에 관해서는 디자인이 이전 세대 RTX 3090과 유사하지만 후드 아래의 개선 사항은 온도에 좋습니다. 최신 모델은 블레이드 수를 줄이면서 팬이 더 커졌습니다. RTX 4090에 대한 충분한 테스트를 거친 후 3DMark Fire Strike Ultra 테스트에서 3090(총 650W)보다 100W 더 많은 전력을 생산했습니다.

하드웨어 사양을 설명한 후 렌더링 기능에 대해 이야기해 보겠습니다.

Ada 스트리밍 멀티프로세서에서 실제 변화가 발생했습니다. 래스터라이제이션 구성 요소는 매우 유사할 수 있지만 3세대 RT Core는 극적으로 변경되었습니다. 처음 2세대 RT Core에는 한 쌍의 전용 유닛인 직육면체 교차 엔진과 삼각형 교차 엔진이 포함되어 있어 레이 트레이싱 코어의 BVH(경계 볼륨 계층 구조) 알고리즘을 계산할 때 SM의 나머지 부분에서 대량의 RT 워크로드를 추출했습니다.

Ada는 SM에서 더 많은 작업을 오프로드하기 위해 Opacity Micromap Engine(OME)과 Displaced Micro-Mesh Engine이라는 두 개의 추가 독립 장치를 도입했습니다. 첫 번째 방법은 장면의 투명도를 처리할 때 계산 속도를 크게 높이고, 두 번째 방법은 기하학적으로 복잡한 객체를 분해하여 전체 BVH 계산을 완료하는 데 필요한 시간을 줄이는 것을 목표로 합니다.

왼쪽: 광선이 버프 삼각형에 여러 번 부딪혀 각 적중마다 한 번씩 AnyHit 셰이더를 트리거할 수 있는 암페어 삼각형 교차 다이어그램. 오른쪽: OME와 결합된 Ada의 OMMS(OPACITY MICRO MAPS Shading) 텍스처 렌더링 기술은 알파 순회 후 계산을 크게 줄일 수 있습니다. OMMS 기술을 통해 광선이 위 그림의 하늘색 부분과 만나면 AnyHit 계산이 직접 무시되므로 이러한 개체의 계산량이 크게 늘어납니다.

Displaced Micro-Mesh Engine의 작동 메커니즘에 대한 개략도.

그 외에도 Nvidia는 이를 "1990년대 CPU의 비순차적 실행과 매우 유사한 GPU의 주요 혁신"이라고 부릅니다. SER(Shader Execution Reordering)은 셰이딩 작업 부하를 전환하기 위해 만들어졌으며 Ada 칩은 작업 일정을 즉시 재조정하여 그래픽 파이프라인의 효율성을 크게 향상시킬 수 있습니다.

Intel은 장면에서 레이 트레이싱 발산 광선을 돕기 위해 스레드 순서 지정 장치인 Alchemist GPU(새 탭에서 열림)에 대해 유사한 기능을 개발해 왔습니다. 보고서에 따르면 설정에는 개발자의 입력이 필요하지 않습니다. 현재 Nvidia는 SER을 개발자의 게임 코드에 통합하기 위해 특정 API가 필요하며 Microsoft 및 기타 회사와 협력하여 DirectX 12 및 Vulkan과 같은 표준 그래픽 API에 이 기능을 제공하고 있습니다.

마지막으로 DLSS 3.0의 비장의 카드인 프레임 생성을 살펴보면 DLSS 3는 이제 업그레이드할 뿐만 아니라 전체 게임 프레임 자체를 생성합니다. 처음부터 반드시 그런 것은 아니지만 AI와 딥 러닝의 힘을 사용하여 다음 프레임이 실제로 렌더링될 경우 어떤 모습일지 가장 잘 추측한 다음 실제로 렌더링되는 다음 프레임 전에 AI 생성 프레임을 삽입합니다.

그것은 부두교이고, 흑마술이고, 어둠의 예술이며, 꽤 환상적입니다. 광학 흐름 장치라고 하는 4세대 텐서 코어 내의 향상된 하드웨어 장치를 사용하여 이러한 모든 실시간 계산을 수행한 다음 신경망을 사용하여 이전 프레임, 장면의 모션 벡터 및 광학 흐름 장치의 모든 데이터를 통합하여 레이 트레이싱 및 포스트 프로세스 파이프라인를 포함할 수 있는 완전히 새로운 프레임을 생성합니다.

Nvidia는 DLSS 업스케일링(현재 DLSS 초해상도라고 함)을 사용하여 어떤 경우에는 AI가 업스케일링을 통해 초기 프레임의 3/4을 생성한 다음 프레임 생성을 사용하여 전체 두 번째 프레임을 생성한다고 말합니다. 전체적으로 AI가 전체 디스플레이 픽셀의 7/8을 생성하는 것으로 추정됩니다. 3DMark Time Spy Extreme의 대형 암페어 코어보다 두 배 더 높은 점수를 얻었으며 원시 실리콘은 레이 트레이싱 또는 DLSS가 추가되기 전 Cyberpunk 2077보다 두 배 더 많은 4K 프레임 속도를 제공했습니다.

사이버펑크 2077의 비교 데이터는 다음과 같습니다.

에너지 효율 측면에서는 4090의 평균 전력은 3090보다 약 18% 높고, 와트당 성능은 3090(1080P)보다 1.75배 높으며, 평균 온도는 3090보다 약 4.5% 낮다.

19.9.10 오류와 함정

GPU는 너무 빠르게 발전하고 변화하여 많은 오류와 함정이 발생했으며, 그 중 일부는 이 섹션에서 설명됩니다.

19.9.10.1 오해: GPU는 단지 SIMD 벡터 다중 프로세서일 뿐입니다

GPU가 단순히 SIMD 벡터 멀티프로세서일 뿐이라는 잘못된 결론을 내리기 쉽습니다. GPU에는 프로그래머가 다중 스레드 인스턴스에서 다중 데이터로 실행하는 프로그램을 작성할 수 있는 SPMD 스타일 프로그래밍 모델이 있지만 이러한 스레드의 실행은 순수한 SIMD 또는 벡터가 아니며 실제로 SIMT(단일 명령 다중 스레딩)입니다. 각 GPU 스레드에는 자체 스칼라 레지스터, 스레드별 메모리, 스레드 실행 상태, 스레드 ID, 독립적인 실행 및 분기 경로, 효과적인 프로그램 카운터가 있으며 독립적으로 메모리 주소를 지정할 수 있습니다. 스레드 그룹(예: 32 스레드 워프)은 스레드에 사용되는 PC가 동일할 때 더 효율적으로 수행되지만 필수는 아니므로 멀티프로세서는 순수한 SIMD가 아닙니다. 스레드 실행 모델은 장벽 동기화 및 SIMT 최적화를 갖춘 MIMD입니다. 개별 스레드 로드/저장 메모리 액세스를 블록 액세스로 결합할 수 있다면 더 효율적으로 수행되지만 꼭 필요한 것은 아닙니다. 순수 SIMD 벡터 아키텍처에서는 서로 다른 스레드의 메모리/레지스터 액세스가 일반 벡터 패턴으로 정렬되어야 합니다. GPU에는 레지스터 또는 메모리 액세스에 대한 제한이 없습니다. 그러나 스레드의 워프가 로컬 데이터 블록에 액세스하는 경우 실행이 더 효율적입니다.

순수 SIMD 모델과 비교하여 SIMT GPU는 여러 스레드 워프를 동시에 실행할 수 있습니다. 그래픽 응용 프로그램에는 다중 프로세서 배열에서 동시에 실행되는 여러 세트의 버텍스 프로그램, 픽셀 프로그램 및 기하학 프로그램이 있을 수 있으며 컴퓨팅 프로그램은 동시에 서로 다른 워프에서 서로 다른 프로그램을 실행할 수도 있습니다.

19.9.10.2 오해: GPU 성능은 무어의 법칙보다 빠르게 증가할 수 없습니다

무어의 법칙은 단지 속도일 뿐, 다른 속도에 대한 "빛의 속도" 제한이 아닙니다. 무어의 법칙은 시간이 지남에 따라 반도체 기술이 발전하고 트랜지스터가 소형화됨에 따라 각 트랜지스터 제조 비용이 기하급수적으로 감소할 것이라는 기대를 설명합니다. 즉, 트랜지스터 수는 기하급수적으로 증가하는 반면 제조 비용은 변하지 않습니다. Gordon Moore는 동일한 제조 비용으로 연간 약 2배의 트랜지스터를 사용할 수 있을 것이라고 예측했으며 나중에 이를 2년마다 두 배로 수정했습니다. 무어는 집적 회로당 부품 수가 50개에 불과했던 1965년에 원래 예측을 내렸지만 이 예측은 놀라울 정도로 일치하는 것으로 입증되었습니다. 트랜지스터 크기를 줄이는 것은 역사적으로 트랜지스터당 전력을 낮추고 일정한 전력에서 클럭 속도를 높이는 등 다른 이점도 가져왔습니다.

프로세서, 메모리 및 기타 구성 요소를 만들기 위해 칩 설계자가 점점 더 트랜지스터를 사용하고 있습니다. 한동안 CPU 설계자들은 무어의 법칙과 같은 속도로 프로세서 성능을 높이기 위해 추가 트랜지스터를 사용해 왔으며, 많은 사람들이 18~24개월마다 프로세서 성능이 두 배로 증가하는 것을 무어의 법칙으로 간주합니다. 실제로는 그렇지 않습니다.

마이크로프로세서 설계자는 몇 가지 새로운 트랜지스터를 프로세서 코어에 통합하고 아키텍처와 설계를 개선했으며 파이프라이닝을 통해 더 높은 클럭 속도를 달성했습니다. 나머지 새 트랜지스터는 메모리 액세스 속도를 높이기 위해 더 많은 캐시를 제공하는 데 사용됩니다. 대조적으로, GPU 설계자는 더 많은 캐시를 제공하기 위해 새로운 트랜지스터를 거의 사용하지 않으며, 대부분은 프로세서 코어를 개선하고 더 많은 프로세서 코어를 추가하는 데 사용됩니다. GPU는 다음 네 가지 메커니즘을 통해 속도를 높입니다.

  • GPU 설계자는 기하급수적으로 더 많은 트랜지스터를 적용하여 더 많은 병렬 및 더 빠른 프로세서를 구축함으로써 무어의 법칙의 이점을 직접적으로 얻습니다.

  • GPU 설계자는 시간이 지남에 따라 아키텍처를 개선하여 처리 효율성을 향상시킬 수 있습니다.

  • 무어의 법칙은 비용이 일정하다고 가정하므로 더 큰 칩과 더 많은 트랜지스터를 구입하는 데 더 많은 돈을 쓴다면 분명히 무어의 법칙 비율을 초과할 수 있습니다.

  • GPU 메모리 시스템은 더 빠른 메모리, 더 넓은 메모리, 데이터 압축 및 더 나은 캐싱을 사용하여 처리 속도만큼 빠르게 유효 대역폭을 증가시킵니다.

이 네 가지 접근 방식의 조합을 통해 역사적으로 GPU 성능은 약 12~18개월마다 정기적으로 두 배로 향상되어 무어의 법칙 속도를 초과했습니다. 이는 약 10년 동안 그래픽 애플리케이션에서 입증되었으며 속도 저하의 큰 징후가 보이지 않습니다. 가장 도전적인 속도 제한 장치는 메모리 시스템인 것으로 보이지만, 경쟁하는 혁신도 빠르게 발전하고 있습니다.

19.9.10.3 오류: GPU는 3D 그래픽만 렌더링하고 일반적인 계산을 수행할 수 없습니다

GPU는 3D 그래픽은 물론 2D 그래픽과 비디오를 렌더링하는 데 사용됩니다. 인터페이스에서 표현되는 그래픽 소프트웨어 개발자의 요구와 그래픽 API의 성능/기능 요구 사항을 충족하기 위해 GPU는 대규모 병렬 프로그래밍이 가능한 부동 소수점 프로세서가 되었습니다. 그래픽 세계에서 이러한 프로세서는 그래픽 API와 난해한 그래픽 프로그래밍 언어(OpenGL 및 Direct3D의 GLSL, Cg 및 HLSL)를 통해 프로그래밍됩니다. 그러나 GPU 설계자가 그래픽 API나 난해한 그래픽 언어 없이 병렬 프로세서 코어를 프로그래머에게 노출하는 것을 막을 수는 없습니다.

실제로 Tesla 기반 GPU 제품군은 프로그래머가 C 및 C++를 사용하여 범용 애플리케이션을 개발할 수 있도록 하는 CUDA라는 소프트웨어 환경을 통해 프로세서를 노출합니다. GPU는 Turing-complete 프로세서이므로 CPU가 할 수 있는 모든 프로그램을 실행할 수 있습니다.

19.9.10.4 오해: GPU는 배정밀도 부동 소수점 프로그램을 빠르게 실행할 수 없습니다

과거에는 GPU가 소프트웨어 에뮬레이션을 통하는 것 외에는 배정밀도 부동 소수점 프로그램을 실행할 수 없었고 소프트웨어 에뮬레이션도 전혀 빠르지 않았습니다. GPU는 인덱스 산술 표현(색상 조회 테이블)에서 색상 구성 요소당 8비트 정수, 고정 소수점 산술, 단정밀도 부동 소수점, 나중에는 배정밀도로 발전했습니다. 최신 GPU는 거의 모든 계산에 단정밀도 IEEE 부동 소수점 연산을 사용하며 배정밀도 연산을 사용하기 시작했습니다.

GPU는 약간의 추가 비용으로 배정밀도 부동 소수점과 단정밀도 부동 소수점을 지원할 수 있습니다. 오늘날 배정밀도는 단정밀도보다 약 5~10배 더 느리게 실행됩니다. 추가 비용이 발생하면 더 많은 애플리케이션에서 요구하는 배정밀도 성능이 단정밀도보다 단계적으로 향상될 수 있습니다.

19.9.10.5 오해: GPU는 부동 소수점 연산을 올바르게 수행할 수 없습니다

적어도 Tesla 아키텍처 제품군의 프로세서에서 GPU는 IEEE 754 부동 소수점 표준에 지정된 대로 단정밀도 부동 소수점 처리를 수행합니다. 따라서 정확성 측면에서 GPU는 다른 IEEE 754 호환 프로세서와 같습니다.

오늘날의 GPU는 비정규화된 숫자 처리 및 정확한 부동 소수점 예외 제공과 같이 표준에 설명된 일부 특정 기능을 구현하지 않지만 Tesla T10P GPU는 전체 IEEE 반올림, 융합 곱셈 누산 및 이중 정밀도 비정규화된 숫자 지원을 제공합니다.

19.9.10.6 오류: O(n) 알고리즘은 속도를 높이기가 어렵습니다

GPU가 데이터를 얼마나 빠르게 처리할 수 있는지에 관계없이 장치와 데이터를 주고받는 단계는 O(n) 복잡성(데이터 조각당 적은 양의 작업)으로 알고리즘 성능을 제한할 수 있습니다. PCIe 버스의 최대 전송 속도는 DMA 전송을 사용할 때 약 48GB/초이고, 비DMA 전송의 경우 약간 더 낮습니다. 이에 비해 시스템 메모리에 대한 CPU 액세스는 일반적으로 8~12GB/초입니다. 예를 들어, 벡터 추가는 GPU로의 입력 전송과 계산 반환 출력에 의해 제한됩니다. 데이터 전송 비용을 극복하는 세 가지 방법이 있습니다.

  • 복잡한 알고리즘의 여러 단계 간에 데이터를 앞뒤로 이동하는 대신 GPU에 데이터를 보관해 보세요. CUDA는 이를 지원하기 위해 의도적으로 출시 사이에 GPU에 데이터를 그대로 둡니다.

  • GPU는 복사, 복사 및 계산의 동시 작업을 지원하므로 장치가 계산을 수행하는 동안 데이터가 장치 안팎으로 흐를 수 있습니다. 이 모델은 도착 시 처리될 수 있는 모든 데이터 스트림에 유용합니다. 예를 들어 비디오 처리, 네트워크 라우팅, 데이터 압축/압축 풀기, 대규모 벡터 연산과 같은 더 간단한 계산 등이 있습니다.

  • CPU와 GPU를 함께 사용하면 작업의 하위 집합을 각각에 할당하고 시스템을 이기종 컴퓨팅 플랫폼으로 처리하여 성능이 향상됩니다. CUDA 프로그래밍 모델은 하나 이상의 GPU에 작업을 분배하고 스레드를 사용하지 않고(비동기 GPU 기능을 통해) CPU를 계속 사용할 수 있도록 지원하므로 모든 GPU와 CPU가 동시에 작동하도록 유지하여 문제를 더 빠르게 해결하는 것이 상대적으로 간단합니다.

19.9.10.7 함정: 더 긴 메모리 대기 시간을 처리하려면 더 많은 스레드를 사용하세요

CPU 코어는 일반적으로 단일 스레드를 최고 속도로 실행하도록 설계되었습니다. 최고 속도로 실행하려면 명령어가 실행되는 동안 각 명령어와 해당 데이터를 사용할 수 있어야 합니다. 다음 명령어가 준비되지 않았거나 해당 명령어에 필요한 데이터를 사용할 수 없으면 명령어가 실행될 수 없고 프로세서가 정지됩니다. 외부 메모리는 프로세서와 멀리 떨어져 있기 때문에 메모리에서 데이터를 가져오려면 낭비적인 실행 주기가 많이 필요합니다.

따라서 CPU가 멈추지 않고 계속 실행되려면 대규모 로컬 캐시가 필요하며, 메모리 지연 시간이 길어서 캐시에서 실행을 시도하면 피할 수 있습니다. 경우에 따라 프로그램 작업 세트 요구 사항이 캐시보다 클 수 있으며 일부 CPU는 대기 시간을 허용하기 위해 멀티스레딩을 사용하지만 코어당 스레드 수는 일반적으로 적은 수로 제한됩니다.

GPU 전략은 다릅니다. GPU 코어는 여러 스레드를 동시에 실행하도록 설계되었지만 모든 스레드에서 한 번에 하나의 명령만 실행할 수 있습니다. 다르게 말하면 GPU는 각 스레드를 느리게 실행하지만 전체적으로는 스레드를 효율적으로 실행한다는 것입니다. 실행할 다른 스레드가 있기 때문에 각 스레드는 일정량의 메모리 대기 시간을 허용할 수 있습니다.

단점은 메모리 대기 시간을 처리하기 위해 여러 개의 다중 스레드가 필요하다는 것입니다. 더욱이, 메모리 액세스가 스레드 간에 분산되거나 상호 연관되지 않으면 메모리 시스템은 각 개별 요청에 대한 응답 속도가 점차 느려지고 결국에는 여러 스레드라도 대기 시간을 감당할 수 없게 됩니다. 따라서 대기 시간을 처리하기 위한 "단지 더 많은 스레드 사용" 전략을 위해서는 충분한 스레드가 있어야 하며 메모리 액세스 위치 측면에서 스레드가 제대로 작동해야 한다는 함정이 있습니다.

19.10 파워

이 섹션에서는 모바일 장치용 전력 기술에 중점을 둡니다.

스마트폰은 이제 우리 일상생활에서 대체할 수 없는 필수품이 되었습니다. 직업적이든 개인 생활이든 모든 작업은 어떤 방식으로든 이러한 장치와 관련되어 있습니다. 점점 늘어나는 의존도를 충족시키기 위해 이러한 스마트폰은 나날이 더욱 강력해지고 있습니다. 강력한 프로세서, 더 많은 저장 공간, 향상된 카메라는 모든 구매자가 원하는 기능입니다.

운영 체제 외에도 소비자는 장치의 다양한 센서와 처리 기능을 사용하는 다양한 애플리케이션을 사용합니다. 이러한 모든 프로세스를 실행하려면 전원이 필요하며, 모바일 장치에서는 배터리입니다. 프로세스가 제대로 실행되도록 하려면 이러한 배터리를 수시로 충전해야 합니다. 긴 배터리 수명은 스마트폰 선택의 또 다른 중요한 기준이며, 배터리 수명 최적화와 관련된 기술은 스마트폰 업계의 다른 업종과는 다른 속도로 발전하고 있습니다.

스마트폰 배터리 수명은 하드웨어와 소프트웨어 기술을 통해 향상될 수 있습니다. 하드웨어를 바꾸면 더 큰 배터리를 장착한다는 의미도 있지만, 스마트폰의 크기도 커지는 것을 의미합니다. 효율적인 전력 관리 장치와 효율적인 집적 회로(IC)를 설계하는 것이 실행 가능한 솔루션입니다. 또한 배터리 집약적인 애플리케이션을 관리하고 사용 가능한 배터리를 현명하게 사용하기 위한 운영 체제의 소프트웨어 개선도 문제에 대한 또 다른 잠재적인 해결책으로 간주됩니다.

아래 그림은 휴대용 제품의 전원 관리를 보여줍니다.

스마트폰에는 전력을 소모하는 다양한 부품이 있으며, 일반적인 부품은 아래 그림과 같습니다.

19.10.1 전원 하드웨어

다양한 임베디드 시스템, 칩, 프로세서 및 센서가 통합되어 동기화되어 이러한 모바일 장치를 스마트하게 만듭니다. 각 하드웨어 장치는 실행 시 전력을 소비합니다. 이러한 모든 전자 모듈 중에서 트랜시버 모듈은 들어오는 패킷을 수신하기 위해 오랫동안 활성 상태를 유지하기 때문에 최대 전력을 소비합니다. 이러한 패킷의 데이터 전송을 최적화하기 위해 다양한 소프트웨어 기술이 논의되었습니다. DSP(디지털 신호 프로세서)는 이러한 트랜시버 모듈의 핵심 구성 요소입니다. 멀티미디어 사용을 위해 많은 양의 데이터를 처리합니다. DSP의 전원 전압을 낮추는 것은 전력 소모를 줄이는 직접적인 방법이다.

배터리 수명을 연장하기 위해 스마트폰의 DSP는 통화 중에는 저전력 및 높은 처리량의 MAC(Multiple-Accumulate) 작동이 필요하고 대기 중에는 저전력 간헐 작동이 필요합니다. 1V 다중 임계값 CMOS 회로는 전원 공급 장치 제어에 적합한 향상된 DFF와 함께 사용되는 임베디드 프로세서를 사용하는 전원 관리 기술과 간단한 병렬 아키텍처를 통해 이러한 요구 사항을 충족합니다.

프로세서와 트랜시버 외에도 화면은 배터리 전력을 소모하는 또 다른 주요 요소입니다. 백라이트가 필요한 LED 스크린은 더 많은 전력을 소비하므로 더 적은 전력을 소비하는 OLED와 같이 전력 효율이 더 높은 디스플레이로 교체할 수 있습니다. LED 및 LCD 디스플레이와 달리 OLED에는 백라이트가 필요하지 않습니다. OLED의 각 픽셀은 고유한 색상과 광원을 갖습니다. 따라서 OLED의 검은색 이미지는 완전히 검은색이 되지만 LED와 LCD의 경우에는 그렇지 않습니다.

연구에 따르면 시간이 지남에 따라 배터리 수명이 감소하는 것은 폴리불화비닐리덴(PVDF) 때문일 수도 있습니다. 배터리에서 흑연계 음극이 벗겨지는 것을 방지하기 위해 사용되는 접착제인 PVDF는 전도성이 없으며 접착력이 좋지 않아 전해액에 잘 녹는다. 또한 새로운 n형 공액 공중합체인 비스미노페라퀴논-p-페닐렌(BP) 접착제도 있습니다. 이 접착제는 기존 PVDF 기반 접착제보다 성능이 뛰어나 배터리 수명을 연장하고 노후화에 따른 배터리 성능 저하를 방지합니다.

MC13892의 전원 공급 장치 구조 다이어그램. 배터리 및 인터페이스 컨트롤과 같은 구성 요소가 포함되어 있습니다.

MC13892 전원 관리 및 사용자 인터페이스 블록 다이어그램.

또한 연구원들은 스마트폰에서 다양한 애플리케이션을 실행할 때 프로세서 및 입출력 장치의 다양한 매개변수에 대한 정보를 수집하는 동적 전력 관리 장치(PMU)를 제안했습니다. 그런 다음 PMU는 수집된 정보를 기반으로 예측 전력 인식 관리 솔루션을 제안합니다.

nRF5340이라는 전력 및 클럭 관리 시스템은 초저전력 애플리케이션에 최적화되어 최대 전력 효율성을 보장합니다. 전원 및 클럭 관리 시스템의 핵심은 아래 그림과 같이 PMU(Power Management Unit)입니다.

PMU는 주어진 시간에 시스템의 다양한 구성 요소에 필요한 전력 및 클럭 리소스를 자동으로 추적합니다. 가능한 최저 전력 소비를 달성하기 위해 PMU는 전력 및 클록 요청을 평가하고, 클록 소스를 자동으로 시작 및 중지하고, 조정기 작동 모드를 선택하여 시스템을 최적화합니다.

PMU에는 일반적으로 시스템 ON, 시스템 OFF, 강제 OFF의 세 가지 모드가 있습니다. 구체적인 내용은 다음과 같습니다.

시스템 ON 모드는 전원을 켠 후 재설정되는 기본 작동 모드입니다. System ON에서는 모든 기능 블록(예: CPU 및 주변 장치)이 소프트웨어 설정 구성 및 실행 중인 애플리케이션 상태에 따라 IDLE 또는 RUN 상태일 수 있습니다. 네트워크 코어의 CPU 및 주변 장치는 유휴, 실행 중 또는 강제 종료 모드일 수 있습니다.

PMU는 전원 요구 사항에 따라 적절한 내부 전원 공급 장치를 켜고 끌 수 있습니다. 주변 장치의 전력 요구 사항은 특정 작업이 트리거되거나 이벤트가 생성될 때 증가하거나 감소하는 활동 수준과 직접적인 관련이 있습니다.

  • 전압 및 주파수 스케일링. nRF5340은 내부 전압을 자동으로 조정하여 전력 효율성을 최적화합니다. 일부 구성 옵션에는 더 높은 내부 전압이 필요하며 이는 전력 소비 증가로 나타납니다. 이러한 구성은 다음과 같습니다.

애플리케이션 코어 클록의 주파수를 128MHz로 설정합니다. 이 모드에서 증가된 전력 소비는 CPU가 절전 모드일 때에도 관찰됩니다(예: WFI(인터럽트 대기) 또는 WFE(이벤트 대기) 명령 실행 후). CPU 절전 모드로 전환하기 전에 애플리케이션 코어의 클록을 64MHz로 구성하면 CPU와 주변 장치가 유휴 절전 모드에 있는 동안 시스템이 켜져 있는 동안 전력 소비를 줄일 수 있습니다.

  • 96MHz 클록 주파수를 사용하는 QSPI.

  • USB 주변기기를 사용하세요.

  • 디버깅하는 동안.

  • VREQCTRL을 사용하여 VREGRADIO 전원 공급 장치에 추가 전압을 요청합니다. - 전압 요청 제어.

  • 전원 하위 모드. 시스템 켜짐 모드에서 CPU와 모든 주변 장치가 유휴 상태일 때 시스템은 두 가지 전원 하위 모드 중 하나에 있을 수 있습니다.

전원 하위 모드에는 다음이 포함됩니다.

지속적인 지연. 일정한 대기 시간 모드에서는 CPU 깨우기 대기 시간과 PPI 작업 응답이 일정하게 유지되고 최소로 유지되어 항상 활성화된 리소스 세트에 의해 보호됩니다. 저전력 모드에 비해 지속적이고 예측 가능한 대기 시간을 갖는 장점은 전력 소비가 증가한다는 점입니다. CONSTRAT 작업을 트리거하면 일정한 대기 시간 모드가 선택됩니다.

  • 낮은 전력 소비. 저전력 모드에서 자동 전원 관리 시스템은 가장 전력 효율적인 전원 옵션을 선택하여 CPU 웨이크업 대기 시간과 PPI 작업 응답 변경을 희생하면서 가장 낮은 전력을 달성합니다. LOWPWR 작업을 트리거하여 저전력 모드를 선택합니다.

시스템이 시스템 ON으로 전환되면 기본적으로 저전력 하위 모드로 설정됩니다.

시스템 꺼짐은 시스템이 들어갈 수 있는 가장 깊은 절전 모드입니다. 이 모드에서는 시스템의 핵심 기능이 종료되고 진행 중인 모든 작업이 종료됩니다. 장치를 시스템 끄기 모드로 전환하려면 SYSTEMOFF 레지스터를 사용하십시오. 다음 작업을 수행하면 시스템이 꺼진 상태에서 절전 모드 해제가 시작됩니다.

  • GPIO 주변 장치에서 생성된 DETECT 신호.

  • LPCOMP 주변 장치에 의해 생성된 ANADETECT 신호.

  • NFCT 주변기기에서 생성된 SENSE 신호에 의해 현장에서 절전 모드를 해제합니다.

  • VBUS 핀에서 유효한 USB 전압이 감지되었습니다.

  • 디버깅 세션이 시작되었습니다.

  • 핀 재설정.

장치가 시스템 꺼진 상태에서 깨어날 때 시스템 재설정이 수행됩니다. 주변 장치 VMC(휘발성 메모리 컨트롤러)의 RAM 보존 설정에 따라 하나 이상의 RAM 섹션이 시스템 꺼짐 상태에서 유지될 수 있습니다. 시스템 종료에 들어가기 전에 시스템 종료에 들어갈 때 EasyDMA 지원 주변 장치가 활성화되어서는 안 됩니다. 또한 네트워크 코어가 유휴 상태인 것이 좋습니다. 즉, 주변 장치가 중지되고 CPU가 유휴 상태입니다.

Force-OFF 모드는 네트워크 코어에만 적용됩니다.

애플리케이션 코어는 레지스터 인터페이스 RESET-RESET 제어를 사용하여 네트워크 코어를 강제 종료 모드로 전환합니다. 이 모드에서는 가능한 최저 전력 소비를 달성하기 위해 네트워크 코어가 중지됩니다. 네트워크 코어가 강제 종료 모드에 있으면 애플리케이션 코어만 모드를 해제하여 네트워크 코어를 깨우고 CPU를 다시 시작할 수 있습니다.

아래와 같이 Application Core가 Network Core를 강제 종료 모드로 설정하기 전에 Network Core가 IDLE 상태에 있는 것이 좋습니다.

  • 모든 주변 장치가 중지됩니다.

  • VREQCTRL 전압 요청 제어를 사용하여 VREGRADIO 전원 공급 장치의 추가 전압을 취소합니다.

  • CPU가 IDLE 상태입니다. 이는 WFI 또는 WFE 명령을 실행 중임을 의미합니다.

네트워크 코어가 강제 종료 모드에서 깨어나면 재설정됩니다. 주변 VMC(Volatile Memory Controller)의 RAM 보존 설정에 따라 여러 RAM 섹션을 강제 종료 모드에서 예약할 수 있습니다.

19.10.2 전원 소프트웨어

더 높은 처리 능력과 더 빠른 인터넷 연결을 갖춘 최신 스마트폰의 큰 인기로 인해 Android 및 iOS에서 데이터 및 하드웨어 집약적인 애플리케이션의 수도 증가했습니다. WhatsApp, Instagram, Skype 등과 같은 애플리케이션에는 CPU 리소스가 필요할 뿐만 아니라 24시간 내내 인터넷 연결이 필요합니다. 연구에 따르면 유휴 모드에서 인터넷 사용은 전력 소비의 약 62%를 차지합니다. 또한, 3G/4G는 Wi-Fi에 비해 작은 크기의 데이터 패킷을 자주 교환할 때 배터리 소모가 더 많습니다.

데이터 압축, 패킷 집계, 배치 스케줄링과 같은 다양한 소프트웨어 기술을 사용하여 배터리 수명을 최적화할 수 있습니다. 스마트폰에서 서로 다른 애플리케이션에 의한 무작위 데이터 전송은 더 많은 배터리를 소모하므로 배치 스케줄링 메커니즘을 사용하여 애플리케이션을 통해 데이터를 반복적으로 전송함으로써 수면 시간을 최대화하고 깨우기 빈도를 최소화할 수 있습니다.

3G/4G에서 Wi-Fi로 데이터를 오프로드하는 것은 Wi-Fi가 데이터 전송 측면에서 3G/4G보다 효율적이기 때문에 배터리 수명을 향상시키는 또 다른 효과적인 방법입니다. 또 다른 소프트웨어 기술은 컴퓨팅을 위해 더 높은 수준의 컴퓨팅 작업(예: CPU 집약적 소프트웨어)을 클라우드로 오프로드하는 것입니다. 이 전략은 모바일 장치에서 Office 365 및 MATLAB과 같은 소프트웨어를 실행하는 데 사용할 수 있지만 클라우드와 장치 간의 통신 비용이 증가합니다. ASP(애플리케이션 상태 프록시)는 CPU 리소스뿐만 아니라 인터넷 데이터도 사용하는 백그라운드 애플리케이션을 억제하고 다른 장치로 전송하며 요청 시에만 해당 장치로 가져오는 또 다른 기술입니다.

스마트폰 산업은 처리 능력과 기타 기능이 배터리보다 훨씬 빠르게 발전하고 있으며, 연구자들은 이제 소프트웨어 및 하드웨어 수단을 통해 사용 가능한 배터리 에너지를 효과적으로 관리하는 전력 관리 기술에 중점을 두고 있습니다. 스마트폰의 배터리 수명을 늘리기 위해 위에서 언급한 다양한 기술이 여러 번 사용되고 있습니다.

동시에 실행 중인 애플리케이션에 에너지 소비를 할당하는 것은 어려운 일입니다. 전원 상태 전송이 때로는 다양한 애플리케이션 작업의 누적 결과이기 때문입니다.

예를 들어, 초당 N개의 패킷이 전송될 때 Wi-Fi 인터페이스가 저전력 상태에서 고전력 상태로 전환한다고 가정합니다. 이제 두 개의 애플리케이션이 초당 N/2 패킷으로 전송하여 Wi-Fi 인터페이스가 고전력 상태로 전환된다고 가정합니다. 마찬가지로, 한 애플리케이션이 초당 N 패킷으로 전송하고 다른 애플리케이션이 초당 9N 데이터로 전송한다고 상상해 보세요. 두 경우 모두 Wi-Fi 인터페이스는 고전력 상태이지만 각 애플리케이션에 전력 사용량이 어떻게 할당되는지는 명확하지 않습니다. 첫 번째 경우, 어느 앱도 개별적으로 고전력 상태를 트리거하지 않으므로 충전하는 것이 타당합니까? 두 번째 경우에는 두 앱 모두 고전력 상태를 트리거하므로 대략 동일한 충전량이 있어야 하는데 얼마만큼 충전되어야 할까요?

한 가지 가능한 해결책은 애플리케이션의 작업 부하에 따라 각 애플리케이션 간에 구성 요소 전력을 할당하는 것입니다. 즉, 첫 번째 경우 각 애플리케이션에는 고전력 상태 전력의 절반이 할당되고, 두 번째 경우에는 한 애플리케이션에 전력의 1/10이 할당되고 다른 애플리케이션에는 전력의 9/10이 할당됩니다. 이 솔루션은 애플리케이션 전력 소비의 합이 글로벌 전력 소비와 동일하다는 장점이 있습니다. 그러나 이 솔루션은 전력 사용량이 전송 속도의 선형 함수가 아니기 때문에 순진합니다. 따라서 이러한 방식으로 분석하는 것은 거의 의미가 없습니다. Wi-Fi 인터페이스의 전력 소비가 1차원 함수가 아니라는 점을 고려하면 이 솔루션은 더욱 모호해 보입니다.

대신, 우리는 각 구성 요소의 강력한 기능과 독립적으로 작동하고 응용 프로그램 개발자(PowerTutor의 주요 대상 사용자)에게 직관적인 솔루션이 필요했습니다. 각 구성 요소에 대해 각 특정 애플리케이션이 개별적으로 실행되는 것처럼 전력 소비를 계산합니다. 즉, 사례 1에서는 각 애플리케이션이 저전력 Wi-Fi 상태에 대해 요금을 청구하고 사례 2에서는 각 애플리케이션이 고전력 상태에 대해 요금을 청구합니다. 이는 애플리케이션 전력 소비의 합이 전체 전력 소비와 동일하다는 좋은 특성을 잃습니다(이 두 사례에서 알 수 있듯이 이는 낮지도 높지도 않은 추정치입니다). 그러나 이 정의를 사용하면 실행 중인 다른 응용 프로그램과 독립적으로 응용 프로그램의 전력 소비를 이해할 수 있으므로 PowerTutor 사용자는 리소스 공유에 관계없이 유사한 응용 프로그램 수준의 전력 특성을 관찰할 수 있습니다. 이는 특정 응용 프로그램을 최적화하는 데 중점을 두는 엔지니어에게 유용한 기능입니다. PowerTutor는 정확한 시스템 수준 전력 소비량도 보고합니다.

PowerTutor 인터페이스. (a) 애플리케이션 보기. (b) 차트 보기. (c) 파이 보기. 의도하지 않았지만 부적절한 스마트폰 하드웨어 구성 요소의 사용을 보여주는 이미지.

이 솔루션에는 제한 사항이 있습니다. 첫째, 애플리케이션이 리소스를 놓고 경쟁하는 경우 애플리케이션이 개별적으로 어떻게 작동할지 예측하기 어렵습니다. 둘째, 어떤 경우에는 특정 작업을 수행하기 위해 한 애플리케이션이 다른 애플리케이션을 호출하는 것을 볼 수 있습니다. 이 경우 전력 소비가 어떻게 분배되는지 명확하지 않습니다. 실제 Android 시스템에서는 미디어 서버 프로세스에서 이러한 동작이 자주 발생합니다. 셋째, 다른 응용 프로그램과 동시에 전송하는 등 영리한 기술을 사용하는 응용 프로그램은 이 방식으로 이득을 얻지 못합니다. 그러나 첫 번째 경우에는 우리가 할 수 있는 일이 없습니다. 다른 두 가지 문제를 해결하려면 관련된 애플리케이션의 의미에 대한 높은 수준의 이해가 필요하며 이는 현재 우리 도구의 범위를 벗어납니다.

아래 표에는 ADP1 및 ADP2 전화기의 내부 및 내부 전원 공급 장치 모델 변경 사항이 나와 있습니다. 유형 내 변동은 동일한 유형의 전화기 표본 평균으로 정규화된 표준 편차이고, 유형 간 변동은 두 유형의 전화기의 표본 평균 간의 차이입니다. 표의 전력 모델 매개변수는 특정 워크로드에 대한 전력 측정값으로 볼 수도 있습니다. 즉, 전력 모델 매개변수의 변화는 예측 오류와 선형적으로 관련됩니다. 예를 들어, 오디오 장치를 사용하는 애플리케이션의 경우 ADP1에 대해 파생된 전력 모델을 사용하여 다른 ADP1의 오디오 장치 소비를 예측할 때 4% 미만의 예측 오류가 예상됩니다. 이 데이터는 다음과 같은 결론을 뒷받침합니다.

Android 시스템의 그래픽 아키텍처는 다음과 같습니다.

아래 그림은 DRS(Dynamic Resolution Scaling) 시스템의 아키텍처를 보여줍니다. 해상도 스케일링을 활성화하기 위해 기존 Android 시스템에 두 개의 새로운 레이어가 추가되었습니다.

  • 첫 번째 레이어는 애플리케이션 레이어와 OpenGL ES/EGL 레이어 사이에 위치한 DRS 상위 레이어로, 필요한 OpenGLES 함수 호출과 EGL 함수 호출을 가로채서 적절한 디스플레이 해상도로 그래픽 렌더링이 완료되도록 합니다. 기본 디스플레이 해상도를 대상 해상도로 변환하기 위해 필요한 OpenGL ES/EGL 함수 호출의 매개변수에 배율 인수를 적용합니다.

  • 두 번째 레이어는 DRS 하위 레이어로 SurfaceFlinger 레이어와 Hardware Composer 사이에 위치합니다. 이 계층은 하드웨어 합성기에 전달된 함수 호출을 가로채서 합성이 올바른 디스플레이 해상도에서 완료되도록 합니다. 두 번째 레이어의 역할은 렌더링된 콘텐츠가 기본 디스플레이 해상도로 화면에 올바르게 표시될 수 있도록 DRS의 상위 레이어에서 해상도를 감소시킨 후 해상도를 높이는 것입니다.

두 DRS 레이어는 서로 동기화되어 BufferQueue의 동일한 그래픽 버퍼에 대해 동일한 대상 디스플레이 해상도를 사용하도록 합니다. 이는 사용자가 대상 디스플레이 해상도를 새 값으로 변경하면 상위 DRS 계층이 새 배율 인수를 사용하여 BufferQueue에 그래픽 버퍼를 생성하기 시작하기 때문에 필요합니다. DRS 하위 레이어는 이전에 생성된 그래픽 버퍼에 이전 배율 인수가 사용되고, 합성 중에 새로 생성된 그래픽 버퍼에만 새 배율 인수가 적용되도록 해야 합니다.

다양한 스케일링 요소에 따른 게임 및 벤치마크의 프레임당 정규화된 에너지는 다음과 같습니다.

다양한 디스플레이 해상도에서 얼마나 많은 전력을 절약할 수 있는지 평가하기 위해 14개의 게임을 포함한 15개의 앱과 1개의 벤치마크(위 표에 이름 지정)가 적용 범위 테스트에 포함되었습니다. S5 휴대폰에서 테스트 케이스를 실행하고 Monsoon Power Monitor를 사용하여 시스템 전력을 측정하고 휴대폰을 비행기 모드로 전환하고 GPS, 카메라 등 불필요한 하드웨어 구성 요소를 비활성화하고 백라이트 밝기를 50%로 설정합니다. GPU의 DVFS 추론을 방지하려면 GPU 주파수를 500MHz로 잠그십시오. GPU가 최소 60초 동안 500MHz에서 작동할 수 있는지 확인하기 위해 각 테스트 전에 전화기를 냉각시킵니다. 각 테스트는 3회 반복되었으며 평균 결과가 보고되었습니다.

프레임당 총 시스템 에너지(EPF)는 프로토타입 시스템의 에너지 절약을 평가하는 척도로 사용됩니다. 다양한 결과를 쉽게 비교할 수 있도록 결과는 기본 디스플레이 해상도로 정규화됩니다. 위의 표는 다양한 배율 인수에 대한 정규화된 EPF를 보여줍니다. 배율 인수는 기본 디스플레이 해상도로 정규화됩니다. 즉, 전체 해상도의 경우 배율 인수는 1.0입니다. 디스플레이 해상도가 절반으로 줄어들면(예: 배율 0.5로 디스플레이 해상도가 2560x1440 픽셀에서 1280x720 픽셀로 감소) 평균적으로 EPF는 16개 테스트 사례에 대해 15.7%에서 60.5% 범위로 30.1% 감소할 수 있습니다. 이 14개 게임의 경우 배율 인수 값에 관계없이 항상 고정된 프레임 속도로 실행됩니다. 따라서 실제로도 동일한 절전 효과를 얻을 수 있습니다(이 14개 게임만 계산하면 24.9%). 두 GFXBench 사례 모두 벤치마크는 항상 모든 GPU 처리 능력을 사용하려고 시도하기 때문에 전력 소비는 모든 스케일링 요소에 걸쳐 거의 일정하게 유지됩니다. 그러나 해상도는 프레임 속도에 큰 영향을 미칩니다. 배율이 더 작은 경우 더 높은 프레임 속도로 실행되어 더 나은 사용자 경험을 제공할 수 있습니다.

19.10.3 전력 최적화

애플리케이션 개발을 시작하기 전에 애플리케이션의 요구사항, 범위, 기능을 분석하고 정의하여 효율적인 기능과 원활한 사용자 경험을 보장하세요. 단일 목적을 위해 애플리케이션을 설계하고 사용자에게 가장 적합한 서비스를 분석합니다.

다음 지침은 화면 크기, 입력 방법 지원 등 다양한 특성을 갖춘 모바일 장치용 애플리케이션을 디자인하고 개발하는 데 도움이 됩니다.

  • 타겟 사용자를 이해하세요. 앱을 누가 사용할지, 어떤 용도로 사용할지, 어떤 모바일 장치를 보유하고 있는지 이해한 다음, 특정 사용 상황에 맞게 앱을 디자인하세요.

  • 작은 화면 디자인. 모바일 장치의 화면 크기는 데스크톱 장치의 화면 크기보다 훨씬 작습니다. 데스크톱 애플리케이션에서는 가능한 한 많은 콘텐츠를 화면에 맞추려고 노력하는 것이 합리적이지 않을 수 있으므로 애플리케이션 UI에 표시할 가장 관련성이 높은 콘텐츠가 무엇인지 신중하게 생각해 보세요.

  • 다양한 화면 크기에 맞게 설계되었습니다. 각 컨트롤의 위치와 크기를 모니터 크기에 연결하면 모든 해상도에서 동일한 정보 세트를 화면에 표시할 수 있으며, 고해상도 장치에서는 더 미세한 그래픽만 표시할 수 있습니다.

  • 화면 방향을 변경하는 디자인. 일부 장치는 화면 회전을 지원하며 이러한 장치에서는 방향을 고려하고 화면 회전에 따라 디스플레이를 동적으로 조정하여 응용 프로그램을 세로 또는 가로 방향으로 표시할 수 있습니다.

  • 앱 내에서 움직임을 디자인하는 직관적인 방법. 모바일 장치에는 마우스와 풀사이즈 키보드가 없기 때문에 사용자는 앱을 이동하려면 터치스크린이나 5방향 탐색 패드를 사용해야 하며, 많은 사용자가 한 손으로 장치를 제어합니다. 최적화된 사용자 경험을 만들려면 사용자가 스크롤하고 입력하는 대신 한 번의 클릭으로 정보에 액세스할 수 있도록 허용하세요.

  • 제한된 입력 방법 설계. 애플리케이션은 사용자로부터 현재 작업에 대한 정보를 수집합니다. 터치스크린 입력 외에도 일부 장치에는 5방향 탐색 패드, 키패드, 키패드와 같은 물리적 키가 포함되어 있습니다. 사용자는 목록, 확인란, 라디오 버튼, 텍스트 필드 등 화면 컨트롤을 사용하여 정보를 입력합니다.

  • 응답 시간 개선. 지연 시간으로 인해 사용자 상호 작용이 지연될 수 있습니다. 사용자가 애플리케이션이 느리다고 인식하면 좌절감을 느끼고 사용을 중단할 가능성이 높습니다.

  • 배터리 시간을 절약하세요. 모바일 장치는 항상 전원에 연결되어 있지는 않지만 배터리를 사용하여 전원을 공급합니다. 총 전력 소비를 허용 가능한 수준으로 유지하고 사용자의 배터리 시간이 부족해지는 것을 방지하기 위해 전력 소비를 최적화합니다.

  • 네트워크 문제를 고려하세요. 사용자가 고정 요금제 데이터 요금제나 Wi-Fi 지원을 받지 않는 경우 모바일 네트워크 연결에 비용이 발생합니다. 또한 사용자가 장치를 가지고 이동함에 따라 연결에 사용할 수 있는 네트워크가 지속적으로 변경됩니다.

  • 기기의 처리 제한 사항을 기억하세요. 장치에서 사용할 수 있는 메모리는 제한되어 있으므로 주의해서 사용해야 합니다. 모든 모바일 장치에는 공통 기능이 있지만 각 장치는 사용 가능한 리소스 및 추가 기능 측면에서 독립적이므로 모든 대상 장치의 제약 조건을 고려해야 합니다.

  • 앱 효율성과 배터리 수명을 극대화합니다.

스위치 효율성을 최적화합니다(프로세서가 가장 많이 사용되는 위치를 목표로 함).

  • PFM 및 PWM-PS를 사용하여 저전력 조건에서 효율을 향상시킵니다.

  • BOM(Bill of Materials) 비용 및 면적을 최소화합니다.

  • 배터리 기술(리튬 이온 배터리 1개, 2개 또는 3개).

  • 적용 범위 내에서 전력 소비를 유지하십시오.

  • 소프트웨어 드라이버 지원.

  • 유연한 전원 켜기 순서/기본 전압, 다중 프로세서 및 주변 장치 지원.

  • PMIC 내부 또는 외부 오디오. 내부 장점: 비용 절감, 보드 공간 절약, 외부 장점: 소음 감소 및 유연성 향상.

  • 동적 해상도를 사용합니다. 자세한 내용은 이전 섹션을 참조하세요.

Qualcomm은 핵심 기술 블록과 전체 SoC(시스템온칩)를 맞춤화하여 에너지 절약을 달성하기 위해 전체적인 시스템 접근 방식을 취하고 있습니다.

이 시스템 접근 방식에는 네 가지 주요 수준의 전력 및 열 최적화가 포함됩니다.

  • 전용 처리 엔진. 전용 처리 엔진 및 PMIC(전력 관리 집적 회로), RF(무선 주파수) 칩 등과 같은 기타 주요 구성 요소의 맞춤형 설계

마이크로아키텍처.

aSMP 및 기타 일반적인 SMP 구현.

  • 회로 설계.

  • 트랜지스터 레벨 디자인.

Snapdragon SoC 내의 처리 엔진.

  • 스마트 통합. 영리하게 통합된 기술 블록과 설계된 시스템 아키텍처.

시스템 아키텍처/상호 연결.

  • 캐시 및 메모리 설계.

  • 소프트웨어 및 하드웨어 가속.

  • 시스템 소프트웨어 최적화. 소프트웨어와 하드웨어를 긴밀하게 통합합니다.

소프트웨어 도구 및 API.

  • 소프트웨어, OS 및 컴파일러 최적화.

  • 전력 및 열 알고리즘.

  • 장치 수준 최적화. 모바일 장치의 다른 모든 구성 요소를 신중하게 고려하고 전체 솔루션의 작동을 최적화하십시오.

전력 및 열 모델.

  • 장비 구성 요소 최적화.

  • OEM 모범 사례.

전력 및 열 제약이 있는 장치에서 더 높은 성능을 제공해야 하는 점점 증가하는 과제를 해결하려면 모바일 중심 설계 접근 방식이 중요합니다. Qualcomm은 전력 및 열 관리에 대한 전체적인 시스템 접근 방식을 취하고 있으며, 모바일 SoC는 특수 처리 엔진을 설계하고 이를 영리하게 통합하며 시스템 소프트웨어와 전체 장치를 최적화하여 모바일 장치가 최고의 사용자 경험을 제공할 수 있도록 함으로써 전력과 열 효율성의 최상의 균형을 달성합니다.

전원 공급 장치 기술에 대한 자세한 내용은 다음에서 확인할 수 있습니다.

  • 모바일 시스템 및 애플리케이션을 위한 전력, 성능 모델링 및 최적화

  • 18.11.7 전원 관리

19.11 UE 하드웨어

이 장에서는 UE 5.1의 소스 코드를 기반으로 관련된 하드웨어 인터페이스와 로직을 분석합니다.

19.11.1 CPU

다음 인터페이스는 CPU 성능 수준과 같은 매개변수를 계산할 수 있습니다.

cpp
// GenericPlatformSurvey.h

struct FSynthBenchmarkResults 
{
    FSynthBenchmarkStat CPUStats[2];

    // 계산/산출(Calculate)CPU성능 단계 ,100테이블 는 단계 의 CPU, 100, 100。
    float ComputeCPUPerfIndex(TArray<float>* OutIndividualResults = nullptr) const;
};

다음 인터페이스는 데이터 추적, 활용도, 분석기 등을 포함하여 CPU 성능을 추적할 수 있습니다.

cpp
// CpuProfilerTrace.h

struct FCpuProfilerTrace
{
    static uint32 OutputEventType(const ANSICHAR* Name, const ANSICHAR* File = nullptr, uint32 Line = 0);
    static uint32 OutputEventType(const TCHAR* Name, const ANSICHAR* File = nullptr, uint32 Line = 0);
    static void OutputBeginEvent(uint32 SpecId);
    static void OutputBeginDynamicEvent(const ANSICHAR* Name, const ANSICHAR* File = nullptr, uint32 Line = 0);
    static void OutputBeginDynamicEvent(const TCHAR* Name, const ANSICHAR* File = nullptr, uint32 Line = 0);
    static void OutputBeginDynamicEvent(const FName& Name, const ANSICHAR* File = nullptr, uint32 Line = 0);
    static void OutputEndEvent();
    static void OutputResumeEvent(uint64 SpecId, uint32& TimerScopeDepth);
    static void OutputSuspendEvent();

    class FEventScope
    {
        (...)
    };

    struct FDynamicEventScope
    {
        (...)
    };

    (...)
};

// CpuProfilerTraceAnalysis.h

class FCpuProfilerAnalyzer : public UE::Trace::IAnalyzer
{
public:
    virtual void OnAnalysisBegin(const FOnAnalysisContext& Context) override;
    virtual void OnAnalysisEnd(/*const FOnAnalysisEndContext& Context*/) override;
    virtual bool OnEvent(uint16 RouteId, EStyle Style, const FOnEventContext& Context) override;

private:
    IAnalysisSession& Session;
    IEditableTimingProfilerProvider& EditableTimingProfilerProvider;
    IEditableThreadProvider& EditableThreadProvider;
    TMap<uint32, FThreadState*> ThreadStatesMap;
    TMap<uint32, uint32> SpecIdToTimerIdMap;
    TMap<const TCHAR*, uint32> ScopeNameToTimerIdMap;
    uint32 CoroutineTimerId = ~0;
    uint32 CoroutineUnknownTimerId = ~0;
    uint64 TotalEventSize = 0;
    uint64 TotalScopeCount = 0;
    double BytesPerScope = 0.0;

    (...)
};

다음 인터페이스에는 CPU 클록, 주파수, 선호도 및 기타 정보와 인터페이스가 포함되어 있습니다.

cpp
// GenericPlatformTime.h

// CPU을(를) 활용하여 데이터
struct FCPUTime
{
    float CPUTimePct;         // 개 의 CPU을(를) 활용하여 。
    float CPUTimePctRelative; // 개 개 의 CPU을(를) 활용하여 ,만약 CPUTimePct로 8.0%,디바이스6개 ,이면 로 48.0%。
};

//
struct FGenericPlatformTime
{
    // 、、주파수(Frequency)인터페이스
    static TCHAR* StrDate( TCHAR* Dest, SIZE_T DestSize );
    static TCHAR* StrTime( TCHAR* Dest, SIZE_T DestSize );
    static const TCHAR* StrTimestamp();
    static FString PrettyTime( double Seconds );
    static bool UpdateCPUTime( float DeltaTime );
    static bool UpdateThreadCPUTime(float = 0.0);
    static void AutoUpdateGameThreadCPUTime(double UpdateInterval);
    static FCPUTime GetCPUTime();
    static FCPUTime GetThreadCPUTime();
    static double GetLastIntervalCPUTimeInSeconds();
    static double GetLastIntervalThreadCPUTimeInSeconds();
    static double GetSecondsPerCycle();
    static float ToMilliseconds( const uint32 Cycles );
    static float ToSeconds( const uint32 Cycles );
    static double GetSecondsPerCycle64();
    static double ToMilliseconds64(const uint64 Cycles);
    static double ToSeconds64(const uint64 Cycles);

    (...)

protected:

    static double SecondsPerCycle;
    static double SecondsPerCycle64;
    static double LastIntervalCPUTimeInSeconds;
};

// PlatformAffinity.h

struct FThreadAffinity 
{ 
    uint64 ThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask(); 
    uint16 ProcessorGroup = 0; 
};

19.11.2 메모리

다음 코드에는 메모리 하드웨어 정보, 할당, 캐싱, 풀링 및 기타 인터페이스가 포함되어 있습니다.

cpp
// GenericPlatformMemory.h

struct FGenericPlatformMemory
{
    static bool bIsOOM; // 는 메모리
    static uint64 OOMAllocationSize; // 설정(Set)로 트리거 메모리 의 할당(Allocate)크기 ,이면 로 .
    static uint32 OOMAllocationAlignment; // 설정(Set)로 트리거 메모리 의 페어링(Pairing),이면 로 。
    static void* BackupOOMMemoryPool; // 메모리 의 할당(Allocate)버퍼 。을(를) 활용하여 OOM처리(Process) 및 。
    static uint32 BackupOOMMemoryPoolSize; // BackupOOMMemoryPool의 크기 ()。

    // 을(를) 활용하여 메모리 의 메모리 。의 플랫폼,의 (、GPU)。개 플랫폼로써 추가(Add)의 메모리 ,플랫폼,StatManager개 의 을(를) 활용하여 메모리 (을(를) 활용하여 배열 FPlatformMemory::MCR_max big)의 메모리 .
    enum EMemoryCounterRegion
    {
        MCR_Invalid, // not memory
        MCR_Physical, // main system memory
        MCR_GPU, // memory directly a GPU (graphics card, etc)
        MCR_GPUSystem, // system memory directly accessible by a GPU
        MCR_TexturePool, // presized texture pools
        MCR_StreamingPool, // amount of texture pool available for streaming.
        MCR_UsedStreamingPool, // amount of texture pool used for streaming.
        MCR_GPUDefragPool, // presized pool of memory that can be defragmented.
        MCR_PhysicalLLM, // total physical memory including CPU and GPU
        MCR_MAX
    };

    // 을(를) 활용하여 의 할당(Allocate).
    enum EMemoryAllocatorToUse
    {
        Ansi, // Default C allocator
        Stomp, // Allocator to check for memory stomping
        TBB, // Thread Building Blocks malloc
        Jemalloc, // Linux/FreeBSD malloc
        Binned, // Older binned malloc
        Binned2, // Newer binned malloc
        Binned3, // Newer VM-based binned malloc, 64 bit only
        Platform, // Custom platform specific allocator
        Mimalloc, // mimalloc
    };
    static EMemoryAllocatorToUse AllocatorToUse;

    enum ESharedMemoryAccess
    {
        Read    =        (1 << 1),
        Write    =        (1 << 2)
    };

    // 메모리 의 을(를) 활용하여 테이블
    struct FSharedMemoryRegion
    {
        TCHAR    Name[MaxSharedMemoryName];
        uint32   AccessMode;
        void *   Address;
        SIZE_T   Size;
    };

    // 메모리 .
    static void Init();
    static void OnOutOfMemory(uint64 Size, uint32 Alignment);
    static void SetupMemoryPools();
    static uint32 GetBackMemoryPoolSize()
    static FMalloc* BaseAllocator();
    static FPlatformMemoryStats GetStats();
    static uint64 GetMemoryUsedFast();
    static void GetStatsForMallocProfiler( FGenericMemoryStats& out_Stats );
    static const FPlatformMemoryConstants& GetConstants();
    static uint32 GetPhysicalGBRam();

    static bool PageProtect(void* const Ptr, const SIZE_T Size, const bool bCanRead, const bool bCanWrite);
    
    // 할당(Allocate).
    static void* BinnedAllocFromOS( SIZE_T Size );
    static void BinnedFreeToOS( void* Ptr, SIZE_T Size );
    static void NanoMallocInit();
    static bool PtrIsOSMalloc( void* Ptr);
    static bool IsNanoMallocAvailable();
    static bool PtrIsFromNanoMalloc( void* Ptr);

    // 메모리 및 .
    class FBasicVirtualMemoryBlock
    {
    protected:
        void *Ptr;
        uint32 VMSizeDivVirtualSizeAlignment;

    public:
        FBasicVirtualMemoryBlock(const FBasicVirtualMemoryBlock& Other) = default;
        FBasicVirtualMemoryBlock& operator=(const FBasicVirtualMemoryBlock& Other) = default;
        FORCEINLINE uint32 GetActualSizeInPages() const;
        FORCEINLINE void* GetVirtualPointer() const;

        void Commit(size_t InOffset, size_t InSize);
        void Decommit(size_t InOffset, size_t InSize);
        void FreeVirtual();

        void CommitByPtr(void *InPtr, size_t InSize);
        void DecommitByPtr(void *InPtr, size_t InSize);
        void Commit();
        void Decommit();
        size_t GetActualSize() const;

        static FPlatformVirtualMemoryBlock AllocateVirtual(size_t Size, ...);
        
        static size_t GetCommitAlignment();
        static size_t GetVirtualSizeAlignment();

    };

    // 데이터 및
    static bool BinnedPlatformHasMemoryPoolForThisSize(SIZE_T Size);
    static void DumpStats( FOutputDevice& Ar );
    static void DumpPlatformAndAllocatorStats( FOutputDevice& Ar );

    static EPlatformMemorySizeBucket GetMemorySizeBucket();

    // 메모리 데이터 .
    static void* Memmove( void* Dest, const void* Src, SIZE_T Count );
    static int32 Memcmp( const void* Buf1, const void* Buf2, SIZE_T Count );
    static void* Memset(void* Dest, uint8 Char, SIZE_T Count);
    static void* Memzero(void* Dest, SIZE_T Count);
    static void* Memcpy(void* Dest, const void* Src, SIZE_T Count);
    static void* BigBlockMemcpy(void* Dest, const void* Src, SIZE_T Count);
    static void* StreamingMemcpy(void* Dest, const void* Src, SIZE_T Count);
    static void* ParallelMemcpy(void* Dest, const void* Src, SIZE_T Count, EMemcpyCachePolicy Policy = EMemcpyCachePolicy::StoreCached);

    (...)
};

// 구조체 을(를) 활용하여 저장(Save)플랫폼의 을(를) 활용하여 메모리 상수(Constant)。실행(Execute)의 개 。
struct FGenericPlatformMemoryConstants
{
    // 메모리 ,로써 로 (32코드 의 64디바이스,처리(Process)>4GB)。
    uint64 TotalPhysical;
    // 메모리 ,로써 로
    uint64 TotalVirtual;
    // 의 크기 ,로써 로 ,는 RAM의 PageProtection()、 및 (예: )의 。
    SIZE_T PageSize;

    // 만약 메모리 로써 PageSize의 할당(Allocate),이면 플랫폼(예: VirtualAlloc()의 로 64KB),는 을(를) 활용하여 의 할당(Allocate)크기 。
    SIZE_T OsAllocationGranularity;
    // Binned2 malloc 내에서 “”의 크기 ,로써 로 ,로 64KB。BinnedMloc로부터 BinnedAllocFromOS()반환(Return)의 메모리 및 BinnedPageSize정렬 。
    SIZE_T BinnedPageSize;
    // BinnedMalloc 내에서 의 “할당(Allocate)”,즉, BinnedMlloc로써 의 할당(Allocate)메모리 。만약 로 0,Binned을(를) 활용하여 BinnedPageSize.
    SIZE_T BinnedAllocationGranularity;

    // AddressLimit-개 파라미터 는 BinnedAllocFromOS()반환(Return)의 범위 의 。Binned Malloc조정(Adjust)구조체 ,로써 탐색/조회(Find/Lookup)범위 의 메모리 할당(Allocate)O(1)。개 범위 는 로써 의 ,탐색/조회(Find/Lookup)포인트
    uint64 AddressLimit;
    // RAM(GB),PC의 디바이스1。을(를) 활용하여 “course tuning”,FPlatformMisc::NumberOfCores()。
    uint32 TotalPhysicalGB;
};

// 을(를) 활용하여 저장(Save)플랫폼의 을(를) 활용하여 메모리 정보 ,실행(Execute)의 개 。
struct FGenericPlatformMemoryStats : public FPlatformMemoryConstants
{
    // 현재(Current)을(를) 활용하여 의 메모리 ,로써 로 。
    uint64 AvailablePhysical;
    // 현재(Current)을(를) 활용하여 의 메모리 ()。
    uint64 AvailableVirtual;
    // 을(를) 활용하여 의 메모리 ,로써 로 。
    uint64 UsedPhysical;
    // 을(를) 활용하여 의 메모리 의 ,로써 로
    uint64 PeakUsedPhysical;
    // 을(를) 활용하여 의 메모리 。
    uint64 UsedVirtual;
    // 을(를) 활용하여 의 메모리 의 。
    uint64 PeakUsedVirtual;
    
    // 메모리 상태 ,을(를) 활용하여 을(를) 활용하여 메모리 비활성화(Disable) 또는 메모리 의 플랫폼。
    enum class EMemoryPressureStatus : uint8 
    { 
        Unknown,
        Nominal, 
        Critical, // OOM(Out Of Memory)조건 의
    };
    EMemoryPressureStatus GetMemoryPressureStatus();

    struct FPlatformSpecificStat
    {
        const TCHAR* Name;
        uint64 Value;
    };

    TArray<FPlatformSpecificStat> GetPlatformSpecificStats() const;
    uint64 GetAvailablePhysical(bool bExcludeExtraDevMemory) const;
    // 에 의해 FCsvProfiler::EndFrame호출(Call)로써 설정(Set)플랫폼의 CSV정보 。
    void SetEndFrameCsvStats() const {}
};

다음 인터페이스는 D3D12의 특정 리소스를 CPU 또는 GPU에서 읽고 쓸 수 있는지 여부를 나타냅니다.

cpp
// D3D12Util.h

inline bool IsCPUWritable(D3D12_HEAP_TYPE HeapType, const D3D12_HEAP_PROPERTIES *pCustomHeapProperties = nullptr);
inline bool IsGPUOnly(D3D12_HEAP_TYPE HeapType, const D3D12_HEAP_PROPERTIES *pCustomHeapProperties = nullptr);
inline bool IsCPUAccessible(D3D12_HEAP_TYPE HeapType, const D3D12_HEAP_PROPERTIES* pCustomHeapProperties = nullptr);

다음 코드는 메모리 추적 관련 유형 및 인터페이스입니다.

cpp
// MemoryTrace.h

enum EMemoryTraceRootHeap : uint8
{
    SystemMemory, // RAM
    VideoMemory, // VRAM
    EndHardcoded = VideoMemory,
    EndReserved = 15
};

// 힙 플래그 。
enum class EMemoryTraceHeapFlags : uint16
{
    None = 0,
    Root = 1 << 0,
    NeverFrees = 1 << 1, // The heap doesn't free (e.g. linear allocator)
};
ENUM_CLASS_FLAGS(EMemoryTraceHeapFlags);

enum class EMemoryTraceHeapAllocationFlags : uint8
{
    None = 0,
    Heap = 1 << 0, // Is a heap, can be used to unmark alloc as heap.
};
ENUM_CLASS_FLAGS(EMemoryTraceHeapAllocationFlags);

class FMalloc* MemoryTrace_Create(class FMalloc* InMalloc);
void MemoryTrace_Initialize();
HeapId MemoryTrace_HeapSpec(HeapId ParentId, const TCHAR* Name, EMemoryTraceHeapFlags Flags = EMemoryTraceHeapFlags::None);
HeapId MemoryTrace_RootHeapSpec(const TCHAR* Name, EMemoryTraceHeapFlags Flags = EMemoryTraceHeapFlags::None);
void MemoryTrace_MarkAllocAsHeap(uint64 Address, HeapId Heap, ...);
void MemoryTrace_UnmarkAllocAsHeap(uint64 Address, HeapId Heap);
void MemoryTrace_Alloc(uint64 Address, uint64 Size, uint32 Alignment, HeapId RootHeap = EMemoryTraceRootHeap::SystemMemory);
void MemoryTrace_Free(uint64 Address, HeapId RootHeap = EMemoryTraceRootHeap::SystemMemory);
void MemoryTrace_ReallocFree(uint64 Address, HeapId RootHeap = EMemoryTraceRootHeap::SystemMemory);
void MemoryTrace_ReallocAlloc(uint64 Address, uint64 NewSize, uint32 Alignment, HeapId RootHeap = EMemoryTraceRootHeap::SystemMemory);

(...)

다음 코드에는 GPU 리소스 배열 작업이 포함됩니다.

cpp
// ResourceArray.h

// 리소스 배열 의 타입/유형 의 인터페이스 。
class FResourceArrayInterface
{

public:
    virtual const void* GetResourceData() const = 0;
    virtual uint32 GetResourceDataSize() const = 0;
    virtual void Discard() = 0;
    virtual bool IsStatic() const = 0;
    virtual bool GetAllowCPUAccess() const = 0;
    virtual void SetAllowCPUAccess( bool bInNeedsCPUAccess ) = 0;
};

// 로 리소스 타입/유형 할당(Allocate)GPU메모리 。
class FResourceBulkDataInterface
{
public:
    virtual const void* GetResourceBulkData() const = 0;
    virtual uint32 GetResourceBulkDataSize() const = 0;
    virtual void Discard() = 0;
    
    enum class EBulkDataType
    {
        Default,
        MediaTexture,
        VREyeBuffer,
    };
    virtual EBulkDataType GetResourceType() const;
};

// 로 텍스처(Texture)리소스 할당(Allocate)GPU메모리 。
class FTexture2DResourceMem : public FResourceBulkDataInterface
{
public:
    virtual void* GetMipData(int32 MipIdx) = 0;
    virtual int32 GetNumMips() = 0;
    virtual int32 GetSizeX() = 0;
    virtual int32 GetSizeY() = 0;

    virtual bool IsValid() = 0;
    virtual bool HasAsyncAllocationCompleted() const = 0;
    virtual void FinishAsyncAllocation() = 0;
    virtual void CancelAsyncAllocation() = 0;
};

다음은 운영 체제 페이지 캐시 할당자입니다.

cpp
// CachedOSPageAllocator.h

struct FCachedOSPageAllocator
{
protected:
    struct FFreePageBlock
    {
        void*  Ptr;
        SIZE_T ByteSize;
    };

    void* AllocateImpl(SIZE_T Size, uint32 CachedByteLimit, FFreePageBlock* First, FFreePageBlock* Last, ...);
    void FreeImpl(void* Ptr, SIZE_T Size, uint32 NumCacheBlocks, uint32 CachedByteLimit, FFreePageBlock* First, ...);
    void FreeAllImpl(FFreePageBlock* First, uint32& FreedPageBlocksNum, SIZE_T& CachedTotal, FCriticalSection* Mutex);
};

template <uint32 NumCacheBlocks, uint32 CachedByteLimit>
struct TCachedOSPageAllocator : private FCachedOSPageAllocator
{
    void* Allocate(SIZE_T Size, uint32 AllocationHint = 0, FCriticalSection* Mutex = nullptr);
    void Free(void* Ptr, SIZE_T Size, FCriticalSection* Mutex = nullptr, bool ThreadIsTimeCritical = false);
    void FreeAll(FCriticalSection* Mutex = nullptr);
    void UpdateStats();
    uint64 GetCachedFreeTotal();

private:
    FFreePageBlock FreedPageBlocks[NumCacheBlocks*2];
    SIZE_T         CachedTotal;
    uint32         FreedPageBlocksNum;
};

// CachedOSVeryLargePageAllocator.h

// 의 캐싱(Cache)할당(Allocate)。
class FCachedOSVeryLargePageAllocator
{
    // 설정(Set)로 의 ,개 을(를) 활용하여 할당(Allocate),개 을(를) 활용하여 로 ==SizeOfSubPage의 할당(Allocate)
#if UE_VERYLARGEPAGEALLOCATOR_TAKEONALL64KBALLOCATIONS
    static constexpr uint64 AddressSpaceToReserve = ((1024LL * 1024LL * 1024LL) * UE_VERYLARGEPAGEALLOCATOR_RESERVED_SIZE_IN_GB * 2LL);
    static constexpr uint64 AddressSpaceToReserveForSmallPool = AddressSpaceToReserve/2;
#else
    static constexpr uint64 AddressSpaceToReserve = ((1024 * 1024 * 1024LL) * UE_VERYLARGEPAGEALLOCATOR_RESERVED_SIZE_IN_GB);
    static constexpr uint64 AddressSpaceToReserveForSmallPool = AddressSpaceToReserve;
#endif
    static constexpr uint64 SizeOfLargePage = (UE_VERYLARGEPAGEALLOCATOR_PAGESIZE_KB * 1024);
    static constexpr uint64 SizeOfSubPage = (1024 * 64);
    static constexpr uint64 NumberOfLargePages = (AddressSpaceToReserve / SizeOfLargePage);
    static constexpr uint64 NumberOfSubPagesPerLargePage = (SizeOfLargePage / SizeOfSubPage);

public:
    void* Allocate(SIZE_T Size, uint32 AllocationHint = 0, FCriticalSection* Mutex = nullptr);
    void Free(void* Ptr, SIZE_T Size, FCriticalSection* Mutex = nullptr, bool ThreadIsTimeCritical = false);
    void FreeAll(FCriticalSection* Mutex = nullptr);
    void UpdateStats();

    uint64 GetCachedFreeTotal();
    bool IsPartOf(const void* Ptr);

private:
    (...)

    FLargePage*    FreeLargePagesHead[FMemory::AllocationHints::Max];            // no backing store
    FLargePage*    UsedLargePagesHead[FMemory::AllocationHints::Max];            // has backing store and is full
    FLargePage*    UsedLargePagesWithSpaceHead[FMemory::AllocationHints::Max];    // has backing store and still has room
    FLargePage*    EmptyButAvailableLargePagesHead[FMemory::AllocationHints::Max];    // has backing store and is empty
    FLargePage    LargePagesArray[NumberOfLargePages];
    TCachedOSPageAllocator<CACHEDOSVERYLARGEPAGEALLOCATOR_MAX_CACHED_OS_FREES, CACHEDOSVERYLARGEPAGEALLOCATOR_BYTE_LIMIT> CachedOSPageAllocator;
};
CORE_API extern bool GEnableVeryLargePageAllocator;

// PooledVirtualMemoryAllocator.h

// Class로부터 FMallocBinned2의 OS할당(Allocate)。
struct FPooledVirtualMemoryAllocator
{
    void* Allocate(SIZE_T Size, uint32 AllocationHint = 0, FCriticalSection* Mutex = nullptr);
    void Free(void* Ptr, SIZE_T Size, FCriticalSection* Mutex = nullptr, bool ThreadIsTimeCritical = false);
    void FreeAll(FCriticalSection* Mutex = nullptr);

    // 크기 의 구조체
    struct FPoolDescriptorBase
    {
        FPoolDescriptorBase* Next;
        SIZE_T VMSizeDivVirtualSizeAlignment;
    };
    uint64 GetCachedFreeTotal();
    void UpdateStats();

private:
    enum Limits
    {
        NumAllocationSizeClasses    = 64,
        MaxAllocationSizeToPool        = NumAllocationSizeClasses * 65536,

        MaxOSAllocCacheSize            = 64 * 1024 * 1024,
        MaxOSAllocsCached            = 64
    };

    int32 GetAllocationSizeClass(SIZE_T Size);
    SIZE_T CalculateAllocationSizeFromClass(int32 Class);
    int32 NextPoolSize[Limits::NumAllocationSizeClasses];
    FPoolDescriptorBase* ClassesListHeads[Limits::NumAllocationSizeClasses];
    FCriticalSection     ClassesLocks[Limits::NumAllocationSizeClasses];
    void DecideOnTheNextPoolSize(int32 SizeClass, bool bGrowing);
    FPoolDescriptorBase* CreatePool(SIZE_T AllocationSize, int32 NumPooledAllocations);
    void DestroyPool(FPoolDescriptorBase* Pool);
    FCriticalSection OsAllocatorCacheLock;
    TCachedOSPageAllocator<MaxOSAllocsCached, MaxOSAllocCacheSize> OsAllocatorCache;
};

다음은 가상 메모리 할당자입니다.

cpp
// VirtualAllocator.h

class FVirtualAllocator
{
    struct FFreeLink
    {
        void *Ptr = nullptr;
        FFreeLink* Next = nullptr;
    };
    struct FPerBlockSize
    {
        int64 AllocBlocksSize = 0;
        int64 FreeBlocksSize = 0;
        FFreeLink* FirstFree = nullptr;
    };

    FCriticalSection CriticalSection;

    uint8* LowAddress;
    uint8* HighAddress;
    size_t TotalSize;
    size_t PageSize;
    size_t MaximumAlignment;
    uint8* NextAlloc;
    FFreeLink* RecycledLinks;
    int64 LinkSize;
    bool bBacksMalloc;
    FPerBlockSize Blocks[64]; 
    
    void FreeVirtualByBlock(void* Ptr, FPerBlockSize& Block, size_t AlignedSize);

protected:
    size_t SpaceConsumed;
    virtual uint8* AllocNewVM(size_t AlignedSize)
    {
        uint8* Result = NextAlloc;
        check(IsAligned(Result, MaximumAlignment) && IsAligned(AlignedSize, MaximumAlignment));
        NextAlloc = Result + AlignedSize;
        SpaceConsumed = NextAlloc - LowAddress;
        return Result;
    }

public:
    uint32 GetPagesForSizeAndAlignment(size_t Size, size_t Alignment = 1) const;
    void* AllocateVirtualPages(uint32 NumPages, size_t AlignmentForCheck = 1);
    void FreeVirtual(void* Ptr, uint32 NumPages);

    struct FVirtualAllocatorStatsPerBlockSize
    {
        size_t AllocBlocksSize;
        size_t FreeBlocksSize;
    };

    struct FVirtualAllocatorStats
    {
        size_t PageSize;
        size_t MaximumAlignment;
        size_t VMSpaceTotal;
        size_t VMSpaceConsumed;
        size_t VMSpaceConsumedPeak;

        size_t FreeListLinks;

        FVirtualAllocatorStatsPerBlockSize BlockStats[64];
    };
    void GetStats(FVirtualAllocatorStats& OutStats);
};

19.11.3 GPU

다음 인터페이스는 GPU 성능 수준과 같은 매개변수를 계산할 수 있습니다.

cpp
// GenericPlatformSurvey.h

struct FSynthBenchmarkResults 
{
    FSynthBenchmarkStat GPUStats[7];
    // 계산/산출(Calculate)GPU성능 단계 ,100테이블 는 단계 의 CPU, 100, 100。
    float ComputeGPUPerfIndex(TArray<float>* OutIndividualResults = nullptr) const;
    // 로써 로 반환(Return),을(를) 활용하여 는 (,을(를) 활용하여 WorkScale).
    float ComputeTotalGPUTime() const;
};

// GPU
struct FGPUAdpater    
{
    static const uint32 MaxStringLength = 260;
    //
    TCHAR AdapterName[MaxStringLength];
    //
    TCHAR AdapterInternalDriverVersion[MaxStringLength];
    // 을(를) 활용하여
    TCHAR AdapterUserDriverVersion[MaxStringLength];
    // 의 데이터
    TCHAR AdapterDriverDate[MaxStringLength];
    // 을(를) 활용하여 의 메모리
    TCHAR AdapterDedicatedMemoryMB[MaxStringLength];
};

다음 코드에는 GPU 드라이버 관련 정보 및 작업이 포함됩니다.

cpp
// GenericPlatformDriver.h

// GPU정보 。
struct FGPUDriverInfo
{
    // DirectX VendorId,0(만약 설정(Set)),을(를) 활용하여 로써 함수 설정(Set)/페치/가져오기(Fetch)
    uint32 VendorId;
    // e.g. "NVIDIA GeForce GTX 680" or "AMD Radeon R9 200 / HD 7900 Series"
    FString DeviceDescription;
    // e.g. "NVIDIA" or "Advanced Micro Devices, Inc."
    FString ProviderName;
    // e.g. "15.200.1062.1004"(AMD)
    // e.g. "9.18.13.4788"(NVIDIA) 
    // 개 는 Windows(예: 7:Vista、6:XP、4:Me、9:Win8(1)、10:Win7),5개 UserDriver,로 (https://wiki.mozilla.org/Blocklisting/Blocked_Graphics_Drivers)만약 검사/감지(Detect),이면 TEXT("Unknown") 
    FString InternalDriverVersion;    
    // e.g. "Catalyst 15.7.1"(AMD) or "Crimson 15.7.1"(AMD) or "347.88"(NVIDIA)
    // 로
    FString UserDriverVersion;
    // e.g. 3-13-2015
    FString DriverDate;
    // e.g. D3D11, D3D12
    FString RHIName;

    bool IsValid() const;
    // get VendorId
    bool IsAMD() const { return VendorId == 0x1002; }
    // get VendorId
    bool IsIntel() const { return VendorId == 0x8086; }
    // get VendorId
    bool IsNVIDIA() const { return VendorId == 0x10DE; }
    bool IsSameDriverVersionGeneration(const TCHAR* InOpWithMultiInt) const;
    static FString TrimNVIDIAInternalVersion(const FString& InternalVersion);
    FString GetUnifiedDriverVersion() const;
};

// Hardware.ini 내에서 의 개 개
struct FDriverDenyListEntry
{
    // optional, e.g. "<=223.112.21.1", might includes comparison operators, later even things multiple ">12.22 <=12.44"
    FString DriverVersionString;
    // optional, e.g. "<=MM-DD-YYYY"
    FString DriverDateString;
    // optional, e.g. "D3D11", "D3D12"
    FString RHIName;
    // required
    FString Reason;

    void LoadFromINIString(const TCHAR* In);
    bool IsValid() const
    bool IsLatestDenied() const
};

// GPU정보
struct FGPUHardware
{
    const FGPUDriverInfo DriverInfo;
    FString GetSuggestedDriverVersion(const FString& InRHIName) const
    FDriverDenyListEntry FindDriverDenyListEntry() const
    bool IsLatestDenied() const;
    FString GetVendorSectionName() const;
};

다음 코드는 GPU의 비닝 할당자입니다.

cpp
// MallocBinnedGPU.h

class FMallocBinnedGPU final : public FMalloc
{
    struct FGPUMemoryBlockProxy
    {
        uint8 MemoryModifiedByCPU[32 - sizeof(void*)]; // might be modified for free list links, etc
        void *GPUMemory;  // pointer to the actual GPU memory, which we cannot modify with the CPU
    };
    struct FFreeBlock
    {
        uint16 BlockSizeShifted;        // Size of the blocks that this list points to >> ArenaParams.MinimumAlignmentShift
        uint8 PoolIndex;                // Index of this pool
        uint8 Canary;                    // Constant value of 0xe3
        uint32 NumFreeBlocks;          // Number of consecutive free blocks here, at least 1.
        FFreeBlock* NextFreeBlock;     // Next free block or nullptr
    };
    struct FPoolTable
    {
        uint32 BlockSize;
        uint16 BlocksPerBlockOfBlocks;
        uint8 PagesPlatformForBlockOfBlocks;
        FBitTree BlockOfBlockAllocationBits; // one bits in here mean the virtual memory is committed
        FBitTree BlockOfBlockIsExhausted;    // one bit in here means the pool is completely full
        uint32 NumEverUsedBlockOfBlocks;
        FPoolInfoSmall** PoolInfos;
        uint64 UnusedAreaOffsetLow;
    };

    struct FPtrToPoolMapping
    {
    private:
        /** Shift to apply to a pointer to get the reference from the indirect tables */
        uint64 PtrToPoolPageBitShift;
        /** Shift required to get required hash table key. */
        uint64 HashKeyShift;
        /** Used to mask off the bits that have been used to lookup the indirect table */
        uint64 PoolMask;
        // PageSize dependent constants
        uint64 MaxHashBuckets;
    };

    struct FBundleNode
    {
        FBundleNode* NextNodeInCurrentBundle;
        union
        {
            FBundleNode* NextBundle;
            int32 Count;
        };
    };

    struct FBundle
    {
        FBundleNode* Head;
        uint32       Count;
    };

    // 의 리스트
    struct FFreeBlockList
    {
        bool PushToFront(FMallocBinnedGPU& Allocator, void* InPtr, uint32 InPoolIndex, uint32 InBlockSize, const FArenaParams& LocalArenaParams);
        bool CanPushToFront(uint32 InPoolIndex, uint32 InBlockSize, const FArenaParams& LocalArenaParams);
        void* PopFromFront(FMallocBinnedGPU& Allocator, uint32 InPoolIndex);

        FBundleNode* RecyleFull(FArenaParams& LocalArenaParams, FGlobalRecycler& GGlobalRecycler, uint32 InPoolIndex);
        bool ObtainPartial(FArenaParams& LocalArenaParams, FGlobalRecycler& GGlobalRecycler, uint32 InPoolIndex);
        FBundleNode* PopBundles(uint32 InPoolIndex);
    private:
        FBundle PartialBundle;
        FBundle FullBundle;
    };

    // 스레드(Thread)의 리스트
    struct FPerThreadFreeBlockLists
    {
        static FPerThreadFreeBlockLists* Get(uint32 BinnedGPUTlsSlot);
        static void SetTLS(FMallocBinnedGPU& Allocator);
        static int64 ClearTLS(FMallocBinnedGPU& Allocator);

        void* Malloc(FMallocBinnedGPU& Allocator, uint32 InPoolIndex);
        bool Free(FMallocBinnedGPU& Allocator, void* InPtr, uint32 InPoolIndex, uint32 InBlockSize, const FArenaParams& LocalArenaParams);
        bool CanFree(uint32 InPoolIndex, uint32 InBlockSize, const FArenaParams& LocalArenaParams);
        FBundleNode* RecycleFullBundle(FArenaParams& LocalArenaParams, FGlobalRecycler& GlobalRecycler, uint32 InPoolIndex);
        bool ObtainRecycledPartial(FArenaParams& LocalArenaParams, FGlobalRecycler& GlobalRecycler, uint32 InPoolIndex);
        FBundleNode* PopBundles(uint32 InPoolIndex);

        int64 AllocatedMemory;
        TArray<FFreeBlockList> FreeLists;
    };

    // 글로벌
    struct FGlobalRecycler
    {
        void Init(uint32 PoolCount);
        bool PushBundle(uint32 NumCachedBundles, uint32 InPoolIndex, FBundleNode* InBundle);
        FBundleNode* PopBundle(uint32 NumCachedBundles, uint32 InPoolIndex);

    private:
        struct FPaddedBundlePointer
        {
            FBundleNode* FreeBundles[BINNEDGPU_MAX_GMallocBinnedGPUMaxBundlesBeforeRecycle];
        };
        TArray<FPaddedBundlePointer> Bundles;
    };

    uint64 PoolIndexFromPtr(const void* Ptr);
    uint8* PoolBasePtr(uint32 InPoolIndex);
    uint64 PoolIndexFromPtrChecked(const void* Ptr);
    bool IsOSAllocation(const void* Ptr);

    void* BlockOfBlocksPointerFromContainedPtr(const void* Ptr, uint8 PagesPlatformForBlockOfBlocks, uint32& OutBlockOfBlocksIndex);
    uint8* BlockPointerFromIndecies(uint32 InPoolIndex, uint32 BlockOfBlocksIndex, uint32 BlockOfBlocksSize);
    FPoolInfoSmall* PushNewPoolToFront(FMallocBinnedGPU& Allocator, uint32 InBlockSize, uint32 InPoolIndex, uint32& OutBlockOfBlocksIndex);
    FPoolInfoSmall* GetFrontPool(FPoolTable& Table, uint32 InPoolIndex, uint32& OutBlockOfBlocksIndex);
    bool AdjustSmallBlockSizeForAlignment(SIZE_T& InOutSize, uint32 Alignment);

public:
    FArenaParams& GetParams();
    void InitMallocBinned();

    virtual bool IsInternallyThreadSafe() const override;
    virtual void* Malloc(SIZE_T Size, uint32 Alignment) override;
    virtual void* Realloc(void* Ptr, SIZE_T NewSize, uint32 Alignment) override;
    virtual void Free(void* Ptr) override;
    virtual bool GetAllocationSize(void *Ptr, SIZE_T &SizeOut) override;
    virtual SIZE_T QuantizeSize(SIZE_T Count, uint32 Alignment) override;

    virtual bool ValidateHeap() override;
    virtual void Trim(bool bTrimThreadCaches) override;
    virtual void SetupTLSCachesOnCurrentThread() override;
    virtual void ClearAndDisableTLSCachesOnCurrentThread() override;
    virtual const TCHAR* GetDescriptiveName() override;

    void FlushCurrentThreadCache();
    void* MallocExternal(SIZE_T Size, uint32 Alignment);
    void FreeExternal(void *Ptr);
    bool GetAllocationSizeExternal(void* Ptr, SIZE_T& SizeOut);

    MBG_STAT(int64 GetTotalAllocatedSmallPoolMemory();)
    virtual void GetAllocatorStats(FGenericMemoryStats& out_Stats) override;
    virtual void DumpAllocatorStats(class FOutputDevice& Ar) override;

    uint32 BoundSizeToPoolIndex(SIZE_T Size);
    uint32 PoolIndexToBlockSize(uint32 PoolIndex);

    void Commit(uint32 InPoolIndex, void *Ptr, SIZE_T Size);
    void Decommit(uint32 InPoolIndex, void *Ptr, SIZE_T Size);

    (...)

    // Pool tables for different pool sizes
    TArray<FPoolTable> SmallPoolTables;
    uint32 SmallPoolInfosPerPlatformPage;
    PoolHashBucket* HashBuckets;
    PoolHashBucket* HashBucketFreeList;
    uint64 NumLargePoolsPerPage;

    FCriticalSection Mutex;
    FGlobalRecycler GGlobalRecycler;
    FPtrToPoolMapping PtrToPoolMapping;

    FArenaParams ArenaParams;

    TArray<uint16> SmallBlockSizesReversedShifted; // this is reversed to get the smallest elements on our main cache line
    uint32 BinnedGPUTlsSlot;
    uint64 PoolSearchDiv; // if this is zero, the VM turned out to be contiguous anyway so we use a simple subtract and shift
    uint8* HighestPoolBaseVMPtr; // this is a duplicate of PoolBaseVMPtr[ArenaParams.PoolCount - 1]
    FPlatformMemory::FPlatformVirtualMemoryBlock PoolBaseVMBlock;
    TArray<uint8*> PoolBaseVMPtr;
    TArray<FPlatformMemory::FPlatformVirtualMemoryBlock> PoolBaseVMBlocks;
    // Mapping of sizes to small table indices
    TArray<uint8> MemSizeToIndex;

    FCriticalSection FreeBlockListsRegistrationMutex;
    TArray<FPerThreadFreeBlockLists*> RegisteredFreeBlockLists;
    TArray<void*> MallocedPointers;
};

// GPUDefragAllocator.h

// 의 할당(Allocate),로써 및 。는 스레드(Thread)의 。을(를) 활용하여 TMap탐색/조회(Find/Lookup)의 메모리 ( 및 malloc/free스레드(Thread))을(를) 활용하여 의 리스트 에 의해 할당(Allocate),에 의해 의 에 의해 .
class FGPUDefragAllocator
{
public:
    typedef TDoubleLinkedList<FAsyncReallocationRequest*> FRequestList;
    typedef TDoubleLinkedList<FAsyncReallocationRequest*>::TDoubleLinkedListNode FRequestNode;

    // 할당(Allocate)설정(Set)의
    struct FSettings
    {
        int32        MaxDefragRelocations;
        int32        MaxDefragDownShift;
        int32        OverlappedBandwidthScale;
    };

    enum EMemoryElementType
    {
        MET_Allocated,
        MET_Free,
        MET_Locked,
        MET_Relocating,
        MET_Resizing,
        MET_Resized,
        MET_Max
    };

    struct FMemoryLayoutElement
    {
        int32                Size;
        EMemoryElementType    Type;
    };

    // 할당(Allocate)할당(Allocate)정보 의 。
    struct FRelocationStats
    {
        int64 NumBytesRelocated;
        int64 NumBytesDownShifted;
        int64 LargestHoleSize;
        int32 NumRelocations;        
        int32 NumHoles;
        int32 NumLockedChunks;
    };

    // 개 할당(Allocate) 또는 의 정보 。
    class FMemoryChunk
    {
    public:
        uint8*                    Base;
        int64                    Size;
        int64                    OrigSize;
        bool                    bIsAvailable;    
        int32                    LockCount;
        uint16                    DefragCounter;

        // FBestFitAllocator,FirstChunk、FirstFreeChunk 및 LastChunk。
        FGPUDefragAllocator&    BestFitAllocator;
        FMemoryChunk*            PreviousChunk;
        FMemoryChunk*            NextChunk;
        FMemoryChunk*            PreviousFreeChunk;
        FMemoryChunk*            NextFreeChunk;
        uint32                    SyncIndex;
        int64                    SyncSize;
        void*                    UserPayload;        
        TStatId Stat;
        bool bTail;
    };

    virtual void*   Allocate(int64 AllocationSize, int32 Alignment, TStatId InStat, bool bAllowFailure);    
    virtual void    Free(void* Pointer);
    virtual void    Lock(const void* Pointer);
    virtual void    Unlock(const void* Pointer);
    void*           Reallocate(void* OldBaseAddress, int64 NewSize);
    void            DefragmentMemory(FRelocationStats& Stats);

    void    SetUserPayload(const void* Pointer, void* UserPayload);
    void*    GetUserPayload(const void* Pointer);
    int64    GetAllocatedSize(void* Pointer);
    bool    IsValidPoolMemory(const void* Pointer) const;
    void    DumpAllocs(FOutputDevice& Ar = *GLog);
    int64   GetTotalSize() const;
    int32   GetLargestAvailableAllocation(int32* OutNumFreeChunks = nullptr);
    uint32    GetBlockedCycles() const
    bool    InBenchmarkMode() const
    bool    GetTextureMemoryVisualizeData(FColor* TextureData, int32 SizeX, int32 SizeY, int32 Pitch, const int32 PixelSize);
    void    GetMemoryLayout(TArray<FMemoryLayoutElement>& MemoryLayout);
    virtual int32 Tick(FRelocationStats& Stats, bool bPanicDefrag);
    bool    FinishAllRelocations();
    void    BlockOnAsyncReallocation(FAsyncReallocationRequest* Request);
    void    CancelAsyncReallocation(FAsyncReallocationRequest* Request, const void* CurrentBaseAddress);
    static bool IsAligned(const volatile void* Ptr, const uint32 Alignment);
    int32 GetAllocationAlignment() const;

    (...)
};

동적 해상도는 아래에서 다룹니다.

cpp
// DynamicResolutionProxy.h

// 렌더링(Render)스레드(Thread)는 동적 의 메서드
class FDynamicResolutionHeuristicProxy
{
public:
    static constexpr uint64 kInvalidEntryId = ~uint64(0);

    void Reset_RenderThread();
    uint64 CreateNewPreviousFrameTimings_RenderThread(float GameThreadTimeMs, float RenderThreadTimeMs);
    void CommitPreviousFrameGPUTimings_RenderThread(...);
    void RefreshCurentFrameResolutionFraction_RenderThread();
    float GetResolutionFractionUpperBound() const;

    float QueryCurentFrameResolutionFraction_RenderThread() const;
    float GetResolutionFractionApproximation_GameThread() const;
    static TSharedPtr< class IDynamicResolutionState > CreateDefaultState();

private:
    struct FrameHistoryEntry
    {
        float ResolutionFraction;
        float GameThreadTimeMs;
        float RenderThreadTimeMs;
        float TotalFrameGPUBusyTimeMs;
        float GlobalDynamicResolutionTimeMs;
        bool bGPUTimingsHaveCPUBubbles;
    };

    TArray<FrameHistoryEntry> History;
    int32 PreviousFrameIndex;
    int32 HistorySize;
    int32 NumberOfFramesSinceScreenPercentageChange;
    int32 IgnoreFrameRemainingCount;

    float CurrentFrameResolutionFraction;
    uint64 FrameCounter;
};

다음 코드에는 여러 GPU 및 GPU 마스크와 같은 논리가 포함됩니다.

cpp
// MultiGPU.h

/** A mask where each bit is a GPU index. Can not be empty so that non SLI platforms can optimize it to be always 1.  */
struct FRHIGPUMask
{
private:
    uint32 GPUMask;

    uint32 ToIndex() const;
    bool HasSingleIndex() const;
    uint32 GetLastIndex() const;
    uint32 GetFirstIndex() const;
    bool Contains(uint32 GPUIndex) const;
    bool ContainsAll(const FRHIGPUMask& Rhs) const;
    bool Intersects(const FRHIGPUMask& Rhs) const;
    bool operator ==(const FRHIGPUMask& Rhs) const;
    bool operator !=(const FRHIGPUMask& Rhs) const;
     uint32 GetNative() const;
    static const FRHIGPUMask GPU0() { return FRHIGPUMask(1); }
    static const FRHIGPUMask All() { return FRHIGPUMask((1 << GNumExplicitGPUsForRendering) - 1); }
    static const FRHIGPUMask FilterGPUsBefore(uint32 GPUIndex) { return FRHIGPUMask(~((1u << GPUIndex) - 1)) & All(); }

    struct FIterator
    {
        explicit FIterator(const uint32 InGPUMask) : GPUMask(InGPUMask), FirstGPUIndexInMask(0);
        explicit FIterator(const FRHIGPUMask& InGPUMask) : FIterator(InGPUMask.GPUMask);
        FIterator& operator++();
        FIterator operator++(int);
    private:
        uint32 GPUMask;
        unsigned long FirstGPUIndexInMask;
    };

    friend FRHIGPUMask::FIterator begin(const FRHIGPUMask& NodeMask);
    friend FRHIGPUMask::FIterator end(const FRHIGPUMask& NodeMask);
};

// GPU을(를) 활용하여 ,을(를) 활용하여 페치/가져오기(Fetch)AFR 및 의 정보 。AFR는 의 GPU。AFR는 내에서 의 GPU,실행(Execute)의 。예: ,렌더링(Render)뷰(View)의 개 GPU는 AFR단계 。2개 AFR의 4 GPU설정(Set):개 AFR2개 GPU。0b1010 및 0b0101는 개 , 개 GPU개 단계 GPU。0b1100 및 0b0011는 。
struct AFRUtils
{
    static inline uint32 GetNumGPUsPerGroup();
    static inline uint32 GetGroupIndex(uint32 GPUIndex);
    static inline uint32 GetIndexWithinGroup(uint32 GPUIndex);
    static inline uint32 GetNextSiblingGPUIndex(uint32 GPUIndex);
    static inline FRHIGPUMask GetNextSiblingGPUMask(FRHIGPUMask InGPUMask);
    static inline uint32 GetPrevSiblingGPUIndex(uint32 GPUIndex);
    static inline FRHIGPUMask GetPrevSiblingGPUMask(FRHIGPUMask InGPUMask);
    static inline FRHIGPUMask GetGPUMaskForGroup(uint32 GPUIndex);
    static inline FRHIGPUMask GetGPUMaskForGroup(FRHIGPUMask InGPUMask);
    static inline FRHIGPUMask GetGPUMaskWithSiblings(uint32 GPUIndex);
    static inline FRHIGPUMask GetGPUMaskWithSiblings(FRHIGPUMask InGPUMask);
#if WITH_MGPU
    static TArray<FRHIGPUMask, TFixedAllocator<MAX_NUM_GPUS>> GroupMasks;
    static TArray<FRHIGPUMask, TFixedAllocator<MAX_NUM_GPUS>> SiblingMasks;
#endif
};

다음 유형 또는 인터페이스에는 GPU 제조업체, 드라이버 및 기능이 포함됩니다.

cpp
// RHIDefinitions.h

enum class EGpuVendorId
{
    Unknown        = -1,
    NotQueried    = 0,

    Amd            = 0x1002,
    ImgTec        = 0x1010,
    Nvidia        = 0x10DE, 
    Arm            = 0x13B5, 
    Broadcom    = 0x14E4,
    Qualcomm    = 0x5143,
    Intel        = 0x8086,
    Apple        = 0x106B,
    Vivante        = 0x7a05,
    VeriSilicon    = 0x1EB1,

    Kazan        = 0x10003,    // VkVendorId
    Codeplay    = 0x10004,    // VkVendorId
    Mesa        = 0x10005,    // VkVendorId
};

inline bool RHIHasTiledGPU(const FStaticShaderPlatform Platform);
inline EGpuVendorId RHIConvertToGpuVendorId(uint32 VendorId);

class FGenericDataDrivenShaderPlatformInfo
{
    FName Language;
    ERHIFeatureLevel::Type MaxFeatureLevel;
    uint32 bIsMobile: 1;
    uint32 bIsMetalMRT: 1;
    uint32 bIsPC: 1;
    uint32 bIsConsole: 1;
    uint32 bIsAndroidOpenGLES: 1;
    uint32 bSupportsDebugViewShaders : 1;
    uint32 bSupportsMobileMultiView: 1;
    uint32 bSupportsArrayTextureCompression : 1;
    uint32 bSupportsDistanceFields: 1; // used for DFShadows and DFAO - since they had the same checks
    uint32 bSupportsDiaphragmDOF: 1;
    uint32 bSupportsRGBColorBuffer: 1;
    uint32 bSupportsCapsuleShadows: 1;
    uint32 bSupportsPercentageCloserShadows : 1;
    uint32 bSupportsVolumetricFog: 1; // also used for FVVoxelization
    uint32 bSupportsIndexBufferUAVs: 1;
    uint32 bSupportsInstancedStereo: 1;
    uint32 bSupportsMultiView: 1;
    uint32 bSupportsMSAA: 1;
    uint32 bSupports4ComponentUAVReadWrite: 1;
    uint32 bSupportsRenderTargetWriteMask: 1;
    uint32 bSupportsRayTracing: 1;
    uint32 bSupportsRayTracingProceduralPrimitive : 1;
    uint32 bSupportsRayTracingIndirectInstanceData : 1; // Whether instance transforms can be copied from the GPU to the TLAS instances buffer
    uint32 bSupportsHighEndRayTracingReflections : 1; // Whether fully-featured RT reflections can be used on the platform (with multi-bounce, translucency, etc.)
    uint32 bSupportsPathTracing : 1; // Whether real-time path tracer is supported on this platform (avoids compiling unnecessary shaders)
    uint32 bSupportsGPUSkinCache: 1;
    uint32 bSupportsGPUScene : 1;
    uint32 bSupportsByteBufferComputeShaders : 1;
    uint32 bSupportsPrimitiveShaders : 1;
    uint32 bSupportsUInt64ImageAtomics : 1;
    uint32 bRequiresVendorExtensionsForAtomics : 1;
    uint32 bSupportsNanite : 1;
    uint32 bSupportsLumenGI : 1;
    uint32 bSupportsSSDIndirect : 1;
    uint32 bSupportsTemporalHistoryUpscale : 1;
    uint32 bSupportsRTIndexFromVS : 1;
    uint32 bSupportsWaveOperations : 1; // Whether HLSL SM6 shader wave intrinsics are supported
    uint32 bSupportsIntrinsicWaveOnce : 1;
    uint32 bSupportsConservativeRasterization : 1;
    uint32 bRequiresExplicit128bitRT : 1;
    uint32 bSupportsGen5TemporalAA : 1;
    uint32 bTargetsTiledGPU: 1;
    uint32 bNeedsOfflineCompiler: 1;
    uint32 bSupportsComputeFramework : 1;
    uint32 bSupportsAnisotropicMaterials : 1;
    uint32 bSupportsDualSourceBlending : 1;
    uint32 bRequiresGeneratePrevTransformBuffer : 1;
    uint32 bRequiresRenderTargetDuringRaster : 1;
    uint32 bRequiresDisableForwardLocalLights : 1;
    uint32 bCompileSignalProcessingPipeline : 1;
    uint32 bSupportsMeshShadersTier0 : 1;
    uint32 bSupportsMeshShadersTier1 : 1;
    uint32 MaxMeshShaderThreadGroupSize : 10;
    uint32 bSupportsPerPixelDBufferMask : 1;
    uint32 bIsHlslcc : 1;
    uint32 bSupportsDxc : 1; // Whether DirectXShaderCompiler (DXC) is supported
    uint32 bSupportsVariableRateShading : 1;
    uint32 NumberOfComputeThreads : 10;
    uint32 bWaterUsesSimpleForwardShading : 1;
    uint32 bNeedsToSwitchVerticalAxisOnMobileOpenGL : 1;
    uint32 bSupportsHairStrandGeometry : 1;
    uint32 bSupportsDOFHybridScattering : 1;
    uint32 bNeedsExtraMobileFrames : 1;
    uint32 bSupportsHZBOcclusion : 1;
    uint32 bSupportsWaterIndirectDraw : 1;
    uint32 bSupportsAsyncPipelineCompilation : 1;
    uint32 bSupportsManualVertexFetch : 1;
    uint32 bRequiresReverseCullingOnMobile : 1;
    uint32 bOverrideFMaterial_NeedsGBufferEnabled : 1;
    uint32 bSupportsMobileDistanceField : 1;
    uint32 bSupportsFFTBloom : 1;
    uint32 bSupportsInlineRayTracing : 1;
    uint32 bSupportsRayTracingShaders : 1;
    uint32 bSupportsVertexShaderLayer : 1;
    uint32 bSupportsVolumeTextureAtomics : 1;
    
private:
    static FGenericDataDrivenShaderPlatformInfo Infos[SP_NumPlatforms];
    
    (...)
}

19.11.4 기타

다음 코드는 일부 하드웨어에 대한 정보와 작업을 제공합니다.

cpp
// GenericPlatformMisc.h

struct FGenericPlatformMisc
{
    // 디바이스/
    static FString GetDeviceId();
    static FString GetUniqueAdvertisingId();
    static void SubmitErrorReport( const TCHAR* InErrorHist, EErrorReportMode::Type InMode );
    static bool IsRemoteSession();
    static bool IsDebuggerPresent();
    static EProcessDiagnosticFlags GetProcessDiagnostics();
    static FString GetCPUVendor();
    static uint32 GetCPUInfo();
    static bool HasNonoptionalCPUFeatures();
    static bool NeedsNonoptionalCPUFeaturesCheck();
    static FString GetCPUBrand();
    static FString GetCPUChipset();
    static FString GetPrimaryGPUBrand();
    static FString GetDeviceMakeAndModel();
    static struct FGPUDriverInfo GetGPUDriverInfo(const FString& DeviceDescription);
    
    static void PrefetchBlock(const void* InPtr, int32 NumBytes = 1);
    static void Prefetch(void const* x, int32 offset = 0);

    static const TCHAR* GetDefaultDeviceProfileName();
    static int GetBatteryLevel();
    static void SetBrightness(float bBright);
    static float GetBrightness();
    static bool SupportsBrightness();
    static bool IsInLowPowerMode();
    static float GetDeviceTemperatureLevel();
    static inline int32 GetMaxRefreshRate();
    static inline int32 GetMaxSyncInterval();
    static bool IsPGOEnabled();
    
    static TArray<uint8> GetSystemFontBytes();
    static bool HasActiveWiFiConnection();
    static ENetworkConnectionType GetNetworkConnectionType();

    static bool HasVariableHardware();
    static bool HasPlatformFeature(const TCHAR* FeatureName);
    static bool IsRunningOnBattery();
    static EDeviceScreenOrientation GetDeviceOrientation();
    static void SetDeviceOrientation(EDeviceScreenOrientation NewDeviceOrientation);
    static int32 GetDeviceVolume();

    // 메모리
    static void MemoryBarrier();
    static void SetMemoryWarningHandler(void (* Handler)(const FGenericMemoryWarningContext& Context));
    static bool HasMemoryWarningHandler();

    // I/O
    static void InitTaggedStorage(uint32 NumTags);
    static void ShutdownTaggedStorage();
    static void TagBuffer(const char* Label, uint32 Category, const void* Buffer, size_t BufferSize);
    static bool SetStoredValues(const FString& InStoreId, const FString& InSectionName, const TMap<FString, FString>& InKeyValues);
    static bool SetStoredValue(const FString& InStoreId, const FString& InSectionName, const FString& InKeyName, const FString& InValue);
    static bool GetStoredValue(const FString& InStoreId, const FString& InSectionName, const FString& InKeyName, FString& OutValue);
    static bool DeleteStoredValue(const FString& InStoreId, const FString& InSectionName, const FString& InKeyName);
    static bool DeleteStoredSection(const FString& InStoreId, const FString& InSectionName);
    
    static TArray<FCustomChunk> GetOnDemandChunksForPakchunkIndices(const TArray<int32>& PakchunkIndices);
    static TArray<FCustomChunk> GetAllOnDemandChunks();
    static TArray<FCustomChunk> GetAllLanguageChunks();
    static TArray<FCustomChunk> GetCustomChunksByType(ECustomChunkType DesiredChunkType);
    static void ParseChunkIdPakchunkIndexMapping(TArray<FString> ChunkIndexRedirects, TMap<int32, int32>& OutMapping);
    static int32 GetChunkIDFromPakchunkIndex(int32 PakchunkIndex);
    static int32 GetPakchunkIndexFromPakFile(const FString& InFilename);
    
    static FText GetFileManagerName();
    static bool IsPackagedForDistribution();
    static FString LoadTextFileFromPlatformPackage(const FString& RelativePath);
    static bool FileExistsInPlatformPackage(const FString& RelativePath);
    
    static bool Expand16BitIndicesTo32BitOnLoad();
    static void GetNetworkFileCustomData(TMap<FString,FString>& OutCustomPlatformData);
    static bool SupportsBackbufferSampling();

    (...)
};

// GenericPlatformApplicationMisc.h

struct FGenericPlatformApplicationMisc
{
    // 모듈 //디바이스
    static void LoadPreInitModules();
    static void LoadStartupModules();
    static FOutputDeviceConsole* CreateConsoleOutputDevice();
    static FOutputDeviceError* GetErrorOutputDevice();
    static FFeedbackContext* GetFeedbackContext();
    static bool IsThisApplicationForeground();    
    static void RequestMinimize();
    static bool RequiresVirtualKeyboard();
    static void PumpMessages(bool bFromMainLoop);

    // /
    static void PreventScreenSaver();
    static bool IsScreensaverEnabled();
    static bool ControlScreensaver(EScreenSaverAction Action);
    static struct FLinearColor GetScreenPixelColor(const FVector2D& InScreenPos, float InGamma);
    static bool GetWindowTitleMatchingText(const TCHAR* TitleStartsWith, FString& OutTitle);
    static void SetHighDPIMode();
    static float GetDPIScaleFactorAtPoint(float X, float Y);
    static bool IsHighDPIAwarenessEnabled();
    static bool AnchorWindowWindowPositionTopLeft();
    static EScreenPhysicalAccuracy GetPhysicalScreenDensity(int32& OutScreenDensity);
    static EScreenPhysicalAccuracy ComputePhysicalScreenDensity(int32& OutScreenDensity);
    static EScreenPhysicalAccuracy ConvertInchesToPixels(T Inches, T2& OutPixels);
    static EScreenPhysicalAccuracy ConvertPixelsToInches(T Pixels, T2& OutInches);

    //
    static void SetGamepadsAllowed(bool bAllowed);
    static void SetGamepadsBlockDeviceFeedback(bool bAllowed);
    static void ResetGamepadAssignments();
    static void ResetGamepadAssignmentToController(int32 ControllerId);
    static bool IsControllerAssignedToGamepad(int32 ControllerId);
    static FString GetGamepadControllerName(int32 ControllerId);
    static class UTexture2D* GetGamepadButtonGlyph(...);
    static void EnableMotionData(bool bEnable);
    static bool IsMotionDataEnabled();
    
    (...)
};

// 결과
struct FHardwareSurveyResults
{
    static const int32 MaxDisplayCount = 8; 
    static const int32 MaxStringLength = 260;

    TCHAR Platform[MaxStringLength];

    TCHAR OSVersion[MaxStringLength];
    TCHAR OSSubVersion[MaxStringLength];
    uint32 OSBits;
    TCHAR OSLanguage[MaxStringLength];

    TCHAR RenderingAPI[MaxStringLength];
    TCHAR MultimediaAPI_DEPRECATED[MaxStringLength];

    uint32 HardDriveGB;
    uint32 HardDriveFreeMB;
    uint32 MemoryMB;

    float CPUPerformanceIndex;
    float GPUPerformanceIndex;
    float RAMPerformanceIndex;

    uint32 bIsLaptopComputer:1;
    uint32 bIsRemoteSession:1;

    uint32 CPUCount;
    float CPUClockGHz;
    TCHAR CPUBrand[MaxStringLength];
    TCHAR CPUNameString[MaxStringLength];
    uint32 CPUInfo;

    uint32 DisplayCount;
    FHardwareDisplay Displays[MaxDisplayCount];
    FGPUAdpater RHIAdapter;

    uint32 ErrorCount;
    TCHAR LastSurveyError[MaxStringLength];
    TCHAR LastSurveyErrorDetail[MaxStringLength];
    TCHAR LastPerformanceIndexError[MaxStringLength];
    TCHAR LastPerformanceIndexErrorDetail[MaxStringLength];

    FSynthBenchmarkResults SynthBenchmark;
};

// 의 플랫폼구현 페치/가져오기(Fetch)FHardwareSurveyResults。
struct APPLICATIONCORE_API FGenericPlatformSurvey
{
    static bool GetSurveyResults(FHardwareSurveyResults& OutResults, bool bWait);
};

// HardwareInfo.h

// 정보
struct ENGINE_API FHardwareInfo
{
    static void RegisterHardwareInfo( const FName SpecIdentifier, const FString& HardwareInfo );
    static FString GetHardwareInfo(const FName SpecIdentifier);
    static const FString GetHardwareDetailsString();
};

다음 코드는 성능 감지 기능을 제공합니다.

cpp
// GenericPlatformSurvey.h

struct FSynthBenchmarkStat
{
    // 계산/산출(Calculate)선형(Linear)성능 (>0),의 로 100,그러나 .
    float ComputePerfIndex() const;
    
    void SetMeasuredTime(const FTimeSample& TimeSample, float InConfidence = 90);
    float GetNormalizedTime() const;
    float GetMeasuredTotalTime() const;
    float GetConfidence() const;
    float GetWeight() const;

private:

    // -1(만약 정의 ),로써 로 ,는 (의 GPU).
    float MeasuredTotalTime;
    // -1(만약 정의 ),이면 (예: s/g픽셀(Pixel)),WorkScale.
    float MeasuredNormalizedTime;
    // -1(만약 정의 ),이면 로 GPU의 (100,로 NVidia 670).
    float IndexNormalizedTime;

    // 0..100,100:
    float Confidence;
    // 1로 가중치(Weight),0로 가중치(Weight),>1로 가중치(Weight).
    float Weight;
};

다음 코드는 디스크 사용률 추적을 제공합니다.

cpp
// DiskUtilizationTracker.h

struct FDiskUtilizationTracker
{
    struct UtilizationStats
    {
        double GetOverallThroughputBS() const;
        double GetOverallThroughputMBS() const;
        double GetReadThrougputBS() const;
        double GetReadThrougputMBS() const;
        double GetTotalIdleTimeInSeconds() const;
        double GetTotalIOTimeInSeconds() const;
        double GetPercentTimeIdle() const;

        uint64 TotalReads;
        uint64 TotalSeeks;

        uint64 TotalBytesRead;
        uint64 TotalSeekDistance;

        double TotalIOTime;
        double TotalIdleTime;
    };

    UtilizationStats LongTermStats;
    UtilizationStats ShortTermStats;

    FCriticalSection CriticalSection;

    uint64 IdleStartCycle;
    uint64 ReadStartCycle;
    uint64 InFlightBytes;
    int32  InFlightReads;

    FThreadSafeBool bResetShortTermStats;

    void StartRead(uint64 InReadBytes, uint64 InSeekDistance = 0);
    void FinishRead();

    uint32 GetOutstandingRequests() const;
    const struct UtilizationStats& GetLongTermStats() const;
    const struct UtilizationStats& GetShortTermStats() const;
    void ResetShortTermStats();

private:
    static float GetThrottleRateMBS();
    static constexpr float PrintFrequencySeconds = 0.5f;
};

다음은 스토리지 IO 관련 정보 및 인터페이스를 제공합니다.

cpp
// IoStore.h

// I/O저장(Store)TOC。
struct FIoStoreTocHeader
{
    static constexpr char TocMagicImg[] = "-==--==--==--==-";

    uint8    TocMagic[16];
    uint8    Version;
    uint8    Reserved0 = 0;
    uint16    Reserved1 = 0;
    uint32    TocHeaderSize;
    uint32    TocEntryCount;
    uint32    TocCompressedBlockEntryCount;
    uint32    TocCompressedBlockEntrySize;    // For sanity checking
    uint32    CompressionMethodNameCount;
    uint32    CompressionMethodNameLength;
    uint32    CompressionBlockSize;
    uint32    DirectoryIndexSize;
    uint32    PartitionCount = 0;
    FIoContainerId ContainerId;
    FGuid    EncryptionKeyGuid;
    EIoContainerFlags ContainerFlags;
    uint8    Reserved3 = 0;
    uint16    Reserved4 = 0;
    uint32    TocChunkPerfectHashSeedsCount = 0;
    uint64    PartitionSize = 0;
    uint32    TocChunksWithoutPerfectHashCount = 0;
    uint32    Reserved7 = 0;
    uint64    Reserved8[5] = { 0 };
};

// 및 。
struct FIoOffsetAndLength
{
public:
    inline uint64 GetOffset() const;
    inline uint64 GetLength() const;
    inline void SetOffset(uint64 Offset);
    inline void SetLength(uint64 Length);

private:
    uint8 OffsetAndLength[5 + 5];
};

// TOC개 데이터
struct FIoStoreTocEntryMeta
{
    FIoChunkHash ChunkHash;
    FIoStoreTocEntryMetaFlags Flags;
};

// 압축/패킹(Packing)개
struct FIoStoreTocCompressedBlockEntry
{
    static constexpr uint32 OffsetBits = 40;
    static constexpr uint64 OffsetMask = (1ull << OffsetBits) - 1ull;
    static constexpr uint32 SizeBits = 24;
    static constexpr uint32 SizeMask = (1 << SizeBits) - 1;
    static constexpr uint32 SizeShift = 8;

    inline uint64 GetOffset() const;
    inline void SetOffset(uint64 InOffset);
    inline uint32 GetCompressedSize() const;
    inline void SetCompressedSize(uint32 InSize);
    inline uint32 GetUncompressedSize() const;
    inline void SetUncompressedSize(uint32 InSize);
    inline uint8 GetCompressionMethodIndex() const;
    inline void SetCompressionMethodIndex(uint8 InIndex);

private:
    uint8 Data[5 + 3 + 3 + 1];
};

// TOC리소스 로드/읽기(Read)
enum class EIoStoreTocReadOptions
{
    Default,
    ReadDirectoryIndex    = (1 << 0),
    ReadTocMeta            = (1 << 1),
    ReadAll                = ReadDirectoryIndex | ReadTocMeta
};
ENUM_CLASS_FLAGS(EIoStoreTocReadOptions);

// TOC데이터
struct FIoStoreTocResource
{
    enum { CompressionMethodNameLen = 32 };

    FIoStoreTocHeader Header;
    TArray<FIoChunkId> ChunkIds;
    TArray<FIoOffsetAndLength> ChunkOffsetLengths;
    TArray<int32> ChunkPerfectHashSeeds;
    TArray<int32> ChunkIndicesWithoutPerfectHash;
    TArray<FIoStoreTocCompressedBlockEntry> CompressionBlocks;
    TArray<FName> CompressionMethods;
    FSHAHash SignatureHash;
    TArray<FSHAHash> ChunkBlockSignatures;
    TArray<FIoStoreTocEntryMeta> ChunkMetas;

    TArray<uint8> DirectoryIndexBuffer;

    static FIoStatus Read(const TCHAR* TocFilePath, EIoStoreTocReadOptions ReadOptions, FIoStoreTocResource& OutTocResource);
    static TIoStatusOr<uint64> Write(const TCHAR* TocFilePath, FIoStoreTocResource& TocResource, ...);
    static uint64 HashChunkIdWithSeed(int32 Seed, const FIoChunkId& ChunkId);
};

// 로써 는 IO의 、、

// IoDirectoryIndex.h

struct FIoDirectoryIndexEntry
{
    uint32 Name                = ~uint32(0);
    uint32 FirstChildEntry    = ~uint32(0);
    uint32 NextSiblingEntry    = ~uint32(0);
    uint32 FirstFileEntry    = ~uint32(0);
};

struct FIoFileIndexEntry
{
    uint32 Name                = ~uint32(0);
    uint32 NextFileEntry    = ~uint32(0);
    uint32 UserData            = 0;
};

struct FIoDirectoryIndexResource
{
    FString MountPoint;
    TArray<FIoDirectoryIndexEntry> DirectoryEntries;
    TArray<FIoFileIndexEntry> FileEntries;
    TArray<FString> StringTable;
};

class FIoDirectoryIndexWriter
{
public:
    void SetMountPoint(FString InMountPoint);
    uint32 AddFile(const FString& InFileName);
    void SetFileUserData(uint32 InFileEntryIndex, uint32 InUserData);
    void Flush(TArray<uint8>& OutBuffer, FAES::FAESKey InEncryptionKey);

private:
    uint32 GetDirectory(uint32 DirectoryName, uint32 Parent);
    uint32 CreateDirectory(const FStringView& DirectoryName, uint32 Parent);
    uint32 GetNameIndex(const FStringView& String);
    uint32 AddFile(const FStringView& FileName, uint32 Directory);
    static bool IsValid(uint32 Index);

    FString MountPoint;
    TArray<FIoDirectoryIndexEntry> DirectoryEntries;
    TArray<FIoFileIndexEntry> FileEntries;
    TMap<FString, uint32> StringToIndex;
    TArray<FString> Strings;
};

// IoDispatcherPrivate.h

class FIoBatchImpl
{
public:
    TFunction<void()> Callback;
    FEvent* Event = nullptr;
    FGraphEventRef GraphEvent;
    TAtomic<uint32> UnfinishedRequestsCount;
};

다음 코드는 플랫폼 독립적인 선호도 작업을 제공합니다.

cpp
// GenericPlatformAffinity.h

class FGenericPlatformAffinity
{
public:
    static const uint64 GetMainGameMask();
    static const uint64 GetRenderingThreadMask();
    static const uint64 GetRHIThreadMask();
    static const uint64 GetRHIFrameOffsetThreadMask();
    static const uint64 GetRTHeartBeatMask();
    static const uint64 GetPoolThreadMask();
    static const uint64 GetTaskGraphThreadMask();
    static const uint64 GetAudioThreadMask();
    static const uint64 GetNoAffinityMask();
    static const uint64 GetTaskGraphBackgroundTaskMask();
    static const uint64 GetTaskGraphHighPriorityTaskMask();
    static const uint64 GetAsyncLoadingThreadMask();
    static const uint64 GetIoDispatcherThreadMask();
    static const uint64 GetTraceThreadMask();

    static EThreadPriority GetRenderingThreadPriority();
    static EThreadCreateFlags GetRenderingThreadFlags();
    static EThreadPriority GetRHIThreadPriority();
    static EThreadPriority GetGameThreadPriority();
    static EThreadCreateFlags GetRHIThreadFlags();
    static EThreadPriority GetTaskThreadPriority();
    static EThreadPriority GetTaskBPThreadPriority();
};

하드웨어, ISA, 운영 체제, 컴파일러, 그래픽 API 및 해당 기능과 관련된 다양한 매크로가 아래에 정의되어 있습니다.

cpp
// Platform.h

PLATFORM_WINDOWS
PLATFORM_XBOXONE
PLATFORM_MAC
PLATFORM_MAC_X86
PLATFORM_MAC_ARM64
PLATFORM_PS4
PLATFORM_IOS
PLATFORM_TVOS
PLATFORM_ANDROID
PLATFORM_ANDROID_ARM
PLATFORM_ANDROID_ARM64
PLATFORM_ANDROID_X86
PLATFORM_ANDROID_X64
PLATFORM_APPLE
PLATFORM_LINUX
PLATFORM_LINUXARM64
PLATFORM_SWITCH
PLATFORM_FREEBSD
PLATFORM_UNIX
PLATFORM_MICROSOFT
PLATFORM_HOLOLENS
    
PLATFORM_CPU_X86_FAMILY
PLATFORM_CPU_ARM_FAMILY
    
PLATFORM_COMPILER_CLANG
PLATFORM_DESKTOP
PLATFORM_64BITS
PLATFORM_LITTLE_ENDIAN

PLATFORM_SUPPORTS_UNALIGNED_LOADS
PLATFORM_EXCEPTIONS_DISABLED
PLATFORM_SUPPORTS_PRAGMA_PACK
PLATFORM_ENABLE_VECTORINTRINSICS
    
PLATFORM_MAYBE_HAS_SSE4_1
PLATFORM_MAYBE_HAS_AVX
PLATFORM_ALWAYS_HAS_AVX_2
PLATFORM_ALWAYS_HAS_FMA3
    
PLATFORM_HAS_CPUID
PLATFORM_ENABLE_POPCNT_INTRINSIC
PLATFORM_ENABLE_VECTORINTRINSICS_NEON
PLATFORM_USE_LS_SPEC_FOR_WIDECHAR
PLATFORM_USE_SYSTEM_VSWPRINTF
    
PLATFORM_COMPILER_DISTINGUISHES_INT_AND_LONG
PLATFORM_COMPILER_HAS_GENERIC_KEYWORD
PLATFORM_COMPILER_HAS_DEFAULTED_FUNCTIONS
PLATFORM_COMPILER_COMMON_LANGUAGE_RUNTIME_COMPILATION
PLATFORM_COMPILER_HAS_TCHAR_WMAIN
PLATFORM_COMPILER_HAS_DECLTYPE_AUTO
PLATFORM_COMPILER_HAS_IF_CONSTEXPR
PLATFORM_COMPILER_HAS_FOLD_EXPRESSIONS
    
PLATFORM_TCHAR_IS_4_BYTES
PLATFORM_WCHAR_IS_4_BYTES
PLATFORM_TCHAR_IS_CHAR16
PLATFORM_UCS2CHAR_IS_UTF16CHAR
    
PLATFORM_HAS_BSD_TIME
PLATFORM_HAS_BSD_THREAD_CPUTIME
PLATFORM_HAS_BSD_SOCKETS
PLATFORM_HAS_BSD_IPV6_SOCKETS
PLATFORM_HAS_BSD_SOCKET_FEATURE_IOCTL
PLATFORM_HAS_BSD_SOCKET_FEATURE_SELECT
PLATFORM_HAS_BSD_SOCKET_FEATURE_GETHOSTNAME
    
PLATFORM_SUPPORTS_UDP_MULTICAST_GROUP
PLATFORM_USE_PTHREADS
PLATFORM_MAX_FILEPATH_LENGTH_DEPRECATED
PLATFORM_SUPPORTS_TEXTURE_STREAMING
    
PLATFORM_SUPPORTS_VIRTUAL_TEXTURES
PLATFORM_SUPPORTS_VARIABLE_RATE_SHADING
PLATFORM_REQUIRES_FILESERVER
    
PLATFORM_SUPPORTS_MULTITHREADED_GC
PLATFORM_SUPPORTS_TBB
PLATFORM_USES_FIXED_RHI_CLASS
PLATFORM_HAS_TOUCH_MAIN_SCREEN
PLATFORM_SUPPORTS_STACK_SYMBOLS
PLATFORM_HAS_128BIT_ATOMICS
PLATFORM_USE_FULL_TASK_GRAPH
    
PLATFORM_HAS_FPlatformVirtualMemoryBlock
PLATFORM_USE_FULL_TASK_GRAPH
PLATFORM_IS_ANSI_MALLOC_THREADSAFE
    
PLATFORM_SUPPORTS_GPU_FRAMETIME_WITHOUT_MGPU
    
(...)

다음은 플랫폼 속성, 출력 장치, 스택 탐색 등과 관련된 작업을 제공합니다.

cpp
// 출력디바이스플랫폼의 을(를) 활용하여 구현
struct FGenericPlatformOutputDevices
{
    static void                            SetupOutputDevices();
    static FString                        GetAbsoluteLogFilename();
    static FOutputDevice*                GetLog();
    static void                            GetPerChannelFileOverrides(TArray<FOutputDevice*>& OutputDevices);
    static FOutputDevice*                GetEventLog();
    static FOutputDeviceError*            GetError();
    static FFeedbackContext*            GetFeedbackContext();

protected:
    static void ResetCachedAbsoluteFilename();

private:
    static constexpr SIZE_T AbsoluteFileNameMaxLength = 1024;
    static TCHAR CachedAbsoluteFilename[AbsoluteFileNameMaxLength];

    static void OnLogFileOpened(const TCHAR* Pathname);
    static FCriticalSection LogFilenameLock;
};

// 플랫폼
struct FGenericPlatformProperties
{
    static const char* GetPhysicsFormat();
    static bool HasEditorOnlyData();
    static const char* IniPlatformName();
    static bool IsGameOnly();
    static bool IsServerOnly();
    static bool IsClientOnly();
    static bool IsMonolithicBuild();
    static bool IsProgram();
    static bool IsLittleEndian();
    static const char* PlatformName();
    static bool RequiresCookedData();
    static bool HasSecurePackageFormat();
    static bool RequiresUserCredentials();
    static bool SupportsBuildTarget( EBuildTargetType TargetType );
    static bool SupportsAutoSDK();
    static bool SupportsGrayscaleSRGB();
    static bool SupportsMultipleGameInstances();
    static bool SupportsWindowedMode();
    static bool AllowsFramerateSmoothing();
    static bool SupportsAudioStreaming();
    static bool SupportsHighQualityLightmaps();
    static bool SupportsLowQualityLightmaps();
    static bool SupportsDistanceFieldShadows();
    static bool SupportsDistanceFieldAO();
    static bool SupportsTextureStreaming();
    static bool SupportsMeshLODStreaming();
    static bool SupportsMemoryMappedFiles();
    static bool SupportsMemoryMappedAudio();
    static bool SupportsMemoryMappedAnimation();
    static int64 GetMemoryMappingAlignment();
    static bool SupportsVirtualTextureStreaming();
    static bool SupportsLumenGI();
    static bool SupportsHardwareLZDecompression();
    static bool HasFixedResolution();
    static bool SupportsMinimize();
    static bool SupportsQuit();
    static bool AllowsCallStackDumpDuringAssert();
    static const char* GetZlibReplacementFormat();
};

// 을(를) 활용하여 로드 pdb의 모듈 정보 。
struct FStackWalkModuleInfo
{
    uint64 BaseOfImage;
    uint32 ImageSize;
    uint32 TimeDateStamp;
    TCHAR ModuleName[32];
    TCHAR ImageName[256];
    TCHAR LoadedImageName[256];
    uint32 PdbSig;
    uint32 PdbAge;
    struct
    {
        unsigned long  Data1;
        unsigned short Data2;
        unsigned short Data3;
        unsigned char  Data4[8];
    } PdbSig70;
};

// 및 의 정보 。ANSI。
struct FProgramCounterSymbolInfo final
{
    enum
    {
        /** Length of the string used to store the symbol's names, including the trailing character. */
        MAX_NAME_LENGTH = 1024,
    };

    ANSICHAR    ModuleName[MAX_NAME_LENGTH];
    ANSICHAR    FunctionName[MAX_NAME_LENGTH];
    ANSICHAR    Filename[MAX_NAME_LENGTH];
    int32        LineNumber;
    int32        SymbolDisplacement;
    uint64        OffsetInModule;
    uint64        ProgramCounter;
};

// 정보
struct FProgramCounterSymbolInfoEx
{
    FString    ModuleName;
    FString    FunctionName;
    FString    Filename;
    uint32    LineNumber;
    uint64    SymbolDisplacement;
    uint64    OffsetInModule;
    uint64    ProgramCounter;
};

// 힙 순회(Traverse/Iterate)
struct FGenericPlatformStackWalk
{
    typedef FGenericPlatformStackWalk Base;

    struct EStackWalkFlags
    {
        enum
        {
            AccurateStackWalk                =    0,
            FastStackWalk                    =    (1 << 0),
            FlagsUsedWhenHandlingEnsure        =    (FastStackWalk)
        };
    };

    static void Init();
    static bool InitStackWalking()
    static bool InitStackWalkingForProcess(const FProcHandle& Process);
    static bool ProgramCounterToHumanReadableString( int32 CurrentCallDepth, uint64 ProgramCounter, ...);
    static bool SymbolInfoToHumanReadableString( const FProgramCounterSymbolInfo& SymbolInfo, ... );
    static bool SymbolInfoToHumanReadableStringEx( const FProgramCounterSymbolInfoEx& SymbolInfo, FString& out_HumanReadableString );
    static void ProgramCounterToSymbolInfo( uint64 ProgramCounter, FProgramCounterSymbolInfo& out_SymbolInfo);
    static void ProgramCounterToSymbolInfoEx( uint64 ProgramCounter, FProgramCounterSymbolInfoEx& out_SymbolInfo);
    static uint32 CaptureStackBackTrace( uint64* BackTrace, uint32 MaxDepth, void* Context = nullptr );
    static uint32 CaptureThreadStackBackTrace(uint64 ThreadId, uint64* BackTrace, uint32 MaxDepth, void* Context = nullptr);

    static void StackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, ... );
    static void StackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, ...);
    static TArray<FProgramCounterSymbolInfo> GetStack(int32 IgnoreCount, int32 MaxDepth = 100, ...);
    static void ThreadStackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, ...);
    static void StackWalkAndDumpEx(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, ...);
    static void StackWalkAndDumpEx(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, ...);

    static int32 GetProcessModuleCount();
    static int32 GetProcessModuleSignatures(FStackWalkModuleInfo *ModuleSignatures, const int32 ModuleSignaturesSize);
    static TMap<FName, FString> GetSymbolMetaData();

protected:
    static bool WantsDetailedCallstacksInNonMonolithicBuilds();
};

19.12 이 기사 요약

이 기사에서는 주로 컴퓨터 하드웨어 시스템의 상향식 개념과 UE에 의한 하드웨어 계층의 추상화 및 캡슐화에 대해 설명하므로 독자가 이 모듈에 대한 전반적인 이해를 가질 수 있습니다. 보다 기술적인 세부 사항과 원리에 관해서는 독자들이 UE 소스 코드를 직접 연구해야 합니다.

특별 지침

  • 모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.

  • 이 시리즈 기사는 저자가 직접 작성한 것이며 Blog Park 및 Zhihu에만 게시됩니다. 이 글의 링크를 공유하는 것은 환영하지만 동의 없는 재인쇄는 허용되지 않습니다!

  • 계속되는 일련의 기사. 전체 목록을 보려면 언리얼 렌더링 시스템 분석 - 오프닝 노트를 클릭하세요.

  • 블로거들의 루머에 대한 '큰 공장'의 '기술 전문가'의 반응에 대해








📚 참고 자료 및 공식 링크 (References)


🔗 연관 지식 베이스 (Wiki & Tools)

Based on the legendary technical series by Timlly (0向往0 / 毛星云)