루프 내부에 변수 선언, 좋은 연습 또는 나쁜 연습?


265

질문 # 1 : 루프 안에서 변수를 선언하는 것이 좋은 습관입니까, 나쁜 습관입니까?

성능 문제가 있는지 여부에 대한 다른 스레드를 읽었으며 (거의 아니요), 항상 변수를 사용할 위치에 가깝게 선언해야합니다. 내가 궁금해하는 것은 이것을 피해야하는지 아닌지 또는 실제로 선호되는지입니다.

예:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

질문 # 2 : 대부분의 컴파일러는 변수가 이미 선언되었다는 것을 인식하고 그 부분을 건너 뛰거나 실제로 매번 메모리에 변수를 지정합니까?


29
프로파일 링에서 달리 언급하지 않는 한, 사용량에 가깝게 배치하십시오.
Mooing Duck


3
@drnewman 나는 그 스레드를 읽었지만 내 질문에 대답하지 않았습니다. 루프 내부에서 변수를 선언하면 작동한다는 것을 이해합니다. 그렇게하는 것이 좋은 습관인지 또는 피해야 할 것이 있는지 궁금합니다.
JeramyRR

답변:


348

이것은 훌륭한 연습입니다.

루프 내부에 변수를 작성하면 해당 범위가 루프 내부로 제한됩니다. 루프 외부에서 참조하거나 호출 할 수 없습니다.

이 방법:

  • 변수의 이름이 "generic"과 같은 비트 (예 : "i")이면 나중에 코드에서 같은 이름의 다른 변수와 혼합 할 위험이 없습니다 ( -WshadowGCC 의 경고 명령어를 사용하여 완화 할 수도 있음 ).

  • 컴파일러는 변수 범위가 루프 내부로 제한되므로 변수가 실수로 다른 곳에서 참조되는 경우 적절한 오류 메시지를 발행합니다.

  • 마지막으로, 일부 전용 최적화는 변수가 루프 외부에서 사용될 수 없다는 것을 알고 있기 때문에 컴파일러 (가장 중요한 레지스터 할당)에 의해보다 효율적으로 수행 될 수 있습니다. 예를 들어, 나중에 재사용하기 위해 결과를 저장할 필요가 없습니다.

요컨대, 당신은 그것을 할 권리가 있습니다.

그러나 변수는 각 루프 사이에 값을 유지하지 않아야합니다 . 이 경우 매번 초기화해야 할 수도 있습니다. 루프를 포괄하는 더 큰 블록을 만들 수도 있는데, 그 목적은 한 루프에서 다른 루프로 값을 유지해야하는 변수를 선언하는 것입니다. 여기에는 일반적으로 루프 카운터 자체가 포함됩니다.

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

질문 # 2의 경우 : 함수가 호출 될 때 변수가 한 번 할당됩니다. 실제로 할당 관점에서 보면 함수 시작 부분에서 변수를 선언하는 것과 거의 같습니다. 유일한 차이점은 범위입니다. 변수는 루프 외부에서 사용할 수 없습니다. 변수가 할당되지 않았을 수도 있습니다. 사용 가능한 슬롯 (범위가 종료 된 다른 변수에서)을 다시 사용하기 만하면됩니다.

제한적이고 정확한 범위로보다 정확한 최적화가 이루어집니다. 그러나 더 중요한 것은 코드의 다른 부분을 읽을 때 걱정할 필요가 적은 상태 (예 : 변수)로 코드를 더 안전하게 만듭니다.

이것은 if(){...}블록 외부에서도 마찬가지 입니다. 일반적으로 대신 :

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

작성하는 것이 더 안전합니다.

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

특히 작은 예에서는 차이가 작게 보일 수 있습니다. 그러나 더 큰 코드 기반에서는 도움이 될 것입니다. 이제 일부 resultf1()f2()블록 으로 전송할 위험이 없습니다 . 각각 result은 자체 범위로 엄격히 제한되어 역할이 더욱 정확합니다. 리뷰어의 관점에서 보면 걱정하고 추적해야 할 장거리 상태 변수 가 적기 때문에 훨씬 좋습니다.

컴파일러가 더 나은 도움을 줄 것입니다. 미래에 약간의 잘못된 코드 변경 후으로 result제대로 초기화되지 않았다고 가정합니다 f2(). 두 번째 버전은 컴파일 타임에 명확한 오류 메시지 (런타임보다 낫다)를 나타내는 작동을 거부합니다. 첫 번째 버전은 아무 것도 발견하지 못합니다. 그 결과 f1()는 단순히 두 번째로 테스트되어 결과에 대해 혼란스러워 f2()집니다.

보완 정보

오픈 소스 도구 인 CppCheck (C / C ++ 코드를위한 정적 분석 도구)는 최적의 변수 범위에 관한 훌륭한 힌트를 제공합니다.

