
보통 instruction은 sequential 하게 나열 → 순서대로 하나씩 실행 시 overhead ↑
→ 두 개 이상의 instruction을 ‘동시에’ 실행하려면 ?
: 서로 독립적인 명령어들을 최대한 찾고, 이들을 오버랩해서 실행

→ 해당 예시에서 모든 instruction을 실행하려면 5 cycle이 걸림

Scalar processor : 한 번에 하나의 데이터를 처리하는 CPU (1ALU)

Superscalar processor


컴퓨터 성능 → CPU의 기본 처리 속도 = cycle 당 instruction을 한 번 처리하는 것 (IPC = 1)
⇒ 한 cycle에 instruction이 하나 이상 처리되면 CPU는 그만큼 한 번에 여러 개의 연산 처리가 가능 ~ 체감 속도 ↑
ex. “ “
해당 연산 결과를 뽑으라는 instruction을 줬다고 가정했을 때
instruction이 처리되는 path를 여러 개 만들고 각각의 instruction을 해당 path를 통해서 처리하게 하면 됨
하나는 data만 처리할 수 있게
하나는 instruction만 처리할 수 있게
⇒ instruction을 읽어오면서 data pipeline을 통해서 결과를 뽑을 수 있게끔
independent한 instruction을 어떻게 찾냐 → dynamic scheduling을 통해서 ..
Superscalar processor의 장점
프로그램 호환성
: 기존의 프로그램 코드를 변경하지 않고도 성능 향상 가능
processor가 자동으로 instruction 간의 independency 판별, parellel로 실행 가능한 instruction 선정
효율성
: 복수의 ALU를 통해 instruction을 동시에 처리 → 처리 속도가 ↑
(특히 복잡한 계산이나 대량의 data 처리가 필요한 application에서 유용)

자원의 한계
: 동시에 실행할 수 있는 instruction의 수는 결국 processor 내의 ALU의 수와 관련 자원에 의해 제한됨
instruction dependency
: 일부 instruction은 다른 instruction의 result에 의존 → 성능의 병목 현상
메모리 대역폭
: processor가 더 많은 processor를 동시에 처리하려면, memory로부터 data를 더 빠르게 가져와야 함

: clock speed 향상의 한계에 직면 (발열 문제)
→ 여러 개의 처리 core를 하나의 칩에 집적하는 multi-core processor !

여러 개의 작업을 보다 효율적으로 처리하기 위해 2개 이상의 느린 processor가 붙어있는 ‘집적회로’
power가 증가되고 열 손실이 감소한 2개의 processing 엔진 > processing core가 하나일 자원이 부족한 칩
e.g. 80% clock frequency → 2 cores :

다시 돌아와서 .. multi-core processor에서 다음 코드를 돌린다고 가정하면,
→ multi-thread와 같은 기법을 사용하지 않으면, core 한개가 노는 상태 ..
~ 결국 위 코드의 실행 시간은
: process 내에서 process의 자원을 이용하여 실행되는 여러 흐름의 단위
: process 내에서 실제로 작업을 수행하는 주체
→ 운영체제의 스케줄러에 의해 독립적으로 관리될 수 있는 프로그래밍된 명령어의 가장 작은 시퀀스

1. subroutine을 호출하는 경우
process
작업 중인 프로그램
process가 memory에 올라갈 때 OS로부터 시스템 자원을 할당받음
- process마다 각각 독립된 memory 영역
- 기본적으로 process끼리 다른 process의 memory에 직접 접근 불가
- Code/Data/Stack/Heap의 형식
→ 한 프로세스를 실행하다가 오류가 발생해서 프로세스가 강제로 종료된다면, 다른 프로세스에게 어떤 영향이 있을까?
: 공유하고 있는 파일을 손상시키는 경우가 아니라면 아무런 영향을 주지 않는다.
thread
프로세스의 코드에 정의된 절차에 따라 실행되는 특정한 수행 경로

