Cya*_*yan 18 c arm memory-alignment
过去,ARM处理器无法正确处理未对齐的内存访问(ARMv5及更低版本).喜欢的东西u32 var32 = *(u32*)ptr;也只是失败(引发异常),如果ptr没有正确的4字节对齐.
编写这样的语句对于x86/x64可以正常工作,因为这些CPU总是非常有效地处理这种情况.但根据C标准,这不是一种"正确"的写作方式.u32显然等同于4个字节的结构,必须在4个字节上对齐.
在保持正统正确性并确保与任何cpu完全兼容的同时获得相同结果的正确方法是:
u32 read32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
Run Code Online (Sandbox Code Playgroud)
这个是正确的,将为任何能够或不在未对齐位置读取的CPU生成适当的代码.更好的是,在x86/x64上,它已针对单个读取操作进行了适当优化,因此具有与第一个语句相同的性能.它便携,安全,快速.谁能问更多?
好吧,问题是,在ARM上,我们没有那么幸运.
编写memcpy版本确实是安全的,但似乎导致系统谨慎的操作,这对于ARMv6和ARMv7(基本上是任何智能手机)来说都非常慢.
在一个严重依赖读取操作的性能导向应用程序中,可以测量第一版和第二版之间的差异:它在设置时 大于5倍gcc -O2.这太过不容忽视了.
试图找到一种使用ARMv6/v7功能的方法,我寻找了几个示例代码的指导.不幸的是,他们似乎选择了第一个声明(直接u32访问),这不应该是正确的.
这还不是全部:新的GCC版本现在正在尝试实现自动矢量化.在x64上,这意味着SSE/AVX,在ARMv7上意味着NEON.ARMv7还支持一些新的"加载多个"(LDM)和"存储多个"(STM)操作码,这些操作码需要指针对齐.
那是什么意思 ?好吧,编译器可以自由地使用这些高级指令,即使它们没有从C代码中特别调用(没有内在的).为了做出这样的决定,它使用a u32* pointer应该在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;
}
Run Code Online (Sandbox Code Playgroud)
一旦使用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
Run Code Online (Sandbox Code Playgroud)
说得好......
Cya*_*yan 14
好吧,情况比人们想要的更令人困惑.所以,为了澄清,以下是这次旅程的发现:
memcpy之一.我希望通过这个问题得到另一个,但显然它是迄今为止发现的唯一一个.示例代码:
u32 read32(const void* ptr) {
u32 value;
memcpy(&value, ptr, sizeof(value));
return value; }
Run Code Online (Sandbox Code Playgroud)
该解决方案在所有情况下都是安全的 它还load register使用GCC 编译为x86目标上的一个简单操作.
但是,在使用GCC的ARM目标上,它转换为一种太大且无用的装配顺序,这会降低性能.
在ARM目标上使用Clang,memcpy工作正常(请参阅下面的@notlikethat评论).将GCC归咎于GCC很容易,但事情并非如此简单:该memcpy解决方案在GCC上运行时可以使用x86/x64,PPC和ARM64目标.最后,尝试另一个编译器icc13,memcpy版本在x86/x64(4个指令,而一个应该足够)上出乎意料地重.这就是我到目前为止可以测试的组合.
我要感谢godbolt的项目,使这些陈述易于观察.
__packed结构.此解决方案不是C标准,完全取决于编译器的扩展.因此,编写它的方式取决于编译器,有时取决于其版本.这对于维护便携式代码来说是一团糟.话虽如此,在大多数情况下,它会导致更好的代码生成memcpy.在大多数情况下只...
例如,对于memcpy解决方案不起作用的上述情况,以下是调查结果:
__packed解决方案有效__packed解决方案有效在带有GCC的ARMv6上:不起作用.装配看起来甚至比memcpy.
u32访问未对齐的内存位置.此解决方案过去在x86 cpu上工作了几十年,但不建议这样做,因为它违反了一些C标准原则:编译器被授权将此语句视为数据正确对齐的保证,从而导致错误的代码生成.不幸的是,至少在一种情况下,它是唯一能够从目标中提取性能的解决方案.即用于ARMv6上的GCC.
不要将此解决方案用于ARMv7:GCC可以生成为对齐的内存访问保留的指令,即LDM(加载多个),导致崩溃.
即使在x86/x64上,现在以这种方式编写代码也变得很危险,因为新一代编译器可能会尝试自动向量化一些兼容的循环,基于这些内存位置正确对齐的假设生成SSE/AVX代码,崩溃该程序.
作为回顾,以下是使用约定汇总为表的结果:memcpy> packed> direct.
| compiler | x86/x64 | ARMv7 | ARMv6 | ARM64 | PPC |
|-----------|---------|--------|--------|--------|--------|
| GCC 4.8 | memcpy | packed | direct | memcpy | memcpy |
| clang 3.6 | memcpy | memcpy | memcpy | memcpy | ? |
| icc 13 | packed | N/A | N/A | N/A | N/A |
Run Code Online (Sandbox Code Playgroud)
部分问题可能是您不允许轻松内联和进一步优化。拥有专门的加载函数意味着每次调用时都可能会发出函数调用,这可能会降低性能。
您可以做的一件事是 use static inline,这将允许编译器内联该函数load32(),从而提高性能。但是,在更高级别的优化下,编译器应该已经为您内联了这一点。
如果编译器内联一个 4 字节 memcpy,它可能会将其转换为最有效的一系列加载或存储,这些加载或存储仍然可以在未对齐的边界上工作。因此,如果即使启用了编译器优化,您仍然看到低性能,这可能是您正在使用的处理器上未对齐读取和写入的最大性能。既然你说“__packed指令”产生与 相同的性能memcpy(),那么情况似乎就是这样。
此时,除了对齐数据之外,您几乎无能为力。但是,如果您正在处理连续的未对齐数组u32,您可以做一件事:
#include <stdint.h>
#include <stdlib.h>
// get array of aligned u32
uint32_t *align32 (const void *p, size_t n) {
uint32_t *r = malloc (n * sizeof (uint32_t));
if (r)
memcpy (r, p, n);
return r;
}
Run Code Online (Sandbox Code Playgroud)
这只是使用分配一个新数组malloc(),因为malloc()和朋友为所有内容分配正确对齐的内存:
malloc() 和 calloc() 函数返回指向已分配内存的指针,该指针适合任何类型的变量。
这应该相对较快,因为每组数据只需执行一次。此外,在复制它时,memcpy()将能够仅针对初始缺乏对齐进行调整,然后使用可用的最快对齐加载和存储指令,之后您将能够使用正常对齐读取和写入来处理数据表现。
| 归档时间: |
|
| 查看次数: |
6656 次 |
| 最近记录: |