对大小数据进行安全(且无成本)的重新解释
Posted
技术标签:
【中文标题】对大小数据进行安全(且无成本)的重新解释【英文标题】:Safe (and costless) reinterpretation of sized data 【发布时间】:2019-08-01 07:31:40 【问题描述】:我想编写自己的“小向量”类型,第一个障碍是弄清楚如何实现堆栈存储。
我偶然发现了std::aligned_storage
,它似乎是专门为实现任意堆栈存储而设计的,但我不清楚什么是安全的,什么是不安全的。 cppreference.com 方便地有一个 example 使用 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();
;
这里的几乎所有内容对我来说都是有意义的,除了那两个 cmets 说:
注意:从 C++17 开始需要
std::launder
“as of”子句本身就相当混乱;这是否意味着
此代码不正确或不可移植,可移植版本应使用std::launder
(在 C++17 中引入),或者
C++17 对内存别名/重新解释规则进行了重大更改?
除此之外,从性能的角度来看,std::launder
的使用让我感到担忧。我的理解是,在大多数情况下,允许编译器对内存别名做出非常严格的假设(特别是指向不同类型的指针不引用同一内存)以避免冗余内存负载。
我想保持编译器方面的别名确定性级别(即可以从我的小向量访问T
与访问普通@987654331 的访问同样可优化@ 或T *
),尽管从我读过的std::launder
来看,这听起来像是一个完整的别名障碍,即编译器必须假设它对清洗指针的来源一无所知。我担心在每个 operator[]
处使用它会干扰通常的加载存储消除。
也许编译器比这更聪明,或者我可能误解了std::launder
首先是如何工作的。无论如何,我真的不觉得我知道我在用这种级别的 C++ 内存黑客做什么。很高兴知道我必须为这个特定用例做些什么,但如果有人能就更一般的规则启发我,那将不胜感激。
更新(进一步探索)
Reading up 在这个问题上多说一点,我目前的理解是,我在这里粘贴的示例在标准下具有未定义的行为,除非使用std::launder
。也就是说,展示我认为未定义行为的小型实验并没有表明 Clang 或 GCC 没有标准似乎允许的那样严格。
让我们从在别名指针的情况下显然不安全的事情开始:
float definitelyNotSafe(float *y, int *z)
*y = 5.0;
*z = 7;
return *y;
正如人们所预料的那样,Clang 和 GCC(启用了优化和严格别名)生成的代码总是返回 5.0
;如果传递了 y
和 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
不过,当别名指针的创建对编译器可见时,事情就变得有点奇怪了:
float somehowSafe(float x)
// Make some aliasing pointers
auto y = &x;
auto z = reinterpret_cast<int *>(y);
*y = 5.0;
*z = 7;
return x;
在这种情况下,Clang 和 GCC(使用 -O3
和 -fstrict-aliasing
)都会生成观察 x
到 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
也就是说,编译器并不能保证“利用”未定义的行为;毕竟,它是未定义的。在这种情况下,假设*z = 7
没有任何效果是没有利润的。那么如果我们“激励”编译器利用严格的别名呢?
int stillSomehowSafe(int x)
// Make some aliasing pointers
auto y = &x;
auto z = reinterpret_cast<float *>(y);
auto product = float(x) * x * x * x * x * x;
*y = 5;
*z = product;
return *y;
假设*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
我对这种行为感到很困惑。我知道我们得到了零保证编译器在存在未定义行为的情况下会做什么,但我也很惊讶 Clang 和 GCC 在这些优化方面都没有更具侵略性。这让我想知道我是否误解了标准,或者 Clang 和 GCC 对“严格别名”的定义是否较弱(且已记录在案)。
【问题讨论】:
这个可悲的事实是,大量代码依赖于技术上未定义的行为,即严格的别名违规。只需查看“经典”快速平方根的具体示例即可。 【参考方案1】:std::launder
的存在主要用于处理像std::optional
或您的small_vector
这样的场景,随着时间的推移,相同的存储可能会被多个对象重用,这些对象可能是const
,或者可能有const
或参考成员。
它对优化器说“这里有一个T
,但它可能与您之前的@987654328@ 不同,因此const
成员可能已更改,或者引用成员可能引用其他内容”。
在没有const
或参考成员的情况下,std::launder
什么都不做,也没有必要。见http://eel.is/c++draft/ptr.launder#5
【讨论】:
以上是关于对大小数据进行安全(且无成本)的重新解释的主要内容,如果未能解决你的问题,请参考以下文章
python - 如何按python中的因子级别对pandas数据框中的行进行重新排序?
Oracle:修改 varchar 列大小后重新创建数据库视图