Bug 60621 - std::vector::emplace_back generates massively more code than push_back
Summary: std::vector::emplace_back generates massively more code than push_back
Status: NEW
Alias: None
Product: gcc
Classification: Unclassified
Component: libstdc++ (show other bugs)
Version: 4.7.2
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: missed-optimization
Depends on:
Blocks: std::vector
  Show dependency treegraph
 
Reported: 2014-03-22 23:32 UTC by Marc Mutz
Modified: 2026-06-19 05:00 UTC (History)
4 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2026-06-18 00:00:00


Attachments
Source of the programme used to generate the mentioned numbers (212 bytes, text/x-c++src)
2014-03-22 23:32 UTC, Marc Mutz
Details
New version of the test programme. (250 bytes, text/x-c++src)
2015-02-11 11:13 UTC, Marc Mutz
Details
New version of marc's code (323 bytes, text/x-csrc)
2015-07-22 13:53 UTC, julien.blanc
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Marc Mutz 2014-03-22 23:32:26 UTC
Created attachment 32429 [details]
Source of the programme used to generate the mentioned numbers

Compiling the attached program on Linux AMD64 with the following command lines:

    $ g++ -O2 -std=c++11 -o emplace-vs-push_back{.pb,.cpp}
    $ g++ -O2 -std=c++11 -o emplace-vs-push_back{.eb,.cpp} -DEMPLACE_BACK

and stripping the resulting executables:

    $ strip emplace-vs-push_back.*

I get the following sizes:

    $ size emplace-vs-push_back.*
       text    data     bss     dec     hex filename
       5570     696      40    6306    18a2 emplace-vs-push_back.eb
       4338     672      40    5050    13ba emplace-vs-push_back.pb

IOW: the emplace_back version generates roughly 1K more text (code).

This is surprising, since functionally, emplace_back is the same as push_back(S&&), except that it saves one move ctor and one dtor call due to in-place construction. This should result in _less_ code generated, not more.

   $ g++ -v
   Using built-in specs.
   COLLECT_GCC=g++
   COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.7/lto-wrapper
   Target: x86_64-linux-gnu
   Configured with: ../src/configure -v --with-pkgversion='Debian 4.7.2-5' --with-bugurl=file:///usr/share/doc/gcc-4.7/README.Bugs --enable-languages=c,c++,go,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.7 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.7 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --with-arch-32=i586 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
   Thread model: posix
   gcc version 4.7.2 (Debian 4.7.2-5)
Comment 1 Marc Glisse 2014-03-23 08:24:07 UTC
Some things that help:
-fabi-version=0
-fwhole-program (so it knows emplace_back won't be used anywhere else, and it can inline it and remove the unneeded paths)
Comment 2 Marc Mutz 2014-03-23 11:26:46 UTC
Yes, that helps a bit, but emplace_back still generates larger code than the corresponding rvalue-push_back. Considering that the latter also needs to generate the implicitly defined move ctor for S, this is still somewhat surprising and runs counter to the motivation to have emplace_back in the first place.
Comment 3 Marc Mutz 2015-02-11 11:12:46 UTC
Now, what is _really_ weird is that push_back(T&&) _calls_ emplace_back(). I also tried the magic incantation

   g++ --param large-unit-insns=100000000 \
       --param inline-unit-growth=100000000 \
       --param max-inline-insns-single=100000000 \
       --param large-function-growth=100000000 \
       --param large-function-insns=100000000 -O2

to no avail. I can get the two version to within 80 bytes of text of each other by adding -fno-exceptions, so it's probably related to that. The (implicit) move ctor of S cannot throw, but the std::string(const char*) ctor can. Ie. in the rvalue-push_back case, emplace_back only dabbles in noexcept operations, and in the 3xconst char* case, it needs to deal with three throwing ctors.

I can reduce the text size to within a few hundreds of bytes by marking both emplace_back and _M_emplace_back_aux as __attribute__((always_inline)), so something prevents gcc from inlining even when turning the inlining paramters all the way up.

I can also reduce the text size by passing std::strings instead of conat char*s:

   text    data     bss     dec     hex filename
   5628     672      40    6340    18c4 emplace-vs-push_back.eb
   4991     672      40    5703    1647 emplace-vs-push_back.nt
   4516     648      40    5204    1454 emplace-vs-push_back.pb

(where .nt is EMPLACE_BACK_NOTHROW). Still a large gap...

Have we accepted another auto_ptr into the standard? :)
Comment 4 Marc Mutz 2015-02-11 11:13:54 UTC
Created attachment 34723 [details]
New version of the test programme.
Comment 5 julien.blanc 2015-07-22 13:52:40 UTC
Testing a bit, it really looks like the issue resides in how and where the temporary string objects are created.

