Malloc和new的区别——不同的填充方式

112
我正在为使用MPI进行高性能计算(10 ^ 5-10 ^ 6个核心)的项目审核别人的C ++代码。该代码旨在允许不同架构上的(可能)不同机器之间的通信。他写了一条注释,大致意思是:
“我们通常会使用 new delete ,但在这里我使用 malloc free 。这是必需的,因为某些编译器在使用 new 时会以不同的方式填充数据,导致在不同平台之间传输数据时出现错误。这不会发生在使用 malloc 中。”
这与我从标准的 new vs malloc 问题中所知道的任何内容都不符合。
”是一个非常流行的问题,但只涉及到 new 使用构造函数而 malloc 不使用,这与此无关。

33
仅就各种 Stack Overflow 帖子的研究而言,给它点赞! - iammilind
7
这是我在SO上见过的最好的“自助寻找解决方案”的研究工作之一。希望我能多次点赞。 - WhozCraig
1
转移代码是否假定数据以特定方式对齐,例如从八字节边界开始?这可能因mallocnew而异,在某些环境中,new分配一个块,在开头添加一些数据,并返回指向此数据右侧位置的指针。(我同意其他人的观点,在数据块内,mallocnew必须使用相同类型的填充。) - Lindydancer
1
哇,我没想到这个问题会这么受欢迎!@Lindydancer,我不认为假定任何8字节边界。不过这是一个有趣的观点。 - hcarver
1
使用一种分配方法而不是另一种的一个原因是当“某人”正在释放对象时。如果这个“某人”使用free来删除对象,您必须使用malloc进行分配。(而垫片问题是一个红色的干扰物。) - Lindydancer
这是正确的,但不是这里的问题。 - hcarver
8个回答

27
据我所知,有一个挑剔的点。 malloc保证返回任何标准类型对齐的地址。而::operator new(n)仅保证返回任何标准类型对齐的地址不大于n,如果T不是字符类型,则只需要为T对齐即可,new T[n]也是如此。
但是,这仅在您玩实现特定的技巧时才相关,例如使用指针的底部几位来存储标志,或者依赖于地址具有比它严格需要的更多对齐方式。
这不会影响对象内的填充,无论您如何分配其占用的内存,其布局必须完全相同。因此,很难看出这种差异如何导致传输数据时出现错误。
那个评论作者是否表达了他对堆栈或全局对象的看法,无论在他看来它们是“像malloc一样填充”还是“像new一样填充”?这可能会提供关于他的想法来源的线索。
也许他感到困惑,但也许他所谈论的代码不仅是malloc(sizeof(Foo) * n)new Foo[n]之间的简单差异。也许更像:
malloc((sizeof(int) + sizeof(char)) * n);

对比。

struct Foo { int a; char b; }
new Foo[n];

也就是说,他可能在“我使用malloc”,但实际上意思是“我手动将数据打包到不对齐的位置,而不是使用结构体”。实际上,为了手动打包结构体,不需要使用malloc,但没有意识到这一点是一种较小程度的混淆。必须定义发送到网络的数据布局。当使用结构体时,不同的实现会以不同的方式填充数据。


感谢您提供有关对齐的建议。涉及到的数据是一个字符数组,因此我怀疑这不是一个对齐问题,也不是结构体问题 - 尽管这也是我的第一个想法。 - hcarver
5
@Hbcdev说:好的,“char”数组从不填充,所以我会继续使用“混淆”作为解释。 - Steve Jessop

5
你的同事可能想到了new[]/delete[]的魔术数字(这是实现在删除数组时使用的信息)。但是,如果使用从new[]返回的地址开始的分配(而不是分配器的地址),这将不会成为问题。 打包似乎更有可能。 ABI的变化可能会导致结构体末尾添加不同数量的尾随字节(这受对齐方式的影响,也考虑数组)。使用malloc,可以指定结构体的位置,因此更容易移植到外部ABI。通常通过指定传输结构的对齐和打包来防止这些变化。

2
这是我最初的想法,即“结构体大于其部分之和”的问题。也许这就是他最初的想法来源。 - hcarver

3

我认为你是正确的。填充是由编译器完成的,而不是newmalloc。即使您声明一个数组或结构体而不使用newmalloc,填充考虑仍然适用。无论如何,虽然我可以看到不同实现的newmalloc可能会在平台之间移植代码时引起问题,但我完全没有看到它们如何会导致在平台之间传输数据时出现问题。


