스택의 할당과 해제는 CPU 명령어 각각 하나씩으로 끝나기 때문에 가벼움.
변수의 수명과 메모리 사용량이 컴파일 시에 확정될 수 있는 경우에만 사용됨.
Go의 스택 메모리는 계속 증가하는 동적 메모리 풀임
실행 시 malloc 호출로 동적으로 메모리를 할당하고, 이후 GC가 할당된 개체를 참조되지 않게 되었는지 주기적으로 검사할 필요가 있기 때문에 상당히 무거움.
사용자가 직접 스택에 할당할지 힙에 할당할지 정하지 않고 컴파일러가 선택함.
기본적인 아이디어: 가비지 컬렉션 작업을 컴파일 시 할 수 있는 부분을 하는 것.
컴파일러가 코드 영역세 걸쳐 변수의 범위를 추적하여 수명이 특정 범위로 한정될 수 있거나 메모리 크기가 컴파일 시에 확정될 경우 스택에 할당하고, 확정할 수 없는 경우 힙에 할당.
스펙이 정해져 있지 않기 때문에, 이전에는 heap에 할당되던 게 컴파일러가 발전하면서 stack에 할당하게 될 수도 있음.
포인터는 스택 할당의 저해 요인!
함수의 인수나 메소드 리시버도 값을 복사하는 게 대부분 더 가벼움.
포인터의 디레퍼런스 때는 nil 체크도 해야 하므로 그만큼 처리가 늘어남.
포인터를 사용하지 않고 값을 복사하는 것이 메모리 지역성을 높여 CPU 캐시 적중률이 오름.
포인터를 포함하지 않는 메모리 영역은 GC가 검사를 생략할 수 있음.
포인터를 사용하는 경우: 소유권을 나타내는 경우와 값을 변경 가능하게 하는 경우.
슬라이스는 크기가 동적으로, 컴파일 시에는 결정되지 않으므로 백스토어(슬라이스 내의 포인터가 참조하는 곳) 배열이 힙에 할당됨.
문자열도 바이트 슬라이스이므로 마찬가지.
배열을 사용할 경우 배열은 크기가 고정되므로 스택에 할당.
시간대 정보를 포인터로 가짐.
time.Time을 계속 갖고 있는 것보다 unix time 정수를 갖고 있는 게 GC에 좋음.(time.Time은 힙에 할당되므로)
반환 문자열을 byte 슬라이스에 추가하고 싶은 경우, string으로 반환해서 byte 슬라이스에 추가하기 보다 byte 슬라이스를 받고, 함수 안에서 추가 후 다시 해당 byte 슬라이스를 반환하는 게 좋음.
func (t Time) Format (layout string) string
func (t Time) AppendFormat (b [] byte, layout string) [] byte
strconv의 Itoa와 FormatFloat 등은 가능하다면 AppendInt와 AppendFloat를 사용하는 게 좋음.
인터페이스의 메소드 호출은 구조체의 메소드 호출보다 무거움.
인터페이스를 저장하는 변수는 실제 구현된 구조체에 대한 포인터.
실행이 잦은 구간은 인터페이스를 사용하지 않고 heap 할당이 발생하지 않도록 할 수 있음.
함수 리터럴을 생성해서 반환할 경우 함수 포인터가 반환되므로 해당 함수 리터럴은 heap에 할당.
함수 리터럴 내부에서 외부 변수에 접근할 경우 해당 변수를 내부 상태로 가져옴.(Closure 개념)
임시 인스턴스를 반복 사용하는 경우 sync.Pool도 성능 향상에 도움이 됨.
메모리를 할당하되 초기화는 하지 않음.
m := new(MyType)
m := &MyType{}
위 두 코드는 같음.
메모리 할당과 더불어 미리 사용할 준비가 되도록 초기화도 함께 진행.
포인터를 반환하지 않음.
슬라이스, 맵, 채널에 사용 가능.
Switch문과 Loop unrolling 기법으로 메모리 복사를 최적화한 기법.
void copy_byte( char* dst, char* src, int cnt )
{
while ( cnt-- > 0 )
*dst++ = *src++;
}
void copy_duff( char* dst, char* src, int cnt )
{
int repeat = ( cnt + 7 ) / 8;
switch ( cnt % 8 )
{
case 0: do { *dst++ = *src++;
case 7: *dst++ = *src++;
case 6: *dst++ = *src++;
case 5: *dst++ = *src++;
case 4: *dst++ = *src++;
case 3: *dst++ = *src++;
case 2: *dst++ = *src++;
case 1: *dst++ = *src++;
} while ( --repeat > 0 );
}
}
위 코드가 정상적으로 작동하는 이유
1. switch 구문의 느슨한 명세. case 라벨은 다른 어떤 구문 앞에 prefix 형태로 존재해도 문법적으로 유효함.
2. C 언어에서 loop의 중간 부분으로 jump 할 수 있음.
즉 다음과 같이 동작함.
switch ( cnt % 8 )
{
case 0: goto label0;
case 1: goto label1;
case 2: goto label2;
case 3: goto label3;
case 4: goto label4;
case 5: goto label5;
case 6: goto label6;
case 7: goto label7;
}
while ( --repeat > 0 )
{
label0:
*dst++ = *src++;
label7:
*dst++ = *src++;
label6:
*dst++ = *src++;
label5:
*dst++ = *src++;
label4:
*dst++ = *src++;
label3:
*dst++ = *src++;
label2:
*dst++ = *src++;
label1:
*dst++ = *src++;
}
이를 통해 비교 횟수를 count / 8로 줄일 수 있음.