skip to Main Content

I have a large program that’s generating object files that are much larger than I expect. My suspicion is that somewhere in the program, someone is using inefficient template metaprogramming that’s generating O(n**2) template types. Is there a command-line tool that I can use to list all of the template types that exist in an object file (.o)?

Normally I would suspect nm or objdump is the right tool for this kind of thing, but it’s not obvious to me what flags to pass to list the template types.

I’ve verified that the information is in the .o file using this simple test program:

template <typename T, typename... Ts>
    struct foo : public foo<Ts...> {};

template <>
    struct foo<int> {};

void bar() {
  foo<int, int, int, int, int, int, int, int> x;
}

Then running:

gcc -g -c test.c -o test.o && strings test.o

Outputs:

foo<int, int, int, int, int, int, int, int>
GNU C++17 13.2.0 -mtune=generic -march=x86-64 -g -fasynchronous-unwind-tables
_Z3barv
foo<int, int, int, int, int, int, int>
foo<int, int, int, int>
foo<int, int, int, int, int, int>
foo<int, int, int>
foo<int>
foo<int, int>
foo<int, int, int, int, int>
/tmp
test.cc
/tmp
test.cc
test.cc
GCC: (Debian 13.2.0-10) 13.2.0
test.cc
_Z3barv
.symtab
.strtab
.shstrtab
.text
.data
.bss
.rela.debug_info
.debug_abbrev
.rela.debug_aranges
.rela.debug_line
.debug_str
.debug_line_str
.comment
.note.GNU-stack
.rela.eh_frame

I’m looking for a command that will output foo<int>, foo<int, int>, etc. from test.o.

2

Answers


  1. Using nm is one way to do this.

    the -C flag will give a readable output that shows templates rather than the mangled names:

    nm -C myfile.o

    example output:

    myfile.o:
    0000000000000000 U Box<int>::func(int)
    ...
    
    Login or Signup to reply.
  2. Is there a command-line tool that I can use to list all of the template types that exist in an object file (.o)?

    No. But you might be able to manage anyway.

    By the time you’ve got to an object file, classes have no representation; therefore
    classes that instantiate templates have no representation. An object file is
    ready for consumption by the linker, and the linker knows nothing of classes. It knows only of public
    symbols, which are either function symbols or data symbols. When, for example,
    this C++ source file is compiled:

    $ cat example.cpp
    #include <array>
    
    template<typename T, std::size_t Dims>
    struct point {
        
        point() = default;
        
        template<typename First, typename ...Rest>
        explicit point(First f, Rest... rest)
        : _coords{static_cast<T>(f),static_cast<T>(rest)...}{}
    
        std::array<T,Dims> _coords;
        
        constexpr std::size_t size() const {
            return Dims;
        }
    };
    
    point<int,2> pi2{1,2};
    point<double,3> pd3{pi2.size() + 0.5,pi2.size() + 0.6,pi2.size() + 0.7};
    

    the public symbols in example.o will represent just the fully qualified mangled names
    of the functions and data objects that are defined by compiling the source file, these being:

    • Data objects pi2, pd3, which happen to be objects of distinct class types that both instantiate
      template struct point<T,Dims>
    • Functions which happen all to be member functions of the aforementioned classes instantiating
      template struct point<T,Dims>, which are actually called.

    If you simply nm the global symbol table of object file:

    $ nm -g example.o
    0000000000000010 B pd3
    0000000000000000 B pi2
    0000000000000000 W _ZN5pointIdLm3EEC1IdJddEEET_DpT0_
    0000000000000000 W _ZN5pointIdLm3EEC2IdJddEEET_DpT0_
    0000000000000000 W _ZN5pointIiLm2EEC1IiJiEEET_DpT0_
    0000000000000000 W _ZN5pointIiLm2EEC2IiJiEEET_DpT0_
    0000000000000000 W _ZNK5pointIiLm2EE4sizeEv
    

    you see the mangled symbols. But if you ask for them to be demangled:

    $ nm -gC example.o
    0000000000000010 B pd3
    0000000000000000 B pi2
    0000000000000000 W point<double, 3ul>::point<double, double, double>(double, double, double)
    0000000000000000 W point<double, 3ul>::point<double, double, double>(double, double, double)
    0000000000000000 W point<int, 2ul>::point<int, int>(int, int)
    0000000000000000 W point<int, 2ul>::point<int, int>(int, int)
    0000000000000000 W point<int, 2ul>::size() const
    

    it’s quite obvious that the source file1: –

    • Created data objects pd3 and pi2
    • Created objects instantiating classes point<double, 3ul> and point<int, 2ul> from a template point<T,unsigned long>,
    • because it invoked constructors point<double, 3ul>::point<double, double, double>(double, double, double) and
      point<int, 2ul>::point<int, int>(int, int)
    • and it called function point<int, 2ul>::size() const

    You cannot see that pd3 pi2 are the objects created by the constructor invocations, but you don’t need know that. The
    output ought to be sufficient for you to identify massively bloated template instantiations, provided you can cope with
    the volume of files and output you need to survey. Scripting and regexes to the rescue perhaps.


    1. Incidentally you may wonder why each constructor is listed twice in the demangled output. Actually, the "duplicates" are
      different constructors that have the same demangling, by design. You can spot that by looking carefully at
      the corresponding mangled names, _ZN5pointIdLm3EEC1IdJddEEET_DpT0_ v. _ZN5pointIdLm3EEC2IdJddEEET_DpT0_ and noting where they differ.
      For technical reasons C++ actually may emit up to three versions of a constructor/destructor.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search