数组未对齐的内存访问
Unaligned memory acces with array
在 C 程序中,有一个用作缓冲区的数组 FooBuffer[]
用于存储结构的数据成员的内容,如下所示:
struct Foo {
uint64_t data;
};
我被告知此行可能会导致未对齐的访问:
uint8_t FooBuffer[10] = {0U};
我知道未对齐访问取决于处理器的对齐偏移量,一般来说,它会消耗更多 read/write 周期。在什么情况下这会导致未对齐的内存访问,我该如何防止它?
编辑:
struct Foo 类型的变量将存储在缓冲区中。特别是,它的成员 data 将被拆分成八个字节,存储在数组 FooBuffer 中。请参阅带有一些选项的附加代码。
#include <stdio.h>
#include <string.h>
typedef unsigned long uint64;
typedef unsigned char uint8;
struct Foo
{
uint64 data;
};
int main()
{
struct Foo foo1 = {0x0123456789001122};
uint8 FooBuffer[10] = {0U};
FooBuffer[0] = (uint8)(foo1.data);
FooBuffer[1] = (uint8)(foo1.data >> 8);
FooBuffer[2] = (uint8)(foo1.data >> 16);
FooBuffer[3] = (uint8)(foo1.data >> 24);
FooBuffer[4] = (uint8)(foo1.data >> 32);
FooBuffer[5] = (uint8)(foo1.data >> 40);
FooBuffer[6] = (uint8)(foo1.data >> 48);
FooBuffer[7] = (uint8)(foo1.data >> 56);
struct Foo foo2 = {0x9876543210112233};
uint8 FooBuffer2[10] = {0U};
memcpy(FooBuffer2, &foo2, sizeof(foo2));
return 0;
}
但是,由于一个私有软件执行该操作,因此不清楚该过程是如何完成的。 “转换”后可能导致未对齐内存访问的场景是什么?
定义诸如struct Foo { uint64_t data; }
的结构或诸如uint8_t FooBuffer[10];
的数组并以正常方式使用它们不会导致未对齐的访问。 (为什么 FooBuffer
使用 10?这个例子只需要 8 个字节?)
新手有时尝试的一种可能导致未对齐访问的方法是尝试将字节数组重新解释为数据结构。例如,考虑:
// Get raw bytes from network or somewhere.
uint8_t FooBuffer[10];
CallRoutineToReadBytes(FooBuffer,...);
// Reinterpret bytes as original type.
struct Foo x = * (struct Foo *) FooBuffer; // Never do this!
这里的问题是 struct Foo
有一些对齐要求,但 FooBuffer
没有。所以 FooBuffer
可以在任何地址,但是转换为 struct Foo *
试图强制它到 struct Foo
的地址。如果对齐不正确,则行为不是由 C 标准定义的。即使系统允许它并且程序“工作”,它也可能在不正确对齐的地址访问 struct Foo
并遇到性能问题。
为避免这种情况,重新解释字节的正确方法是将它们复制到新对象中:
struct Foo x;
memcpy(&x, FooBuffer, sizeof x);
编译器通常会识别出这里发生了什么,特别是如果 struct Foo
不大,以有效的方式实现 memcpy
,可能是两个 load-four-byte 指令或一个load-eight-byte 说明。
您可以做的事情是要求编译器通过使用 _Alignas
关键字声明它来对齐 FooBuffer
:
uint8_t _Alignas(Struct Foo) FooBuffer[10];
请注意,如果您需要从缓冲区中间获取字节,例如从包含前面的协议字节和其他数据的网络消息中获取字节,这可能无济于事。而且,即使它确实提供了所需的对齐方式,也永远不要使用上面显示的 * (struct Foo *) FooBuffer
。它的问题不仅仅是对齐,其中之一是 C 标准不保证像这样重新解释数据的行为。 (在 C 中支持的方法是通过联合,但 memcpy
是一个很好的解决方案。)
在您显示的代码中,使用位移位将字节从 foo1.data
复制到 FooBuffer
。这也不会导致对齐问题;像这样操作数据的表达式工作得很好。但是它有两个问题。一是它名义上是一个一个地操作单个字节。这在 C 中是完全合法的,但它可能很慢。编译器可能会优化它,并且可能有 built-ins 或库函数来协助它,具体取决于您的平台。
另一个问题是它根据字节的位置值对字节进行排序:low-position-value 字节首先放入缓冲区。相反,memcpy
方法按照字节在内存中的存储顺序复制字节。您要使用哪种方法取决于您要解决的问题。要将数据存储在一个系统上并稍后在同一系统上读回,memcpy
方法很好。要使用相同的字节顺序在两个系统之间发送数据,memcpy
方法就可以了。但是,如果要将数据从 Internet 上的一个系统发送到另一个系统,并且这两个系统在内存中不使用相同的字节顺序,则需要就网络包中使用的顺序达成一致。在这种情况下,通常使用 arrange-bytes-by-position-value 方法。同样,您的平台可能具有内置函数或库例程来协助完成此操作。例如,htonl
和 ntohl
例程是 BSD 例程,它们采用普通的 32 位无符号整数和 return 它的字节按网络顺序排列或 vice-versa.
在 C 程序中,有一个用作缓冲区的数组 FooBuffer[]
用于存储结构的数据成员的内容,如下所示:
struct Foo {
uint64_t data;
};
我被告知此行可能会导致未对齐的访问:
uint8_t FooBuffer[10] = {0U};
我知道未对齐访问取决于处理器的对齐偏移量,一般来说,它会消耗更多 read/write 周期。在什么情况下这会导致未对齐的内存访问,我该如何防止它?
编辑: struct Foo 类型的变量将存储在缓冲区中。特别是,它的成员 data 将被拆分成八个字节,存储在数组 FooBuffer 中。请参阅带有一些选项的附加代码。
#include <stdio.h>
#include <string.h>
typedef unsigned long uint64;
typedef unsigned char uint8;
struct Foo
{
uint64 data;
};
int main()
{
struct Foo foo1 = {0x0123456789001122};
uint8 FooBuffer[10] = {0U};
FooBuffer[0] = (uint8)(foo1.data);
FooBuffer[1] = (uint8)(foo1.data >> 8);
FooBuffer[2] = (uint8)(foo1.data >> 16);
FooBuffer[3] = (uint8)(foo1.data >> 24);
FooBuffer[4] = (uint8)(foo1.data >> 32);
FooBuffer[5] = (uint8)(foo1.data >> 40);
FooBuffer[6] = (uint8)(foo1.data >> 48);
FooBuffer[7] = (uint8)(foo1.data >> 56);
struct Foo foo2 = {0x9876543210112233};
uint8 FooBuffer2[10] = {0U};
memcpy(FooBuffer2, &foo2, sizeof(foo2));
return 0;
}
但是,由于一个私有软件执行该操作,因此不清楚该过程是如何完成的。 “转换”后可能导致未对齐内存访问的场景是什么?
定义诸如struct Foo { uint64_t data; }
的结构或诸如uint8_t FooBuffer[10];
的数组并以正常方式使用它们不会导致未对齐的访问。 (为什么 FooBuffer
使用 10?这个例子只需要 8 个字节?)
新手有时尝试的一种可能导致未对齐访问的方法是尝试将字节数组重新解释为数据结构。例如,考虑:
// Get raw bytes from network or somewhere.
uint8_t FooBuffer[10];
CallRoutineToReadBytes(FooBuffer,...);
// Reinterpret bytes as original type.
struct Foo x = * (struct Foo *) FooBuffer; // Never do this!
这里的问题是 struct Foo
有一些对齐要求,但 FooBuffer
没有。所以 FooBuffer
可以在任何地址,但是转换为 struct Foo *
试图强制它到 struct Foo
的地址。如果对齐不正确,则行为不是由 C 标准定义的。即使系统允许它并且程序“工作”,它也可能在不正确对齐的地址访问 struct Foo
并遇到性能问题。
为避免这种情况,重新解释字节的正确方法是将它们复制到新对象中:
struct Foo x;
memcpy(&x, FooBuffer, sizeof x);
编译器通常会识别出这里发生了什么,特别是如果 struct Foo
不大,以有效的方式实现 memcpy
,可能是两个 load-four-byte 指令或一个load-eight-byte 说明。
您可以做的事情是要求编译器通过使用 _Alignas
关键字声明它来对齐 FooBuffer
:
uint8_t _Alignas(Struct Foo) FooBuffer[10];
请注意,如果您需要从缓冲区中间获取字节,例如从包含前面的协议字节和其他数据的网络消息中获取字节,这可能无济于事。而且,即使它确实提供了所需的对齐方式,也永远不要使用上面显示的 * (struct Foo *) FooBuffer
。它的问题不仅仅是对齐,其中之一是 C 标准不保证像这样重新解释数据的行为。 (在 C 中支持的方法是通过联合,但 memcpy
是一个很好的解决方案。)
在您显示的代码中,使用位移位将字节从 foo1.data
复制到 FooBuffer
。这也不会导致对齐问题;像这样操作数据的表达式工作得很好。但是它有两个问题。一是它名义上是一个一个地操作单个字节。这在 C 中是完全合法的,但它可能很慢。编译器可能会优化它,并且可能有 built-ins 或库函数来协助它,具体取决于您的平台。
另一个问题是它根据字节的位置值对字节进行排序:low-position-value 字节首先放入缓冲区。相反,memcpy
方法按照字节在内存中的存储顺序复制字节。您要使用哪种方法取决于您要解决的问题。要将数据存储在一个系统上并稍后在同一系统上读回,memcpy
方法很好。要使用相同的字节顺序在两个系统之间发送数据,memcpy
方法就可以了。但是,如果要将数据从 Internet 上的一个系统发送到另一个系统,并且这两个系统在内存中不使用相同的字节顺序,则需要就网络包中使用的顺序达成一致。在这种情况下,通常使用 arrange-bytes-by-position-value 方法。同样,您的平台可能具有内置函数或库例程来协助完成此操作。例如,htonl
和 ntohl
例程是 BSD 例程,它们采用普通的 32 位无符号整数和 return 它的字节按网络顺序排列或 vice-versa.