development

“컴파일 타임에 할당 된 메모리”는 실제로 무엇을 의미합니까?

big-blog 2020. 6. 15. 07:53
반응형

“컴파일 타임에 할당 된 메모리”는 실제로 무엇을 의미합니까?


C 및 C ++와 같은 프로그래밍 언어에서 사람들은 종종 정적 및 동적 메모리 할당을 참조합니다. 나는 개념을 이해하지만 "컴파일 시간 동안 모든 메모리가 할당 (예약)되었다"는 문구는 항상 혼란스러워합니다.

내가 이해 한 것처럼 컴파일은 고급 C / C ++ 코드를 기계 언어로 변환하고 실행 파일을 출력합니다. 컴파일 된 파일에서 메모리는 어떻게 "할당"됩니까? 메모리가 항상 모든 가상 메모리 관리 항목과 함께 RAM에 할당되지 않습니까?

정의에 의한 메모리 할당은 런타임 개념이 아닙니까?

C / C ++ 코드에서 1KB 정적 할당 변수를 만들면 실행 파일의 크기가 같은 양만큼 증가합니까?

이것은 "정적 할당"이라는 제목으로 문구가 사용 된 페이지 중 하나입니다.

기본으로 돌아 가기 : 메모리 할당


컴파일 타임에 할당 된 메모리는 프로세스 메모리 맵 내에서 특정 사항이 할당 될 때 컴파일 타임에 컴파일러가 확인하는 것을 의미합니다.

예를 들어, 전역 배열을 고려하십시오.

int array[100];

컴파일러는 컴파일 타임에 배열의 크기와의 크기를 int알고 있으므로 컴파일 타임에 배열의 전체 크기를 알고 있습니다. 또한 전역 변수에는 기본적으로 정적 저장 기간이 있습니다. 이는 프로세스 메모리 공간의 정적 메모리 영역 (.data / .bss 섹션)에 할당됩니다. 해당 정보가 주어지면 컴파일러는 컴파일 중에 해당 정적 메모리 영역의 주소가 배열로 결정됩니다 .

물론 메모리 주소는 가상 주소입니다. 프로그램은 자체 메모리 공간이 있다고 가정합니다 (예 : 0x00000000에서 0xFFFFFFFF까지). 그렇기 때문에 컴파일러는 "어레이는 주소 0x00A33211에있을 것"과 같은 가정을 할 수 있습니다. 런타임시 해당 주소는 MMU 및 OS에 의해 실제 / 하드웨어 주소로 변환됩니다.

값이 초기화 된 정적 스토리지는 약간 다릅니다. 예를 들면 다음과 같습니다.

int array[] = { 1 , 2 , 3 , 4 };

첫 번째 예에서 컴파일러는 배열이 할당 될 위치 만 결정하고 해당 정보를 실행 파일에 저장했습니다.
값이 초기화 된 것들의 경우 컴파일러는 실행 파일에 배열의 초기 값을 주입하고 프로그램 시작시 배열 할당 후 배열에 이러한 값을 채워야 함을 프로그램 로더에 알려주는 코드를 추가합니다.

다음은 컴파일러가 생성 한 어셈블리의 두 가지 예입니다 (x86 대상이있는 GCC4.8.1).

C ++ 코드 :

int a[4];
int b[] = { 1 , 2 , 3 , 4 };

int main()
{}

출력 어셈블리 :

a:
    .zero   16
b:
    .long   1
    .long   2
    .long   3
    .long   4
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, %eax
    popq    %rbp
    ret

보시다시피 값은 어셈블리에 직접 주입됩니다. a표준에 따르면 정적 저장 항목은 기본적으로 0으로 초기화되어야하므로 array 에서 컴파일러는 16 바이트의 0 초기화를 생성합니다.

8.5.9 (초기화 기) [참고] :
정적 초기화 기간의 모든 객체는 다른 초기화가 시작되기 전에 프로그램 시작시 0으로 초기화됩니다. 경우에 따라 나중에 추가 초기화가 수행됩니다.

