我可以在C ++中使用memcpy来复制没有指针或虚函数的类

时间:2010-06-11 08:34:12

标签: c++ performance shallow-copy

说我有一个课程,如下所示;

class MyClass
{
public:
  MyClass();
  int a,b,c;
  double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

如果我的类没有指针或虚拟方法,使用以下内容是否安全?

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

我问的原因是,我正在处理非常大的分页数据集合,如here所述,其中性能至关重要,而memcpy与迭代分配相比具有显着的性能优势。我怀疑它应该没问题,因为'this'指针是一个隐含的参数而不是存储的任何东西,但是我还应该注意其他任何隐藏的恶意吗?

修改:

根据sharptooths评论,数据不包括任何句柄或类似的参考信息。

根据Paul R的评论,我已经分析了代码,在这种情况下避免复制构造函数的速度提高了4.5倍。这里的部分原因是我的模板化数组类比给定的简单示例稍微复杂一些,并且在为不允许浅复制的类型分配内存时调用了一个放置'new'。这实际上意味着调用默认构造函数以及复制构造函数。

第二次修改

或许值得指出的是,我完全接受以这种方式使用memcpy是不好的做法,在一般情况下应该避免使用。它正在使用的特定情况是高性能模板化数组类的一部分,它包含一个参数'AllowShallowCopying',它将调用memcpy而不是复制构造函数。这对于诸如删除数组开头附近的元素以及将数据分入和分出二级存储之类的操作具有很大的性能影响。更好的理论解决方案是将类转换为简单的结构,但考虑到这需要对大型代码库进行大量重构,避免使用它是我不想做的事情。

11 个答案:

答案 0 :(得分:12)

根据标准,如果程序员没有为类提供复制构造函数,编译器将合成一个构造函数,该构造函数展示默认成员初始化。 (12.8.8)但是,在12.8.1中,标准也说,

  

可以将一个类对象复制为两个   方式,通过初始化(12.1,8.5),   包括用于函数参数   传递(5.2.2)和函数值   返回(6.6.3),并通过转让   (5.17)。从概念上讲,这两个   操作由副本实现   构造函数(12.1)和复制赋值   算子(13.5.3)。

这里的操作词是“概念性的”,根据Lippman,编译器设计者在“普通”(12.8.6)隐式定义的拷贝构造函数中实际进行成员初始化的“out”。

在实践中,编译器必须为这些类合成复制构造函数,这些类表现出行为,就好像它们正在进行成员初始化一样。但如果该类表现出“按位复制语义”(Lippman,第43页),则编译器不必合成复制构造函数(这将导致函数调用,可能内联)并执行按位复制。这个声明显然是在ARM中备份的,但我还没看好。

使用编译器验证某些内容是否符合标准总是一个坏主意,但编译代码并查看生成的程序集似乎验证编译器没有在合成拷贝构造函数中进行成员初始化,而是执行{ {1}}代替:

memcpy

#include <cstdlib> class MyClass { public: MyClass(){}; int a,b,c; double x,y,z; }; int main() { MyClass c; MyClass d = c; return 0; } 生成的程序集是:

MyClass d = c;

...其中000000013F441048 lea rdi,[d] 000000013F44104D lea rsi,[c] 000000013F441052 mov ecx,28h 000000013F441057 rep movs byte ptr [rdi],byte ptr [rsi] 28h

这是在调试模式下在MSVC9下编译的。

编辑:

这篇文章的长篇小论是:

1)只要进行按位复制会产生与成员复制相同的副作用,标准允许琐碎的隐式复制构造函数执行sizeof(MyClass)而不是成员复制。

2)有些编译器实际上是memcpy而不是合成一个简单的复制构造函数,它会执行成员复制。

答案 1 :(得分:11)

让我给你一个实证答案:在我们的实时应用程序中,我们一直这样做,并且它运行得很好。对于Wintel和PowerPC的MSVC以及Linux和Mac的GCC,即使对于具有构造函数的类也是如此。

我不能引用C ++标准的章节和经文,仅仅是实验证据。

答案 2 :(得分:9)

可以。但首先要问问自己:

为什么不使用编译器提供的复制构造函数来执行成员复制?

您是否遇到需要优化的特定性能问题?

当前实现包含所有POD类型:当有人更改它时会发生什么?

答案 3 :(得分:9)

你的类有一个构造函数,因此在C结构的意义上不是POD。因此,使用memcpy()复制它是不安全的。如果需要POD数据,请删除构造函数。如果你想要非POD数据,那么控制构造是必不可少的,不要使用memcpy() - 你不能同时拥有它们。

答案 4 :(得分:8)

  

[...]但是还有其他隐藏的恶意   我应该知道吗?

是的:您的代码会做出既不建议也不记录的某些假设(除非您专门记录它们)。这是噩梦的维护。

另外,你的实现基本上是黑客攻击(如果它是必要的并不是坏事),它可能依赖于(不确定)当前编译器如何实现的东西。

