Bug 92385 - extremely long and memory intensive compilation for brace construction of array member
Summary: extremely long and memory intensive compilation for brace construction of arr...
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 7.4.0
: P3 normal
Target Milestone: 12.0
Assignee: Jason Merrill
URL:
Keywords: compile-time-hog, memory-hog
: 65503 65591 71165 87680 94957 109421 (view as bug list)
Depends on:
Blocks:
 
Reported: 2019-11-05 21:23 UTC by Carl
Modified: 2023-06-25 00:10 UTC (History)
6 users (show)

See Also:
Host:
Target:
Build:
Known to work: 12.0
Known to fail:
Last reconfirmed: 2019-11-06 00:00:00


Attachments
preprocessed source that exhibits bad compilation performance (173 bytes, text/plain)
2019-11-05 21:23 UTC, Carl
Details
original source file with bad compilation performance (106 bytes, text/plain)
2019-11-05 21:25 UTC, Carl
Details
source that does not exhibit bad compilation performance (105 bytes, text/plain)
2019-11-05 21:26 UTC, Carl
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Carl 2019-11-05 21:23:05 UTC
Created attachment 47178 [details]
preprocessed source that exhibits bad compilation performance

Greetings!

I stumbled upon strange compiler-performance behavior when initializing an array member of non-POD type in a struct constructor with the (C++11) brace-initialization syntax, rather than with parens.

For example, if you have the following types:

        struct item {
                int i;
                item();
        };

        struct item_array {
                item a[SIZE];
                item_array();
        };

I am referring to defining the item_array constructor as:

        item_array::item_array() : a{} {}

Instead of:

        item_array::item_array() : a() {}


In the case with the brace initializer, especially if SIZE is non-trivial (eg, 100s of thousands), the compiler takes an inordinately long time and much memory to compile a trivial piece of code, where with the parens instead it compiles instantly and with very little memory used.

I've boiled it down to a very simple program, which does not #include any headers.  The (not attached) source file "good.cpp" uses the parens, and compiles instantly, where the (not attached) "bad.cpp", which differs only in that it uses brace initialization rather than parens, takes 58 seconds to compile, with the max resident set size of g++ at nearly 3GB.  (This is with SIZE=512*1024)