我之前认为new只是malloc的一个不错的封装,但从其他答案中看来,这并不完全正确。一致的意见似乎是无论使用哪种方式,填充应该是相同的;我认为在平台之间传输数据的问题只有在传输机制有缺陷时才会出现 :) - hcarver

3

一个对象的布局不应该依赖于它是使用 malloc 还是 new 分配的。这两种方法都返回相同类型的指针,当你将这个指针传递给其他函数时,它们不会知道对象是如何分配的。 sizeof *ptr 只与 ptr 的声明有关,而不是它如何被赋值。


0
在C++中,new关键字用于根据某些数据结构分配一些特定字节的内存。例如,您定义了某个类或结构,并且想要为其对象分配内存。
myclass *my = new myclass();

或者

int *i = new int(2);

但在所有情况下,您需要定义的数据类型(类、结构体、联合体、int、char等)并且只会分配所需对象/变量的字节内存(即该数据类型的倍数)。

但在使用malloc()方法时,您可以分配任意字节的内存,并且不必始终指定数据类型。在这里,您可以观察到malloc()的几种可能性:

void *v = malloc(23);

或者

void *x = malloc(sizeof(int) * 23);

或者

char *c = (char*)malloc(sizeof(char)*35);

0

这是我猜测这个问题来自何处的野猜想。正如你所提到的,问题在于通过MPI传输数据。

就我个人而言,为了发送/接收MPI上的复杂数据结构,我总是实现序列化/反序列化方法将整个结构打包/解包到一个字符数组中。现在,由于填充,我们知道该结构的大小可能大于其成员的大小,因此还需要计算数据结构的未填充大小,以便我们知道正在发送/接收多少字节。

例如,如果您想使用上述技术在MPI上发送/接收 std::vector<Foo> A,通常假定生成的字符数组的大小为 A.size()* sizeof(Foo) 是错误的。换句话说,每个实现序列化/反序列化方法的类也应该实现一种报告数组大小的方法(或更好的是将数组存储在容器中)。这可能成为错误的原因。然而,这与本线程中指出的 new vs malloc 没有任何关系。


将char数组复制可能会有问题--你的一些核心可能是小端架构,而另一些则是大端架构(可能不太可能,但有可能)。你需要进行XDR编码或其他操作,但你可以使用用户定义的MPI数据类型。它们很容易考虑到填充。但我可以理解你所说的误解可能的原因--这就是我所说的“结构体大于其部分之和”的问题。 - hcarver
是的,定义MPI数据类型是另一种/正确的方法。关于字节序的观点很好。虽然,我真的怀疑这会在实际集群上发生。无论如何,我认为如果他们遵循相同的策略,这可能会导致错误... - mmirzadeh

0

当我想控制我的普通数据结构的布局时,我使用MS Visual编译器中的#pragma pack(1)。我想这样的预编译指令被大多数编译器支持,例如gcc

这会导致结构体中的所有字段一个接一个地对齐,没有空格。

如果另一端的平台也这样做了(即使用填充1编译其数据交换结构),那么在两端检索的数据就可以很好地适配。 因此,我从来没有必要在C++中使用malloc。

最坏的情况下,我会考虑重载new运算符,使其执行某些巧妙的操作,而不是直接在C++中使用malloc。


有哪些情况下您想要控制数据结构的布局?只是好奇。 - hcarver
还有谁知道是否有支持#pragma pack或类似功能的编译器?我意识到这不会成为标准的一部分。 - hcarver
gcc支持这个功能。我需要什么情况下使用它:在两个不同平台之间共享二进制数据流,例如在windows和palmOS之间,或在windows和linux之间共享二进制数据流。关于gcc的链接:http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html - Stephane Rolland

-1

malloc是C++中的一种函数类型,而new则是一种数据类型。如果我们在C++中使用malloc,则必须使用类型转换,否则编译器会报错。但是,如果我们使用new数据类型来分配内存,则无需进行类型转换。


1
我认为你应该尝试更充分地阐述你的答案。 - Carlo
这似乎没有涉及到它们在填充方面执行不同操作的问题,这正是我上面真正想问的。 - hcarver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接