39

假设我有一堂课,如下所示;

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));

我问的原因是,我正在处理非常大的分页数据集合,如此所述,其中性能至关重要,而 memcpy 提供了比迭代分配显着的性能优势。我怀疑应该没问题,因为“this”指针是一个隐式参数,而不是任何存储的参数,但是我应该注意其他隐藏的问题吗?

编辑:

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

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

第二次编辑

或许值得指出的是,我完全接受以这种方式使用 memcpy 是不好的做法,在一般情况下应避免使用。使用它的具体情况是作为高性能模板化数组类的一部分,其中包括一个参数“AllowShallowCopying”,它将调用 memcpy 而不是复制构造函数。这对诸如删除数组开头附近的元素以及将数据分页进出辅助存储等操作具有很大的性能影响。更好的理论解决方案是将类转换为简单的结构,但考虑到这涉及对大型代码库的大量重构,避免它不是我热衷于做的事情。

4

11 回答 11

19

根据标准,如果程序员没有为类提供复制构造函数,编译器将合成一个具有默认成员初始化的构造函数。(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)隐式定义的复制构造函数中实际执行成员初始化。

因此,在实践中,编译器必须为这些表现出行为的类合成复制构造函数,就好像它们在进行成员初始化一样。但是,如果该类显示“按位复制语义”(Lippman,第 43 页),则编译器不必合成复制构造函数(这将导致函数调用,可能是内联的)并改为按位复制。这种说法显然在ARM中得到了支持,但我还没有查到这一点。

使用编译器来验证某些东西是否符合标准总是一个坏主意,但是编译代码并查看生成的程序集似乎可以验证编译器没有在合成的复制构造函数中进行成员初始化,而是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哪里sizeof(MyClass)

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

编辑:

这篇文章的长短是:

1) 只要进行按位复制会表现出与按成员复制相同的副作用,标准就允许简单的隐式复制构造函数执行 amemcpy而不是按成员复制。

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

于 2010-06-11T11:15:19.143 回答
12

让我给你一个经验性的答案:在我们的实时应用程序中,我们一直这样做,而且效果很好。对于 Wintel 和 PowerPC 的 MSVC 和 Linux 和 Mac 的 GCC 就是这种情况,即使对于具有构造函数的类也是如此。

我不能为此引用 C++ 标准的章节,只是实验证据。

于 2010-06-11T10:34:59.127 回答
9

可以。但首先问问自己:

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

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

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

于 2010-06-11T08:36:24.813 回答
9

您的类有一个构造函数,因此从 C 结构的意义上说不是 POD。因此,使用 memcpy() 复制它是不安全的。如果需要 POD 数据,请删除构造函数。如果您想要非 POD 数据,其中受控构造是必不可少的,请不要使用 memcpy() - 您不能同时拥有两者。

于 2010-06-11T08:48:52.220 回答
8

[...] 但是还有其他我应该注意的隐藏问题吗?

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

此外,您的实现基本上是黑客攻击(如果有必要,这不是一件坏事),它可能取决于(不确定)您当前的编译器如何实现事物。

这意味着,如果您从现在开始一年(或五年)升级编译器/工具链(或者只是更改当前编译器中的优化设置),没有人会记得这个 hack(除非您付出很大努力使其可见)并且您最终可能会你手上有未定义的行为,并且开发人员在几年后诅咒“谁做了这件事”。

并不是这个决定是不合理的,而是它是(或将会)出乎维护者意料之外的。

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

代替:

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 结构的实现附近。

于 2010-06-11T13:00:23.497 回答
6

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

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

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

POD 数组本身就是 POD。一个结构或联合,其所有非静态数据成员都是 POD,如果它具有以下条件,它本身就是 POD:

  • 没有用户声明的构造函数。
  • 没有私有或受保护的非静态数据成员。
  • 没有基类。
  • 没有虚函数。
  • 没有引用类型的非静态数据成员。
  • 没有用户定义的复制赋值运算符。
  • 没有用户定义的析构函数。
于 2010-06-11T08:42:59.147 回答
3

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

我的问题是维护问题之一。您是否确信没有人会在此类中包含会破坏您出色优化的字段?我不知道,我是工程师而不是先知。

所以与其尝试改进复制操作....为什么不尝试完全避免它呢?

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

例如,你知道blist(Python 模块)。例如,B+Tree 可以允许索引访问的性能与向量非常相似(诚然慢一点),同时最大限度地减少插入/删除时随机播放的元素数量。

与其匆忙而肮脏,也许您应该专注于寻找更好的收藏品?

于 2010-06-11T13:42:34.463 回答
1

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

于 2010-06-11T08:52:47.770 回答
1

在谈论您所指的情况时,我建议您声明struct 's 而不是class 'es。它使它更容易阅读(并且不那么有争议:))并且默认访问说明符是公共的。

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

于 2010-06-11T09:17:29.017 回答
1

正如 John Dibling 所指出的,您不应该memcpy手动使用。相反,使用std::copy. 如果您的课程支持 memcpy,std::copy则会自动执行memcpy. 它可能比手动 memcpy 还要快

如果你使用std::copy,你的代码是可读的并且它总是使用最快的方式来复制。如果您稍后更改类的布局,使其不再支持 memcpy,则使用的代码std::copy不会中断,而您手动调用 memcpy 会。

现在,您如何知道您的课程是否支持 memcpy?以同样的方式,std::copy检测到。它使用:std::is_trivially_copyable. 您可以使用 astatic_assert来确保支持此属性。

注意std::is_trivially_copyable只能检查类型信息。它不理解语义。以下类是可简单复制的类型,但按位复制将是一个错误:

#include <type_traits>

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

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

按位复制后,ptr副本的 仍将指向原始内存。另见三法则

于 2019-11-27T16:24:54.373 回答
0

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

POD 的定义是没有虚函数、没有构造函数、解构函数没有虚继承……等等。

于 2010-06-11T08:42:28.220 回答