(I guess I can only attach a single file, and that is supposed to be the preprocessed "bad.ii", so that's the one attached.)

If I increase the array length from 512k to 1024k (which has 4MB total size, with sizeof (int) == 4), then g++ runs furiously for some time and eventually crashes.

Again, with parens instead of braces, the problem goes away.  But I have no idea why this would be, as I thought the behavior would be equivalent.


Is this (kind of terrible) performance/behavior expected?

Thanks!
Carl


Bug report details:


Compile with:

        g++ -O3 -Wall -std=c++11  bad.cpp -o bad

I've also tried compiling with and without the options mentioned on the "Reporting Bugs" page (-Wall -Wextra -fno-strict-aliasing -fwrapv -fno-aggressive-loop-optimizations -fsanitize=undefined) as well as with and without optimization (-O3), none of which cause the bad compilation performance to go away.

When it does compile though, it compiles without any output, even with "-Wall -Wextra".

If I collect the "time" and rusage data from the above compilation step, I get:

        rtime: 58.421
        utime: 57.607
        stime: 0.798
        maxrss: 2925112k


Compared to compiling "good.cpp" (which only changes "a{}" to "a()"), I get:

        rtime: 0.076
        utime: 0.047
        stime: 0.007
        maxrss: 23120k


I've also compiled with "-save-temps" and am attaching the requested (*.i*) preprocessed file, which is "bad.ii".


Version detail:

$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
Comment 1 Carl 2019-11-05 21:25:16 UTC
Created attachment 47179 [details]
original source file with bad compilation performance

Note the brace member initialization in the constructor: "item_array() : a{} {}"
Comment 2 Carl 2019-11-05 21:26:39 UTC
Created attachment 47181 [details]
source that does not exhibit bad compilation performance

Note the constructor initializes the member with parens: "item_array() : a() {}"
Comment 3 Carl 2019-11-05 21:37:47 UTC
Original "good.cpp" and "bad.cpp" sources now attached.
Comment 4 Richard Biener 2019-11-06 08:35:58 UTC
It's yet another case where the C++ FE should emit loops rather than
individual array element initializations.

;; Function item_array::item_array() (null)
;; enabled by -tree-original


<<cleanup_point <<< Unknown tree: expr_stmt
  *(struct
  {
    struct item a[524288];
  } &) this = {CLOBBER} >>>>>;
{
  <<cleanup_point <<< Unknown tree: expr_stmt
  (void) (((struct item_array *) this)->a = {TARGET_EXPR <D.2340, <<< Unknown tree: aggr_init_expr
  4
  __ct_comp
  D.2340
  (struct item *) <<< Unknown tree: void_cst >>> >>>>, TARGET_EXPR <D.2350, <<< Unknown tree: aggr_init_expr
  4
  __ct_comp
  D.2350
  (struct item *) <<< Unknown tree: void_cst >>> >>>>, TARGET_EXPR <D.2351, <<< Unknown tree: aggr_init_expr
...
 2 million lines later
...
  D.526635
  (struct item *) <<< Unknown tree: void_cst >>> >>>>, TARGET_EXPR <D.526636, <<< Unknown tree: aggr_init_expr
  4
  __ct_comp
  D.526636
  (struct item *) <<< Unknown tree: void_cst >>> >>>>}) >>>>>;

also probably not too intelligent individual elements either.
Comment 5 Richard Biener 2019-11-06 08:36:42 UTC
Whereas with ():

;; Function item_array::item_array() (null)
;; enabled by -tree-original


<<cleanup_point <<< Unknown tree: expr_stmt
  *(struct
  {
    struct item a[524288];
  } &) this = {CLOBBER} >>>>>;
{
  <<cleanup_point <<< Unknown tree: expr_stmt
  (void) (((struct item_array *) this)->a = <<< Unknown tree: vec_init_expr
  D.2341
   >>>) >>>>>;
}

I'm not sure if () and {} are semantically equivalent [in this case].
Comment 6 Carl 2019-11-06 19:03:29 UTC
> I'm not sure if () and {} are semantically equivalent [in this case].

For what it's worth, (not sure if I'm allowed to paste a link here, but) on cppreference.com [1] under "Constructors and member initializer lists", it describes the following forms for "member-initializers" in "the body of a function definition of any constructor" :

    class-or-identifier ( expression-list(optional) )   (1) 	
    class-or-identifier brace-init-list                 (2) (since C++11)

And about these it says:

    1) Initializes the base or member named by class-or-identifier
       using direct initialization or, if expression-list is empty,
       value-initialization

    2) Initializes the base or member named by class-or-identifier
       using list-initialization (which becomes value-initialization
       if the list is empty and aggregate-initialization when
       initializing an aggregate)

So it seems in this case of "a()" (1) vs "a{}" (2), either one results in "value-initialization", which is further described here [2].

The following forms are described for value-initialization:

    T()  (1)
    T{}  (5)  (since C++11)

About which it says:

    1,5) when a nameless temporary object is created with the
         initializer consisting of an empty pair of parentheses
         [or braces (since C++11)];

But then it goes on to say:

    In all cases, if the empty pair of braces {} is used and T is
    an aggregate type, aggregate-initialization is performed instead
    of value-initialization.

(And notably, an array is an aggregate type.)

Then the page on aggregate-initialization[3] goes on to describe the many semantic effects of aggregate initialization...  The part that seems to apply is

    If the number of initializer clauses is less than the number
    of members or initializer list is completely empty, the
    remaining members are initialized by empty lists, in accordance
    with the usual list-initialization rules (which performs
    value-initialization for non-class types and non-aggregate
    classes with default constructors, and aggregate initialization
    for aggregates).   (since C++11)


I presume our "struct item" counts as a "non-aggregate class with default constructor", which would result in value-initialization, but my lawyer glasses are a little fuzzy here.


Whereas, the way I read the value-initialization rules, if "()" is used (that is, not "the empty pair of braces {}"), then this rule applies:

    3) if T is an array type, each element of the array is
       value-initialized;

Which means that for every element of the array this applies:

    1) if T is a class type with [...] a user-provided [...] constructor,
       the object is default-initialized;


So, a bit round-about, but it seems like both routes would result in value-initialization, which ends up meaning each element gets default-initialized.

