stack은 좋지만 모든 문제를 해결할 수 없다.
size가 compile time에 정해지지 않은 data는 어떻게 할 것인가?
Collections나 strings, 그리고 다른 dynamically-sized data는 stack에 모두 할당될 수 없다.
그래서 heap이 등장했다.
당신은 heap을 큰 메모리의 덩어리로 시각화할 수 있다.(큰 배열이라 생각해도 된다.)
heap에 data를 저장할 때마다, heap의 어떤 영억을 할당받기 위해,
allocator라는 특별한 프로그램에 요청해야한다.
우리는 이러한 상호작용을 heap allocation이라 한다.
만약 allocation이 성공하면, allocator는 예약된 block의 시작 pointer를 당신에게 줄 것이다.
heap은 stack과 구조적으로 꽤 다르다.
heap allocations는 연속적이지 않고, heap안에 어디에든 위치할 수 있다.
+---+---+---+---+---+---+-...-+-...-+---+---+---+---+---+---+---+
| Allocation 1 | Free | ... | ... | Allocation N | Free |
+---+---+---+---+---+---+ ... + ... +---+---+---+---+---+---+---+
** 이 내용은 시스템프로그래밍 과목의 malloc을 직접 구현함으로써, 자세히 알 수 있었다 **
** 동적할당의 할당 우선순위, free 방법론 등등 여러 최적화를 거쳐 performance를 높이는 것이 과제였다. **
어느 heap의 부분이 사용되고 있고 어느 heap의 부분이 free되어 있는지
추적하는 것이 allocator의 일이다.
allocator는 당신이 할당한 memory를 자동으로 free하지 않는다.
따라서, 당신은 필요없는 memory를 allocator를 다시 불러서 고의적으로 free해줘야한다.
heap의 유연성은 비용이 있다.
heap allocations는 stack allocations보다 느리다.
heap allocations에는 더 많은 bookkeeping이 포함되어 있다!
bookkeeping이란?
메모리 할당 추적, 메모리 해제 관리, 단편화 방지, 메모리 풀 관리, 메모리 보호 및 검사 등의 작업을 포함한다.
이러한 작업에는 추가적인 데이터 구조와 알고리즘이 필요하다.
따라서, heap allocations를 최소화시키고,
가능하다면 stack-allocated data를 사용하는 것을 추천한다.
당신이 String type의 지역 변수를 만들 때, Rust는 heap에 할당한다.
왜냐하면, 사용자가 얼마나 많은 text를 넣을지 직접 알 수 없기 때문이다.
그래서 정확한 양의 공간을 stack에 예약할 수 없다.
하지만, String은 완전 heap-allocated되지 않는다.
다음과 같은 몇몇 data는 stack에 남긴다.
- 할당받은 heap 영역에 대한 pointer
- string의 길이(string에 진짜 얼마의 bytes가 사용되고 있는지)
- string의 용량(string에 대해 heap에 할당된 bytes가 얼마인지)
이것을 좀 더 잘 이해하기 위해 다음 예시를 보자.
let mut s = String::with_capacity(5);
이 코드를 실행시키면, memory는 다음과 같은 layout을 가질 것이다.
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 0 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | ? | ? | ? | ? | ? |
+---+---+---+---+---+
우리는 5 bytes의 text를 위한 String을 요청했다.
String::with_capacity는 allocator에게 가서 heap memory의 5 bytes를 달라고 요청한다.
allocator는 memory block의 시작 pointer를 return해준다.
String이 비어있거나 꽉 차있지 않을 수 있어서,
우리는 stack에서 length와 capacity 정보로 String의 진짜 text 길이 정보와
초기 할당 크기 정보를 구별할 수 있다.
만약 몇몇 text를 String에 넣으면, 다음과 같이 작동한다.
s.push_str("Hey");
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 3 | 5 |
+--| ----+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | y | ? | ? |
+---+---+---+---+---+
s는 3 bytes의 text다.
s의 length는 3으로 update되지만, capacity는 여전히 5이다.
5 bytes 중 3 bytes가 H, e, y를 저장하기 위해 사용되고 있다.
pointer와 length, capacity를 stack에 저장하기 위해 얼마나 많은 공간이 필요할까?
그건 당신이 실행시키고 있는 machine의 architecture에 따라 다르다.
machine에서 모든 memory location은 address를 갖고 있다.
주로 unsigned integer로 표현된다.
address space의 maximum size에 따라, 이 integer는 다른 size를 가질 수 있다.
최근 대부분은 machines는 32-bit나 64-bit address space 중 하나를 사용한다.
Rust는 usize type을 통해 이러한 architecture-specific details를 추출할 수 있다.
그래서 32-bit machine의 usize는 u32,
64-bit machine의 usize는 u64다.
Capacity, length, pointers는 모두 Rust에서 usize만큼 stack 영역을 차지한다.
std::mem::size_of는 type이 stack의 얼만큼을 차지하는지를 return한다.
이건 size of the type이라고도 한다.
String이 heap에서 관리하는 memory buffer는 어떨까?
String의 size에 대한 부분이 아닌가?
아니다.
heap allocation은 String이 관리하는 resource다.
compiler에 의해 String type의 부분으로 간주되지 않는다.
*부연설명: Rust의 String type은 heap memory를 관리하는 구조체다.
*그리고 해당 구조체 내에 pointer, length, capacity가 존재하는 것이다.
*그래서 String type의 크기를 std::mem::size_of로 호출한다면 위에서
*설명한 architecture에 따른 크기인 64bits system에선 24bytes. 32bits system에선 12bytes다.
*즉, heap에 할당된 크기에 대한 계산은 전혀 들어가지 않는다.
std::mem::size_of는 추가적으로 heap-allocate된 data나
pointer를 통해 관리되는 영역에 대해 신경쓰지 않는다.
안타깝게도 std::mem::size_of와 같이 heap 영역의 memory 크기를 측정하는 method는 없다.
몇몇 types는 heap 사용에 대해 검사하는 methods를 제공할 수 있지만,
Rust에서 runtime heap 사용량을 검색하는 범용 API는 존재하지 않는다.
하지만, 당신은 DHAT나 a custom allocator와 같은 memory profiler tool을 사용할 수 있다.
해당 tools는 heap memory usage를 검사해준다.
09_heap exercise는 String과 관련된 size를 알아내는 작업이다.
구조체 안에 String들이 fields로 존재하고 String의 내부 구조를 아는 것에 대해 묻는 것이었다.
당연히 size_of::<usize>()를 사용해 나의 machine의 architecture에 맞는 size를 찾았고
그렇게 String에 대한 size를 3을 곱해줘서 구했다.

