SSE 和 C++ 容器

11

以下代码为什么会导致段错误?

#include <vector>
#include <emmintrin.h>

struct point {
    __m128i v;

  point() {
    v = _mm_setr_epi32(0, 0, 0, 0);
  }
};

int main(int argc, char *argv[])
{
  std::vector<point> a(3);
}

谢谢

编辑:我正在使用Linux/i686上的g++ 4.5.0,我可能不知道我在这里做什么,但是因为即使以下代码也会导致段错误:

int main(int argc, char *argv[])
{
  point *p = new point();
}

我真的认为这一定是对齐问题。


3
可能出现的明显问题是,如果 v 没有正确对齐。但是由 vector 动态分配,所以不会受到堆栈未对齐问题的影响。尽管如此,在尝试分配之前,最好先打印出 v 的地址,以确保它已经正确分配。 - Ben Voigt
1
顺便提一下,当您看到段错误时,最好说明您使用的编译器、版本和平台。我会尝试重现您的错误,但是如果不知道具体信息,我无法做到。 - Ben Voigt
1
32位堆分配器不能比8位更好地对齐。您需要使用vector<>的Allocator类型参数,并使用可以在16处对齐的分配器。检查您的CRT是否有一个,或通过超额分配自己来制作一个。 - Hans Passant
@Ben:没问题,只有SSE指令需要对齐。vector<>类没有这个要求。 - Hans Passant
显示剩余5条评论
4个回答

11
成功的可能性非常高,除非v没有被正确对齐。 但是,它是由vector动态分配的,因此不会受到堆栈错位问题的影响。
然而,正如phooji正确指出的那样,一个“模板”或“原型”值会被传递给std::vector构造函数,并将被复制到向量的所有元素中。这就是std::vector::vector的参数,将被放置在堆栈上并可能未对齐的原因。
一些编译器有一个用于控制函数内堆栈对齐的pragma(基本上,编译器会浪费额外的空间,以便按需要正确对齐所有局部变量)。
根据Microsoft文档,Visual C++ 2010应自动为SSE类型设置8字节堆栈对齐自Visual C++ 2003以来一直如此
至于gcc,我不知道。
在C++0x下,使new point()返回未对齐的存储是严重不符合规范的。[basic.stc.dynamic.allocation]指出(使用草案n3225中的措辞):

分配函数尝试分配所请求的存储量。如果成功,则应返回一个指向至少与请求的大小一样大的字节长度块的起始地址。在从分配函数返回之后,分配存储的内容没有任何限制。连续调用同一分配函数分配的存储的顺序、连续性和初始值是未指定的。返回的指针应适当地对齐,以便可以将其转换

将指向任何完整对象类型的指针满足基本对齐要求(3.11),然后用于访问分配的存储器中的对象或数组(直至通过调用相应的释放函数显式释放存储器)。

[basic.align]还说明:

此外,对于运行时分配无法满足所请求的对齐方式的动态存储器的请求应视为分配失败。

您可以尝试使用更新版本的gcc来解决这个问题吗?


感谢您的评论,事实上gcc 4.6 C++0x支持状态报告称,尚未实现对齐支持(N2341)。 - andreabedini
很奇怪这被接受为答案,因为它是错误的...问题不在于堆栈对齐,而在于堆对齐。他没有调用new point(当然这样做没问题);std::vector类正在调用带有一些大小的::operator new,并且在32位GCC上返回的内存块仅对齐8字节(即使使用我尝试过的最新版本GCC 7.3)。 - Nemo
@Nemo:你还没有排除堆栈对齐问题。在任何特定的系统上,你可能需要检查两者,而且其中任何一个都可能出现问题。你说你在i686 gcc上测试了::operator new(),并得到了8字节的对齐方式,但你传递了什么参数? - Ben Voigt
我没有测试::operator new。我试图在一个__m128i向量上使用push_back(更准确地说,是一个包含此类型单个成员的结构体向量)。我在段错误处反汇编,发现了一个对8字节对齐堆地址的对齐存储。这些类型在堆栈上的不对齐是极不可能的;这将是一个严重的编译器错误,更不用说这些类型是通过SSE寄存器传递的了。这个问题仅仅是由于32位和64位Linux平台上的malloc/new有不同的对齐保证,以及GCC在移动这些类型时使用对齐的加载/存储。 - Nemo
@Nemo:堆上的不对齐是一个严重的工具链错误 - 我引用了适用的标准规则,因此您不能根据此得出哪个更可能。 - Ben Voigt
据我所知,32位平台上最新的GCC / glibc仍然只保证由malloc / new返回的内存块具有8字节对齐。这些16字节的SSE类型不是标准的,这可能就是使其符合行为的原因。(否则这是一个15多年的老漏洞。) - Nemo

