gpt4 book ai didi

c - 在编写干净的 C 代码时利用 ARM 未对齐的内存访问

转载 作者:太空狗 更新时间:2023-10-29 16:29:49 33 4
gpt4 key购买 nike

过去,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;
}

这是正确的,将为任何能够或不能在未对齐位置读取的 CPU 生成正确的代码。更好的是,在 x86/x64 上,它针对单个读取操作进行了适当的优化,因此具有与第一个语句相同的性能。它便携、安全且快速。谁能问更多?

好吧,问题是,在 ARM 上,我们就没那么幸运了。

memcpy版本确实是安全的,但似乎会导致系统的谨慎操作,这对于 ARMv6 和 ARMv7(基本上,任何智能手机)都非常慢。

在严重依赖读取操作的面向性能的应用程序中,可以测量第 1 版和第 2 版之间的差异:它位于 > 5x gcc -O2设置。这太多了,不容忽视。

为了找到一种使用 ARMv6/v7 功能的方法,我寻找了一些示例代码的指导。不幸的是,他们似乎选择了第一条语句(直接 u32 访问),这不应该是正确的。

这还不是全部:新的 GCC 版本现在正在尝试实现自动矢量化。在 x64 上,这意味着 SSE/AVX,在 ARMv7 上意味着 NEON。 ARMv7 还支持一些新的“加载多个”(LDM) 和“存储多个”(STM) 操作码,它们需要指针对齐。

这意味着什么 ?好吧,编译器可以自由地使用这些高级指令,即使它们不是从 C 代码中专门调用的(不是内在的)。为了做出这样的决定,它使用了 u32* pointer 的事实。应该在 4 个字节上对齐。如果不是,那么所有的赌注都将关闭:未定义的行为、崩溃。

这意味着即使在支持未对齐内存访问的 CPU 上,现在使用直接 u32 也是危险的。访问,因为它可能导致在高优化设置下生成错误代码 ( -O3 )。

所以现在,这是一个难题:如何在不写入不正确版本的情况下访问未对齐内存访问 ARMv6/v7 的 native 性能 u32使用权 ?

PS:我也试过 __packed()指令,从性能的角度来看,它们的工作方式似乎与 memcpy 完全相同。方法。

[编辑]:感谢到目前为止收到的优秀元素。

查看生成的程序集,我可以确认@Notlikethat 发现 memcpy version 确实生成了正确的 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;
}

一旦在 -O3 或 -O2 使用 ARM GCC 4.8.2 编译:
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

说得很有道理....

最佳答案

好吧,情况比人们想象的更困惑。因此,为了澄清,以下是这次旅程的发现:

访问未对齐的内存

  • 访问未对齐内存的唯一可移植 C 标准解决方案是 memcpy一。我希望通过这个问题得到另一个,但显然这是迄今为止发现的唯一一个。

  • 示例代码:
    u32 read32(const void* ptr)  { 
    u32 value;
    memcpy(&value, ptr, sizeof(value));
    return value; }

    此解决方案在所有情况下都是安全的。它还编译成一个微不足道的 load register使用 GCC 在 x86 目标上进行操作。

    但是,在使用 GCC 的 ARM 目标上,它会转化为过大且无用的汇编序列,从而降低性能。

    在 ARM 目标上使用 Clang, memcpy工作正常(见下面的@notlikethat 评论)。归咎于 GCC 很容易,但没那么简单: memcpy解决方案在具有 x86/x64、PPC 和 ARM64 目标的 GCC 上运行良好。最后,尝试另一个编译器 icc13,memcpy 版本在 x86/x64 上出奇地重(4 条指令,一个应该足够了)。这只是我目前可以测试的组合。

    不得不感谢godbolt的项目做出这样的声明 easy to observe .
  • 第二种解决方案是使用 __packed结构。这个解决方案不是 C 标准的,完全取决于编译器的扩展。因此,编写它的方式取决于编译器,有时还取决于它的版本。这对于维护可移植代码来说是一团糟。

  • 话虽如此,在大多数情况下,它比 memcpy 产生更好的代码生成。 .大多数情况下只...

    例如,对于上述情况, memcpy解决方案不起作用,以下是调查结果:
  • 在带有 ICC 的 x86 上:__packed解决方案
  • 在带有 GCC 的 ARMv7 上:__packed解决方案
  • 在带有 GCC 的 ARMv6 上:不起作用。组装看起来比memcpy还要丑.
  • 最后的解决方案是使用直接u32访问未对齐的内存位置。这个解决方案曾经在 x86 cpu 上工作了几十年,但不推荐,因为它违反了一些 C 标准原则:编译器被授权将此声明视为数据正确对齐的保证,从而导致错误代码生成。

  • 不幸的是,至少在一种情况下,它是唯一能够从目标中提取性能的解决方案。即针对 ARMv6 上的 GCC。

    但是不要在 ARMv7 上使用这个解决方案:GCC 可以生成为对齐内存访问保留的指令,即 LDM (Load Multiple),导致崩溃。

    即使在 x86/x64 上,如今以这种方式编写代码也变得危险,因为新一代编译器可能会尝试自动矢量化一些兼容循环,基于这些内存位置正确对齐的假设生成 SSE/AVX 代码,从而导致崩溃该程序。

    回顾一下,以下是汇总为表格的结果,使用约定:memcpy > 打包 > 直接。
    | 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 |

    关于c - 在编写干净的 C 代码时利用 ARM 未对齐的内存访问,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32062894/

    33 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com