&String이나 &mut String과 같은 references는 어떨까?
memory에 어떻게 표현될까?
Rust의 대부분은 references는 memory location의 pointer로 표현된다.
그래서, 그것들의 크기는 각각 pointer의 size인 usize와 똑같다.
당신은 std::mem::size_of를 사용해 이것을 확인할 수 있다.
assert_eq!(std::mem::size_of::<&String>(), 8);
assert_eq!(std::mem::size_of::<&mut String>(), 8);
특히, &String은 String의 metadata가 저장된 곳의 pointer다.
아래 코드를 단편적으로 실행시키면
let s = String::from("Hey");
let r = &s;
다음과 같은 memory 구조를 얻을 수 있다.
--------------------------------------
| |
+----v----+--------+----------+ +----|----+
Stack | pointer | length | capacity | | pointer |
| | | 3 | 5 | | |
+--| ----+--------+----------+ +---------+
| s r
|
v
+---+---+---+---+---+
Heap | H | e | y | ? | ? |
+---+---+---+---+---+
heap-allocated data에 대한 pointer의 pointer다.
&mut String 또한 똑같이 동작한다.
위의 예시들을 보여준 이유는 모든 pointers가 heap 영역을 가리키고 있지 않음을 알려주기 위해서이다.
pointer는 그냥 memory 영역을 가리키는 것 뿐이다.
10_references_in_memory exercise는 reference에 대한 size를 확인하는 작업이었다.
그냥 모든 reference는 pointer이므로 usize와 같다.
그래서 그냥 usize의 크기를 넣어줬다.

