问题描述
|
我最近已经学到了一些有关对齐的知识,但是我不确定在哪种情况下会出现问题。我想知道两种情况:
第一个是使用数组时:
struct Foo {
char data[3]; // size is 3,my arch is 64-bit (8 bytes)
};
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
// will this be padded to 16?
void testArray() {
Foo foo1 = array[0];
Foo foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should one expect issues here?
}
第二种情况是使用内存池时:
struct Pool {
Pool(std::size_t size = 256) : data(size),used(0),freed(0) { }
template<class T>
T * allocate() {
T * result = reinterpret_cast<T*>(&data[used]);
used += sizeof(T);
return result;
}
template<class T>
void deallocate(T * ptr) {
freed += sizeof(T);
if (freed == used) {
used = freed = 0;
}
}
std::vector<char> data;
std::size_t used;
std::size_t freed;
};
void testPool() {
Pool pool;
Foo * foo1 = pool.allocate<Foo>(); // points to data[0]
Foo * foo2 = pool.allocate<Foo>(); // points to data[3],// alignment issue here?
pool.deallocate(foo2);
pool.deallocate(foo1);
}
我的问题是:
两个代码示例中是否存在任何对齐问题?
如果是,那么如何解决?
在哪里可以了解更多信息?
更新资料
我在达尔文GCC上使用64位Intel i7处理器。
但是我也将Linux,Windows(VC2008)用于32位和64位系统。
更新2
池现在使用向量而不是数组。
解决方法
struct Foo {
char data[3]; // size is 3,my arch is 64-bit (8 bytes)
};
[编辑:我应该更明确:在此允许填充,在data
成员之后的结构中(但不允许在其之前)]。
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
此处不允许填充。数组必须是连续的。
[编辑:但是数组中的结构之间不允许填充-数组中的一个struct
必须紧跟在另一个之后-但如上所述,每个结构本身都可以包含填充。]
void testArray() {
Foo * foo1 = array[0];
Foo * foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should I expect issues here?
}
同样,非常好-编译器必须允许this1。
对于您的内存池,预后并不好。您已分配了一个数组char
,它必须足够对齐才能作为char
进行访问,但是不能保证像其他任何类型一样对其进行访问。但是无论如何,该实现都不允许对访问数据施加任何对齐限制,例如“ 7”。
通常,在这种情况下,您将创建一个您关心的所有类型的并集,并为其分配一个数组。这样可以确保将数据对齐以用作联合中任何类型的对象。
另外,您可以动态分配您的块-malloc
和operator ::new
保证任何内存块都可以对齐以用作任何类型。
编辑:将池更改为使用ѭ12会改善这种情况,但效果稍有改善。这意味着您分配的第一个对象将起作用,因为向量持有的内存块将被分配(间接)为operator ::new
(因为您没有另外指定)。不幸的是,这并没有多大帮助-第二分配可能会完全错位。
例如,假设每种类型都需要“自然”对齐-即对齐等于其自身大小的边界。可以在任何地址分配一个字符。我们假设short是2个字节,并且需要一个偶数地址,int和long是4个字节,并且需要4字节对齐。
在这种情况下,请考虑以下情况会发生什么:
char *a = Foo.Allocate<char>();
long *b = Foo.Allocate<long>();
我们开始使用的块必须针对任何类型进行对齐,因此绝对是一个偶数地址。当我们分配“ 7”时,我们仅用完一个字节,因此下一个可用地址为奇数。然后,我们为ѭ16分配足够的空间,但是它位于一个奇数地址,因此尝试取消引用它会得到UB。
1无论如何-最终,编译器可以以超出实现限制的名义拒绝几乎任何东西。看到真正的编译器对此有问题,我会感到惊讶。
, 还没有人提到内存池。这具有巨大的对准问题。
T * result = reinterpret_cast<T*>(&data[used]);
那不好当您接管内存管理时,您需要接管内存管理的所有方面,而不仅仅是分配。尽管您可能分配了适当的内存量,但您根本没有解决对齐问题。
假设您使用new
或malloc
分配一个字节。打印它的地址。再次执行此操作,并打印此新地址:
char * addr1 = new char;
std::cout << \"Address #1 = \" << (void*) addr1 << \"\\n\";
char * addr2 = new char;
std::cout << \"Address #2 = \" << (void*) addr2 << \"\\n\";
在Mac等64位计算机上,您会看到两个打印地址都以零结尾,并且通常相隔16个字节。您尚未在此处分配两个字节。您已分配32!那是因为ѭ10总是返回一个对齐的指针,这样它可以用于任何数据类型。
以十六进制打印时,在不以8或0结尾的地址上放置一个double或long long int,这样很可能会导致核心转储。 Double和long long int必须与8个字节边界对齐。类似的约束适用于普通的旧香草整数(int32_t);这些需要在4个字节的边界上对齐。您的内存池没有这样做。
,通常,对于大多数数据结构而言,不必担心预先对齐。编译器通常会做正确的事情。对于未对齐的数据,出汗时间惩罚的日子至少比我们落后了20年。
剩下的唯一问题是非法的未对齐数据访问,这种访问仅发生在少数CPU架构上。编写代码,使之有意义。测试一下。如果发生未对齐的数据异常,那么该是找出解决方法的时候了。通过添加命令行选项,可以轻松解决大多数情况。有一些需要更改结构:重新排序元素,或显式插入未使用的填充元素。
, 对齐方式由编译器透明地处理-sizeof和数组访问始终会考虑任何对齐方式,您不必在意。
内存池示例中有一个错误-如果调用deallocate(),它将始终取消分配最后分配的指针,而不是给定的指针。