메모리를 서로 공유할 수 있음
heap 메모리는 공유하기 때문에 서로 다른 스레드에서 가져와 읽고 쓸 수 있음
→ 어떤 스레드 하나에서 오류가 발생한다면 같은 프로세스 내의 다른 스레드 모두가 강제로 종료
공유되지 않는 resource
#include <iostream>
const int N = 100;
int main()
{
int a[N], b[N], k[N], c[N];
for(int i=0;i<N;i++) {
a[i] = i;
b[i] = 1000*i;
k[i] = 10;
c[i] = 0;
}
for(int i=0;i<N;i++) {
c[i] = k[i]*a[i];
c[i] += k[i]*b[i];
}
return 0;
}
#include <iostream>
#include <thread>
#include <vector>
// 정수 포인터를 선언하여 동적으로 할당된 배열을 가리키는 역할
int *a, *b, *k, *c;
// 벡터 연산을 수행하는 함수
// mac : multiply-accumulate, 주어진 두 배열을 곱하고 결과를 누적하는 역할
// tid : thread의 index, num_threads : 전체 thread의 수
void mac(int tid, int num_threads)
{
// 전체 vector의 크기를 전체 thread 수로 나누어 각 thread가 처리할 부분을 결정
for(int i=0;i<N/num_threads;i++)
{
// idx : 현재 thread가 처리할 index
int idx = tid*(N/num_threads) + i;
c[idx] = k[idx] * a[idx];
c[idx] += k[idx] * b[idx];
}
return;
}
int main(int argc, char* argv[])
{
...
// thread를 저장할 vector를 선언
std::vector<std::thread> threads;
// 전체 thread 수만큼 반복하면서 thread를 생성
for(int t=0;t<NT;t++) {
// 새로운 스레드를 생성하고 벡터 threads에 추가
// 각 스레드는 mac 함수를 호출하며, 스레드의 인덱스(t)와 전체 스레드 수(NT)를 전달
threads.push_back(std::thread(mac, t, NT));
}
// 모든 스레드가 종료될 때까지 대기
for(auto& thread: threads) {
thread.join();
}
return 0;
}

병렬화를 위한 loop나 반복문을 쉽게 target으로 지정할 수 있는 개념
→ 프로그래머가 루프의 반복이 독립적이라고 선언하고, 해당 루프를 병렬로 실행할 수 있도록 지시
#include <iostream>
//const int N = 1000000;
#defiine N 1000000000LL
int main()
{
...
#pragma omp parallel for
for(long long int i=0;i<N;i++) {
c[i] = k[i]*a[i];
c[i] += k[i]*b[i];
}
return 0;
}
프로그래머는 OpenMP의 지시문을 사용하여 루프가 독립적임을 선언
OpenMP는 해당 지시문을 해석하여 루프를 병렬로 실행하는 코드를 생성
→ 프로그래머는 직접 thread를 관리하지 않고도 병렬 처리를 수행할 수 있음
이전의 예시에서

→ 하나의 명령어가 다수의 연산장치(ALU)에서 동시에 실행되도록 : 병렬성 ↑, 연산량 효율적으로 처리
#include <immintrin.h>
#pragma omp parallel for
for(long long int i=0;i<N;i+=8) {
// __m256i : 256비트(32바이트)크기의 정수형 벡터를 나타내는 데이터 타입
// _mm256_load_si256 : AVX register에 data load
__m256i A = _mm256_load_si256((__m256i*)(&a[i]));
__m256i B = _mm256_load_si256((__m256i*)(&b[i]));
__m256i K = _mm256_load_si256((__m256i*)(&k[i]));
// _mm256_mullo_epi32 : 두 개의 256비트 정수형 벡터의 각 요소를 곱한 결과를 반환
__m256i C1 = _mm256_mullo_epi32(A,K);
__m256i C2 = _mm256_mullo_epi32(B,K);
// _mm256_add_epi32 : 두 개의 256비트 정수형 벡터의 각 요소를 더한 결과를 반환
__m256i C = _mm256_add_epi32(C1,C2);
// _mm256_store_si256 : AVX register의 데이터를 메모리에 저장
_mm256_store_si256((__m256i*)(&c[i]), C);
}
return 0;
}

벡터 처리에서 조건부 실행을 구현하는 방법
⇒ Predication : Masking






일반적으로 병렬성을 향상시키는 데 사용되지 않음


→ 특정 유형의 작업에 최적화된 특수 목적 프로세서에서 사용
잘 보고 갑니다~