사용하지 않는 멤버 변수가 메모리를 차지합니까?


92

멤버 변수를 초기화하고 참조 / 사용하지 않으면 런타임 중에 RAM을 더 많이 차지합니까, 아니면 컴파일러가 단순히 해당 변수를 무시합니까?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

위의 예에서 멤버 'var1'은 콘솔에 표시되는 값을 가져옵니다. 그러나 'Var2'는 전혀 사용되지 않습니다. 따라서 런타임 중에 메모리에 쓰는 것은 리소스 낭비입니다. 컴파일러는 이러한 종류의 상황을 고려하고 단순히 사용되지 않는 변수를 무시합니까, 아니면 Foo 개체의 멤버 사용 여부에 관계없이 항상 동일한 크기입니까?


25
이것은 컴파일러, 아키텍처, 운영 체제 및 사용 된 최적화에 따라 다릅니다.
Owl

16
하드웨어 데이터 프레임 크기와 일치하도록 패딩하고 원하는 메모리 정렬을 얻기위한 해킹으로 특별히 아무것도하지 않는 구조체 멤버를 추가하는 메트릭 톤의 저수준 드라이버 코드가 있습니다. 컴파일러가이를 최적화하기 시작하면 많은 손상이있을 것입니다.
Andy Brown

2
@Andy 다음 데이터 멤버의 주소가 평가되므로 실제로 아무것도하지 않습니다. 이것은 패딩 멤버의 존재가 프로그램에서 관찰 가능한 동작을 가지고 있음을 의미합니다. 여기, var2그렇지 않습니다.
YSC

4
컴파일러가 이러한 구조체를 처리하는 컴파일 단위가 동일한 구조체를 사용하는 다른 컴파일 단위에 링크 될 수 있고 컴파일러가 별도의 컴파일 단위가 멤버를 처리하는지 여부를 알 수 없다는 점을 감안할 때 컴파일러가이를 최적화 할 수 있다면 놀랄 것입니다.
Galik

2
@geza sizeof(Foo)는 정의에 따라 감소 할 수 없습니다. 인쇄 sizeof(Foo)하면 8(일반 플랫폼에서) 양보해야합니다 . 컴파일러는 에서 사용하지 않는 공간을 최적화 할 var2(를 통해 아무리 경우 new에도 LTO 또는 전체 프로그램 최적화를하지 않고, 그들이 그것을 합리적으로 찾을 수있는 상황 또는 스택 또는 함수 호출에를 ...). 그것이 가능하지 않은 곳에서는 다른 최적화와 마찬가지로 그렇게하지 않을 것입니다. 나는 받아 들여지는 답변에 대한 편집으로 인해 오해 될 가능성이 훨씬 적다고 믿습니다.
Max Langhof

답변:


106

"그대로 경우"규칙 황금 C ++ 1 , 미국 경우 생성 관찰 행동 의 프로그램이 사용되지 않는 데이터의 존재 - 부재에 의존하지 않는 거리를 최적화하기 위해, 컴파일러는 허용된다 .

사용하지 않는 멤버 변수가 메모리를 차지합니까?

아니요 ( "정말"사용하지 않은 경우).


이제 두 가지 질문이 있습니다.

  1. 관찰 가능한 행동은 언제 회원의 존재에 의존하지 않습니까?
  2. 실생활 프로그램에서 그런 상황이 발생합니까?

예를 들어 보겠습니다.

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

gcc에이 번역 단위를 컴파일 하도록 요청하면 다음 과 같이 출력됩니다.

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2은와 동일 f1하며 실제 Foo2::var2. ( Clang은 비슷한 일을합니다 ).

토론

일부는 이것이 두 가지 이유로 다르다고 말할 수 있습니다.

  1. 이것은 너무 사소한 예입니다.
  2. 구조체는 완전히 최적화되어 있으며 계산되지 않습니다.

글쎄요, 좋은 프로그램은 복잡한 것들의 단순한 병치보다는 단순한 것들의 똑똑하고 복잡한 조립입니다. 실생활에서는 컴파일러가 최적화하는 것보다 단순한 구조를 사용하여 수많은 간단한 함수를 작성합니다. 예를 들면 :

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

이것은 데이터 멤버 (여기서는 std::pair<std::set<int>::iterator, bool>::first)가 사용되지 않는 진정한 예입니다 . 뭔지 맞춰봐? 그것은 최적화되어 있습니다 ( 그 어셈블리가 당신을 울게 만드는 경우 더미 세트로 더 간단한 예 ).

이제 Max Langhof의 훌륭한 답변읽을 수있는 완벽한 시간이 될 것 입니다. 결국 컴파일러가 출력하는 어셈블리 수준에서 구조 개념이 의미가없는 이유를 설명합니다.

