【问题标题】:unaligned access store on intel processor英特尔处理器上的未对齐访问存储
【发布时间】:2017-09-22 11:12:21
【问题描述】:

考虑下面的示例。它在标记行出现 gcc 5.4 的段错误时 我用g++ -O3 -std=c++11 编译它。它在指令movaps 处失败,我怀疑它执行未对齐的内存访问。可能是 gcc 为这样一个简单的示例生成了非法代码,还是我遗漏了一些东西? 我在 Intel i5-5200U 上运行它。

#include <vector>
#include <memory>
#include <cstdint>

using namespace std;

__attribute__ ((noinline))
void SerializeTo(const vector<uint64_t>& v, uint8_t* dest) {
  for (size_t i = 0; i < v.size(); ++i) {
    *reinterpret_cast<uint64_t*>(dest) = v[i];  // Segfaults here.
    dest += sizeof(uint64_t);
  }
}

int main() {
 std::vector<uint64_t> d(64);

 unique_ptr<uint8_t[]> tmp(new uint8_t[1024]);

 SerializeTo(d, tmp.get() + 6);

 return 0;
}

【问题讨论】:

  • 你可以用g++ -S -O2 -fverbose-asm查看生成的代码
  • 使用 -O2 不会生成矢量化代码。
  • 然后用你想要的任何优化替换-O2,例如-O3 -march=native
  • 然后它会出现段错误。我不关注
  • 非常感谢@BasileStarynkevitch。我也相信更多的编译器。这就是为什么我在这里寻求建议的原因。你的不是很有帮助:)

标签: c++ gcc


【解决方案1】:

您在数组中步进了 6 个字节,因此它现在未对齐。编译器无法知道它必须避免需要对齐的指令;这就是类型双关语是未定义行为的原因。

【讨论】:

  • 是的,它没有对齐。我认为如果不能确定地址是否对齐,编译器应该生成可以处理未对齐地址的代码。你不同意吗?
  • 编译器“知道”地址是对齐的,因为所有使此类指针的有效方法都会产生对齐的地址。 (这里,“知道”的意思是“可以假设,因为替代方案是结果无关紧要的未定义行为”。)
  • @Roman:即使数据是对齐的,当您违反严格别名时,编译器也没有义务生成有效代码。 (这是未定义的行为)
  • @Roman:内核和处理器将允许您使用对其有效的指令写入任何内存位置。但是你的牛肉是编译器(又名 C++),而不是操作系统。 C++ 说你不能这样做,现在你知道为什么了。 memcpy 取void*,所以它当然不知道有什么对齐要求。
  • Roman:如果您想了解更多信息,请参阅标准中以“如果程序尝试通过以下类型之一以外的泛左值访问对象的存储值,行为未定义"
【解决方案2】:

在 c++ 中合法地执行类型双关的方法很少。

魔术函数std::memcpy是这里的首选工具:

__attribute__ ((noinline))
void SerializeTo(const vector<uint64_t>& v, uint8_t* dest) {
  for (size_t i = 0; i < v.size(); ++i) {
      std::memcpy(dest, std::addressof(v[i]), sizeof(v[i]));
    dest += sizeof(uint64_t);
  }
}

-std=c++11 -O3 -march=native -Wall -pedantic 的结果输出

SerializeTo(std::vector<unsigned long, std::allocator<unsigned long> > const&, unsigned char*):   # @SerializeTo(std::vector<unsigned long, std::allocator<unsigned long> > const&, unsigned char*)
        mov     rax, qword ptr [rdi]
        cmp     qword ptr [rdi + 8], rax
        je      .LBB0_3
        xor     ecx, ecx
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
        mov     rax, qword ptr [rax + 8*rcx]
        mov     qword ptr [rsi + 8*rcx], rax
        add     rcx, 1
        mov     rax, qword ptr [rdi]
        mov     rdx, qword ptr [rdi + 8]
        sub     rdx, rax
        sar     rdx, 3
        cmp     rcx, rdx
        jb      .LBB0_2
.LBB0_3:
        ret

https://godbolt.org/g/ReGA9N

【讨论】:

  • 谢谢理查兹。那么下面的解决方案呢? void SerializeTo(const vector& v, uint8_t* dest) { union U { uint64_t val; uint8_t buf[sizeof(uint64_t)]; }; for (size_t i = 0; i (dest)->val = v[i]; dest += sizeof(uint64_t); } }
  • @Roman 它只在 c++ 中工作,因为目标类型是 uin8_t[]。通常,通过联合的类型双关语在 C++ 中是 UB,但是有一个特定的规则允许它用于 uint8_t。在一般情况下,memcpy 更好(效率也不低,因为编译器将其识别为魔法)。
  • @Roman re 更短的代码,你的意思是汇编程序还是源代码?对于 gcc7 上的我来说,它产生完全相同的目标代码。
  • 试试这个:clang 发疯了,并创建了两条代码路径 - 一种用于对齐地址和大块的超高效路径以及处理未对齐边缘情况的代码__attribute__ ((noinline)) void SerializeTo(const vector&lt;uint64_t&gt;&amp; v, uint8_t* dest) { for(auto&amp;&amp; x : v) { std::memcpy(dest, std::addressof(x), sizeof(x)); dest += sizeof(x); } }
猜你喜欢
  • 2015-09-25
  • 1970-01-01
  • 2017-01-04
  • 2017-12-16
  • 2017-05-23
  • 2016-01-27
  • 2011-01-08
  • 2014-09-30
  • 1970-01-01
相关资源
最近更新 更多