Чрезмерно нетерпеливая инициализация нулями C++ union с constexpr
Ниже приведен сокращенный пример помеченного шаблона объединения "Хранение", который может предполагать два типа L и R, заключенные в объединение, плюс логическое значение, указывающее, какой из них сохраняется. При создании экземпляра используются два типа разного размера, причем меньший фактически пустой.
#include <utility>
struct Empty
{
};
struct Big
{
long a;
long b;
long c;
};
template<typename L, typename R>
class Storage final
{
public:
constexpr explicit Storage(const R& right) : payload{right}, isLeft{false}
{
}
private:
union Payload
{
constexpr Payload(const R& right) : right{right}
{
}
L left;
R right;
};
Payload payload;
bool isLeft;
};
// Toggle constexpr here
constexpr static Storage<Big, Empty> createStorage()
{
return Storage<Big, Empty>{Empty{}};
}
Storage<Big, Empty> createStorage2()
{
return createStorage();
}
- Конструктор инициализирует R-член с помощью Empty и вызывает только конструктор объединения для этого члена.
- Объединение никогда не инициализируется по умолчанию в целом
- Все конструкторы constexpr
Поэтому функция createStorage2 должна заполнять только тег bool и оставлять объединение в покое. Поэтому я ожидал бы результата компиляции с оптимизацией по умолчанию "-O":
createStorage2():
mov rax, rdi
mov BYTE PTR [rdi+24], 0
ret
И GCC, и ICC вместо этого генерируют что-то вроде
createStorage2():
mov rax, rdi
mov QWORD PTR [rdi], 0
mov QWORD PTR [rdi+8], 0
mov QWORD PTR [rdi+16], 0
mov QWORD PTR [rdi+24], 0
ret
обнуление всей 32-байтовой структуры, в то время как clang генерирует ожидаемый код. Вы можете воспроизвести это с помощью https://godbolt.org/z/VsDQUu. GCC вернется к желаемой инициализации тега bool только тогда, когда вы удалите constexpr из статической функции createStorage, в то время как ICC останется без впечатлений и все равно заполнит все 32 байта.
Это, вероятно, не является стандартным нарушением, поскольку неиспользуемые биты, являющиеся "неопределенными", позволяют все, в том числе обнуление и использование ненужных циклов процессора. Но досадно, если вы ввели профсоюз в первую очередь из соображений эффективности, а члены вашего профсоюза сильно различаются по размеру.
Что здесь происходит? Можно ли каким-либо образом обойти это поведение при условии, что удаление constexpr из конструкторов и статической функции не является вариантом?
Примечание: кажется, что ICC выполняет некоторые дополнительные операции, даже когда все constexpr удалены, как в https://godbolt.org/z/FnjoPC:
createStorage2():
mov rax, rdi #44.16
mov BYTE PTR [-16+rsp], 0 #39.9
movups xmm0, XMMWORD PTR [-40+rsp] #44.16
movups xmm1, XMMWORD PTR [-24+rsp] #44.16
movups XMMWORD PTR [rdi], xmm0 #44.16
movups XMMWORD PTR [16+rdi], xmm1 #44.16
ret #44.16
Какова цель этих инструкций по перемещению?
1 ответ
(Это просто мои предположения, но это слишком долго для комментариев)
Что здесь происходит?
Поскольку конструкторы constexpr
, может быть, Payload
в целом имеет некоторое значение, вычисленное во время компиляции. Затем во время выполнения этот полныйPayload
возвращается. Насколько мне известно, компилятору не требуется распознавать, что определенная часть значения времени компиляции не инициализирована и что он не должен генерировать для нее код.
В каком-нибудь сумасшедшем компиляторе могло случиться так, что время компиляции Payload
имеет значения мусора в неинициализированном разделе, и тогда он будет производить, например:
createStorage2():
mov rax, rdi
mov QWORD PTR [rdi], 0xbaadf00d
mov QWORD PTR [rdi+8], 0xbaadf00d
mov QWORD PTR [rdi+16], 0xbaadf00d
mov QWORD PTR [rdi+24], 0
ret
В основном constexpr
не любит неинициализированные значения, но союзы - это немного способ обойти это.