gpt4 book ai didi

c++ - GCC : how does it decide?中的`movaps`与`movups`

转载 作者:行者123 更新时间:2023-12-02 15:57:28 26 4
gpt4 key购买 nike

我最近在一个用GCC 8编译的软件中研究了段错误。代码如下所示(这只是一个草图)

struct Point
{
int64_t x, y;
};

struct Edge
{
// some other fields
// ...
Point p; // <- at offset `0xC0`

Edge(const Point &p) p(p) {}
};

Edge *create_edge(const Point &p)
{
void *raw_memory = my_custom_allocator(sizeof(Edge));
return new (raw_memory) Edge(p);
}

这里的关键点是 my_custom_allocator()将指针返回未对齐的内存。代码崩溃是因为为了将原始点 p复制到新对象的字段 Edge::p中,编译器在[内联]构造函数代码中使用了 movdqu/ movaps
movdqu 0x0(%rbp), %xmm1  ; read the original object at `rbp`
...
movaps %xmm1, 0xc0(%rbx) ; store it into the new `Edge` object at `rbx` - crash!

最初,这里的一切似乎都很清楚:内存未正确对齐, movaps崩溃。我的错。

但是,是吗?

尝试在Godbolt上重现该问题,我观察到GCC 8实际上试图相当智能地处理它。当确定内存正确对齐后,就使用 movaps,就像在我的代码中一样。这
#include <new>
#include <cstdlib>

struct P { unsigned long long x, y; };

unsigned char buffer[sizeof(P) * 100];

void *alloc()
{
return buffer;
}

void foo(const P& s)
{
void *raw = alloc();
new (raw) P(s);
}

结果
foo(P const&):
movdqu xmm0, XMMWORD PTR [rsi]
movaps XMMWORD PTR buffer[rip], xmm0
ret

https://godbolt.org/z/a3uSid

但是,如果不确定,它将使用 movups。例如。如果我在上面的示例中“隐藏”了分配器的定义,它将选择同一代码中的 movups
foo(P const&):
push rbx
mov rbx, rdi
call alloc()
movdqu xmm0, XMMWORD PTR [rbx]
movups XMMWORD PTR [rax], xmm0
pop rbx
ret

https://godbolt.org/z/cNKe5A

因此,如果应该以这种方式运行,为什么在我在本文开头提到的软件中使用 movaps呢?就我而言, my_custom_allocator()的实现在调用时对于编译器是不可见的,这就是为什么我希望GCC选择 movups的原因。

还有哪些其他因素在起作用?这是GCC中的错误吗?如何强制GCC使用 movups,最好在任何地方使用?

最佳答案

更新:由于x86-64系统V上的alignof(Edge)long double为16,因此在UB的未对齐地址处添加一个是UB。这告诉GCC使用movaps是安全的。

IDK为什么从(%rbp)加载它也没有使用movaps。我以为隐含的Edge不会对齐16字节,因此基于该猜测(我将其移至最后),此答案的整个内容都是如此。

某些类型可能需要16字节对齐,尤其是long double
x86-64系统V上的 alignof(max_align_t) == 16替代malloc至少需要返回对齐的内存,以分配16字节或更大的内存。

(当然,较小的分配不能容纳16个字节的对象,因此不需要16个字节的对齐方式。您可以要求对象的特定实例与alignas(16) int foo;过度对齐,但是类型本身是否具有更高的对齐方式?对齐方式还具有较大的sizeof,因此数组仍将遵守常规规则,并且每个元素都满足对齐要求。)

另请参见Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?,其中带有未对齐的uint16_t*的自动矢量化会导致段错误。同样,Pascal Cuoq's blog about alignment并具有比alignof(T)少的对齐方式的对象也是未定义的行为,并且对于编译器来说,没有UB的假设如何深入。

指令选择

GCC和clang只要能证明必须充分对齐内存就使用movaps(假设没有UB)。在Core2和更早版本以及K10和更早版本上,即使内存在运行时碰巧对齐,未对齐的存储指令也会变慢。

Nehalem和Bulldozer对此进行了更改,但即使只能在具有廉价movaps的CPU上执行,GCC仍然使用-mtune=haswell甚至使用vmovaps,甚至使用-march=haswellvmovups

