在不同架构上与数据对齐有关的问题是什么?

时间:2014-01-12 21:38:37

标签: c++ c struct iostream memory-alignment

按照this comment之前的一个问题,我确信定义了一个struct,其中的字段具有适当的类型,具有已知且定义良好的大小,并提供此{{1}的实例1}}到struct,足以以安全的方式从read读取数据。

我失踪的那块拼图是什么?我的stream表示我试图从文件中读取的标题的内部定义,可能出现的问题是什么以及这个简单设计选择的弱点是什么?

4 个答案:

答案 0 :(得分:2)

首先,考虑字节排序(aka endianness)。如果文件的数据是在一个小端环境中编写的,而您现在正在大端架构中读取它,会发生什么?一切都会变得一团糟。

除此之外,请记住,如果需要,可以在任何两个连续组件之间或结构布局中的最后一个组件之后出现孔或填充,以允许内存中组件的正确对齐。坏的是这是依赖于平台的,所以你不能像你描述的那样可移植地编写代码。您的struct可能在不同的计算机上有不同的填充和漏洞,因此当您在不同的计算机中读取此文件时,您将获得任意和不同的行为。它会(错误地)用文件数据填充填充位。它不是可移植的代码,读起来肯定不优雅,因为我想这涉及很多丑陋的演员。

所以,正如上面提到的评论所说的那样,你真的不希望将文件读入一个结构并使其工作 - 如果你这样做并且从未遇到过任何问题,那绝对是运气。你真的不能依靠这个。不同的体系结构具有不同的对齐要求,它具有高度的平台依赖性。

答案 1 :(得分:2)

可能会出现几个问题:

  • 不同体系结构上基本类型的定义可能不同。假设你有struct这样:

    struct MyStruct
    {
        char c[9];
        int a;
        long b;
    };
    

    在Windows上几乎任何32位或64位编译器上编译,c需要9个字节,a需要4个字节,b需要4个字节。在64位Linux long上的OTOH通常是8个字节,因此{64}上的struct所理解的gcc是非常不同的;

  • struct定义的变化,艺术建筑注意事项和编译器情绪可能会影响填充;在MyStruct之上的32位编译器通常会在c之后引入3个字节的填充以将a对齐到4个字节的边界,而64位编译器可能想要添加额外的填充来对齐东西到8个字节的边界;

  • 取决于体系结构,整数的内部表示可能有不同的endianness,因此,即使整数大小和填充匹配,从文件读取的整数的字节可能必须交换为有意义

通过准确指定这些歧义区域来解决所有这些问题:对于磁盘格式,您应该使用:

  • 固定长度类型(int32_t表示有符号的32位整数,uint64_t表示无符号的64位整数,...);
  • 确定的填充 - 如果有的话;几乎所有编译器都提供了一些#pragma或其他方法来精确控制对齐和填充;
  • 固定字节顺序;你决定一些字节序设置(big-endian,如果你喜欢TCP / IP& co。的“网络订单”选项,小端,如果你更实用,你想推迟问题,直到你需要互操作使用big-endian设备)并设置代码以相应地交换字节,如果代码是在磁盘格式不同于磁盘格式的所选端点的机器上编译的。

请注意,由于在转储结构时字节顺序和填充可能会很麻烦,因此最好不要在没有填充的情况下序列化单个字段(应用必要的字节序转换),而不是转储整个struct。< / p>


对于一个很好的C ++ - 解决“二进制序列化问题”的方法,我建议你看看Qt的QDataStream类和related stuff。它们提供operator<<QDataStream序列化基元类型(强烈建议使用它们的固定宽度类型),没有填充,默认情况下是big-endian格式;然后,您可以为您的课程提供operator<<operator>>(可能包括某种版本控制),让您的每个班级只处理其字段。

答案 2 :(得分:1)

您需要遵循两个基本规则:

  1. 您的结构的每个实例都必须位于一个内存地址,该地址可以被结构中最大字段的大小整除。

  2. 结构中的每个字段必须位于偏移量(在结构内),该偏移量可以被该字段本身的大小整除。

  3. 例如,以下结构的每个实例都必须驻留在可被sizeof(uint32)整除的内存地址中:

    struct
    {
        uint16 a; // offset 0 (OK, because 0 is divisible by sizeof(uint16))
        uint08 b; // offset 2 (OK, because 2 is divisible by sizeof(uint08))
        uint08 c; // offset 3 (OK, because 3 is divisible by sizeof(uint08))
        uint32 d; // offset 4 (OK, because 4 is divisible by sizeof(uint32))
    }
    

    例外:

    • 如果CPU架构支持未对齐的加载和存储操作,则可能违反规则#1。然而,这样的操作通常效率较低(要求编译器在“之间”添加NOP)。理想情况下,即使编译器支持未对齐操作,也应该努力遵循规则#1 ,并让编译器知道数据已经完全对齐(使用专用#pragma),按顺序允许编译器在可能的情况下使用对齐的操作。

    • 如果编译器自动生成所需的填充,则可能违反规则#2。 当然,这会改变结构的每个实例的大小。建议始终使用显式填充(而不是依赖于当前编译器,可以在以后的某个时间点替换它。)

    补充:

    这两个规则本质上是单个规则的反映 - 每个变量必须分配在一个可被其大小整除的内存地址(1,2,4或8)。

    在大多数计算机程序中,只有在使用结构时才会出现对齐问题。

    但这只是因为结构实例更容易“落入内存中未对齐的位置”,而不会产生任何编译警告。

    如果我们“努力”,那么我们可以用简单的变量重现同样的问题。例如,在下面的代码中,4个分配中的3个将导致未对齐的内存访问冲突:

    char arr[16];
    int p0 = *(int*)(arr+0);
    int p1 = *(int*)(arr+1);
    int p2 = *(int*)(arr+2);
    int p3 = *(int*)(arr+3);
    

答案 3 :(得分:1)

填充是你需要考虑的一件事。另一个问题是,根据体系结构,访问未对齐的指针可以正常工作或使程序崩溃。

例如,假设您有一个char[12]数组,并希望在其中存储一个4字节int和8字节double。这样做很有诱惑力:

*((int*)&array[0]) = myInt;
*((double*)&array[4]) = myDouble;

在您的标准PC(x86 / x64)上,此代码可以正常工作(尽管您可能会注意到它有点慢)。然后你将它移植到CUDA,它崩溃了。那是因为(AFAIR)CUDA无法访问未正确对齐的内存。

这就是为什么struct必须填充,以便每个地址都正确对齐。但这确实意味着,如果您尝试将此类struct解释为连续的字节区域,则最终会遇到填充字节。