uninitialized_copy memcpy / memmove optimization

时间:2017-11-24 00:20:15

标签: c++ visual-c++ stl

我最近开始在MSVC的实现中检查STL。那里有一些不错的技巧,但我不知道为什么使用以下标准。

如果符合某些条件,则std::uninitialized_copy会针对简单memcpy/memmove进行优化。根据我的理解,如果源类型为T的目标类型为U memcpy,则输入范围可以是is_trivially_copy_constructible'到未初始化区域。

然而,在选择memcpy而不是逐个复制构造元素之前,MSVC实现检查了很多东西。我不想在这里粘贴相关代码,而是我通过pastebin分享它,如果有人感兴趣的话:https://pastebin.com/Sa4Q7Qj0

uninitialized_copy的基本算法是这样的(可读性省略了异常处理)

template <typename T, typename... Args>
inline void construct_in_place(T& obj, Args&&... args)
{
    ::new (static_cast<void*>(addressof(obj)) T(forward<Args>(args)...);
}

template <typename In, typename Out>
inline Out uninitialized_copy(In first, In last, Out dest)
{
    for (; first != last; ++first, ++dest)
        construct_in_place(*dest, *first);
}

如果复制构造不执行任何“特殊”操作(可以简单地复制构造),则可以优化为memcpy/memmove

MS的实施需要以下内容:

  • T可分配给U
  • 可以简单地复制到U
  • 琐事
  • 额外检查(如sizeof(T)== sizeof(U))如果T!= U

例如,以下结构不能是memcpy'd:

struct Foo
{
    int i;
    Foo() : i(10) { }
};

但以下是可以的:

struct Foo
{
    int i;
    Foo() = default; // or simply omit
};

检查类型U是否可以从类型T简单地复制构造是否应该足够?因为所有这些都是uninitialized_copy。

例如,我无法理解为什么MS的STL实现不会记忆以下内容(注意:我知道原因,它是用户定义的构造函数,但我不明白它背后的逻辑):

struct Foo
{
    int i;

    Foo() noexcept
        : i(10)
    {
    }

    Foo(const Foo&) = default;
};

void test()
{
    // please forgive me...
    uint8 raw[256];
    Foo* dest = (Foo*)raw;
    Foo src[] = { Foo(), Foo() };

    bool b = std::is_trivially_copy_constructible<Foo>::value;  // true
    bool b2 = std::is_trivially_copyable<Foo>::value;           // true

    memcpy(dest, src, sizeof(src)); // seems ok

    // uninitialized_copy does not use memcpy/memmove, it calls the copy-ctor one-by-one
    std::uninitialized_copy(src, src + sizeof(src) / sizeof(src[0]), dest);
}

相关SO帖子:Why doesn't gcc use memmove in std::uninitialized_copy?

更新

正如@Igor Tandetnik在评论中指出的那样,如果没有用户定义的拷贝构造函数那么说是不安全的,那么类型T可以简单地复制构造。他提供了以下例子:

struct Foo
{
    std::string data;
};

在此示例中,没有用户定义的复制构造函数,并且它仍然不是简单的可复制构造。感谢您的更正,我根据反馈修改了原帖。

1 个答案:

答案 0 :(得分:2)

uninitialized_copy有两个职责:首先,它必须确保正确的位模式进入目标缓冲区。其次,它必须开始该缓冲区中C ++对象的生存期。也就是说,它必须调用某种构造函数,除非C ++ Standard专门授予它跳过该构造函数调用的权限。

根据我非常不完整的研究,看来现在只有trivially copyable类型可以保证由memcpy / memmove保留其位模式;记忆任何其他类型的类型(即使碰巧是普通的可复制构造的和/或普通的可复制的!)也会产生不确定的行为。

此外,现在似乎只有trivial类型可以在没有构造函数调用的情况下“出现”。 (P0593 "Implicit creation of objects..."在此领域提出了许多更改,也许在C ++ 2b中。)

Jonathan Wakely对libstdc++ bug 68350的评论似乎表明,GNU libstdc ++试图通过从未出现任何非平凡类型的对象(即使作为C ++实现)也不会“冒出来”,从而试图保留在法律之内。 ,他们确实可以以性能的名义利用平台特定的行为。我想出于类似的原因(无论这些原因是什么),MSVC也会遵循类似的逻辑。


通过比较供应商对“普通复制但不琐碎”的类类型优化std::copystd::uninitialized_copy的意愿,可以看到供应商不愿意“弹出对象”。普通复制意味着std::copy可以使用memcpy来分配现有对象;但是std::uninitialized_copy,要使这些对象首先弹出来,仍然感到需要循环调用 some 构造函数-即使它是琐碎的复制构造函数!

class C { int i; public: C() = default; };
class D { int i; public: D() {} };
static_assert(std::is_trivially_copyable_v<C> && !std::is_aggregate_v<C>);
static_assert(std::is_trivially_copyable_v<D> && !std::is_aggregate_v<D>);

void copyCs(C *p, C *q, int n) {
    std::copy(p, p+n, q);  // GNU and MSVC both optimize
    std::uninitialized_copy(p, p+n, q);  // GNU and MSVC both optimize
}
void copyDs(D *p, D *q, int n) {
    std::copy(p, p+n, q);  // GNU and MSVC both optimize
    std::uninitialized_copy(p, p+n, q);  // neither GNU nor MSVC optimizes :(
}

您写道:

  

检查U型是否可以从T型简单复制构造就足够了吗?因为这就是uninitialized_copy的全部。

是的,但是当T和U 不同时,您不是在进行“琐碎的复制构建”;您正在执行的“琐碎构造”不是 复制构造。不幸的是,C ++标准将is_trivially_constructible<T,U>定义为不同于人类“琐碎”的含义!我的博客文章"Trivially-constructible-from" (July 2018)给出了以下示例:

assert(is_trivially_constructible_v<u64, u64b>);
// Yay!

using u16 = short;
assert(is_trivially_constructible_v<u64, u16>);
// What the...

assert(is_trivially_constructible_v<u64, double>);
// ...oh geez.

这解释了MSVC的一些

  

如果T!= U,则进行额外检查(如sizeof(T)== sizeof(U))

具体来说,MSVC的_Ptr_cat_helper<T*,U*>::_Really_trivial特性依靠那些额外的检查来检测某些(但不是全部)常见的情况,其中从T到U的转换在人/位意义上“确实”是微不足道的,而不仅仅是微不足道的在C ++标准意义上。这使MSVC可以优化将int*数组复制到const int*数组中,这是libstdc ++无法做到的:

using A = int*;
using B = const int*;

void copyAs(A *p, B *q, int n) {
    std::uninitialized_copy(p, p+n, q);  // only MSVC optimizes
}
void copyBs(B *p, B *q, int n) {
    std::uninitialized_copy(p, p+n, q);  // GNU and MSVC both optimize
}