할당에 대한 의견에 대한 답변 : 위의 규칙은 C에서는 true이지만 일부 C ++ 클래스에는 해당되지 않을 수 있습니다.

표준 유형 및 구조의 경우 변수 크기는 컴파일 타임에 알려져 있습니다. C에는 "구성"과 같은 것이 없으므로 함수를 호출 할 때 변수를위한 공간이 스택에 할당됩니다 (초기화없이). 이것이 루프 내부에서 변수를 선언 할 때 "제로"비용이 발생하는 이유입니다.

그러나 C ++ 클래스의 경우이 생성자 항목이 훨씬 적습니다. 컴파일러는 동일한 공간을 재사용하기에 충분히 영리해야하기 때문에 할당이 문제가되지 않을 것이라고 생각하지만 초기화는 각 루프 반복에서 발생할 수 있습니다.


4
멋진 답변입니다. 이것은 내가 찾던 것이었고, 내가 알지 못하는 것에 대한 통찰력을 주었다. 스코프가 루프 내부에만 남아 있다는 것을 알지 못했습니다. 답변 주셔서 감사합니다!
JeramyRR

22
"하지만 기능의 시작 부분에 할당하는 것보다 결코 느리지 않습니다." 항상 그런 것은 아닙니다. 변수는 한 번만 할당되지만 필요한만큼 여러 번 구성 및 소멸됩니다. 예제 코드의 경우 11 배입니다. Mooing의 의견을 인용하려면 "프로파일 링에서 달리 언급하지 않는 한 사용량에 가깝게 배치하십시오."
IronMensan

4
@JeramyRR : 물론 아닙니다. 컴파일러는 객체가 생성자 또는 소멸자에 의미있는 부작용이 있는지 알 방법이 없습니다.
ildjarn

2
@Iron : 반면에, 항목을 먼저 선언하면 할당 연산자를 여러 번 호출합니다. 일반적으로 객체를 만들고 파괴하는 것과 거의 같은 비용이 듭니다.
Billy ONeal

4
@BillyONeal : 내용 stringvector, 할당 연산자는 (루프)에 따라 각 루프 버퍼 할당 재사용 할 구체적 상당한 시간을 절약 할 수있다.
Mooing Duck

22

일반적으로 매우 가깝게 유지하는 것이 좋습니다.

경우에 따라 루프에서 변수를 꺼내는 것을 정당화하는 성능과 같은 고려 사항이 있습니다.

귀하의 예에서, 프로그램은 매번 문자열을 작성하고 파기합니다. 일부 라이브러리는 작은 문자열 최적화 (SSO)를 사용하므로 경우에 따라 동적 할당을 피할 수 있습니다.

중복 생성 / 할당을 피하려고한다고 가정하면 다음과 같이 작성합니다.

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

또는 상수를 꺼낼 수 있습니다.

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

대부분의 컴파일러는 변수가 이미 선언되었으며 해당 부분을 건너 뛰거나 실제로 매번 메모리에 스팟을 생성합니까?

변수가 소비 하는 공간을 재사용 할 수 있으며 루프에서 불변을 가져올 수 있습니다. const char 배열 (위)의 경우 해당 배열을 가져올 수 있습니다. 그러나 생성자와 소멸자는 객체 (예 :와 같은 std::string) 의 각 반복에서 실행되어야합니다 . 의 경우 std::string해당 'space'에는 문자를 나타내는 동적 할당이 포함 된 포인터가 포함됩니다. 그래서 이거:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

각 경우에 중복 복사가 필요하고 변수가 SSO 문자 수에 대한 임계 값보다 높은 경우 동적 할당 및 해제가 필요합니다 (그리고 SSO는 std 라이브러리에 의해 구현 됨).

이것을하는 것 :

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

각 반복에서 문자의 실제 사본이 여전히 필요하지만 문자열을 할당하기 때문에 양식에 하나의 동적 할당이 발생할 수 있으며 구현시 문자열의 백업 할당 크기를 조정할 필요가 없어야합니다. 물론,이 예제에서는 그렇게하지 않을 것입니다 (여러 개의 대체 대안이 이미 입증 되었기 때문에). 문자열이나 벡터의 내용이 다를 때 고려할 수도 있습니다.

그렇다면 모든 옵션 (및 그 이상)으로 무엇을하십니까? 비용을 잘 이해하고 언제 이탈해야 할지를 알 때까지 기본값을 매우 가깝게 유지하십시오.


1
float 또는 int와 같은 기본 데이터 유형과 관련하여 루프 내부의 변수를 선언하면 루프 외부에서 해당 변수를 선언하는 것보다 속도가 느려집니다. 각 반복마다 변수에 대한 공간을 할당해야합니까?
Kasparov92

