루프 내에서 변수를 선언하는 데 오버 헤드가 있습니까? (C ++)


158

다음과 같이하면 속도 나 효율성이 저하 될지 궁금합니다.

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

int var백 번 선언합니다 . 있을 것 같지만 확실하지 않습니다. 대신 이것을하는 것이 더 실용적 / 빠른 것입니까?

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

아니면 속도와 효율성이 동일합니까?


7
명확히하기 위해 위의 코드는 var를 100 번 "선언"하지 않습니다.
jason

1
@Rabarberski : 참조 된 질문은 언어를 지정하지 않기 때문에 정확히 중복되지 않습니다. 이 질문은 C ++에만 해당됩니다 . 그러나 참조 된 질문에 게시 된 답변에 따르면 답변은 언어 및 컴파일러에 따라 다릅니다.
DavidRR

2
@jason 코드의 첫 번째 조각이 변수 'var'를 100 번 선언하지 않으면 무슨 일이 일어나고 있는지 설명 할 수 있습니까? 변수를 한 번만 선언하고 100 번 초기화합니까? 루프의 모든 것이 100 번 실행되기 때문에 코드가 변수를 100 번 선언하고 초기화한다고 생각했을 것입니다. 감사.
randomUser47534

답변:


194

지역 변수의 스택 공간은 일반적으로 함수 범위에 할당됩니다. 따라서 루프 내에서는 스택 포인터 조정이 발생하지 않으며 4를 var. 따라서이 두 조각의 오버 헤드는 동일합니다.


50
나는 대학 밖에서 가르치는 사람들이 최소한이 기본적인 것을 알고 있었으면한다. 한 번 그는 루프 내에서 변수를 선언하는 나를 비웃었 고 그가 그렇게하지 않는 이유로 성능을 언급하기 전까지 나는 "WTF !?"와 같았다.
mmx 2009-06-11

18
당장 스택 공간에 대해 이야기해야할까요? 이와 같은 변수는 레지스터에있을 수도 있습니다.
toto

3
@toto 이와 같은 변수는 어디에도 없을 수 있습니다 . var변수는 초기화되었지만 사용되지 않았으므로 합리적인 옵티마이 저는 변수를 완전히 제거 할 수 있습니다 ( 변수가 루프 이후 어딘가에서 사용 된 경우 두 번째 스 니펫 제외 ).
CiaPan

@Mehrdad Afshari 루프의 변수는 반복 당 한 번 호출되는 생성자를 가져옵니다. 편집-나는 당신이 이것을 아래에 언급했지만 받아 들여지는 대답에서도 언급 할 가치가 있다고 생각합니다.
hoodaticus

106

기본 유형 및 POD 유형의 경우 차이가 없습니다. 컴파일러는 함수 시작 부분에 변수에 대한 스택 공간을 할당하고 두 경우 모두 함수가 반환 될 때 할당을 취소합니다.

중요하지 않은 생성자가있는 비 -POD 클래스 유형의 경우 차이를 만들 것입니다. 루프는 루프가 반복 될 때마다 생성자와 소멸자를 호출합니다. 클래스의 생성자, 소멸자 및 할당 연산자가 수행하는 작업에 따라 이것은 바람직 할 수도 있고 그렇지 않을 수도 있습니다.


42
올바른 아이디어 잘못된 이유. 루프 외부에서 변수. 한 번 생성되고 한 번 파괴되지만 서명 연산자는 모든 반복을 적용했습니다. 루프 내부의 변수입니다. Constructe / Desatructor는 모든 반복을 적용했지만 할당 작업은 없습니다.
Martin York

8
이것이 최선의 답변이지만 이러한 의견은 혼란 스럽습니다. 생성자 호출과 할당 연산자 호출에는 큰 차이가 있습니다.
앤드류 그랜트

1
그것은 이다 루프 본문이 아니라 초기화, 어쨌든 임무를 수행하는 경우는 true. 신체 독립적 / 일정한 초기화 만있는 경우 최적화 프로그램이이를 끌어 올릴 수 있습니다.
peterchen 2009-06-11

7
@ 앤드류 그랜트 : 왜. 할당 연산자는 일반적으로 tmp 로의 복사 구조, 스왑 (예외 안전을 위해), tmp 파괴로 정의됩니다. 따라서 할당 연산자는 위의 구성 / 파괴주기와 크게 다르지 않습니다. 일반적인 할당 연산자의 예는 stackoverflow.com/questions/255612/… 를 참조하십시오 .
Martin York

1
구성 / 소멸이 비싸다면 총 비용은 운영자의 비용에 대한 합리적인 상한선입니다. 그러나 할당은 실제로 더 저렴할 수 있습니다. 또한이 논의를 int에서 C ++ 유형으로 확장하면 'var = 4'를 '같은 유형의 값에서 변수 할당'이 아닌 다른 작업으로 일반화 할 수 있습니다.
greggo

69

둘 다 동일하며 컴파일러가 수행하는 작업을 살펴보면 알 수있는 방법이 있습니다 (최적화를 높음으로 설정하지 않아도).

컴파일러 (gcc 4.0)가 간단한 예제에서 수행하는 작업을 살펴보십시오.