나는 항상 사람들이 코드를 디스 어셈블하여 컴파일러가 실제로 C ++ 코드로 무엇을하는지 확인하도록 제안한다. 이것은 스토리지 클래스 / 지속 시간 (이 질문과 같은)에서 고급 컴파일러 최적화에 적용됩니다. 컴파일러에게 어셈블리를 생성하도록 지시 할 수 있지만 인터넷에서이를 친숙한 방식으로 수행 할 수있는 훌륭한 도구가 있습니다. 내가 가장 좋아하는 것은 GCC Explorer 입니다.


컴파일 타임에 할당 된 메모리는 런타임에 추가 할당이 없다는 것을 의미합니다. malloc, new 또는 기타 동적 할당 방법에 대한 호출이 없습니다. 항상 모든 메모리가 필요하지 않더라도 고정 된 양의 메모리 사용량이 있습니다.

정의에 의한 메모리 할당은 런타임 개념이 아닙니까?

메모리는 런타임 전에 사용 되지 않지만 실행 시작 직전에 할당이 시스템에 의해 처리됩니다.

C / C ++ 코드에서 1KB 정적 할당 변수를 만들면 실행 파일의 크기가 같은 양만큼 증가합니까?

단순히 정적을 선언해도 실행 파일의 크기가 몇 바이트 이상 증가하지는 않습니다. 초기 값을 유지하기 위해 0이 아닌 초기 값으로 선언하십시오. 오히려 링커는이 로더를 실행하기 직전에 시스템 로더가 생성하는 메모리 요구 사항에이 1KB 양을 추가하기 만하면됩니다.


컴파일 시간에 할당 된 메모리는 프로그램을로드 할 때 메모리의 일부가 즉시 할당되고이 할당의 크기 및 (상대) 위치는 컴파일시 결정됩니다.

char a[32];
char b;
char c;

이 3 개의 변수는 "컴파일시 할당 됨"이며, 이는 컴파일러가 컴파일시 크기 (고정)를 계산 함을 의미합니다. 변수 a는 메모리의 오프셋이 될 것입니다. 예를 들어 주소 0 b을 가리키면 주소 33과 c34를 가리 킵니다 (정렬 최적화가 필요하지 않음). 따라서 1Kb의 정적 데이터를 할당하면 코드의 크기가 증가하지 않습니다. 코드 내부의 오프셋 만 변경하기 때문입니다. 실제 공간은로드시 할당됩니다 .

커널은이를 추적하고 내부 데이터 구조 (각 프로세스, 페이지 등에 할당 된 메모리 양)를 업데이트해야하므로 실제 메모리 할당은 항상 런타임에 발생합니다. 차이점은 컴파일러가 사용할 각 데이터의 크기를 이미 알고 있으며 프로그램이 실행되는 즉시 할당된다는 것입니다.

또한 상대 주소대해 이야기하고 있음을 기억하십시오 . 변수가있는 실제 주소는 달라집니다. 로드 할 때 커널은 프로세스를 위해 메모리를 확보하고, address x에서 말할 수 있으며, 실행 파일에 포함 된 모든 하드 코딩 된 주소는 x바이트 단위 로 증가 하므로 a예제의 변수 는 address x, b는 address x+33및 곧.


스택에 N 바이트를 차지하는 변수를 추가해도 빈의 크기가 N 바이트만큼 증가하지는 않습니다. 실제로 대부분의 경우 몇 바이트 만 추가합니다.
이제 코드로 1000 개 문자를 추가하는 방법에 대한 예제로 시작하자 것이다 선형 방식으로 빈의 크기를 늘리십시오.

1k가 천 문자의 문자열 인 경우 다음과 같이 선언됩니다

const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end

