Bug 119153 - Static storage for initializer_list no longer shares with array literals
Summary: Static storage for initializer_list no longer shares with array literals
Status: NEW
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 14.1.0
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: missed-optimization
Depends on:
Blocks:
 
Reported: 2025-03-07 01:49 UTC by Zhihao Yuan
Modified: 2026-05-05 22:47 UTC (History)
3 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2025-09-02 00:00:00


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Zhihao Yuan 2025-03-07 01:49:02 UTC
Consider

    void f(std::initializer_list<int> il);
    
    template <std::size_t N> void g(int const (&il)[N]);
    
    void t()
    {
        f({3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5});
        g({3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5});
    }

In GCC 14, after implementing P2752, two redundant copies of rodata are emitted, one for copying to the stack to call g, and one passed to f.

https://godbolt.org/z/1WGjb6dvq
Comment 1 Drea Pinski 2025-03-07 02:12:05 UTC
Note -fmerge-all-constants still produces decent code.

Also note the constant might be duplicated, it is still better than before. There is just a small data section which has been duplicated vs runtime copying which was happening before.  So I am not sure this could be called a regression.
Comment 2 Zhihao Yuan 2025-03-07 02:42:08 UTC
Regression or not, if -fmerge-all-constants is acceptable, P2752 wouldn't exist:

https://godbolt.org/z/TTfbj7jjE
Comment 3 Drea Pinski 2025-09-02 05:17:30 UTC
Confirmed. There are 2 different "constant" pools inside GCC. At least on aarch64 the 2 constants could be the same.
There will always be one copy on the stack though. because you could have:
```
    template <std::size_t N> void g( int const (&il)[N], std::initializer_list<int>);
    
    void t()
    {
        g({3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}, {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5});
    }
```

So the first argument and the address of the initializer_list's data can't be the same.
Comment 4 Arthur O'Dwyer 2026-05-05 18:58:55 UTC
"There will always be one copy on the stack though" — Agreed (since P2752 doesn't extend to array literals). But a sufficiently smart compiler *could* observe that the array temporary on the stack can be initialized *from* the unrelated initializer_list's backing array in rodata; we don't need to keep a whole nother copy of the data just to initialize the array temporary from.

This is complicated in practice because GCC chunks up the array temporary's initializer into a series of 16-byte values stored in .rodata.cst16 — it can do this because they're loaded onto the stack one by one — whereas the initializer_list's backing array cannot be chunked up like that. Chunking up the array's initializer into .rodata.cst16 is a good idea because it allows the compiler and linker to deduplicate repeated chunks, as shown here:

// https://godbolt.org/z/3GfsP99EP
void f(std::initializer_list<int> il);
template <std::size_t N> void g(int const (&&il)[N]);
void t() {
    f({3, 1, 4, 1, 3, 1, 4, 1});
    g({3, 1, 4, 1, 3, 1, 4, 1});
}

That code (on GCC 16) puts C.0.0={3,1,4,1,3,1,4,1} in .rodata.cst32 as the backing array of the initializer_list, and puts .LC0={3,1,4,1} in .rodata.cst16 as the only chunk we need in order to initialize the array temporary. A sufficiently smart GCC could figure out that the latter *could* be a pointer into the former; but I bet that requires several kinds of smarts that GCC doesn't currently have and that would be annoying to implement.

If I were trying to make this issue sound like a big deal, I'd leave the "array temporary" part out of it and simply give a test case like this one:

// https://godbolt.org/z/Yb9qaYY8Y
void f1(std::initializer_list<int> il);
void f2(std::initializer_list<unsigned> il);
void t() {
    f1({3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1});
    f2({3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1});
}

GCC 13 would put .LC0={3,1,4,1} into .rodata.cst16, and do a bunch of loads from there onto the stack for both f1 and f2. GCC 16 puts C.0.0={3,1,4,1,3,1,4,1,3,1,4,1,3,1,4,1} into .rodata, and then again puts C.1.1={3u,1u,4u,1u,3u,1u,4u,1u,3u,1u,4u,1u,3u,1u,4u,1u} into .rodata; GCC 16 is not smart enough to merge these because the types differ, and the linker can't merge them either because they're not in an SHF_MERGE section. So while GCC 16 avoids the runtime cost and stack-blowing risk (thank you!), and we must expect some tradeoff in rodata size as a result, the tradeoff in this specific case is costlier than I wish it were.
Comment 5 Drea Pinski 2026-05-05 22:47:18 UTC
(In reply to Arthur O'Dwyer from comment #4)
> // https://godbolt.org/z/Yb9qaYY8Y
> void f1(std::initializer_list<int> il);
> void f2(std::initializer_list<unsigned> il);
> void t() {
>     f1({3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1});
>     f2({3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1, 3, 1, 4, 1});
> }
> 

I filed PR 125196 for that.