1.c :

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1. 초 :

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2. 초 :

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

이로부터 두 가지를 볼 수 있습니다. 첫째, 코드는 둘 다 동일합니다.

둘째, var에 대한 저장소는 루프 외부에 할당됩니다.

         subl    $24, %esp

마지막으로 루프의 유일한 것은 할당 및 조건 확인입니다.

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

루프를 완전히 제거하지 않고도 가능한 한 효율적입니다.


2
"루프를 완전히 제거하지 않고도 가능한 한 효율적입니다." 루프를 부분적으로 풀면 (패스 당 4 번) 속도가 크게 빨라집니다. 최적화하는 다른 방법이 많이있을 것입니다 ... 대부분의 현대 컴파일러는 아마도 루핑에 전혀 의미가 없음을 인식 할 것입니다. 나중에 'i'가 사용 된 경우 'i'= 100으로 설정됩니다.
darron

그것은 코드가 증가 된 'i'로 변경되었다고 가정합니다.
darron

원래 게시물과 마찬가지로!
Alex Brown

2
나는 증거가있는 이론을 뒷받침하는 답변을 좋아합니다! ASM 덤프가 동일한 코드라는 이론을 뒷받침하는 것을 보니 반갑습니다. +1
Xavi Montero 2014 년

1
실제로 각 버전에 대한 기계어 코드를 생성하여 결과를 생성했습니다. 실행할 필요가 없습니다.
Alex Brown

14

요즘에는 컴파일러가 코드를 더 잘 최적화 할 수 있기 때문에 (변수 범위를 줄임) 상수가 아니면 루프 내에서 선언하는 것이 좋습니다.

편집 :이 답변은 현재 거의 사용되지 않습니다. 포스트 클래식 컴파일러의 등장으로 컴파일러가 그것을 알아낼 수없는 경우가 드물어지고 있습니다. 여전히 구성 할 수 있지만 대부분의 사람들은 구성을 잘못된 코드로 분류합니다.


4
최적화에 영향을 미칠지 의심 스럽습니다. 컴파일러가 어떤 종류의 데이터 흐름 분석을 수행하면 루프 외부에서 수정되지 않는다는 것을 알 수 있으므로 두 경우 모두 동일한 최적화 된 코드를 생성해야합니다.
Adam Rosenfield

3
그래도 동일한 임시 변수 이름을 사용하는 두 개의 다른 루프가 있는지 알아 내지 못합니다.
Joshua

11

대부분의 최신 컴파일러는이를 최적화합니다. 그것은 내가 더 읽기 쉽다는 것을 알게되면 첫 번째 예를 사용할 것이라고 말했습니다.


3
나는 그것을 최적화로 간주하지 않습니다. 지역 변수이기 때문에 스택 공간은 함수 시작 부분에 할당됩니다. 성능에 해를 끼치는 실제 "창조"는 없습니다 (생성자가 호출되지 않는 한, 완전히 다른 이야기입니다).
mmx 2009-06-11

당신 말이 맞아요. "최적화"는 잘못된 단어이지만 더 나은 단어를 찾기 위해 잃어 버렸습니다.
Andrew Hare

문제는 이러한 옵티마이 저가 라이브 범위 분석을 사용하고 두 변수가 모두 죽었다는 것입니다.
MSalters 2009-06-12

"컴파일러는 데이터 흐름 분석을 수행 한 후에는 차이를 보지 못합니다"는 어떻습니까? 개인적으로 저는 변수의 범위가 효율성이 아니라 명확성을 위해 사용되는 위치로 제한되어야하는 것을 선호합니다.
greggo

9

내장 유형의 경우 두 스타일간에 차이가 없을 가능성이 높습니다 (아마도 생성 된 코드까지).

그러나 변수가 중요하지 않은 생성자 / 소멸자가있는 클래스 인 경우 런타임 비용에 큰 차이가있을 수 있습니다. 일반적으로 범위를 가능한 한 작게 유지하기 위해 루프 내부로 변수 범위를 지정하지만 성능에 영향을 미치는 것으로 판명되면 클래스 변수를 루프 범위 외부로 이동하는 방법을 찾습니다. 그러나 ode 경로의 의미가 변경 될 수 있으므로 추가 분석이 필요하므로 의미가 허용하는 경우에만 수행 할 수 있습니다.

RAII 클래스에는이 동작이 필요할 수 있습니다. 예를 들어, 파일 액세스 수명을 관리하는 클래스는 파일 액세스를 적절하게 관리하기 위해 각 루프 반복에서 생성 및 소멸되어야 할 수 있습니다.

LockMgr생성 될 때 중요 섹션을 획득하고 파괴 될 때 해제 하는 클래스가 있다고 가정합니다 .

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

다음과는 상당히 다릅니다.

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

6

두 루프의 효율성은 동일합니다. 둘 다 무한한 시간이 걸립니다. :) 루프 내에서 i를 증가시키는 것이 좋습니다.


아 예, 공간 효율성을 다루는 것을 잊었습니다-괜찮습니다-둘 다에 대해 2 개의 int 프로그래머가 트리의 숲을 놓치고 있다는 것이 나에게는 이상하게 보입니다. 종료되지 않는 일부 코드에 대한 이러한 모든 제안.
Larry Watanabe

