잘 작성한 컴퓨터 프로그램은 좋은 지역성 locality을 보여준다.
1. 시간 지역성 temporal locality
한번 참조된 메모리 위치는 가까운 미래에 다시 여러 번 참조될 가능성이 높은 것
2. 공간 지역성 spatial locality
어떤 메모리 위치가 참조되면, 가까운 미래에 근처의 메모리 위치를 참조할 가능성이 높은 것(예를 들어 배열의 경우)
하드웨어는 지역성을 활용하여 캐시메모리 등 작업을 빠르게 하기 위한 설계가 적용되어 있으므로, 지역성을 이해하고 좋은 지역성을 갖도록 프로그램을 작성하는 것은 성능에서 중요하다.
int sumvec(int v[N]) {
int i, sum = 0;
for (i = 0; i < N; i++)
sum += v[i];
return sum;
}
배열의 합을 계산하는 함수를 생각하자.
위 코드에서 sum
은 반복적으로 참조되므로 좋은 시간 지역성을 가지고 있다.
반면 스칼라 변수로서 인접한 메모리 공간과 연관이 없으므로 공간 지역성이 존재하지 않는다.
v
의 원소들은 순차적으로 읽히므로 v
에 대해서는 좋은 공간 지역성을 가지고 있다.
그러나 각 원소들은 한 번만 접근되므로 나쁜 시간 지역성을 가지고 있다.
함수 sumvec
은 좋은 시간 또는 공간 지역성을 지니므로 전체적으로 좋은 지역성을 지녔다고 볼 수 있다.
이렇게 벡터의 매 k번째 원소를 방문하는 것을 stride-k 참조 패턴이라고 부른다.
sumvec
함수는 stride-1 참조 패턴을 가진다.
일반적으로 k가 증가하면 공간 지역성은 감소한다.
int sumarrayrows(int a[M][N]) {
int i, j, sum = 0;
for (i = 0; i < M; i++)
for (j = 0; j < N; j++)
sum += a[i][j];
return sum
}
이차원 배열의 합을 계산하는 함수를 생각하자.
위 함수는 stride-1 참조 패턴을 갖는다.
int sumarraycols(int a[M][N]) {
int i, j, sum = 0;
for (j = 0; j < N; j++)
for (i = 0; i < M; i++)
sum += a[i][j];
return sum
}
똑같은 동작을 하지만 열우선 참조를 하는 다른 함수를 생각하자.
이제 stride-N 참조 패턴을 갖는다.
이렇게 같은 역할을 하더라도 공간 지역성에서 차이가 커질 수 있다.
프로그램의 인스트럭션 역시 메모리에 저장되고 CPU가 읽어들여야 하므로 인스트럭션 선입(읽기)에 대한 지역성도 평가할 수 있다.
예를 들어 sumvec
함수에서 사용한 for루프 내의 인스트럭션들은 순차적인 메모리 순서대로 반복해 실행되며, 좋은 공간 지역성을 갖게 된다.
시스템은 프로그램의 실행과는 관련 없는 시스템 상태 변화에도 반응할 수 있어야 한다.
시스템은 제어흐름의 갑작스런 변화를 만드는 방법(예외적인 제어흐름 exceptional control flow (ECF))으로 이러한 상황에 반응한다.
예외상황은 하드웨어 또는 운영체제에 의해 구현된 예외적인 제어흐름의 한 형태이다.
예외상황에 대한 처리 방식을 간단히 나타낸 그림이다.
명령어 를 실행하고 있을 때 프로세서 상태에 중요한 변화(이벤트)가 일어난다면,
상태는 예외처리 핸들러로 보내지고 보내진 상태에 따라 예외를 처리한 뒤 프로그램을 종료시키거나 다시 프로그램의 실행 흐름으로 되돌아 간다.
예외상황은 하드웨어와 소프트웨어가 긴밀하게 협력해야한다.
하드웨어와 소프트웨어 사이에 작업이 분배되는 모습을 자세히 살펴본다.
시스템은 가능한 예외상황마다 예외번호를 할당하고 있다.
일부는 프로세서(하드웨어) 설계자가, 나머지는 운영체제 커널(소프트웨어) 설계자가 할당한다.
프로세서 설계가 할당한 예외번호의 예: divide by zero, 페이지 오류, 메모리 접근 위반, breakpoint, 산술연산 오버플로우
커널 설계자가 할당한 예외번호의 예: 시스템 콜, 외부 I/O 디바이스로부터의 시그널
시스템 부팅 시 운영체제는 예외 테이블[^8-1]을 할당하고 예외번호별 처리 핸들러로 할당한다.
즉, 예외 테이블의 엔트리(배열 인덱스) k가 예외상황 k에 대한 핸들러의 주소를 갖는다.
프로세서가 이벤트 발생을 감지하면 해당되는 예외번호 k를 결정하고, 예외 테이블의 k를 참조해 핸들러를 호출한다.
[^8-1]: 예외 테이블의 주소는 예외 테이블 베이스 레지스터라는 특별한 레지스터에 저장한다.
예외상황은 프로시저 콜과 유사하지만 중요한 차이점이 있다:
입출력 디바이스로부터 신호를 받아 발생하는 예외이다.
특정 인스트럭션의 실행 여부와 관련이 없기 때문에 비동기적(Async)이다.[^8-2]
프로세서가 인스트럭션 실행을 완료하고 인터럽트 시그널을 감지하게 되면, 시스템 버스에서 예외번호를 읽어 해당 인터럽트 핸들러를 호출한다.
핸들러가 리턴할 때엔 항상 제어를 다음 인스트럭션으로 돌려준다. 즉, 프로그램은 인터럽트가 발생하지 않은 상황과 같이 계속 실행된다.
[^8-2]: 다른 예외의 종류들은 오류 인스트럭션 faulting instruction의 실행에 의해 동기적으로 일어난다. 비동기적 예외를 외부 인터럽트 External Interrupt 또는 하드웨어 인터럽트 Hardware Interrupt, 동기적 예외를 내부 인터럽트 Internal Interrupt 또는 소프트웨어 인터럽트 Software Interrupt라고 부르기도 한다.
트랩은 의도적인 예외상황이며 어떤 인스트럭션을 실행한 결과로 발생한다(Sync).
프로그램이 시스템 콜을 호출하였을 때나 예외 상황이 발생하여 시스템으로 제어를 넘기기 위해 발생시키는 예외상황이다.
시스템 콜은 사용자 프로그램에서 커널의 동작을 요청할 때 사용하는 프로시저와 유사한 인터페이스이다.
시스템 콜은 커널 모드에서 돌아가며, 이로 인해 커널 내에서 정의된 스택에 접근하며, 시스템을 제어하는 모든 인스트럭션을 실행할 수 있다.
x86-64에서 시스템 콜은 syscall
이라는 트랩 인스트럭션을 통해서 제공된다.
리눅스 시스템 콜에 전달되는 모든 인자들은 범용 레지스터를 통해서 이루어진다.
%rax 레지스터에 시스템 콜 번호를 보관하고, argument용 레지스터에 최대 여섯 개의 인자들을 보관한 후 호출하게 된다.
핸들러가 정정할 수 있을 가능성이 있는 에러 조건일 때 발생한다.
오류가 발생하면 프로세서는 오류 핸들러로 제어를 이동한다.
핸들러가 에러 조건을 정정할 수 있다면, 오류를 발생시킨 인스트럭션(현재 인스트럭션)으로 제어를 돌려주어 프로그램 실행을 계속한다.
정정할 수 없다면, 핸들러는 커널 내부의 abort 루틴으로 리턴하여 프로그램을 종료한다.
대표적인 예시로 페이지 오류 예외가 있다.
인스트럭션이 참조하는 메모리가 물리 메모리에 페이지되어 있지 않은 상황에 발생한다.
핸들러는 디스크에 있는 페이지를 물리 메모리로 로드하고 다시 오류를 일으킨 인스트럭션으로 돌아가게 해준다.
하듸웨어 같은 치명적인 에러에서 발생한다. 중단 핸들러는 무조건 응용프로그램을 중단하는 abort 루틴으로 제어를 넘겨준다.
직접 메모리 접근(Direct Memory Access, DMA)은 주변기기에서 CPU의 처리를 거치지 않고 직접 메인 메모리에 접근해서 데이터를 가져오는 기능이다.
PIO는 DMA의 반대개념으로써, 장치들 사이에 전송되는 모든 데이터가 CPU를 거쳐가는 방식이다.
PIO는 주변기기가 CPU에 필요한 메모리에 대한 정보를 주고 CPU가 메인 메모리에서 데이터를 받아 요청한 기기에 전송한다.
DMA는 CPU를 거치지 않고 직접 해당 기기의 메모리에 접근해서 필요한 정보를 가져온다.
설명회 들으면서 메모한 내용.
전부 쓸 수는 없어서 민감한 부분은 많이 잘라냈다.
순간 효율만 추구하면 잘하는 걸 찾을 수 없다(오버피팅)
급한 일은 언제나 있다.
중요한 일에 쓸 시간을 고정시켜둬라
나머지 시간에 일이 생겨도 하루를 망친 느낌이 안 든다.
정글이 끝나도 실무 관련된 경험을 쌓지 않는 한 취업 가능성은 0일 것이란 생각이 들었다.
다른 분야도 마찬가지겠지만, 기본기를 쌓을 수 있는 직장은 거의 없는 것 같다.