Programing

변수 선언은 비용이 많이 듭니까?

lottogame 2020. 10. 23. 07:28
반응형

변수 선언은 비용이 많이 듭니까?


C로 코딩하는 동안 다음과 같은 상황이 발생했습니다.

int function ()
{
  if (!somecondition) return false;

  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}

if위 코드 명령문이 함수에서 반환 될 수 있다는 점을 고려하면 두 위치에서 변수를 선언 할 수 있습니다.

  1. if성명 전에 .
  2. if진술 .

프로그래머로서 저는 ifStatement 뒤에 변수 선언을 유지하려고 생각합니다 .

신고 장소에 비용이 듭니까? 아니면 다른 방법보다 한 가지 방법을 선호하는 다른 이유가 있습니까?


C99 이상 (또는 C89에 대한 공통 확장 확장)에서는 명령문과 선언을 자유롭게 혼합 할 수 있습니다.

이전 버전에서와 마찬가지로 (컴파일러가 더 똑똑하고 공격적 일수록) 컴파일러는 레지스터 및 스택을 할당하는 방법을 결정하거나 as-if-rule을 따르는 다른 최적화를 수행합니다.
즉, 성능면에서 차이를 기대할 수 없습니다.

어쨌든 그것이 허용 된 이유가 아니 었습니다.

범위를 제한하기위한 것이기 때문에 코드를 해석하고 확인할 때 사람이 염두에 두어야하는 컨텍스트줄였습니다 .


말이되는 것은 무엇이든하라. 그러나 현재의 코딩 스타일은 변수 선언을 가능한 한 사용에 가깝게 두는 것을 권장한다.

실제로 변수 선언은 첫 번째 컴파일러 이후 거의 모든 컴파일러에서 무료입니다. 이는 거의 모든 프로세서가 스택 포인터 (및 프레임 포인터)로 스택을 관리하기 때문입니다. 예를 들어, 두 가지 기능을 고려하십시오.

int foo() {
    int x;
    return 5; // aren't we a silly little function now
}

int bar() {
    int x;
    int y;
    return 5; // still wasting our time...
}

최신 컴파일러에서 컴파일하면 (그리고 똑똑하지 않고 사용하지 않는 로컬 변수를 최적화하지 말라고) 다음과 같이 표시됩니다 (x64 어셈블리 예제 .. 다른 것들은 비슷합니다).

foo:
push ebp
mov  ebp, esp
sub  esp, 8    ; 1. this is the first line which is different between the two
mov  eax, 5    ; this is how we return the value
add  esp, 8    ; 2. this is the second line which is different between the two
ret

bar:
push ebp
mov  ebp, esp
sub  esp, 16    ; 1. this is the first line which is different between the two
mov  eax, 5     ; this is how we return the value
add  esp, 16    ; 2. this is the second line which is different between the two
ret

참고 : 두 함수 모두 동일한 수의 opcode를가집니다!

이는 사실상 모든 컴파일러가 필요한 모든 공간을 미리 할당하기 때문입니다 ( alloca별도로 처리되는 것과 같은 멋진 것 제외 ). 사실, 64에, 그 것이다 필수 그들이이 효율적인 방법으로 그렇게 할 것이다.

(편집 : Forss가 지적했듯이 컴파일러는 일부 지역 변수를 레지스터로 최적화 할 수 있습니다. 좀 더 기술적으로는 스택으로 "넘치는"첫 번째 변수는 2 개의 opcode가 필요하고 나머지는 무료라고 주장해야합니다.)

같은 이유로 컴파일러는 모든 지역 변수 선언을 수집하고 바로 앞에 공간을 할당합니다. C89는 1 패스 컴파일러로 설계 되었기 때문에 모든 선언이 선행되어야합니다. C89 컴파일러가 할당 할 공간을 알기 위해서는 나머지 코드를 방출하기 전에 모든 변수를 알아야했습니다. C99 및 C ++와 같은 현대 언어에서 컴파일러는 1972 년 이전보다 훨씬 더 똑똑 할 것으로 예상되므로 개발자의 편의를 위해 이러한 제한이 완화됩니다.

현대 코딩 관행은 변수를 사용에 가깝게 배치 할 것을 제안합니다.

이것은 컴파일러와는 아무런 관련이 없습니다 (분명히 어떤 식 으로든 상관 할 수 없습니다). 대부분의 인간 프로그래머는 변수가 사용되는 위치에 가까워지면 코드를 더 잘 읽는 것으로 나타났습니다. 이것은 스타일 가이드 일 뿐이므로 동의하지 않으셔도됩니다. 그러나 이것이 "올바른 방법"이라는 개발자들 사이에서 주목할만한 합의가 있습니다.

