对大小数据进行安全(且无成本)的重新解释

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;如果传递了 yz 别名,则此函数将不会具有“所需”行为:

.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)都会生成观察 xz 修改的代码:

.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数据框中的行进行重新排序?

openstack名词解释

Oracle:修改 varchar 列大小后重新创建数据库视图

使用 pyspark 对 parquet 文件进行分区和重新分区

一事无成的意思

关于kafka重新消费数据问题