MSVC和ICC从不使用movaps,这会损害非常旧的CPU上的性能,但有时会让您摆脱数据对齐问题。它们会将对齐后的负载折叠到SSE指令(如paddd xmm0, [rdi])(需要对齐,不同于AVX1等效文件)的SSE指令的内存操作数中,因此它们有时仍会生成会因对齐错误而出错的代码,但通常仅在启用优化的情况下。 IMO并不是特别好。
alignof(Point)只能为8(继承其最对齐的成员int64_t的对齐方式)。因此,GCC只能证明任意Point的8字节对齐,而不是16。

对于静态存储,GCC可以知道它选择将数组对齐16,因此可以使用movaps/movdqa从该数组加载。 (此外,x86-64 System V ABI要求将16字节或更大的静态数组与16对齐,因此,即使对于在其他某个编译单元中定义的extern unsigned char buffer[]全局变量,GCC也可以假定这样做。)

您尚未显示Edge的定义,因此IDK为什么它具有16字节对齐,但可能是alignof(Edge) == 16?否则,可能是编译器错误。

但是它使用Edge从堆栈中加载原始movups对象的事实似乎表明alignof(Edge) < 16raw_memory = __builtin_assume_aligned(raw_memory, 8);可能有帮助吗? IDK是否可以告诉GCC假设比其他人认为的要低。

您可以告诉GCC,可以通过定义如下的typedef来使Edge(或int)始终未对齐:

typedef long __attribute__((aligned(1), may_alias)) unaligned_aliasing_long;
may_alias实际上与对齐正交,但是值得一提,因为这种情况的用例之一是从 char[]缓冲区中加载用于解析字节流。在这种情况下,您会两者都想要。这是使用 memcpy(tmp, src, sizeof(tmp));进行未对齐的严格混叠安全加载的替代方法。

GCC使用may_alias定义__m128,并使用may_alias,aligned(1)作为定义_mm_loadu_ps的一部分(未对齐的SIMD负载(如movups)的内在函数)。 (不需要 may_aliasfloat数组中加载float vector ,但是需要 may_alias从其他内容中加载float。)

与glibc的后备C实现不同,请参阅 Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?中的标量代码,我认为该标量代码对于未对齐/别名化的 unsigned long是安全的。 (必须在不使用 -flto的情况下进行编译,因此它不能内联到其他glibc函数中,并且由于违反严格混叠而中断。)

分配者和假定的对齐方式

(本节假设使用 alignof(Edge) < 16编写。这里不是这种情况,即使不是问题的起因,函数属性也可能很有用,即使它们不是问题的起因。也可能不是可行的解决方法。)

您也许可以在分配器上使用 __attribute__ ((assume_aligned (8)))来告知GCC它返回的指针的对齐方式。

GCC可能出于某种原因假设您的分配器返回了可用于任何对象的内存(由于 alignof(max_align_t) == 16等原因,x86-64 System V上的 long double,以及Windows x64上的 mmap)。

如果不是这种情况,您也许可以说出来。这个 Why does glibc's strlen need to be so complicated to run quickly?,我们可以看到GCC确实“知道”了 malloc并对其进行了特殊处理。但是,如果您的函数没有ISO C或C++定义的名称或GNU C属性,那将令人惊讶。 IDK,如果不是编译器错误,那么根据您所显示的内容,这是迄今为止最好的猜测。 (这是可能的。)

void* mis-alignment Q&A:

void* my_alloc1 (size_t) __attribute__((assume_aligned (16)));
void* my_alloc2 (size_t) __attribute__((assume_aligned (32, 8)));

declares that my_alloc1 returns 16-byte aligned pointers and that my_alloc2 returns a pointer whose value modulo 32 is equal to 8.



我不知道为什么会假设一个函数返回并转换为另一种类型的 movups会比正在构造的对象的类型具有更多的对齐方式。 我们可以使用Edge从某个地方加载alignof(Edge) < 16。那似乎表明__attribute__((alloc_size(1)))

另一个相关的是 the GCC manual,它告诉GCC该函数的第一个arg是一个大小。如果您的函数将显式对齐方式作为arg,请使用 alloc_align (position)进行指示,否则不要这样做。

关于c++ - GCC : how does it decide?中的`movaps`与`movups`,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61196175/

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