이제 몇 가지 코너 케이스에 대해 설명합니다.

  • 생성자와 함께 C ++를 사용하는 경우 컴파일러는 공간 을 미리 할당 합니다 (이렇게하는 것이 더 빠르고 아프지 않기 때문에). 그러나 변수는 코드 흐름에서 올바른 위치가 될 때까지 해당 공간에서 생성 되지 않습니다 . 어떤 경우에는 변수를 사용에 가깝게 두는 것이 앞에 두는 것보다 더 빠를 수 있음을 의미 합니다. 흐름 제어는 변수 선언을 안내 할 수 있으며,이 경우 생성자를 호출 할 필요도 없습니다.
  • alloca이 위의 레이어에서 처리됩니다. 궁금한 사람들을 위해 alloca구현은 스택 포인터를 임의의 양 아래로 이동시키는 효과가있는 경향이 있습니다. 사용 alloca하는 함수 는이 공간을 어떤 식 으로든 추적하고 나가기 전에 스택 포인터가 위쪽으로 다시 조정되는지 확인하는 데 필요합니다.
  • 일반적으로 16 바이트의 스택 공간이 필요한 경우가있을 수 있지만 한 가지 조건에서 50kB의 로컬 배열을 할당해야합니다. 코드에서 변수를 어디에 넣든 거의 모든 컴파일러는 함수가 호출 될 때마다 50kB + 16B의 스택 공간을 할당합니다. 이것은 거의 중요하지 않지만 강박 적으로 재귀적인 코드에서는 스택이 오버플로 될 수 있습니다. 50kB 배열로 작동하는 코드를 자체 함수로 이동하거나 alloca.
  • 일부 플랫폼 (예 : Windows)은 한 페이지 이상의 스택 공간을 할당하는 경우 프롤로그에서 특수 함수 호출이 필요합니다. 이것은 분석을 전혀 변경해서는 안됩니다 (구현시 페이지 당 1 단어 만 찌르는 매우 빠른 리프 함수입니다).

C에서는 모든 변수 선언이 함수 선언의 맨 위에있는 것처럼 적용됩니다. 블록으로 선언하면 범위 지정 일이라고 생각합니다 (C ++에서 동일하다고 생각하지 않습니다). 컴파일러는 변수에 대한 모든 최적화를 수행하며 일부는 더 높은 최적화에서 기계 코드에서 효과적으로 사라질 수 있습니다. 그런 다음 컴파일러는 변수에 필요한 공간을 결정한 다음 나중에 실행 중에 변수가있는 스택이라는 공간을 만듭니다.

함수가 호출되면 함수에서 사용하는 모든 변수가 호출되는 함수에 대한 정보 (예 : 반환 주소, 매개 변수 등)와 함께 스택에 배치됩니다. 변수가 선언 된 위치중요하지 않습니다. 선언 된 것뿐입니다. 변수는 상관없이 스택에 할당됩니다.

변수를 선언하는 것은 그 자체로 "비싸지"않습니다. 변수로 사용되지 않을만큼 쉬운 경우 컴파일러는 변수로 제거 할 것입니다.

이것 좀 봐:

다 스택

호출 스택에 대한 Wikipedia , 스택의 다른 장소

물론이 모든 것은 구현과 시스템에 따라 다릅니다.


예, 명확성이 떨어질 수 있습니다. 함수가 어떤 조건에서 아무 작업도 수행하지 않아야하는 경우 (귀하의 경우 전역 거짓을 찾을 때와 같이), 위에 표시 한 상단에 확인을 배치하는 것이 확실히 이해하기 쉽습니다. 디버깅 및 / 또는 문서화 중에 필수적인 것입니다.


궁극적으로 컴파일러에 따라 다르지만 일반적으로 모든 로컬은 함수 시작 부분에 할당됩니다.

그러나 로컬 변수를 할당하는 비용은 스택에 배치 (또는 최적화 후 레지스터에 배치)되기 때문에 매우 적습니다.


선언을 가능한 한 사용 된 위치에 가깝게 유지하십시오. 이상적으로는 중첩 된 블록 안에 있습니다. 따라서이 경우 if위에 변수를 선언하는 것은 의미가 없습니다 .


가장 좋은 방법은 게으른 접근 방식 을 적용하는 것입니다. 즉, 정말로 필요할 때만 선언하십시오. 그 결과 다음과 같은 이점이 있습니다.

해당 변수가 가능한 한 사용 장소에 가깝게 선언되면 코드가 더 읽기 쉽습니다.


이게 있으면

int function ()
{
   {
       sometype foo;
       bool somecondition;
       /* do something with foo and compute somecondition */
       if (!somecondition) return false;
   }
   internalStructure  *str1;
   internalStructure *str2;
   char *dataPointer;
   float xyz;

   /* do something here with the above local variables */    
}