그리고 당신은 vim your_compiled_bin실제로, 당신 실제로 그 어딘가 빈에서 그 문자열을 볼 수있을 것입니다. 이 경우 예 : 문자열이 완전히 포함되어 실행 파일이 1k 더 커집니다.
그러나 스택에 ints, chars 또는 longs 배열을 할당 하고이 행을 따라 무언가를 루프로 할당하면

int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);

그런 다음 아니오 : bin을 늘리지 않습니다 ... 1000*sizeof(int)
컴파일시 할당에 의해 현재 의미를 이해하게 된 것을 의미합니다 (의견에 따라) : 컴파일 된 bin에는 시스템이 얼마나 많은 메모리를 알기 위해 필요한 정보가 포함 응용 프로그램에 필요한 스택 크기에 대한 정보와 함께 실행될 때 필요한 기능 / 블록 그것이 빈을 실행할 때 시스템이 할당하는 것입니다. 프로그램은 프로세스가됩니다 (음, 빈 실행은 프로세스입니다 ... 음, 내가 말한 것을 얻습니다).
물론, 여기에 전체 그림을 그리지는 않습니다. 저장소에는 실제로 저장소에 필요한 스택의 크기에 대한 정보가 들어 있습니다. 이 정보를 바탕으로 시스템은 스택이라고 불리는 메모리 덩어리를 예약하여 프로그램이 일종의 자유 통치권을 얻습니다. 프로세스 (빈 실행 결과)가 시작될 때 스택 메모리는 여전히 시스템에 의해 할당됩니다. 그런 다음 프로세스가 스택 메모리를 관리합니다. 함수 또는 루프 (모든 유형의 블록)가 호출되거나 실행되면 해당 블록에 대한 로컬 변수가 스택으로 푸시 되고 다른 사람이 사용할 수 있도록 제거됩니다 (스택 메모리가 "해제 됨" ). 기능 / 블록. 그래서 선언int some_array[100]빈에 몇 바이트의 추가 정보 만 추가하면 시스템에 함수 X에 100*sizeof(int)+ 여유 공간 이 필요하다는 것을 알 수 있습니다 .


많은 플랫폼에서 각 모듈 내의 모든 전역 또는 정적 할당은 컴파일러에 의해 3 개 이하의 통합 할당 (초기화되지 않은 데이터 (보통 "bss"라고도 함), 초기화 된 쓰기 가능한 데이터 (보통 "데이터"라고 함))으로 통합됩니다. ), 상수 데이터에 대한 하나 ( "const") 및 프로그램 내에서 각 유형의 모든 전역 또는 정적 할당은 링커에 의해 각 유형에 대해 하나의 전역으로 통합됩니다. 예를 들어, int4 바이트 라고 가정하면 모듈은 정적 할당으로 다음을 갖습니다.

int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;

링커는 bss의 경우 208 바이트, "data"의 경우 16 바이트, "const"의 경우 28 바이트가 필요하다는 것을 링커에게 알려줍니다. 또한 변수에 대한 모든 참조는 영역 선택기 및 오프셋으로 대체되므로 a, b, c, d 및 e는 bss + 0, const + 0, bss + 4, const + 24, data로 대체됩니다. +0 또는 bss + 204

프로그램이 연결되면 모든 모듈의 모든 bss 영역이 함께 연결됩니다. 마찬가지로 데이터 및 const 영역. 각 모듈에 대해 bss 관련 변수의 주소는 모든 이전 모듈의 bss 영역의 크기만큼 증가합니다 (데이터 및 const와 마찬가지로). 따라서 링커가 완료되면 모든 프로그램에 하나의 bss 할당, 하나의 데이터 할당 및 하나의 const 할당이 있습니다.