"하지만 X를하면 사용하지 않는 멤버가 최적화되어 있다는 사실이 문제!"

일부 작업 (예 assert(sizeof(Foo2) == 2*sizeof(int)):)이 무언가를 손상시킬 수 있기 때문에이 답변이 잘못되었다고 주장하는 의견이 많이 있습니다 .

X가 프로그램 2 의 관찰 가능한 동작의 일부인 경우 컴파일러는 최적화를 허용하지 않습니다. 프로그램에 눈에 띄는 영향을 미칠 "사용되지 않은"데이터 멤버를 포함하는 객체에 대한 많은 작업이 있습니다. 이러한 작업이 수행되거나 컴파일러가 수행 된 작업이 없음을 증명할 수없는 경우 해당 "사용되지 않은"데이터 멤버는 프로그램의 관찰 가능한 동작의 일부이며 최적화 할 수 없습니다 .

관찰 가능한 동작에 영향을 미치는 작업에는 다음이 포함되지만 이에 국한되지는 않습니다.

  • 객체 유형 ( sizeof(Foo)) 의 크기를 취하고 ,
  • "미사용"뒤에 선언 된 데이터 멤버의 주소를 가져옵니다.
  • 와 같은 함수로 객체 복사 memcpy,
  • 객체의 표현을 조작 (예 :) memcmp,
  • 객체를 volatile로 한정 ,
  • .

1)

[intro.abstract]/1

이 문서의 의미 론적 설명은 매개 변수화 된 비 결정적 추상 기계를 정의합니다. 이 문서는 준수 구현의 구조에 대한 요구 사항을 지정하지 않습니다. 특히 추상 기계의 구조를 복사하거나 에뮬레이트 할 필요가 없습니다. 오히려, 아래에 설명 된 것처럼 추상 기계의 관찰 가능한 동작을 에뮬레이트하기 위해서는 준수 구현이 필요합니다.

2) 어설 션 통과 또는 실패와 같습니다.


답변에 대한 개선을 제안하는 댓글 이 채팅보관되었습니다 .
Cody Gray

1
심지어는 assert(sizeof(…)…)실제로 제한하지 않는 컴파일러가 제공 할 수있는 sizeof같은 것들을 사용하여 코드를 허용하는 memcpy작업에를하지만 그들이 이러한 노출 될 수 않는 한 컴파일러는 어떻게 든 많은 바이트를 사용하는 데 필요한 것은 아니다 memcpy는 것을 할 수 't의 재 작성 어쨌든 정확한 값을 생성합니다.
Davis Herring

@ 데이비스 절대적으로.
YSC

63

컴파일러가 생성하는 코드에는 데이터 구조에 대한 실제 지식이 없으며 (어셈블리 수준에 존재하지 않기 때문에) 옵티마이 저도 마찬가지라는 사실을 인식하는 것이 중요합니다. 컴파일러는 데이터 구조가 아닌 각 함수에 대한 코드 만 생성 합니다 .

좋아, 그것은 또한 상수 데이터 섹션 등을 씁니다.

이를 바탕으로 옵티마이 저가 데이터 구조를 출력하지 않기 때문에 멤버를 "제거"하거나 "제거"하지 않을 것이라고 이미 말할 수 있습니다. 멤버를 사용 하거나 사용 하지 않을 수있는 코드를 출력 하며, 그 중 목표는 멤버의 무의미한 사용 (즉, 쓰기 / 읽기)을 제거하여 메모리 나주기를 절약 하는 것입니다.


요점은 "컴파일러가 함수의 범위 (인라인 된 함수 포함) 내에서 사용되지 않는 멤버가 함수가 작동하는 방식 (및 반환하는 내용)에 아무런 차이가 없음을 증명할 수 있는 경우 구성원의 존재로 인해 오버 헤드가 발생하지 않습니다. "

함수와 외부 세계와의 상호 작용을 컴파일러에게 더 복잡하거나 불명확하게 만들면 (예 : 더 복잡한 데이터 구조를 가져 오거나 반환합니다. 예를 들어 std::vector<Foo>, 다른 컴파일 단위에서 함수 정의 숨기기, 인라인 금지 / 비 인센티브 화 등) , 컴파일러가 사용하지 않는 멤버가 효과가 없음을 증명할 수 없을 가능성이 점점 더 높아집니다.

컴파일러가 수행하는 최적화에 따라 다르기 때문에 여기에는 엄격한 규칙이 없습니다.하지만 YSC의 답변에 표시된 것과 같은 사소한 작업을 수행하는 한 오버 헤드가없는 반면 복잡한 작업 (예 : 반환 std::vector<Foo>인라인에 대한 너무 큰 함수로부터는) 아마도 오버 헤드가 발생합니다.


요점을 설명하기 위해 다음 예를 고려 하십시오 .

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

