我什么时候应该担心对齐?

时间:2011-06-24 22:14:37

标签: c++

我最近学到了很多关于对齐的内容,但我不确定在哪种情况下它会成为一个问题。我怀疑有两种情况:

第一个是使用数组时:

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位系统。

更新2

Pool现在使用向量而不是数组。

4 个答案:

答案 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访问数据施加任何对齐限制。

通常对于这种情况,您可以创建所关注的所有类型的并集,并分配一个数组。这保证了数据被对齐以用作联合中任何类型的对象。

或者,您可以动态分配您的块 - mallocoperator ::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]);

这不好。当你接管内存管理时,你需要接管所有的内存管理方面,而不仅仅是分配。虽然您可能已经分配了适当数量的内存,但您根本没有解决对齐问题。

假设您使用newmalloc来分配一个字节。打印它的地址。再次执行此操作,并打印此新地址:

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(),它总是释放最后分配的指针而不是给定的指针。