종료하지 않아도 괜찮습니다. 둘 다 호출되지 않습니다. :-)
Nosredna 2009-06-12

2

한 번 성능 테스트를 실행했는데 놀랍게도 사례 1이 실제로 더 빠르다는 것을 알았습니다! 루프 내부에서 변수를 선언하면 범위가 줄어들어 더 일찍 해제되기 때문일 수 있습니다. 그러나 그것은 아주 오래된 컴파일러에서 오래 전이었습니다. 현대 컴파일러가 차이를 최적화하는 더 나은 작업을 수행한다고 확신하지만 변수 범위를 가능한 한 짧게 유지하는 것은 여전히 ​​문제가되지 않습니다.


차이는 아마도 범위의 차이에서 비롯됩니다. 범위가 작을수록 컴파일러는 변수의 직렬화를 제거 할 수 있습니다. 작은 루프 범위에서 변수는 레지스터에 배치되고 스택 프레임에 저장되지 않았습니다. 루프에서 함수를 호출하거나 컴파일러가 가리키는 위치를 실제로 알지 못하는 포인터를 역 참조하는 경우 함수 범위에있는 경우 루프 변수를 유출합니다 (포인터에를 포함 할 수 있음 &i).
Patrick Schlüter 2015

설정 및 결과를 게시하십시오.
jxramos

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

위의 코드는 항상 100 번 10 번 출력하므로 루프 내부의 지역 변수는 각 함수 호출마다 한 번만 할당됩니다.


0

확실하게하는 유일한 방법은 시간을 맞추는 것입니다. 그러나 차이가 있다면 미시적이므로 강력한 큰 타이밍 루프가 필요합니다.

요컨대, 첫 번째는 변수 var를 초기화하고 다른 하나는 초기화되지 않은 상태로두기 때문에 더 나은 스타일입니다. 이것은 변수를 가능한 한 사용 지점에 가깝게 정의해야한다는 지침은 일반적으로 첫 번째 형식이 선호되어야 함을 의미합니다.


"확실한 유일한 방법은 시간을 맞추는 것입니다." -1 사실이 아닙니다. 미안하지만 다른 포스트는 생성 된 기계어를 비교하고 본질적으로 동일하다는 것을 발견함으로써 이것이 틀렸다는 것을 증명했습니다. 나는 일반적으로 당신의 대답에 아무런 문제가 없지만 -1이 무엇인지 틀리지 않습니까?
Bill K

내 보낸 코드를 조사하는 것은 확실히 유용하며 이와 같은 간단한 경우에는 충분할 수 있습니다. 그러나 더 복잡한 경우에는 참조의 위치와 같은 문제가 머리를 뒤덮고 실행 타이밍을 통해서만 테스트 할 수 있습니다.

-1

두 개의 변수 만 있으면 컴파일러는 둘 다에 대해 레지스터를 할당 할 가능성이 높습니다. 이 레지스터는 어쨌든 거기에 있으므로 시간이 걸리지 않습니다. 두 경우 모두 레지스터 쓰기 2 개와 레지스터 읽기 명령어 1 개가 있습니다.


-2

나는 대부분의 답변에서 고려해야 할 주요 요점이 누락되었다고 생각합니다. "이것이 분명합니까?"그리고 모든 토론에서 분명히 사실입니다. 전혀 그렇지 않다. 대부분의 루프 코드에서 효율성은 거의 문제가되지 않는다고 제안합니다 (화성 착륙선을 계산하지 않는 한). 그래서 실제로 유일한 질문은 무엇이 더 현명하고 읽기 쉽고 유지 관리 할 수 ​​있는지입니다. 루프 앞과 외부의 변수-이것은 단순히 더 명확하게 만듭니다. 그렇다면 당신과 같은 사람들은 그것이 유효한지 아닌지 온라인으로 확인하는 데 시간을 낭비하지 않을 것입니다.


-6

그것은 사실이 아니지만 오버 헤드가 있지만 무시할 수있는 오버 헤드가 있습니다.

아마도 그들은 스택의 같은 위치에있을 것이지만 여전히 그것을 할당합니다. 해당 int에 대해 스택에 메모리 위치를 할당 한 다음} 끝에 해제합니다. 힙이없는 의미에서는 sp (스택 포인터)를 1만큼 이동합니다. 그리고 로컬 변수가 하나만 있다는 점을 고려하면 단순히 fp (프레임 포인터)와 sp를 동일시합니다.

짧은 대답은 다음과 같습니다. 두 가지 방법 모두 거의 동일하게 작동합니다.

그러나 스택이 어떻게 구성되는지 더 읽어보십시오. 내 학부 학교는 그것에 대해 꽤 좋은 강의를 가졌습니다. 더 많은 것을 읽고 싶다면 여기 http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html


다시 말하지만, -1은 사실이 아닙니다. 어셈블리를 본 게시물을 읽으십시오.
Bill K

아니, 틀렸어. 그 코드로 생성 된 어셈블러 코드를 보면
grobartn
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.