But you have to squint real hard, and the rules about how to get there seem to follow surprisingly different-looking paths...  Maybe that (partially) explains the different code-paths in g++.

Carl


[1] https://en.cppreference.com/w/cpp/language/initializer_list
[2] https://en.cppreference.com/w/cpp/language/value_initialization
[3] https://en.cppreference.com/w/cpp/language/aggregate_initialization
Comment 7 Andrew Pinski 2021-11-25 23:52:44 UTC
*** Bug 94957 has been marked as a duplicate of this bug. ***
Comment 8 Andrew Pinski 2021-11-25 23:53:06 UTC
*** Bug 71165 has been marked as a duplicate of this bug. ***
Comment 9 GCC Commits 2022-01-07 00:24:26 UTC
The master branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:e948436eab818c527dd60b0ef939c4f42fbe8ba4

commit r12-6326-ge948436eab818c527dd60b0ef939c4f42fbe8ba4
Author: Jason Merrill <jason@redhat.com>
Date:   Sat Jan 1 16:00:09 2022 -0500

    c++: loop over array elts w/o explicit init [PR92385]
    
    The PR complains that initializing a large array with {} takes a long time
    to compile; this was because digest_init would turn {} into a long
    CONSTRUCTOR with an initializer for each element, instead of more sensibly
    generating a loop.  The standard doesn't specify this implementation, but it
    does allow for it by specifying that a temporary created "when a default
    constructor is called to initialize an element of an array with no
    corresponding initializer" is destroyed "before the construction of the next
    array element, if any." rather than living until the end of the complete
    object initialization as usual.
    
    This change is also needed before the PR94041 fix extends the lifetime of
    temporaries from elements with explicit initializers.
    
    To implement this, I change digest_init so that in cases where
    initialization of trailing array elements isn't constant, we return a
    VEC_INIT_EXPR instead of a bare CONSTRUCTOR; when it is encountered later,
    we call build_vec_init to generate the actual initialization code.
    
            PR c++/92385
    
    gcc/cp/ChangeLog:
    
            * typeck2.c (PICFLAG_VEC_INIT): New.
            (process_init_constructor_array): Set it.
            (process_init_constructor): Handle it.
            (split_nonconstant_init_1): Handle VEC_INIT_EXPR.
            * init.c (build_vec_init): Likewise.
            * cp-gimplify.c (cp_gimplify_expr): Factor out...
            * tree.c (expand_vec_init_expr): ...this function.
            (build_vec_init_elt): Handle BRACE_ENCLOSED_INITIALIZER_P.
            (build_vec_init_expr): Likewise.
            * constexpr.c (cxx_eval_vec_init): Likewise.
            (reduced_constant_expression_p): Check arrays before C++20.
            * cp-tree.h (expand_vec_init_expr): Declare.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/init/array61.C: New test.
Comment 10 Andrew Pinski 2022-01-07 00:46:28 UTC
*** Bug 87680 has been marked as a duplicate of this bug. ***
Comment 11 Andrew Pinski 2022-01-07 00:48:26 UTC
*** Bug 65503 has been marked as a duplicate of this bug. ***
Comment 12 Jason Merrill 2022-01-07 02:56:14 UTC
*** Bug 65591 has been marked as a duplicate of this bug. ***
Comment 13 Jason Merrill 2022-01-28 04:33:30 UTC
Fixed for GCC 12.
Comment 14 GCC Commits 2022-02-05 05:57:33 UTC
The master branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:119cea98f664764cce04963243c39c8f6d797d33