heap에 대해 설명할 때, 할당한 memory를 free해줘야하는 의무가 있다고 말했다.
borrow-checker에 대해 설명할 때, Rust에선 직접 memory를 관리할 필요 없다고 말했다.
이 두 말은 처음엔 모순적으로 보일 수 있다.
scopes와 destructors에 대해 소개하면서 두 말이 어떻게 매치되는지 보자.
변수의 scope는 변수가 valid하거나 alive한 Rust code의 영역이다.
변수의 scope는 변수의 선언으로부터 시작한다.
그리고 아래 두 경우 중 하나라도 일어날 때, 끝난다.
1. 변수가 선언된 block의 끝(중괄호{} 사이)
fn main() {
// `x` is not yet in scope here
let y = "Hello".to_string();
let x = "World".to_string(); // <-- x's scope starts here...
let h = "!".to_string(); // |
} // <-------------- ...and ends here
2. 변수의 ownership이 누군가에게 이전될 때(함수나 다른 변수에게)
fn compute(t: String) {
// Do something [...]
}
fn main() {
let s = "Hello".to_string(); // <-- s's scope starts here...
// |
compute(s); // <------------------- ..and ends here
// because `s` is moved into `compute`
}
value의 owner가 scope를 벗어나면, Rust는 destructor를 부른다.
destructor는 해당 value에 사용된 resource, 특히 memory를 정리하려한다.
당신은 std::mem::drop을 통해 value의 destructor를 직접 부를 수 있다.
compiler가 수행하는 작업을 "spell out"하기 위해 명시적인 호출을 삽입하여 drop할 수 있다.
다음 예시를 보자.
fn main() {
let y = "Hello".to_string();
let x = "World".to_string();
let h = "!".to_string();
}
이 코드는
fn main() {
let y = "Hello".to_string();
let x = "World".to_string();
let h = "!".to_string();
// Variables are dropped in reverse order of declaration
drop(h);
drop(x);
drop(y);
}
이 코드와 같다.
그리고 다음 예시를 보자.
fn compute(s: String) {
// Do something [...]
}
fn main() {
let s = "Hello".to_string();
compute(s);
}
이 코드는
fn compute(t: String) {
// Do something [...]
drop(t); // <-- Assuming `t` wasn't dropped or moved
// before this point, the compiler will call
// `drop` here, when it goes out of scope
}
fn main() {
let s = "Hello".to_string();
compute(s);
}
이 코드와 같다.
다음과 같은 차이점을 알자.
compute가 호출된 뒤, s가 더이상 유효하지 않더라도, main에 drop(s)가 없다.
function으로 값의 ownership을 이전할 때, cleaning it up할 책임도 같이 이전한다.
value의 destructor가 최대 한번 호출되게 하기 위함으로, double free bugs를 예방하기 위한 설계다.
만약 drop된 value를 사용하려하면 어떻게 될까?
let x = "Hello".to_string();
drop(x);
println!("{}", x);
위 code를 compile하려하면, 다음과 같은 error가 뜬다.
error[E0382]: use of moved value: `x`
--> src/main.rs:4:20
|
3 | drop(x);
| - value moved here
4 | println!("{}", x);
| ^ value used here after move
drop은 호출된 value를 consume한다.
무슨 의미냐면, drop이 호출되면 호출된 value는 더이상 valid하지 않다는 뜻이다.
compiler는 use-after-free bugs를 피하기위해 drop된 value를 사용하는 것을 예방할 것이다.
만약 variable이 reference를 포함하면 어떨까?
다음 예시를 보자.
let x = 42i32;
let y = &x;
drop(y);
drop(y)를 호출할 때, 아무것도 일어나지 않는다.
만약 이 code를 compile하려한다면, 다음과 같은 warning을 볼 수 있다.
warning: calls to `std::mem::drop` with a reference
instead of an owned value does nothing
--> src/main.rs:4:5
|
4 | drop(y);
| ^^^^^-^
| |
| argument has type `&i32`
|
이전에 말한 것으로 돌아가보자.
우리는 하나의 value에 대한 destructor가 한번 호출되기를 원한다.
당신은 하나의 value에 대해 여러개의 references를 가질 수 있다.
그 중 하나가 범위를 벗어날 때, 해당 value에 대한 destructor를 호출한다면, 다른 reference들은 어떻게 될까?
그들은 소위 dangling pointer라 불리는 더이상 유효하지 않은 memory 영역을 참조할 것이다.
이는 use-after-free bugs와도 매우 밀접한 관계가 있다.
Rust의 ownership system은 이러한 bugs를 design을 통해 배제시켰다.
11_destructor에 대한 exercise는 traits와 interior mutability에 대해 배운 후 제대로 하겠다고 한다.
destructor에 대한 이해가 됐는지만 확인받았다.

Chapter 3에서 Rust의 많은 기본적인 개념들을 배웠다.
Chapter 4로 가기전에 배운 것을 조합하여 마지막 exercise를 진행해야한다.
최소한의 guidance를 제공받았으며, 한번 해보도록 하겠다.





기본 test부터 validation test까지 모두 마쳤다.
public으로 구조체를 만들어주고 요구하는 사항에 따라 methods를 구현했다.
각 조건에 따라 panic을 일으키도록 구현했으며, new, setter, getter methods를 만들었다.
이전에 여러 방법으로 설명했는데 본인은 reference를 사용할 수 있으면 되도록 reference를 사용했다.
ownership관련하여 문제가 발생하는 것은 피하기 위해서이다.
하지만 무수히 많은 값을 연쇄적으로 변경할 때는
당연히 reference를 이용하지 않는 것이 이롭다는 것을 인지하고 있다.