sdmello
sdmello

Reputation: 429

Unused C++ static member functions/variables not optimized out

I have enabled compiler and linker optimization in the hopes of removing all unused code/data from my ARM32 executable. From my map file, I can see that the unused sections of code were indeed discarded so the optimization flags are mostly working, except for static member functions/variables of an unused class. Any ideas on how to get rid of this would be really appreciated, because this adds up to quite a bit on a resource-constrained embedded platform!

Here's an MVCE compiled using g++ 7.5. on Ubuntu 18.04

#include <string>

#include <iostream>
#include <string.h>

class unusedClass {
public:
  unusedClass() {};
  ~unusedClass() {};
  static std::string className;

  void initArray(void) {
    memset(a, 0, sizeof(a));
  }

  void printArray(void) {
    for (auto& i:a) {
      std::cout<< i << std::endl;
    }
  }

  static void printClassName(void) {
    std::cout << "This is a static member function of the class \"UNUSED CLASS\""<< std::endl;
  }

private:
  int a[1000];
};

std::string unusedClass::className = "unusedClass";

int main() {
  std::cout << "Running the Clean-up Dead Code Test" << std::endl;
  return 0;
}


Compile with optimization flags to remove unused code

g++ -Os -flto test.cpp

To check if the static member variable was compiled into the executable

readelf -a --wide a.out | awk '$4 == "OBJECT" { print }'
29: 0000000000201140    32 OBJECT  LOCAL  DEFAULT   24 _ZN11unusedClass9classNameB5cxx11E

Upvotes: 0

Views: 2416

Answers (2)

Klaus
Klaus

Reputation: 25643

That is really strange! I simplified your code example and added some variables and functions to see if the observation is valid in general.

uc.h:

#include <string>

extern std::string mymystring1;
extern std::string mymystring2;

extern int mymyx1;
extern int mymyx2;

int mymyf1();
int mymyf

uc.cpp:

#include <string>

std::string mymystring1 ="This is a very simple test what happens if fdata sections did not work!";
std::string mymystring2 ="This variable should be used";

int mymyx1 = 11111; // unused
int mymyx2 = 22222;

int mymyf1() { return 1; } // unused
int mymyf2() { return 2; }

main.cpp:

#include "uc.h"
int main() {
  std::cout << mymystring1 << std::endl;
  std::cout << mymyx1 << std::endl;
  std::cout << mymyf1() << std::endl;
  return 0;
}

Even if I compile and link with uc.cpp as a library, I get the unused string object linked in my code.

 63: 00000000004041c0    32 OBJECT  GLOBAL DEFAULT   24 _Z11mymystring1B5cxx11
 78: 00000000004041a0    32 OBJECT  GLOBAL DEFAULT   24 _Z11mymystring2B5cxx11
 90: 0000000000404070     4 OBJECT  GLOBAL DEFAULT   23 mymyx1

Looking inside the uc.o file with objdump -h uc.o

11 .data.mymyx2  00000004  0000000000000000  0000000000000000  000001d0  2**2
              CONTENTS, ALLOC, LOAD, DATA
12 .data.mymyx1  00000004  0000000000000000  0000000000000000  000001d4  2**2
              CONTENTS, ALLOC, LOAD, DATA
13 .bss._Z11mymystring2B5cxx11 00000020  0000000000000000  0000000000000000  000001e0  2**5
              ALLOC
14 .bss._Z11mymystring1B5cxx11 00000020  0000000000000000  0000000000000000  000001e0  2**5
              ALLOC

We see that the data for mymyx1 and mymyx2 are in separate data sections. String data generates bss sections? And where is the data?

OK, we take a look: objdump -s go

Contents of section .rodata:

402000 62617369 635f7374 72696e67 3a3a5f4d  basic_string::_M
402010 5f636f6e 73747275 6374206e 756c6c20  _construct null 
402020 6e6f7420 76616c69 64005468 69732069  not valid.This i
402030 73206120 76657279 2073696d 706c6520  s a very simple 
402040 74657374 20776861 74206861 7070656e  test what happen
402050 73206966 20666461 74612073 65637469  s if fdata secti
402060 6f6e7320 64696420 6e6f7420 776f726b  ons did not work
402070 21005468 69732076 61726961 626c6520  !.This variable 
402080 73686f75 6c642062 65207573 656400    should be used.

If I move the second string to another file, it is removed!

Contents of section .rodata:

402000 54686973 20697320 61207665 72792073  This is a very s
402010 696d706c 65207465 73742077 68617420  imple test what 
402020 68617070 656e7320 69662066 64617461  happens if fdata
402030 20736563 74696f6e 73206469 64206e6f   sections did no
402040 7420776f 726b2100                    t work!.  

For me this is simply a compiler/linker bug!

From J.Schulke answer:

static member variables can't just be optimized away because they could be accessed in multiple translation units. They must be compiled so that the linker knows when the same static variable is used in different places.

They must be compiled, but they can be removed in link stage, if they are never accessed. As given in the above code, functions and the integer variables are removed if not used but std::string vars are not if they are in the same file with used data. If we move each std::string variable in a separate file and linking via a static library, they are moved out. This should also be happen if we have -fdata-sections in place, but is not working here! Why? I don't know.

Upvotes: 2

Jan Schultke
Jan Schultke

Reputation: 39859

As StoryTeller pointer out, _ZN11unusedClass9classNameB5cxx11E is not a member function, but the member variable static std::string className.

static member variables can't just be optimized away because they could be accessed in multiple translation units. They must be compiled so that the linker knows when the same static variable is used in different places.

Using constexpr

If you want functions and variables to not be compiled into the executable, using constexpr instead of static often leads to this result. In your example, the class name is known at compile time, so using constexpr would be the more idiomatic solution anyways.

constexpr implies inline for functions and implies "inlinability" for variables. Since the values of constexpr variables are all known at compile time, there is no need for them to be stored in the compiled binary. This is why the compiler will optimize them out if unused, even on -O0. Here's a working example, in which the variable is not compiled: https://godbolt.org/z/bqsuTA

Note: You can only use const char * in a constexpr context, not std::string.

Edit: If you used a static const char* instead, it would also not be optimized out. Only constexpr leads to the desired result: https://godbolt.org/z/DYNk2G

Using Anonymous namespace

See the following example:

namespace {
struct unused1 {;
  static const int x;
};
}

struct unused2 {;
  static const int x;
};

const int unused1::x = 1;
const int unused2::x = 2;

Out of the two variables .long 2 will always be found in the binary, even on -O3.

unused2::x:
        .long   2

Putting a class into an anonymous namespace is akin to using the static modifier for a function and makes compilation of these unused constants unnecessary.

Warning: If you put this class into an anonymous namespace, different translation units will no longer use the same static variable, but their own copy of it!

Upvotes: 4

Related Questions