여기서는 사소한 일을하지 않지만 ( 바이트 표현 에서 주소를 가져오고, 검사하고, 바이트를 추가 ) 최적화 프로그램은이 플랫폼에서 결과가 항상 동일하다는 것을 알아낼 수 있습니다.

test(): # @test()
  mov eax, 7
  ret

멤버 Foo들은 어떤 기억도 차지 Foo하지 않았을뿐만 아니라 존재조차하지 않았다! 최적화 할 수없는 다른 용도가있는 경우 예를 들어 sizeof(Foo)문제가 될 수 있지만 해당 코드 세그먼트에만 해당됩니다! 모든 사용이 이와 같이 최적화 될 수 있다면 eg의 존재 var3는 생성 된 코드에 영향을 미치지 않습니다. 그러나 다른 곳에서 사용 되더라도 test()최적화 된 상태로 유지됩니다!

요약 :의 각 사용은 Foo독립적으로 최적화됩니다. 일부는 불필요한 멤버로 인해 더 많은 메모리를 사용할 수 있지만 일부는 그렇지 않을 수 있습니다. 자세한 내용은 컴파일러 설명서를 참조하십시오.


6
Mic drop "자세한 내용은 컴파일러 설명서를 참조하십시오." : D
YSC

22

컴파일러는 변수를 제거해도 부작용이없고 프로그램의 어떤 부분도 Foo동일한 크기에 의존하지 않는다는 것을 증명할 수있는 경우에만 사용되지 않는 멤버 변수 (특히 공용 변수)를 최적화 합니다.

구조가 실제로 전혀 사용되지 않는 한 현재 컴파일러가 이러한 최적화를 수행한다고 생각하지 않습니다. 일부 컴파일러는 최소한 사용하지 않는 개인 변수에 대해 경고 할 수 있지만 일반적으로 공용 변수에 대해서는 경고하지 않습니다.


1
그러나 그것은 : godbolt.org/z/UJKguS + 컴파일러는 사용하지 않는 데이터 멤버에 대해 경고하지 않습니다.
YSC

@YSC clang ++는 사용되지 않은 데이터 멤버 및 변수에 대해 경고합니다.
Maxim Egorushkin

3
@YSC 나는이 완전히 구조 멀리 최적화 약간 다른 상황, 생각 그냥 직접 5 인쇄
앨런 Birtles

4
@AlanBirtles 나는 그것이 어떻게 다른지 모르겠습니다. 컴파일러는 프로그램의 관찰 가능한 동작에 영향을 미치지 않는 개체의 모든 것을 최적화했습니다. 따라서 첫 번째 문장 "컴파일러는 사용되지 않는 멤버 변수를 최적화 할 가능성이 매우 낮습니다."는 잘못된 것입니다.
YSC

2
구조가 실제로 사용되는보다는 부작용에 대한 구성되어 실제 코드에 @YSC은 아마 더 가능성이 멀리 최적화 될 수는
앨런 Birtles에게

7

일반적으로, 예를 들어 "사용되지 않은"멤버 변수가 거기에있는 것과 같이 요청한 것을 얻는다고 가정해야합니다.

귀하의 예제에서 두 멤버가 모두이므로 public컴파일러는 일부 코드 (특히 다른 변환 단위 = 별도로 컴파일 된 다음 링크 된 다른 * .cpp 파일)가 "사용되지 않는"멤버에 액세스하는지 알 수 없습니다.

YSC의 대답은 클래스 유형이 자동 저장 기간의 변수로만 사용되고 해당 변수에 대한 포인터가 사용되지 않는 매우 간단한 예를 제공합니다. 거기에서 컴파일러는 모든 코드를 인라인 할 수 있으며 모든 데드 코드를 제거 할 수 있습니다.

다른 번역 단위로 정의 된 함수 사이에 인터페이스가있는 경우 일반적으로 컴파일러는 아무것도 알지 못합니다. 인터페이스는 일반적으로 몇 가지 (같은 ABI를 미리 팔로우 하는 다른 오브젝트 파일은 아무 문제없이 서로 연결 될 수 있음) 등. 일반적으로 ABI는 멤버의 사용 여부에 따라 차이가 없습니다. 따라서 이러한 경우 두 번째 멤버는 나중에 링커에 의해 제거되지 않는 한 물리적으로 메모리에 있어야합니다.

그리고 당신이 언어의 경계 안에있는 한, 당신은 제거가 일어나는 것을 관찰 할 수 없습니다. 전화 sizeof(Foo)하면 2*sizeof(int). 의 배열을 만드는 경우 Foo연속 된 두 객체의 시작 사이의 거리는 Foo항상 sizeof(Foo)바이트입니다.