2
@ Kasparov92 짧은 대답은 "아니요. 가독성 / 지역성을 개선하기 위해 가능한 경우 최적화를 무시하고 루프에 배치하십시오. 컴파일러는 마이크로 최적화를 수행 할 수 있습니다." 더 자세하게 말하자면, 플랫폼, 최적화 수준 등에 가장 적합한 것을 기반으로 컴파일러가 결정할 수 있습니다. 일반적으로 루프 내부의 일반적인 int / float는 스택에 배치됩니다. 컴파일러는 루프 외부로 확실히 이동하여 최적화가 있으면 스토리지를 재사용 할 수 있습니다. 실용적인 목적으로, 이것은 매우 작은 최적화 일 것입니다…
justin

1
@ Kasparov92… (계속) 모든 사이클이 계산되는 환경 / 애플리케이션에서만 고려할 것입니다. 이 경우 어셈블리 사용을 고려할 수 있습니다.
저스틴

14

C ++의 경우 수행중인 작업에 따라 다릅니다. 좋아, 바보 같은 코드지만 상상 해봐

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};
myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}

myFunc의 출력을 얻을 때까지 55 초 동안 기다립니다. 각 루프 생성자와 소멸자가 함께 완료하는 데 5 초가 걸리기 때문입니다.

myOtherFunc의 출력을 얻을 때까지 5 초가 필요합니다.

물론 이것은 미친 예입니다.

그러나 생성자 및 / 또는 소멸자가 약간의 시간이 필요할 때 각 루프가 동일한 구성을 수행 할 때 성능 문제가 될 수 있음을 보여줍니다.


2
글쎄, 기술적으로 두 번째 버전에서는 객체를 아직 파괴하지 않았기 때문에 2 초만에 결과를 얻을 수 있습니다 ...
Chrys

12

나는 JeremyRR의 질문에 답하기 위해 글을 올리지 않았다. 대신, 나는 단지 제안을하기 위해 게시했다.

JeremyRR에게 다음을 수행 할 수 있습니다.

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

나는 (프로그래밍을 처음 시작할 때 몰랐다.) "괄호 (쌍으로 묶인 한)"가 "if", "for", "다음에 오는 것이 아니라 코드의 어느 곳에 나 위치 할 수 있다는 것을 알고 있는지 모른다. while "등

내 코드는 Microsoft Visual C ++ 2010 Express에서 컴파일되었으므로 작동한다는 것을 알고 있습니다. 또한 변수가 정의 된 대괄호 외부에서 변수를 사용하려고 시도하고 오류가 발생하여 변수가 "파괴"되었음을 알았습니다.

레이블이없는 많은 괄호가 코드를 빠르게 읽을 수 없게 만들 수 있지만 일부 주석은 문제를 해결할 수 있으므로이 방법을 사용하는 것이 나쁜 습관인지 잘 모르겠습니다.


4
나에게 이것은 매우 합법적 인 답변이며 질문에 직접 연결된 제안을 제공합니다. 당신은 내 투표를했습니다!
Alexis Leclerc

0

위의 모든 대답이 질문의 이론적 측면을 제공하므로 코드를 엿볼 수 있습니다 .GEEKSFORGEEKS보다 DFS를 해결하려고했습니다. 최적화 문제가 발생했습니다 ... 루프 외부의 정수를 선언하는 코드를 해결하면 최적화 오류가 발생합니다.

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

이제 루프 안에 정수를 넣으면 정답을 얻을 수 있습니다 ...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

이것은 @justin 선생님이 두 번째 의견에서 말한 것을 완전히 반영합니다 .... https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . 그냥 쏴라 .... 알겠다.이 도움을 바란다.


나는 이것이 질문에 적용되지 않는다고 생각합니다. 분명히, 위의 경우에는 중요합니다. 문제는 코드의 동작을 변경하지 않고 변수 정의를 다른 곳에서 정의 할 수있는 경우를 다루었습니다.
pcarter

게시 한 코드에서 문제는 정의가 아니라 초기화 부분입니다. 반복 할 flag때마다 0으로 다시 초기화해야합니다 while. 그것은 정의 문제가 아니라 논리 문제입니다.
Martin Véronneau

0

4.8 장 K & R의 C 프로그래밍 언어 에서의 블록 구조 2.Ed. :

블록에 선언되고 초기화 된 자동 변수는 블록을 입력 할 때마다 초기화됩니다.

나는 책에서 다음과 같은 관련 설명을 보지 못했을 수도 있습니다.

블록에서 선언되고 초기화 된 자동 변수는 블록이 입력되기 전에 한 번만 할당됩니다.

그러나 간단한 테스트를 통해 가정을 증명할 수 있습니다.

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.