Changing marc’s code to have

struct S {
   S(const char* a, const char * b, const char *c);
};

makes it reverting back to only 0.3k more text (which can be explained because two emplace_back function instanciation are needed vs one), and better insertion performance (insertion time is worse otherwise, which breaks emplace_back purpose).

The same goes if strings are constructed before being passed to S constructor.

(see new attachment).

Looks like an optimizer issue to me.

(note : tested with gcc 4.9.2)
Comment 6 julien.blanc 2015-07-22 13:53:36 UTC
Created attachment 36032 [details]
New version of marc's code
Comment 7 Marc Mutz 2016-07-07 08:04:05 UTC
Retesting with GCC 6.1, it looks better now:

  $ g++ -O2 -o emplace-vs-push_back{.pb,.cpp}
  $ g++ -O2 -o emplace-vs-push_back{.eb,.cpp} -DEMPLACE_BACK
  $ strip emplace-vs-push_back.*
  $ size emplace-vs-push_back.*
   text    data     bss     dec     hex filename
   4474     680       8    5162    142a emplace-vs-push_back.eb
   4830     680       8    5518    158e emplace-vs-push_back.nt
   5083     656       8    5747    1673 emplace-vs-push_back.pb

somewhat at the expense of pessimising push_back(), which used to be 500b smaller in Comment 3, but at least the relation between emplace_back and push_back, and between emplace_back(char[2], char[2], char[2]) and emplace_back(std::string, std::string, std::string) are now as expected.
Comment 8 Jonathan Wakely 2016-07-12 11:19:14 UTC
Using the code in comment 6, with 4.9.3, 5.3.0, 6.1.0 and recent 7.0 trunk:

   text    data     bss     dec     hex filename
   5606     696      40    6342    18c6 493.eb
   4943     696      40    5679    162f 493.nt
   4476     672      40    5188    1444 493.pb
   4609     676       4    5289    14a9 530.eb
   4881     704       8    5593    15d9 530.nt
   4996     652       4    5652    1614 530.pb
   4527     704       8    5239    1477 610.eb
   4729     704       8    5441    1541 610.nt
   4974     680       8    5662    161e 610.pb
   4960     704       8    5672    1628 700.eb
   5037     696       8    5741    166d 700.nt
   5234     672       8    5914    171a 700.pb