프로그램이로드되면 일반적으로 플랫폼에 따라 다음 4 가지 중 하나가 발생합니다.

  1. 실행 파일은 각 종류의 데이터 및 초기화 된 데이터 영역에 필요한 초기 바이트 수를 나타내는 바이트 수를 나타냅니다. 또한 bss, data 또는 constative 상대 주소를 사용하는 모든 명령어 목록이 포함됩니다. 운영 체제 또는 로더는 각 영역에 적절한 공간을 할당 한 다음 해당 영역의 시작 주소를 필요한 각 명령에 추가합니다.

  2. 운영 체제는 세 종류의 데이터를 모두 보유하기 위해 메모리 청크를 할당하고 애플리케이션에 해당 메모리 청크에 대한 포인터를 제공합니다. 정적 또는 전역 데이터를 사용하는 모든 코드는 해당 포인터를 기준으로이를 역 참조합니다 (대부분의 경우 포인터는 응용 프로그램 수명 동안 레지스터에 저장 됨).

  3. 운영 체제는 이진 코드를 보유하고있는 것을 제외하고는 처음에는 메모리를 응용 프로그램에 할당하지 않지만 응용 프로그램은 운영 체제에 적절한 할당을 요청하는 것이 가장 우선합니다.

  4. 운영 체제는 처음에 응용 프로그램을위한 공간을 할당하지 않지만 시작시 응용 프로그램에 적절한 할당을 요청합니다 (위와 같이). 이 응용 프로그램에는 메모리가 할당 된 위치를 반영하기 위해 업데이트해야하는 주소 목록이 포함 된 명령어 목록이 포함되지만 (첫 번째 스타일과 같이) OS 로더가 응용 프로그램을 패치하지 않고 응용 프로그램 자체에 패치를 적용하기에 충분한 코드가 포함됩니다 .

All four approaches have advantages and disadvantages. In every case, however, the compiler will consolidate an arbitrary number of static variables into a fixed small number of memory requests, and the linker will consolidate all of those into a small number of consolidated allocations. Even though an application will have to receive a chunk of memory from the operating system or loader, it is the compiler and linker which are responsible for allocating individual pieces out of that big chunk to all the individual variables that need it.


The core of your question is this: "How is memory "allocated" in a compiled file? Isn't memory always allocated in the RAM with all the virtual memory management stuff? Isn't memory allocation by definition a runtime concept?"

I think the problem is that there are two different concepts involved in memory allocation. At its basic, memory allocation is the process by which we say "this item of data is stored in this specific chunk of memory". In a modern computer system, this involves a two step process:

  • Some system is used to decide the virtual address at which the item will be stored
  • The virtual address is mapped to a physical address

The latter process is purely run time, but the former can be done at compile time, if the data have a known size and a fixed number of them is required. Here's basically how it works:

  • The compiler sees a source file containing a line that looks a bit like this:

    int c;
    
  • It produces output for the assembler that instructs it to reserve memory for the variable 'c'. This might look like this:

    global _c
    section .bss
    _c: resb 4
    
  • When the assembler runs, it keeps a counter that tracks offsets of each item from the start of a memory 'segment' (or 'section'). This is like the parts of a very large 'struct' that contains everything in the entire file it doesn't have any actual memory allocated to it at this time, and could be anywhere. It notes in a table that _c has a particular offset (say 510 bytes from the start of the segment) and then increments its counter by 4, so the next such variable will be at (e.g.) 514 bytes. For any code that needs the address of _c, it just puts 510 in the output file, and adds a note that the output needs the address of the segment that contains _c adding to it later.

  • The linker takes all of the assembler's output files, and examines them. It determines an address for each segment so that they won't overlap, and adds the offsets necessary so that instructions still refer to the correct data items. In the case of uninitialized memory like that occupied by c (the assembler was told that the memory would be uninitialized by the fact that the compiler put it in the '.bss' segment, which is a name reserved for uninitialized memory), it includes a header field in its output that tells the operating system how much needs to be reserved. It may be relocated (and usually is) but is usually designed to be loaded more efficiently at one particular memory address, and the OS will try to load it at this address. At this point, we have a pretty good idea what the virtual address is that will be used by c.

  • The physical address will not actually be determined until the program is running. However, from the programmer's perspective the physical address is actually irrelevant—we'll never even find out what it is, because the OS doesn't usually bother telling anyone, it can change frequently (even while the program is running), and a main purpose of the OS is to abstract this away anyway.