3
您正在使用的vector构造函数实际上是这样定义的:
explicit vector ( size_type n, const T& value= T(), const Allocator& = Allocator() );

请参考例如 http://www.cplusplus.com/reference/stl/vector/vector/ 等网站。

换句话说,一个元素是默认构造的(即调用构造函数时的默认参数值),然后其余的元素通过复制第一个元素来创建。我猜测你需要为point编写一个拷贝构造函数,以正确处理__m128i值的(非)拷贝。

更新:当我尝试使用Visual Studio 2010 (v. 10.0.30319.1)构建你的代码时,我收到以下构建错误:

error C2719: '_Val': formal parameter with __declspec(align('16')) won't be aligned c:\program files\microsoft visual studio 10.0\vc\include\vector 870 1   meh

这表明本问题与对齐有关,Ben的看法是正确的。

2
__m128i 应该是可平移的,我不认为这是问题。即使它不是,point 的自动生成复制构造函数也会调用 __m128i 的正确复制构造函数。 - Ben Voigt
@Ben Voigt:可能是因为错误指向了 vector 的一个 resize 重载函数。 - phooji
@Ben:这是因为标准库中定义的 new 运算符没有考虑对齐指令,正如 @phooji 的 VS2010 错误信息所示。 - rwong
@rwong:在Visual C++ 2010上,动态分配的内存是正确对齐的(我相信,但还没有测试过)。@phooji收到的错误消息与参数有关,这些参数不使用动态分配,但可能会受到堆栈错位的影响。 - Ben Voigt
@Cygnus:我在我的回答中引用了这段话(它不适合放在评论中)。 - Ben Voigt
显示剩余4条评论

1

你的编译器STL实现中默认分配器分配的内存可能没有对齐。这将取决于具体的平台和编译器供应商。

通常,默认分配器使用operator new,它通常不保证超过字大小(32位或64位)的对齐。为了解决这个问题,可能需要实现一个自定义分配器,它使用_aligned_malloc

此外,一个简单的修复方法(虽然不是令人满意的方法)是将值分配给本地__m128i变量,然后使用非对齐指令将该变量复制到结构中。例如:

struct point {
    __m128i v;
    point() {
        __m128i temp = _mm_setr_epi32(0, 0, 0, 0);
        _mm_storeu_si128(&v, temp);
    }
};

2
我已经测试了您提出的修复方案,对于我的 MS VS 版本,它产生了与原始代码相同的编译器错误。 - phooji
这确实不会崩溃。谢谢。 - andreabedini
不幸的是,您无法将 v 用于 SSE2 操作。但我认为向量元素未对齐。 - Ben Voigt
你需要一个访问器函数来对 v 进行 loadu 操作。正如你所说,这并不是一个令人满意的解决方案,因为对齐存储更加便宜。 - Peter Cordes

1

SSE指令集要求内存对齐到16字节。当你在栈上分配一个__m128时,没有问题,因为编译器会自动正确地对齐它们。处理动态内存分配的std::vector<>的默认分配器不会产生对齐的分配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接