그런 다음 스택 공간을 예약 foo하고 somecondition분명히 재사용 할 수 있으므로을 str1선언 if하면 스택 공간을 절약 할 수 있습니다. 컴파일러의 최적화 기능에 따라, 스택 공간의 절약 할 수 있습니다 중괄호의 내부 쌍을 제거하여 fucntion을 평평하게하는 경우도 발생하거나 당신이 선언 할 경우 str1전과 등 if; 그러나이를 위해서는 컴파일러 / 최적화 프로그램 이 범위가 "실제로"겹치지 않는다는 사실을 인지해야합니다. if개선 된 코드 가독성은 말할 것도없고 최적화 없이도이 동작을 용이하게 한 후에 선언을 포지 팅함으로써 .


나는 우리가 그것을하는 이유를 문서화하는 것 외에도 함수의 맨 위에 "조기 종료"상태를 유지하는 것을 선호합니다. 여러 변수 선언 뒤에 넣으면 코드에 익숙하지 않은 사람이 코드를 찾아야한다는 것을 알지 못하면 쉽게 놓칠 수 있습니다.

"조기 종료"조건을 문서화하는 것만으로는 항상 충분하지는 않습니다. 코드에서도 명확하게하는 것이 좋습니다. 조기 종료 조건을 맨 위에 놓으면 나중에 조기 종료 조건을 제거하거나 이러한 조건을 추가하기로 결정한 경우와 같이 문서를 코드와 동기화 상태로 유지하는 것이 더 쉬워집니다.


실제로 중요한 경우 변수 할당을 피하는 유일한 방법은 다음과 같습니다.

int function_unchecked();

int function ()
{
  if (!someGlobalValue) return false;
  return function_unchecked();
}

int function_unchecked() {
  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}

그러나 실제로는 성능상의 이점이 없다고 생각합니다. 사소한 오버 헤드가 있다면.

Of course if you were coding C++ and some of those local variables had non-trivial constructors you would probably need to place them after the check. But even then I don't think it would help to split the function.


Whenever you allocate local variables in a C scope (such as a functions), they have no default initialization code (such as C++ constructors). And since they're not dynamically allocated (they're just uninitialized pointers), no additional (and potentially expensive) functions need to be invoked (e.g. malloc) in order to prepare/allocate them.

Due to the way the stack works, allocating a stack variable simply means decrementing the stack pointer (i.e. increasing the stack size, because on most architectures, it grows downwards) in order to make room for it. From the CPU's perspective, this means executing a simple SUB instruction: SUB rsp, 4 (in case your variable is 4 bytes large--such as a regular 32-bit integer).

Moreover, when you declare multiple variables, your compiler is smart enough to actually group them together into one large SUB rsp, XX instruction, where XX is the total size of a scope's local variables. In theory. In practice, something a little different happens.

In situations like these, I find GCC explorer to be an invaluable tool when it comes to finding out (with tremendous ease) what happens "under the hood" of the compiler.

So let's take a look at what happens when you actually write a function like this: GCC explorer link.

C code

int function(int a, int b) {
  int x, y, z, t;

  if(a == 2) { return 15; }

  x = 1;
  y = 2;
  z = 3;
  t = 4;

  return x + y + z + t + a + b;
}

Resulting assembly

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-20], edi
    mov DWORD PTR [rbp-24], esi
    cmp DWORD PTR [rbp-20], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    -- snip --
.L3:
    pop rbp
    ret

As it turns out, GCC is even smarter than that. It doesn't even perform the SUB instruction at all to allocate the local variables. It just (internally) assumes that the space is "occupied", but doesn't add any instructions to update the stack pointer (e.g. SUB rsp, XX). This means that the stack pointer is not kept up to date but, since in this case no more PUSH instructions are performed (and no rsp-relative lookups) after the stack space is used, there's no issue.

Here's an example where no additional variables are declared: http://goo.gl/3TV4hE

C code

int function(int a, int b) {
  if(a == 2) { return 15; }
  return a + b;
}

Resulting assembly

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov DWORD PTR [rbp-8], esi
    cmp DWORD PTR [rbp-4], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    mov edx, DWORD PTR [rbp-4]
    mov eax, DWORD PTR [rbp-8]
    add eax, edx
.L3:
    pop rbp
    ret

If you take a look at the code before the premature return (jmp .L3, which jumps to the cleanup and return code), no additional instructions are invoked to "prepare" the stack variables. The only difference is that the function parameters a and b, which are stored in the edi and esi registers, are loaded onto the stack at a higher address than in the first example ([rbp-4] and [rbp - 8]). This is because no additional space has been "allocated" for the local variables like in the first example. So, as you can see, the only "overhead" for adding those local variables is a change in a subtraction term (i.e. not even adding an additional subtraction operation).

So, in your case, there is virtually no cost for simply declaring stack variables.


if 문 뒤에 변수를 선언하고 함수에서 즉시 반환하면 컴파일러는 스택의 메모리를 확약하지 않습니다.

참고 URL : https://stackoverflow.com/questions/27729930/is-declaration-of-variables-expensive

반응형