유형은 표준 레이아웃 유형 입니다. 즉, 컴파일시 계산 된 오프셋 ( offsetof매크로 참조 )을 기반으로 멤버에 액세스 할 수도 있습니다 . 또한 charusing 배열에 복사하여 객체의 바이트 단위 표현을 검사 할 수 있습니다 std::memcpy. 이 모든 경우에 두 번째 구성원이 거기에있는 것으로 볼 수 있습니다.


의견은 확장 된 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Cody Gray

2
+1 : 공격적인 전체 프로그램 최적화 만이 로컬 구조체 객체가 완전히 최적화되지 않은 경우 데이터 레이아웃 (컴파일 시간 크기 및 오프셋 포함)을 조정할 수 있습니다. gcc -fwhole-program -O3 *.c이론적으로는 할 수 있지만 실제로는 그렇지 않을 것입니다. (예를 들어,이 경우 프로그램은 정확한 값이 무엇인지에 대한 몇 가지 가정을 만드는 sizeof()이 대상이, 그리고 그들이 원하는 경우 프로그래머가 손으로해야한다고 정말 복잡 최적화이기 때문에.)
피터 코르

6

이 질문에 대한 다른 답변에 의해 제공되는 예 var2는 단일 최적화 기술인 상수 전파 및 전체 구조의 후속 제거 (단지 제거가 var2아님)를 기반으로합니다. 이것은 간단한 경우이며 최적화 컴파일러가이를 구현합니다.

관리되지 않는 C / C ++ 코드의 경우 컴파일러가 일반적으로 var2. 내가 아는 한 디버깅 정보에서 이러한 C / C ++ 구조체 변환에 대한 지원이 없으며 구조체가 디버거에서 변수로 액세스 할 수있는 경우 제거 var2할 수 없습니다. 내가 아는 한 현재 C / C ++ 컴파일러는의 제거에 따라 함수를 전문화 할 수 var2없으므로 구조체가 인라인 var2되지 않은 함수로 전달되거나 반환 되면 제거 될 수 없습니다.

JIT 컴파일러를 사용하는 C # / Java와 같은 관리 언어의 경우 컴파일러가 var2사용 중인지 여부와 관리되지 않는 코드로 이스케이프되는지 여부를 정확하게 추적 할 수 있으므로 안전하게 제거 할 수 있습니다. 관리되는 언어에서 구조체의 물리적 크기는 프로그래머에게보고되는 크기와 다를 수 있습니다.

2019 년 C / C ++ 컴파일러는 var2전체 구조체 변수가 제거되지 않는 한 구조체에서 제거 할 수 없습니다 . var2구조체에서 를 제거하는 흥미로운 경우에 대한 대답은 아니오입니다.

일부 미래의 C / C ++ 컴파일러는 var2구조체에서 제거 될 수 있으며 컴파일러를 중심으로 구축 된 에코 시스템은 컴파일러에서 생성 된 제거 정보를 처리하도록 적응해야합니다.


1
디버그 정보에 대한 귀하의 단락은 "디버깅을 더 어렵게 만드는 경우이를 최적화 할 수 없습니다"로 요약되며 이는 명백히 잘못된 것입니다. 아니면 잘못 읽은 것입니다. 명확히 해 주시겠습니까?
Max Langhof

컴파일러가 구조체에 대한 디버그 정보를 내 보내면 var2를 제거 할 수 없습니다. (1) 구조체의 물리적 표현에 해당하지 않는 경우 디버그 정보를 방출하지 마십시오, 디버그 정보 (2) 지원 구조체 멤버 생략하고 디버그 정보 방출 : 옵션은
atomsymbol

아마도 더 일반적인 것은 Scalar Replacement of Aggregates (그리고 죽은 스토어 제거 )를 참조하는 것입니다.
Davis Herring

4

컴파일러와 최적화 수준에 따라 다릅니다.

gcc에서를 지정 -O하면 다음 최적화 플래그가 설정됩니다 .

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdceDead Code Elimination을 의미합니다 .

__attribute__((used))gcc가 정적 저장소를 사용하여 사용하지 않는 변수를 제거하는 것을 방지 하는 데 사용할 수 있습니다 .

정적 저장소가있는 변수에 연결된이 속성은 변수가 참조되지 않는 것처럼 보이더라도 변수를 내 보내야 함을 의미합니다.

C ++ 클래스 템플릿의 정적 데이터 멤버에 적용될 때 속성은 클래스 자체가 인스턴스화되면 멤버가 인스턴스화됨을 의미하기도합니다.


이는 사용되지 않는 인스턴스 별 멤버가 아닌 정적 데이터 멤버를 위한 것입니다 (전체 개체가 그렇지 않으면 최적화되지 않음). 하지만 예, 그게 중요하다고 생각합니다. BTW, 사용하지 않는 정적 변수를 제거하는 것은 GCC가 용어를 구부리지 않는 한 죽은 코드 제거 가 아닙니다 .
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.