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

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

struct Foo {
    int var1;
    int var2;

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

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



답변

“그대로 경우”규칙 황금 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) 어설 션 통과 또는 실패와 같습니다.


답변

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

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

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


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

함수와 외부 세계와의 상호 작용을 컴파일러에게 더 복잡하거나 불명확하게 만들면 (예 : 더 복잡한 데이터 구조를 가져 오거나 반환합니다. 예를 들어 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독립적으로 최적화됩니다. 일부는 불필요한 멤버로 인해 더 많은 메모리를 사용할 수 있지만 일부는 그렇지 않을 수 있습니다. 자세한 내용은 컴파일러 설명서를 참조하십시오.


답변

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

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


답변

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

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

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

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

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

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


답변

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

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

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

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

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


답변

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

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