lcm*_*lin 6 c++ memory reinterpret-cast c++17 stdlaunder
我想编写自己的“小向量”类型,第一个障碍是弄清楚如何实现堆栈存储。
我偶然发现了std::aligned_storage,它似乎是专门为实现任意堆栈存储而设计的,但我很不清楚什么是安全的,什么是不安全的。cppreference.com方便地提供了一个使用 的示例std::aligned_storage,我将在这里重复一下:
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
public:
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
Run Code Online (Sandbox Code Playgroud)
这里几乎所有内容对我来说都是有意义的,除了那两条评论:
注意:
std::launder从 C++17 开始需要
“as of”条款本身就相当令人困惑;这是否意味着
此代码不正确或不可移植,应使用可移植版本std::launder(在 C++17 中引入),或
C++17 对内存别名/重新解释规则进行了重大更改?
超越这一点,std::launder从性能的角度来看,使用 令我担忧。我的理解是,在大多数情况下,编译器可以对内存别名做出非常强的假设(特别是指向不同类型的指针不引用相同的内存),以避免冗余内存加载。
我想保持编译器的别名确定性水平(即,T从我的小向量中进行的访问与对普通T[]或的访问同样可优化T *),尽管从我读到的内容来看std::launder,这听起来像完全的别名障碍,即编译器必须假设它对被洗掉的指针的来源一无所知。我担心每次使用它operator[]都会干扰通常的加载存储消除。
也许编译器比这更聪明,或者也许我std::launder一开始就误解了它的工作原理。不管怎样,我真的不觉得我知道我在用这种级别的 C++ 内存黑客做什么。很高兴知道我必须为这个特定的用例做什么,但如果有人能启发我了解更一般的规则,那将不胜感激。
进一步阅读这个问题,我目前的理解是,我在此处粘贴的示例在标准下具有未定义的行为,除非std::launder使用。也就是说,较小的实验证明了我认为未定义的行为,但并没有表明 Clang 或 GCC 与标准似乎允许的那么严格。
让我们从在别名指针的情况下明显不安全的事情开始:
float definitelyNotSafe(float *y, int *z) {
*y = 5.0;
*z = 7;
return *y;
}
Run Code Online (Sandbox Code Playgroud)
正如人们所预料的那样,Clang 和 GCC(启用了优化和严格别名)都会生成始终返回5.0;的代码。如果传递 ay和z该别名,则该函数将不会具有“所需”行为:
.LCPI1_0:
.long 1084227584 # float 5
definitelyNotSafe(float*, int*): # @definitelyNotSafe(float*, int*)
mov dword ptr [rdi], 1084227584
mov dword ptr [rsi], 7
movss xmm0, dword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero,zero,zero
ret
Run Code Online (Sandbox Code Playgroud)
然而,当编译器可以看到别名指针的创建时,事情会变得有点奇怪:
.LCPI1_0:
.long 1084227584 # float 5
definitelyNotSafe(float*, int*): # @definitelyNotSafe(float*, int*)
mov dword ptr [rdi], 1084227584
mov dword ptr [rsi], 7
movss xmm0, dword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero,zero,zero
ret
Run Code Online (Sandbox Code Playgroud)
在这种情况下,Clang 和 GCC(使用-O3和-fstrict-aliasing)都会生成观察xthrough修改的代码z:
.LCPI0_0:
.long 7 # float 9.80908925E-45
somehowSafe(float): # @somehowSafe(float)
movss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
Run Code Online (Sandbox Code Playgroud)
也就是说,编译器并不能保证“利用”未定义的行为;毕竟它是未定义的。在这种情况下,假设没有*z = 7影响就没有任何好处。那么,如果我们“激励”编译器利用严格别名呢?
float somehowSafe(float x) {
// Make some aliasing pointers
auto y = &x;
auto z = reinterpret_cast<int *>(y);
*y = 5.0;
*z = 7;
return x;
}
Run Code Online (Sandbox Code Playgroud)
假设对;*z = product的值没有影响,显然对编译器有利。*y这样做将允许编译器将此函数简化为始终返回的函数5。尽管如此,生成的代码并没有做出这样的假设:
stillSomehowSafe(int): # @stillSomehowSafe(int)
cvtsi2ss xmm0, edi
movaps xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
movd eax, xmm1
ret
Run Code Online (Sandbox Code Playgroud)
我对这种行为感到相当困惑。我知道我们对编译器在存在未定义行为的情况下会做什么给予零保证,但我也感到惊讶的是,Clang 和 GCC 在此类优化方面都没有更积极。这让我想知道我是否误解了标准,或者 Clang 和 GCC 是否对“严格别名”的定义较弱(且有记录)。
std::launder存在主要是为了处理类似std::optional或 您的场景small_vector,其中随着时间的推移,相同的存储可能会被多个对象重用,并且这些对象可能是const或可能具有const或引用成员。
它对优化器说“这里有一个,但它可能与您之前的T不同,因此成员可能已更改,或者引用成员可能引用其他内容”。Tconst
const在没有或没有引用成员的情况下,std::launder不执行任何操作并且没有必要。请参阅http://eel.is/c++draft/ptr.launder#5
| 归档时间: |
|
| 查看次数: |
195 次 |
| 最近记录: |