这意味着如果你从现在起一年(或五年)升级编译器/工具链(或者只更改当前编译器中的优化设置),没有人会记住这个黑客(除非你努力让它保持可见)并且你可能最终会出现未定义的行为,开发人员在未来几年内诅咒“无论是谁做过这件事”。

并不是决定不合理,而是维护人员意外(或将会)意外。

为了最大限度地减少这种情况(意外情况?),我会根据类的当前名称将类移动到命名空间内的结构中,结构中根本没有内部函数。然后你明确表示你正在查看一个内存块并将其视为内存块。

而不是:

class MyClass
{
public:
    MyClass();
    int a,b,c;
    double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

你应该:

namespace MyClass // obviously not a class, 
                  // name should be changed to something meaningfull
{
    struct Data
    {
        int a,b,c;
        double x,y,z;
    };

    static const size_t PageSize = 1000000; // use static const instead of #define


    void Copy(Data* a1, Data* a2, const size_t count)
    {
        memcpy( a1, a2, count * sizeof(Data) );
    }

    // any other operations that you'd have declared within 
    // MyClass should be put here
}

MyClass::Data Array1[MyClass::PageSize],Array2[MyClass::PageSize];
MyClass::Copy( Array1, Array2, MyClass::PageSize );

这样你:

  • 明确指出MyClass :: Data是一个POD结构,而不是一个类(二进制它们将是相同或非常接近 - 如果我没记错的话,它们是相同的)但是这样它对程序员来说也是可见的代码。

  • 在两年内集中使用memcpy(如果你必须更改为std :: copy或其他东西),你可以在一个点上完成。

  • 将memcpy的使用保留在POD结构的实现附近。

答案 5 :(得分:5)

您可以使用memcpy复制POD类型数组。为boost::is_pod添加静态断言是一个好主意。您的课程现在不是POD类型。

  

算术类型,枚举类型,指针类型和指向成员类型的指针都是POD。

     

POD类型的cv限定版本本身就是POD类型。

     

POD阵列本身就是POD。   结构或联合,其所有非静态数据成员都是POD,如果它具有:

,则它本身就是POD      
      
  • 没有用户声明的构造函数。
  •   
  • 没有私有或受保护的非静态数据成员。
  •   
  • 没有基类。
  •   
  • 没有虚拟功能。
  •   
  • 没有引用类型的非静态数据成员。
  •   
  • 没有用户定义的副本分配运算符。
  •   
  • 没有用户定义的析构函数。
  •   

答案 6 :(得分:3)

我会注意到你承认这里有问题。你知道潜在的缺点。

我的问题是维护问题。你是否有信心没有人会在这个课程中加入一个会破坏你的优化的领域?我没有,我是工程师而不是先知。

所以不要试图改进复制操作....为什么不试着完全避免它呢?

是否可以更改用于存储的数据结构以停止移动元素...或者至少不会那么多。

例如,您知道blist(Python模块)吗?例如,B + Tree可以允许索引访问,其性能与向量非常相似(有点慢),同时最小化插入/移除时要随机播放的元素数量。

或许你应该专注于寻找更好的收藏品,而不是快速而又肮脏?

答案 7 :(得分:1)

在非POD类上调用memcpy是未定义的行为。我建议按照基里尔的提示进行断言。使用memcpy可以更快,但如果复制操作在代码中不是性能关键,那么只需使用按位复制。

答案 8 :(得分:1)

在谈到您所指的案例时,我建议您声明 struct 而不是 class 。这使得它更容易阅读(并且不那么有争议:))并且默认访问说明符是公共的。

当然你可以在这种情况下使用memcpy,但要注意不建议在结构中添加其他类型的元素(比如C ++类)(由于显而易见的原因 - 你不知道memcpy将如何影响它们)。

答案 9 :(得分:0)

它可以工作,因为(POD-)类与C ++中的结构(不完全是默认访问...)相同。您可以使用memcpy复制POD结构。

POD的定义不是虚函数,没有构造函数,解构函数没有虚拟继承......等等。

答案 10 :(得分:0)

正如John Dibling指出的那样,您不应该手动使用memcpy。而是使用std::copy。如果您的班级具有记忆能力,则std::copy将自动执行memcpyIt may be even faster than a manual memcpy

如果使用std::copy,则代码是可读的,并且始终使用最快的方式进行复制。而且,如果您稍后更改类的布局以使其不再可存储,则使用std::copy的代码将不会中断,而手动调用memcpy将会。

现在,您如何知道您的班级是否可以使用memcpy?同样,std::copy会检测到这一点。它使用:std::is_trivially_copyable。您可以使用static_assert来确保保留此属性。

请注意,std::is_trivially_copyable仅可以检查类型信息。它不理解语义。以下类是普通可复制的 type ,但是按位复制将是一个错误:

#include <type_traits>

struct A {
  int* p = new int[32];
};

static_assert(std::is_trivially_copyable<A>::value, "");

按位复制后,副本的ptr仍将指向原始内存。另请参见Rule of Three