Comment 9 Jan Hubicka 2024-12-15 16:01:38 UTC
With recent changes to std::string (including not yet reviewed https://gcc.gnu.org/pipermail/gcc-patches/2024-December/671599.html)
and std::vector we now get:

jh@ryzen3:~> ~/trunk-install2/bin/g++ -O2 -std=c++11 empl2.C  ; size   a.out 
   text    data     bss     dec     hex filename
   3792     656       8    4456    1168 a.out
jh@ryzen3:~> ~/trunk-install2/bin/g++ -O2 -std=c++11 empl2.C -DEMPLACE_BACK ; size   a.out 
   text    data     bss     dec     hex filename
   5095     680       8    5783    1697 a.out

note that text size includes also EH tables. with emplace back we now get:

int main ()
{
  void * D.46214;
  struct S * const vs$8;
  struct S * const vs$D40641$_M_impl$D39953$_M_start;
  ptrdiff_t __dif;
  const char * c;
  const char * b;
  const char * a;
  struct vector vs;
  int _7;
  long int _15;
  void * _66;

  <bb 2> [local count: 1073741824]:
  MEM[(struct _Vector_impl_data *)&vs] ={v} {CLOBBER(bob)};
  MEM[(struct _Vector_impl_data *)&vs]._M_end_of_storage = 0B;
  a = "a";
  b = "b";
  c = "c";
  MEM <vector(2) long unsigned int> [(struct vector *)&vs] = { 0, 0 };
  std::vector<S>::_M_realloc_append<const char*&, const char*&, const char*&> (&vs, &a, &b, &c);

  <bb 3> [local count: 1073741824]:
  vs$D40641$_M_impl$D39953$_M_start_143 = MEM <struct S * const> [(struct vector *)&vs];
  vs$8_144 = MEM <struct S * const> [(struct vector *)&vs + 8B];
  _15 = vs$8_144 - vs$D40641$_M_impl$D39953$_M_start_143;
  __dif_16 = _15 /[ex] 96;
  _7 = (int) __dif_16;
  std::vector<S>::~vector (&vs);
  vs ={v} {CLOBBER(eos)};
  a ={v} {CLOBBER(eos)};
  b ={v} {CLOBBER(eos)};
  c ={v} {CLOBBER(eos)};
  return _7;

  <bb 4> [count: 0]:
<L3>:
  std::vector<S>::~vector (&vs);
  _66 = __builtin_eh_pointer (2);
  __builtin_unwind_resume (_66);

}

So _M_realloc_apped is offlined and quite large since it constructs strings and we do not know that the strings fits to local buffer.

Without emplace back everything gets inlined.  The main difference is that here the construction happens in main().

Now inlining is limited since we know that main is called once.  Modifying testcase:

jh@ryzen3:~> cat empl2.C
#include <vector>
#include <string>

struct S {
#ifdef USE_CHAR
    S(const char*a, const char*b, const char*c)
#else
    S(std::string const&a, std::string const&b, std::string const &c)
#endif
        : a(a), b(b), c(c) {}
    std::string a, b, c;
};

int main2() {
    std::vector<S> vs;
#ifdef USE_STRING
        std::string a("a"),b("b"),c("c");
#else
        char const* a = "a", *b = "b", *c = "c";
#endif
#ifdef EMPLACE_BACK
    vs.emplace_back(a, b, c);
#elif defined(EMPLACE_BACK_NOTHROW)
                    vs.emplace_back(std::string(a), std::string(b), std::string(c));
#else
    vs.push_back(S{a, b, c});
#endif
    return vs.size();
}

int main()
{
        return main2();
}

I get:
int main2 ()
{
  <bb 2> [local count: 1073741824]:
  return 1;
    
}       
int main ()
{
  <bb 2> [local count: 1073741824]:
  return 1;

}

which is as small as it can get :)
With emplace_back we only get everything inlined and otimized if --param max-inline-insns-auto=160 is used. Default is 15 for -O2 and 30 for -O3.

Inline summary is:
IPA function summary for void std::vector<_Tp, _Alloc>::_M_realloc_append(_Args&& ...) [with _Args = {const char*&, const char*&, const char*&}; _Tp = S; _Alloc = std::allocator<S>]/760 inlinable
  global time:     840.049387
  self size:       81
  global size:     309
  min size:       288
  self stack:      123
  global stack:    123
  estimated growth:4
    size:167.500000, time:388.798296
    size:3.000000, time:2.000000,  executed if:(not inlined)
    size:3.000000, time:3.000000,  executed if:(op0 not sra candidate) && (not inlined)
    size:3.000000, time:3.000000,  executed if:(op0 not sra candidate)
    size:0.500000, time:0.500000,  executed if:(op3 not sra candidate) && (not inlined)
    size:0.500000, time:0.500000,  executed if:(op3 not sra candidate)
    size:0.500000, time:0.500000,  executed if:(op2 not sra candidate) && (not inlined)
    size:0.500000, time:0.500000,  executed if:(op2 not sra candidate)
    size:0.500000, time:0.500000,  executed if:(op1 not sra candidate) && (not inlined)
    size:0.500000, time:0.500000,  executed if:(op1 not sra candidate)
    size:0.500000, time:0.500000,  executed if:(op0 not sra candidate),  nonconst if:(op0[ref offset: 64] changed) && (op0 not sra candidate)
    size:0.500000, time:0.500000,  executed if:(op0 not sra candidate),  nonconst if:(op0[ref offset: 0] changed) && (op0 not sra candidate)
    size:8.000000, time:8.000000,  nonconst if:(op0[ref offset: 64] changed || op0[ref offset: 0] changed)

Getting size down from 160 to 15 will be quite some work.  I think to do that we need to understand that lengths of strings are known. I.e. at IPA-prop time understand that:

  _5 = __builtin_strlen (__s_7(D));
  std::__cxx11::basic_string<char>::_M_construct<true> (this_3(D), __s_7(D), _5);

will be constant and since it is smaller than 15 bytes the constructor will optimize.  This will need extension of jump functions.

Also at the moment _M_create is not instantiated (see PR94960) which prevents additional propagation druing early opts.