我最近学到了很多关于对齐的内容,但我不确定在哪种情况下它会成为一个问题。我怀疑有两种情况:
第一个是使用数组时:
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);
}
我的问题是:
我正在使用64位Intel i7处理器和Darwin GCC。 但我也将Linux,Windows(VC2008)用于32位和64位系统。
Pool现在使用向量而不是数组。
答案 0 :(得分:14)
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?
}
再次,非常好 - 编译器必须允许此 1 。
对于你的记忆库,预后并不是那么好。您已经分配了一个char
数组,该数组必须充分对齐才能作为char
进行访问,但是以其他任何类型访问它不保证可以正常工作。在任何情况下,都不允许实施对char
访问数据施加任何对齐限制。
通常对于这种情况,您可以创建所关注的所有类型的并集,并分配一个数组。这保证了数据被对齐以用作联合中任何类型的对象。
或者,您可以动态分配您的块 - malloc
和operator ::new
都保证任何内存块都对齐以用作任何类型。
编辑:更改池以使用vector<char>
可以改善情况,但只是略有改进。这意味着您分配的第一个对象将起作用,因为向量所拥有的内存块将({间接)用operator ::new
分配(因为您没有另外指定)。不幸的是,这没有多大帮助 - 第二次分配可能完全错位。
例如,假设每种类型都需要“自然”对齐 - 即,对齐到与其自身大小相等的边界。可以在任何地址分配字符。我们假设short是2个字节,需要偶数地址,int和long是4个字节,需要4个字节的对齐。
在这种情况下,请考虑如果您这样做会发生什么:
char *a = Foo.Allocate<char>();
long *b = Foo.Allocate<long>();
我们开始使用的块必须针对任何类型进行对齐,因此它绝对是一个偶数地址。当我们分配char
时,我们只使用一个字节,因此下一个可用地址是奇数。然后我们为long
分配足够的空间,但它位于奇数地址,因此尝试取消引用它会产生UB。
1 大多数情况下 - 最终,编译器可以在超出实现限制的幌子下拒绝任何事情。我很惊讶地发现真正的编译器存在问题。
答案 1 :(得分:4)
还没有人提到内存池。这有很大的对齐问题。
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个!那是因为malloc
总是返回一个对齐的指针,这样它就可以用于任何数据类型。
将一个double或long long int放在一个不以8或0结尾的地址上,当以十六进制打印时,您可能会获得核心转储。双精度和长long整数需要与8字节边界对齐。类似的约束适用于普通的旧vanilla整数(int32_t);这些需要在4字节边界上对齐。你的内存池没有这样做。
答案 2 :(得分:3)
通常 - 也就是说,对于大多数数据结构 - 不必担心提前对齐。编译器通常会做正确的事情。对于未对齐数据,出汗时间处罚的日子至少比我们晚了20年。
剩下的唯一问题是非法的未对齐数据访问,这种访问只发生在少数CPU架构上。编写代码使其有意义。测试一下。如果发生未对齐的数据异常,那么是时候弄清楚如何避免它。通过添加命令行选项可以轻松修复大多数情况。一些需要改变结构:重新排序元素,或显式插入未使用的填充元素。
答案 3 :(得分:3)
编译器透明地处理对齐 - sizeof和数组访问总是考虑到任何对齐,你不必关心它。
但是内存池示例中存在一个错误 - 如果调用deallocate(),它总是释放最后分配的指针而不是给定的指针。