메모리
- 휘발성이지 않은 데이터(외움)
- 사라질 수도 있는 데이터(망각)
- 저장할 수 있으면 메모리다
bit
- 메모리를 사용하기 위해 도입한 방법
- 8bit = 1byte
- 4byte = 32bit
- 8byte = 64bit
- bit는 컴퓨터에서 사용할 수 있는 슬롯수(2^n)만큼 사용가능, 즉 술롯을 하나만 늘려도 2^n-1의 두배만큼 늘어난다.
진수
- 수가 그 진수만큼 나오면 옆으로 넘김
- 4bit = 2^4 = 16 = 0xF
- 8bit = 2^8 = 256 = 0xFF
- 12bit = 2^12 = 4096 = 0xFFF
- 16bit = 2^16 = 65536 = 0xFFFF
- bit를 16진수로 표현하면 보기도 편하고 외우기도 쉽다.
메모리 종류와 캐싱
- 용량단위로 크기도 작고 비싸기 때문에, 그래서 필요한게 캐싱
- 많이 쓰일수록 상위로 올리자
캐싱
- 데이터는 전체를 옮기는 것이 이닌 일정부분만 상위로 올림
- 게임 CD를 넣으면 CD ROM 디바이스를 통해 감지해서 가상볼륨을 만듬
- 가상볼륨을 HDD로 옮김
- HDD에 있는 내용을 RAM에 옮김
- RAM에서 게임에 실행시키기에 최소한의 값들을 CPU에 올림
- CPU는 그 값들을 빠르게 처리
- L2와 L2에서는 그 메모리를 저장해서 추후에 다시 사용할때 캐싱된 데이터를 사용해서 빠르게 처리(=캐시 히트)
코드단위 메모리 구분법
- al Memory : 함수 안에서 사용되는 메모리
- Global Memory : 함수 밖에서 사용되는 메모리
- Static Memory : 프로그램 단위를 나타내는 메모리
- External Memory : 라이브러리 / 모듈 단위 나누는 메모리
쓰레드 프로레스 단위 메모리 구분법
- 쓰레드의 TLB
- 프로세스
프로레스 단위 메모리 구분법
- 데이타섹션
- 코드섹션
- BSS
- Static
리틀엔디안 VS 빅엔디안
- 1234를 12를 넣고, 34를 넣으면 이것이 빅엔디안(큰값부터 넣는다)
- 1245를 34를 넣고, 12를 넣으면 이것이 리틀엔디안(작은값부터 넣는다)
- 1234를 보낼때 리틀(34, 12) -> 리틀(12, 34) => 34, 12로 보냈는데 12, 34로 읽게되어버림
- 그래서 통신을 할때는 빅엔디안을 사용한다.
- 빅 -> 빅 => 빅
- 빅엔디안을 쓰는 이유는 쉽고 바뀌지 않기 떄문
- 리틀엔디안을 쓰는 이유는 다운캐스팅을 할때 0x12345678(int) -> 0x5678(short) 할때 리틀엔디안을 쓰면 그냥 짜르면 그만 즉 메모리 접근의 효율이 좋다.
short value = 0x1234;
short* pv = &value;
printf(*pv); //1234
char* cpv = pv;
printf(*cpv); //34
포인터
- 점을 찍는 행위자
- 주소값을 가리킨다.
- 포인터의 증감연산자는 다음 섹션(=스크린)을 가리킨다. 즉, 포인터가 지정할 수 있는 크기의 다음 부분을 가리킨다.
- p는 0번지 주소값을 가리키고, *p를 해줘야 역참조해서 0123이 출력된다.
- s는 0번지 주소값을 가리키고, *s로 역참조를 해줄경우 01이 출력된다.
- p포인터의 ++증감 연산은 다음 스크린으로 넘겨 4567데이터의 주소를 가리킨다.
- s포인터의 ++증감 연산은 다음 스크린으로 넘겨 23데이터의 주소를 가리킨다.
- 위의 그림은 위가 리틀엔디안 아래가 빅엔디안인 경우 입니다.
- 리틀엔디안의 경우 int* p 에서 short* s로 다운캐스팅을 할경우 그대로 7856을 사용할 수 있습니다.
- 빅엔디안의 경우 int* p 에서 short* s로 다운캐스팅을 할경우 1234가 나오게 되어 원하는 값을 얻기 위해서는 s++를 코드상 추가적으로 작업을 해줘야 합니다.
- 즉 최적화 측면에서 리틀엔디안을 사용한다.
페이지 레이아웃
char* a = "test"
a++;
- 위 코드에서 char* a = 은 코드영역
- 위 코드에서 test는 데이터영역
- 전역변수는 bss / static 영역에 저장된다.
- 스택메모리는 리턴가능한 공간에서만 쓸 수있다. (=로컬메모리)
- a는 스택영역에 int형이기 때문에 리틀엔디안 방식으로 23 01 00 00이 입력됨
- a를 가르키는 지시자 역할은 SP(=Stack Point)가 전담
- SP의 ++의 기준은 Register 0x86 32bit 4byte만큼 움직인다.
- 만약 Short일경우 23 01만 쓰게 되고 나머지 영역은 비어있게 된다.
- OS는 23 01을 사용하고 -2를 시켜서 남은공간을 쓰는 것보다 4바이트를 넘겨서 다음 영역에 쓰는것이 더 효율적이라고 판단했기 때문이다.
SP(Stack Pointer)
void main(){
int a = 123;
int b = 123;
f()
int c = 123;
return;
}
void f(){
int a = 123;
return;
}
- 스택메모리의 크기를 12byte라고 가정하고 위의 코드를 실행한다.
- 스택메모리 0x0에 a를 저장한다.
- 스택메모리 0x4에 b를 저장한다.
- f()를 불러 0x8에 a를 저장하면 스택메모리의 공간이 꽉차게 된다.
- f() 함수에서 return을 만나면 쓰레드가 현재 실행한 코드 수만큼 SP를 --한다.
- 함수에서 리턴을 만나 --된 SP의 현재 주소값은 0x4를 가리킨다.
- c를 0x4 덮어 씌워 저장한다.
IP(instrution Pointer)
- IP는 함수를 만나면 함수의 주소값을 얻는다. 즉 현재함수와 호출함수의 오프셋을 받아 오프셋만큼 더한다.
- IP는 함수를 만나면 현재 주소값 0x0 + 명령어의 크기만큼 더해서 스택메모리에 저장해서 넘어간다.
BP
- IP를 SP에 넣을당시의 주소를 저장하는 레지스터
- return을 만나면 SP - BP + 메모리 크기 => 함수 호출하고 실행한크기를 알아냄
- 실행한 크기만큼 SP--시킴
RET
- Return문을 만나면 수행하는 어셈블리 명령어
- 현재 SP를 읽어서 IP에 집어넣는 역할
Heap 영역
- new 연산자로 만들어지는 변수들
- 개발자가 할당 / 초기화 / 삭제를 모두 해줘야되는 영역
- new 연산자가 포인터를 돌려주는데 그걸로 SP나 IP의 역할들을 개발자가 다루는 형태
- 제대로 소멸시키지 않으면 메모리 누수
세그먼테이션
- 메모리를 어떻게 나눌지
- 코드영역에 실제 주소가 0x0이고, 스택영역이 0x10이면, 스택영역을 항상 0x0으로 볼수있게 하는 역할
- 스택영역의 실제주소는 DS에 저장됨
- 코드영역의 실제주소는 CS에 저장됨
- 스택영역의 a라는 데이터가 0x1000에 위치한다면 그 값은 스택영역의 0부터의 오프셋이라는 크기이다.
- 즉 스택영역의 실제 주소가 0x10이라고 가정하면 0x10(DS) + 0x1000(Offset)을 해줘야 실제 주소가 나오게 된다.
정리
void main()
{
1. int a = 123;
2. F();
7. int c = 456;
8. return;
}
void F()
{
3.
4. int a = 123;
5. int c = 123;
6. return;
}
SP | BP | IP | Stack |
0x0 | 1 | 123 |
- SP는 0x0을 가리키며, SP를 통해서 123을 스택에 집어넣는다.
- 그 후 SP는 ++되어 0x4가 된다.
SP | BP | IP | Stack |
0x4 | 2 | [123, 7(IP + 명령어길이)] |
- 함수를 만나고 SP에 함수가 끝났을때의 주소값을 넣어준다. (현재 IP + 명령어길이)
- 그 후 SP는 ++되어 0x8이 된다.
SP | BP | IP | Stack |
0x8 | 0x8 | 3 | [123, 7(IP + 명령어길이)] |
- 함수 내부로 들어와서 BP에 SP의 주소값을 넣어준다. (함수프롤로그)
- 그 후 SP는 ++되어 0xC이 된다.
SP | BP | IP | Stack |
0xC | 0x8 | 4 | [123, 7(IP + 명령어길이), 123 |
- 변수 123을 현재 SP에 넣는다.
- 그 후 SP는 ++되어 0x10이 된다.
SP | BP | IP | Stack |
0x10 | 0xC | 5 | [123, 7(IP + 명령어길이), 123, 123] |
- 리턴을 만난뒤 SP에 BP의 주소값을 넣는다. (0x8)
- 그 후 SP는 --되어 0x4가 된다.
- ret을 만나서 IP에 SP가 가리키는 주소값(7)을 넣는다.
- SP가 올라가서 할당이 해제되었지만, 데이터는 안지워진상태
SP | BP | IP | Stack |
0x8 | 0x8 | 7 | [123, 7(IP + 명령어길이), 456, 123 |
- 변수 456을 현재 SP에 넣는다.
- 기존에 있던 123이 456으로 덮어씌워진다
SP | BP | IP | Stack |
0x0 | 0x8 | 7 | [123, 7(IP + 명령어길이), 456, 123] |
- 마지막으로 호출규약(c++, window)에 의해 sp가 0x0으로 초기화된다.
논리주소, 선형주소, 물리주소
논리주소 -> 선형주소 -> 물리주소로 변경이 된다.
요즘은 세그먼테이션을 잘 안쓰이기 때문에 논리주소 -> 물리주소로 변경된다.
논리주소
- 0x1234
- 개발자들이 쓰기 편한 주소
선형주소
- CS:0x04
- CS세그먼테이션에서 0x04번 오프셋
물리주소
- 실제 물리주소
- RAM에 3번째 칩셋에 있다.
외부 프래그먼트
위의 그림과 같이 메모리가 다음과 같이 사용하고 있을 때, 6바이트의 공간이 비어있지만 3바이트 크기의 메모리는 사용하지 못하는 현상
페이징
- 외부 프래그먼트를 해결하기 위해 나온 방법
- 메모리를 미리 짤라(4kb만큼) 놓는 상태
- 데이터 가운데 할당되지 않게 하기위해 해놓은 상태
- 하지만 4kb보다 적은 메모리가 할당되었을 경우 안쪽데이터가 남는 내부 프래그먼트가 발생한다.
더 깊게으로 들어와서 개발자가 사용하는 논리주소는 3가지로 나눠져 있습니다.
만약 논리주소가 0x02010100 이라면 다음과 같습니다.
- PET는 물리주소가 매핑되어있다.
- PDT에는 PDDT의 오프셋이 매핑되어있다.
- PDDT는 PET의 오프셋이 매핑되어있다.
- 02 : PDT에 2번째 오프셋 테이블 리스트
- 01 : 2번째 PDDT에서 1번째 4byte가 매핑되어있는 PET번호를 가리킨다.
- 01 : 해당 PET가 가리키는 물리메모리는 10번째에서 00번째 4byte를 가리킨다.
- 00 : PET가 매핑된 물리메모리의 00번쨰 (이것이 물리주소)
결론적으로 이렇게 나눈 이유는 트리구조이기 때문에, 데이터 구분의 장점으로 데이터를 효율적으로 사용이 가능하다.
메모리 주소 변환
논리주소 CS:0x03 만약 세그먼테이션을 사용하게 된다면, CS offset이 0이 아니고 0x34같이 오프셋을 가지게 된다. 그래서 이값을 0x03 + 0x34 더해서 나온주소가 선형주소이다.
0x37의 비트를 쪼개서 이것을 이용해서 PDT을 접근한다.
그래서 나온 최종결과가 물리주소가 된다.
가상 메모리
램이 1GB 안되는데, SSD쪽에 접근할 경우 서로의 주소가 바꿔서 사용하는것이다.
페이지폴트
- 만약 내가 접근하는 주소가 메모리에 올라가 있지 않을 경우, PET에 페이지폴트 플래그가 0으로 되어있음.
- mmu(memory management unit)에 의해서 CPU에 페이지폴트 인터럽트를 날림
- CPU는 메모리들 중 가장 안쓰는 페이지(victim page)를 선택하여 페이지 폴트 플래그를 1에서 0으로 바꾼다.
- 내가 접근하는 주소의 페이지 폴트 플래그를 1로 바꿔 서로 스위칭한다.
페이지폴트 쓰레싱
- 만약 victim page를 선택하는 알고리즘이 안좋아서 많이 쓰는 페이지를 선택해서 계속해서 퀀텀때마다 페이지가 교환될 경우 발생하는 현상
- CPU 스케쥴링 속도보다 페이지 교체속도가 더 길어서 컴퓨터가 느려짐
copy on write
- 메모장을 키고 아무것도 안하고, 또 메모장을 키면 동일한 메모리 주소값을 보고있다.
- 그상태에서 메모장 하나에 글을 적을경우 메모리에 새롭게 적재되어 주소값이 변경된다.
- 이러한 방식으로 최적화 된다.
메모리 로컬리티
- 페이지 폴트를 적게 일으키자.
- 메모리가 가까운곳으로 접근할 수 있게 짜자
for(int i=0; i<4; i++){
for(int j=0; j<100; j++){
arr[j][i]
}
}
// arr[0][0]
// arr[1][0]
// arr[2][0]
// ..
for(int i=0; i<100; i++){
for(int j=0; j<4; j++){
arr[i][j]
}
}
// arr[0][0]
// arr[0][1]
// arr[0][2]
// ...
- 위의 두코드는 모두 400번을돌지만 아래의 코드가 훨씬 페이지폴트가 적게 발생하여 효율적으로 돌아간다.
Spatial locality / Temporal locality
- Spatial locality : 우리가 짠 코드에서 자주 참조가 되는 변수여야한다.
- Temporal locality : 그 주변 변수도 자주 참조 되어야 한다고 생각하고 코드를 짜야된다.