commit r12-7069-g119cea98f664764cce04963243c39c8f6d797d33
Author: Jason Merrill <jason@redhat.com>
Date:   Wed Feb 2 18:36:41 2022 -0500

    c++: assignment, aggregate, array [PR104300]
    
    The PR92385 fix meant that we see more VEC_INIT_EXPR outside of INIT_EXPR;
    in such cases, we need to wrap them in TARGET_EXPR.  I previously fixed
    that in build_array_copy; we also need it in process_init_constructor.
    After fixing that, I needed to adjust a few places to recognize the
    VEC_INIT_EXPR even inside a TARGET_EXPR.  And prevent cp_fully_fold_init
    from lowering VEC_INIT_EXPR too soon.  And handle COMPOUND_EXPR inside
    TARGET_EXPR better.
    
            PR c++/104300
            PR c++/92385
    
    gcc/cp/ChangeLog:
    
            * cp-tree.h (get_vec_init_expr): New.
            (target_expr_needs_replace): New.
            * cp-gimplify.cc (cp_gimplify_init_expr): Use it.
            (struct cp_fold_data): New.
            (cp_fold_r): Only genericize inits at end of fn.
            (cp_fold_function): Here.
            (cp_fully_fold_init): Not here.
            * init.cc (build_vec_init): Use get_vec_init_expr.
            * tree.cc (build_vec_init_expr): Likewise.
            * typeck2.cc (split_nonconstant_init_1): Likewise.
            (process_init_constructor): Wrap VEC_INIT_EXPR in
            TARGET_EXPR.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp0x/initlist-array14.C: New test.
Comment 15 GCC Commits 2022-04-09 03:27:48 UTC
The master branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:4822108e61ab879067482704f2f7d1670813d61a

commit r12-8066-g4822108e61ab879067482704f2f7d1670813d61a
Author: Jason Merrill <jason@redhat.com>
Date:   Fri Apr 8 15:33:41 2022 -0400

    c++: constexpr non-trivial aggregate init [PR105191]
    
    My patch for PR92385 made us use VEC_INIT_EXPR for aggregate initialization
    of an array where some elements are not explicitly initialized.  Constexpr
    handling of that was treating initialization from {} as equivalent to
    value-initialization, which is problematic for classes with default member
    initializers that make the default constructor non-trivial; in older
    standard modes, not initializing all members makes a constructor
    non-constexpr, but aggregate initialization is fine.
    
            PR c++/105191
            PR c++/92385
    
    gcc/cp/ChangeLog:
    
            * tree.cc (build_vec_init_elt): Do {}-init for aggregates.
            * constexpr.cc (cxx_eval_vec_init): Only treat {} as value-init
            for non-aggregate types.
            (build_vec_init_expr): Also check constancy of explicit
            initializer elements.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp0x/constexpr-array28.C: New test.
Comment 16 GCC Commits 2022-05-15 16:28:25 UTC
The master branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:ce46d6041358052dfa26f3720732f0357c5d72e7

commit r13-463-gce46d6041358052dfa26f3720732f0357c5d72e7
Author: Jason Merrill <jason@redhat.com>
Date:   Fri May 13 16:07:10 2022 -0400

    c++: array {}-init [PR105589]
    
    My patch for 105191 made us use build_value_init more frequently from
    build_vec_init_expr, but build_value_init doesn't like to be called to
    initialize a class in a template.  That's caused trouble in the past, and
    seems like a strange restriction, so let's fix it.
    
            PR c++/105589
            PR c++/105191
            PR c++/92385
    
    gcc/cp/ChangeLog:
    
            * init.cc (build_value_init): Handle class in template.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp0x/initlist-array16.C: New test.
Comment 17 GCC Commits 2022-05-15 16:29:40 UTC
The releases/gcc-12 branch has been updated by Jason Merrill <jason@gcc.gnu.org>:

https://gcc.gnu.org/g:40f749b364b740f41ea6b211f81c21919a2e8bee

commit r12-8380-g40f749b364b740f41ea6b211f81c21919a2e8bee
Author: Jason Merrill <jason@redhat.com>
Date:   Fri May 13 16:07:10 2022 -0400

    c++: array {}-init [PR105589]
    
    My patch for 105191 made us use build_value_init more frequently from
    build_vec_init_expr, but build_value_init doesn't like to be called to
    initialize a class in a template.  That's caused trouble in the past, and
    seems like a strange restriction, so let's fix it.
    
            PR c++/105589
            PR c++/105191
            PR c++/92385
    
    gcc/cp/ChangeLog:
    
            * init.cc (build_value_init): Handle class in template.
    
    gcc/testsuite/ChangeLog:
    
            * g++.dg/cpp0x/initlist-array16.C: New test.
Comment 18 Andrew Pinski 2023-04-05 17:25:40 UTC
*** Bug 109421 has been marked as a duplicate of this bug. ***