【问题标题】:Correctness of misaligned access in C++C++ 中未对齐访问的正确性
【发布时间】:2017-06-25 11:35:35
【问题描述】:

根据我的阅读,未对齐的访问主要意味着两件事:

  1. 可能会出现性能损失
  2. 失去对齐访问所具有的加载和存储的原子性

假设性能不是问题,并且我希望从软件中获得正确性,那么未对齐的访问有多糟糕?我的理解是 x86 CPU 将正确处理此类访问,但可能需要做额外的工作来获取数据。

让我提出这个问题的原因是使用-fsanitize=undefined 编译我的代码。我收到了许多关于未对齐存储/加载的错误。我不确定这是否是个问题,因为:

  1. 存储仅在数据准备期间执行,这是一个单线程进程,因此我不担心原子性丢失
  2. 加载在多线程进程中执行,其中许多线程(四个或更多)访问数据,但数据不会被任何线程修改(保存在 const uint8_t* 变量中)

访问未对齐的原因是 const uint8_t* 数组包含来自许多不同类型的字节(uint8_tuint16_tuint32_tuint64_tint64_t)。

我确信没有负载超出分配的uint8_t 数组的范围(例如,程序永远不会从指向已分配内存块的最后一个、两个或三个字节的地址加载uint64_t),并且确信我的访问都是正确的 - 只是未对齐。

我读到的另一件事是,这样的加载可能违反了严格的别名规则,但代码编译时没有使用-Wstrict-aliasing -Werror(我很久以前启用)发出任何警告。

我应该填充uint8_t 数组中的数据以确保访问对齐,还是可以安全地忽略警告?

【问题讨论】:

  • 如果您希望避免未定义的行为,则只能通过 std::memcpy 访问数组中的数据(进入/来自目标数据类型)。 ponters(uint8_t*char* 除外)的直接访问是 UB。
  • “我的理解是 CPU 会正确处理此类访问,但可能需要做额外的工作来获取数据。” 哪个 CPU? x86 对非对齐访问很好,但在其他 CPU 上,它们是致命错误。在其他 CPU 上,用户可以选择错位是否是致命错误,或者是否会以巨大的性能成本进行修复。
  • @CodyGray 谢谢,我澄清了这个问题。
  • @RichardCritten 谢谢,如果您将您的评论作为答案,我会接受。使用 std::memcpy() 修复了消毒剂引发的警告。

标签: c++ memory undefined-behavior memory-alignment


【解决方案1】:

有些平台不支持非对齐访问(您会遇到崩溃)。而且,有些平台支持非对齐访问,但有些 asm 指令需要对齐访问。例如,在ARM上,有LDRD指令,需要对齐内存地址。而且,不幸的是,编译器可以免费使用该指令。但是,通常,有一个编译器扩展告诉编译器指针未对齐,因此它不会使用 LDRD。

在支持UA的平台上,有你提到的惩罚。

我推荐你使用 memcpy。它适用于所有平台,现在编译器可以很好地优化它(所以你不会得到 memcpy 调用,而是快速 mov 指令)。

【讨论】:

  • “现在编译器可以很好地优化它(所以你不会得到 memcpy 调用,而是快速的 mov 指令)” 嗯,一些编译器。 MSVC 不做这种优化。但是,它没有-fsanitize=undefined,也没有抓住标准提供的每一个机会以可能破坏代码的方式进行优化(换句话说,它的优化器是well distinguished from an adversary)。
  • @Cody:哪个版本不这样做?我检查了VS2015(编译器版本19.00.24213.1),并对其进行了优化
  • 那么基本上是 VS 2015 之前的每个版本!不过,最近对优化器进行了一些重大更改,所以这可能就是其中之一。很高兴看到这一点。但我仍然怀疑它在检测优化可能性方面有多好。你写的测试代码可能很简单。我已经反汇编了真实的代码,发现它没有应用优化。
  • @Cody:是的,很简单。你为什么怀疑?据我了解,检测和应用这种优化很容易
【解决方案2】:

主要问题不是性能或原子性,而是正确性。未对齐的访问会根据 C 和 C++ 标准调用未定义的行为,因此您不能依赖任何特定的结果。它可能会工作,也可能会崩溃。或者它可能先工作,然后停止工作。这是您收到的错误消息的本质。如果您知道错误总是对您有用,您可以选择忽略这些错误,但是由于您要求通过使用相应的编译器开关标记此类错误,因此您应该努力避免它们是合理的,特别是如果您不确定您的代码是否会永远保留在此平台上。此外,你怎么知道即使在同一个平台上它也总是对你有用?

从你写的看,数据似乎是由稍后读取它的同一台机器写入的,只是线程不同。如果是这样,您应该尝试以正确对齐的方式写入数据,即在适当的地方使用填充。您可以通过将数据打包在正确定义的结构而不是非结构化缓冲区中来获得编译器的帮助。这也将为您提供更多的类型安全性。

否则,您将不得不担心的不仅仅是对齐问题。例如,您还需要考虑字节序。在这种情况下,您可能正在编写一种可能最终在不同机器上的外部数据记录。您正在寻找一种与机器无关的外部数据表示,您可以自己定义它,或者最好使用为 RPC 发明的几种标准表示中的一种,它的优点是您可以找到库来进行读取和写入。

【讨论】:

  • 我知道字节序问题。我认为我无法从编译器那里获得帮助,因为我有点需要将数据作为非结构化缓冲区。
  • 那你可能想看看std::alignalignof
  • 该标准承认非便携式程序的合法性,并且作者认识到该语言在许多领域的有用性源于实现对标准未强制要求的功能的支持,包括作为“用于各种平台的高级汇编程序。指定它仅用于寻求以这种方式使用的编译器的代码可能无法移植到不适合这种目的的编译器,但这几乎不会使其“不正确”。
猜你喜欢
  • 1970-01-01
  • 2017-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-07
  • 2012-07-23
相关资源
最近更新 更多