공부하다 보면 함수의 오버헤드가 발생했다, 메모리 오버헤드가 발생했다 등등 overhead 라는 용어가 자주 등장한다.
여기서 말하는 (컴퓨터과학 관점에서의) overhead는 특정 작업을 수행하기 위해 필요하지만, 실제 유용한 작업에 직접 기여하지는 않는 추가적인 리소스나 시간을 의미한다. 즉 특정 작업을 수행할 때, 그 핵심 동작을 위한 비용은 아니지만 괜히 간접적으로 낭비되는, 따라서 더 최적화할 여지가 있는 비용이라는 거다.
GPT한테 물어보니 다음과 같은 예시를 준다.
CPU 아키텍처가 32-bit에서 64-bit로 이동하면서 다음과 같은 변화가 발생하였다:
즉, 더 큰 워드 크기와 더 큰 메모리 주소 범위 덕분에 더 큰 데이터 세트를 더 빠르게 처리할 수 있게 되었다. 또한 메모리에 저장할 수 있는 크기도 더 커졌다. 이는 컴퓨터 성능의 전반적인 개선으로 이어진다.
x86-64 아키텍처에서의 플래그 레지스터. (32bit 모드에서는 'EFLAGS' 라는 이름이었음)
CF (Carry Flag), ZF (Zero Flag), SF (Sign Flag), OF (Overflow Flag) 등의 플래그들이 해당 레지스터 내에 위치한다.
이들 외에도 여러 상태 플래그, 시스템 플래그, 그리고 컨트롤 플래그를 포함하며, 이러한 플래그는 프로세서의 상태를 나타내고 명령어 실행을 제어하는 데 사용된다.
각 플래그는 특정 연산의 결과에 따라 설정되거나 클리어된다.
예를 들어:
이 플래그들은 조건 분기 명령어나 조건 명령어 실행 등에 사용되어, 프로그램의 흐름을 제어하는 데 매우 중요
cmp, test, 그리고 set 인스트럭션은 조건부 실행과 관련이 있다.
이 인스트럭션들은 종종 함께 사용되어 조건부 실행을 수행한다.
예를 들어, 두 값이 동일한지 확인하고 결과에 따라 레지스터를 설정하려면 cmp, je (jump if equal), sete 명령을 함께 사용한다.
간단한 예제를 통해 이를 살펴보자.
<C>
int compare(int a, int b) {
return a == b;
}
<ATT 어셈블리 코드>
is_equal:
cmp %edi, %esi # 레지스터 %edi와 %esi를 비교
sete %al # 만약 두 값이 동일하다면, %al 레지스터에 1을 설정
movzbl %al, %eax # %al의 값을 %eax로 확장 (즉, %eax의 나머지 비트를 0으로 설정)
ret # 결과를 반환하고 함수를 종료
이 예에서 sete %al 명령은 두 값이 동일한지 여부에 따라 %al 레지스터에 1 또는 0을 설정한다. 이 값은 나중에 반환될 값으로 사용되며, 이는 두 수가 같은지 여부를 나타낸다.
movzbl 명령은 %al의 값을 %eax로 확장(%al 보다 상위에 있는 24 비트를 0으로 채움)하여, 이 함수의 반환 값을 준비한다.
%al과 %eax는 동일한 레지스터 내에 존재하며, 단지 서로 다른 비트 영역에 접근하는 방식이다. %al은 %eax 레지스터의 하위 8비트에 접근하고, %eax는 전체 32비트에 접근한다. 이러한 방식으로, 레지스터의 서로 다른 부분에 접근할 수 있도록 하는 것은 아키텍처의 유연성을 제공하며, 특정 연산에 필요한 만큼의 데이터만 사용할 수 있게 한다.
Q. 왜 %al의 값을 %eax로 확장하는 작업이 필요할까?
함수의 반환 값은 일반적으로 %eax 레지스터에 저장된다. 그러나 set 명령어는 오직 8비트 레지스터인 %al에만 값을 설정한다. 따라서 %al의 값을 %eax로 확장하여 전체 %eax 레지스터에 해당 값을 설정해야 한다. 이렇게 하면 함수가 올바른 반환 값을 가지게 된다.
movzbl 명령어는 Move Zero-extended Byte to Long의 약자이다. (from값 to값 크기에 따라 bw, bq, wl, wq 도 가능) 이 명령어는 %al 레지스터의 값을 가져와서 이를 %eax 레지스터에 저장하며, 나머지 상위 비트를 0으로 설정한다. 이렇게 하면, %eax 레지스터는 이제 32비트 값으로 확장된 %al의 값을 가지게 되며, 이 값은 함수의 반환 값으로 사용된다.
Q. 꼭 movzbl을 통해 확장해야 할까? 간단하게 mov 인스트럭션을 사용해서 값을 복사하면 안될까?
만약 movzbl을 사용하지 않고 단순히 %al을 %eax에 복사하려고 한다면, 데이터 손실 및 올바르지 않은 값이 저장되는 등의 문제가 발생할 수 있다.
%al은 8비트 레지스터이고 %eax는 32비트 레지스터이다. 따라서 %al의 값만을 %eax에 복사하면, %eax의 나머지 비트는 변경되지 않아 기존에 가지고 있던 무언가의 값이 그대로 유지된다. 이로 인해 데이터 손실이 발생하거나, 이상한 값으로 인식될 수 있다.
movzbl 명령은 이러한 문제를 방지하기 위해 %al의 값을 %eax로 올바르게 확장한다. 이 명령은 %al의 8비트 값을 %eax의 하위 8비트에 복사하고, %eax의 나머지 상위 24비트를 0으로 설정하여 데이터 손실이나 잘못된 값이 발생하지 않도록 한다.
Q. 왜 반환 값을 저장할 용도로 %rax가 아닌 %eax를 쓰는 걸까?
32비트를 초과하는 값이나 64비트 아키텍처만 작동하는 함수가 아닌 경우에는 일반적으로 %eax를 사용한다.
32비트 값을 반환하는 함수의 경우, %eax 레지스터를 사용하여 값이 반환되는 것이 일반적이다. 물론, 64비트 값을 반환하는 함수의 경우 %rax를 사용할 수 있다.
32비트 값이라는 용어는 32비트로 표현할 수 있는 값을 의미한다. 이는 해당 값이 32개의 비트로 구성되며, 이는 2의 32승, 즉 0부터 4,294,967,295까지의 정수를 표현할 수 있다는 것을 뜻한다.
혹은 32비트 아키텍처에서 작동하는 함수나 프로그램을 의미할 수도 있다. 이 경우, 함수의 반환 값, 매개 변수, 내부 변수 등이 모두 32비트로 제한된다. 다음과 같은 이유가 있을 수 있다.
PC-relative 방식은 프로그램의 위치 독립성을 유지하면서 메모리 내의 다른 코드 또는 데이터 영역으로 점프를 수행할 수 있게 해준다. 반면, 절대 주소 방식은 보다 단순하지만 프로그램의 위치 독립성을 유지할 수 없다.
PC-relative (Program Counter-relative) 방법:
.section .text
.global _start
_start:
jmp label # PC-relative jump
label:
# some code
위 코드에서 jmp label 인스트럭션은 현재 프로그램 카운터의 위치에 상대적인 오프셋을 사용하여 label로 점프한다.
jmp label 인스트럭션에서의 오프셋은 jmp 인스트럭션의 위치와 label 위치 사이의 차이(정확히는 label 주소 - jmp 인스트럭션 주소 +1, jmp 다음 바이트 주소를 기준으로 함)를 나타낸다. 이 차이는 바이트 단위로 측정되며, 이 값이 "jmp" 인스트럭션에 인코딩되어, CPU가 "label"로 점프할 정확한 위치를 알 수 있게 한다.
절대 주소 제공 방법:
void function() {
// some code
}
int main() {
function();
return 0;
}
이 C 코드를 컴파일하면 다음과 같은 x86-64 ATT 어셈블리 코드가 생성될 수 있다.
.section .text
.global _start
# 함수 정의 ..
function:
# 코드 ...
ret
# main 함수
_start:
call function # Absolute address call
ret
위 코드에서 call function 인스트럭션은 function 라벨의 절대 주소를 사용하여 함수를 호출한다.
1. 스택, 레지스터 각각의 정의, 용도, 장점을 설명하시오 (자료구조의 관점보다는 Local Storage 관점에서)
스택은 프로그램의 Local Storage 영역으로 사용된다.
스택의 주요 용도 중 하나는 프로시저 호출 시 지역변수, 매개변수, 리턴 주소, 이전 프레임의 스택 포인터를 저장하기 위한 메모리 공간이라는 점.
선언되는 순서와 반대로 메모리가 해제되는 LIFO 구조를 지닌다.
용도:
장점:
레지스터는 프로세서 내부의 고속 작동 메모리로, 프로시저 실행 중 자주 접근하는 변수나 중간 계산값을 저장하기 위해 사용된다.
용도:
장점:
+) 함수호출시 스택의 레이아웃(인자, 로컬변수, 리턴값)과 동작에 대한 이해
2. 꼬리 재귀 최적화(Tail Recursion Optimization)를 Call Stack 관점에서 설명
3. 단어 "DATABASE"와 "ALPHABET"의 LCS를 구하고, 행렬과 탐색경로를 함께 기록
4. 그리디 알고리즘과 DP의 정의, DP에서 탑다운 바텀업의 차이에 대한 설명
5. 플로이드 워셜 알고리즘을 사용하여 방향 그래프의 이행적 폐쇄를 구하기 (단계별 도식화)
함수호출시 스택의 레이아웃(인자, 로컬변수, 리턴값)과 동작을 이해하는지…
DP에서는 상향,하향식 이외에도, DP가 가능한 조건(최적부분구조, 중복부분해)에 대해서…
동적 메모리 할당 | 기본 개념, 메모리 누수, 단편화 (1) | 2023.11.04 |
---|---|
CS:APP | Chapter 3 | (2) 프로그램의 기계수준 표현 (1) | 2023.11.01 |
CS:APP | Chapter 1 | 컴퓨터 시스템 overview (4) | 2023.10.26 |
[혼공컴운] Ch4. CPU의 작동 원리 (레지스터) (0) | 2023.10.04 |
[혼공컴운] Ch4. CPU의 작동 원리 (ALU, 제어장치) (0) | 2023.10.04 |