An executable describes what space to allocate for static variables. This allocation is done by the system, when you run the executable. So your 1kB static variable won't increase the size of the executable with 1kB:

static char[1024];

Unless of course you specify an initializer:

static char[1024] = { 1, 2, 3, 4, ... };

So, in addition to 'machine language' (i.e. CPU instructions), an executable contains a description of the required memory layout.


Memory can be allocated in many ways:

  • in application heap (whole heap is allocated for your app by OS when the program starts)
  • in operating system heap (so you can grab more and more)
  • in garbage collector controlled heap (same as both above)
  • on stack (so you can get a stack overflow)
  • reserved in code/data segment of your binary (executable)
  • in remote place (file, network - and you receive a handle not a pointer to that memory)

Now your question is what is "memory allocated at compile time". Definitely it is just an incorrectly phrased saying, which is supposed to refer to either binary segment allocation or stack allocation, or in some cases even to a heap allocation, but in that case the allocation is hidden from programmer eyes by invisible constructor call. Or probably the person who said that just wanted to say that memory is not allocated on heap, but did not know about stack or segment allocations.(Or did not want to go into that kind of detail).

But in most cases person just wants to say that the amount of memory being allocated is known at compile time.

The binary size will only change when the memory is reserved in the code or data segment of your app.


You are right. Memory is actually allocated (paged) at load time, i.e. when the executable file is brought into (virtual) memory. Memory can also be initialized on that moment. The compiler just creates a memory map. [By the way, stack and heap spaces are also allocated at load time !]


I think you need to step back a bit. Memory allocated at compile time.... What can that mean? Can it mean that memory on chips that have not yet been manufactured, for computers that have not yet been designed, is somehow being reserved? No. No, time travel, no compilers that can manipulate the universe.

So, it must mean that the compiler generates instructions to allocate that memory somehow at runtime. But if you look at it in from the right angle, the compiler generates all instructions, so what can be the difference. The difference is that the compiler decides, and at runtime, your code can not change or modify its decisions. If it decided it needed 50 bytes at compile time, at runtime, you can't make it decide to allocate 60 -- that decision has already been made.


If you learn assembly programming, you will see that you have to carve out segments for the data, the stack, and code, etc. The data segment is where your strings and numbers live. The code segment is where your code lives. These segments are built into the executable program. Of course the stack size is important as well... you wouldn't want a stack overflow!

So if your data segment is 500 bytes, your program has a 500 byte area. If you change the data segment to 1500 bytes, the size of the program will be 1000 bytes larger. The data is assembled into the actual program.

This is what is going on when you compile higher level languages. The actual data area is allocated when it is compiled into an executable program, increasing the size of the program. The program can request memory on the fly, as well, and this is dynamic memory. You can request memory from the RAM and the CPU will give it to you to use, you can let go of it, and your garbage collector will release it back to the CPU. It can even be swapped to a hard disk, if necessary, by a good memory manager. These features are what high level languages provide you.


I would like to explain these concepts with the help of few diagrams.

This is true that memory cannot be allocated at compile time, for sure. But, then what happens in fact at compile time.

Here comes the explanation. Say, for example a program has four variables x,y,z and k. Now, at compile time it simply makes a memory map, where the location of these variables with respect to each other is ascertained. This diagram will illustrate it better.

Now imagine, no program is running in memory. This I show by a big empty rectangle.

empty field

Next, the first instance of this program is executed. You can visualize it as follows. This is the time when actually memory is allocated.

first instance

When second instance of this program is running, the memory would look like as follows.

second instance

And the third ..

third instance

So on and so forth.

I hope this visualization explains this concept well.

참고URL : https://stackoverflow.com/questions/21350478/what-does-memory-allocated-at-compile-time-really-mean

반응형