过去,ARM处理器无法正确处理未对齐的内存访问(ARMv5及以下版本)。如果ptr
未在4字节上正确对齐,则类似于u32 var32 = *(u32*)ptr;
的语句将失败(引发异常)。
对于x86 / x64,编写这样的语句将运行良好,因为这些CPU一直以来都非常有效地处理了这种情况。但根据C标准,这不是“正确”的编写方式。u32
显然相当于一个4字节的结构,必须对齐在4字节上。
实现相同结果的正确方法是保持正统的正确性并确保与任何CPU的完全兼容性:
u32 read32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
这个是正确的,可以生成适用于任何CPU的代码,无论是否能够在不对齐的位置读取。更好的是,在x86 / x64上,它被优化为单个读操作,因此具有与第一个语句相同的性能。它是可移植的,安全的和快速的。还有谁能要求更多呢?
问题是,在ARM上,我们没有那么幸运。
编写memcpy
版本确实是安全的,但似乎会导致系统谨慎操作,这对于ARMv6和ARMv7(基本上是任何智能手机)非常缓慢。
在一个重度依赖读操作的性能导向型应用程序中,第一版和第二版之间的差异可以测量:在gcc -O2
设置下,它超过了> 5倍。这太大了,不能忽视。
试图找到一种使用ARMv6 / v7功能的方法,我查看了一些示例代码的指导。不幸的是,它们似乎选择了第一个语句(直接u32
访问),这是不正确的。
这还不是全部:新的GCC版本现在正在尝试实现自动向量化。在x64上,这意味着SSE/AVX,在ARMv7上,这意味着NEON。ARMv7还支持一些新的“加载多个”(LDM)和“存储多个”(STM)操作码,这些操作码需要指针对齐。
那是什么意思?好吧,即使没有从C代码中特别调用它们(没有内部函数),编译器也可以自由地使用这些高级指令。为了做出这样的决定,它使用了一个应该对齐在4个字节上的事实。如果不是这样,那么所有的赌注都是无效的:未定义的行为,崩溃。
这意味着即使在支持非对齐内存访问的CPU上,直接使用u32
访问现在也是危险的,因为它可能会导致在高优化设置(-O3
)下生成错误的代码。
所以现在,这是一个两难选择:如何在非对齐内存访问时访问ARMv6/v7的本机性能,而不编写不正确的版本u32
访问?
PS:我也尝试过__packed()
指令,从性能角度来看,它们似乎与memcpy
方法完全相同。
[编辑]:非常感谢迄今为止收到的优秀建议。
查看生成的汇编代码,我可以确认@Notlikethat的发现,即memcpy
版本确实生成了正确的ldr
操作码(不对齐加载)。但是,我还发现生成的汇编代码无用地调用了str
命令。因此,完整的操作现在是一个不对齐的加载,一个对齐的存储,然后是最终的对齐加载。这比必要的工作量要大得多。
回答@haneefmubarak,是的,代码已经被正确地内联了。而且,memcpy
远远不能提供最佳速度,因为强制代码接受直接的u32
访问会带来巨大的性能提升。因此,一些更好的可能性必须存在。
非常感谢@artless_noise。godbolt服务的链接是无价的。我从未能够如此清晰地看到C源代码及其汇编表示之间的等价关系。这是非常有启发性的。
我完成了@artless的一个示例,它给出了以下结果:
#include <stdlib.h>
#include <memory.h>
typedef unsigned int u32;
u32 reada32(const void* ptr) { return *(const u32*) ptr; }
u32 readu32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
使用ARM GCC 4.8.2编译时,选择-O3或-O2:
reada32(void const*):
ldr r0, [r0]
bx lr
readu32(void const*):
ldr r0, [r0] @ unaligned
sub sp, sp, #8
str r0, [sp, #4] @ unaligned
ldr r0, [sp, #4]
add sp, sp, #8
bx lr
相当有意思....
ldr r0,[r0];str r0,[sp,#4];ldr r0,[sp,#4]
。遗憾的是它无法完全省略对局部变量的使用,但这就是您的未对齐字加载;没有多个字节的加载或调用memcpy。 - Notlikethat