VC++ SSE 代码生成 - 这是编译器错误吗?

VC++ SSE code generation - is this a compiler bug?

VC++ 中的一个非常特殊的代码序列生成了以下指令(对于 Win32):

unpcklpd    xmm0,xmmword ptr [ebp-40h]

出现2个问题:

(1) 据我了解英特尔手册,unpcklpd 接受 128 对齐的内存地址作为第二个参数。如果地址是相对于栈帧对齐的,则不能强制。这真的是编译器错误吗?

(2) 仅当 运行 来自调试器 时,才会在执行此指令 时抛出异常,即便如此也并非总是如此。即使附加到进程并执行此代码也不会抛出。怎么会这样??

抛出的特定异常是 0xFFFFFFFF 处的访问冲突,但据我所知,这只是未对齐的代码。


[编辑:] 这是一些演示错误代码生成的来源 - 但通常不会导致崩溃。 (这主要是我想知道的)

[编辑 2:] 代码示例现在重现了实际的崩溃。这个也会在调试器之外崩溃 - 我怀疑会出现差异,因为调试器会在不同的典型基地址启动程序。

    // mock.cpp
    #include <stdio.h>
    struct mockVect2d
    {
        double x, y;
        mockVect2d()    {}
        mockVect2d(double a, double b) : x(a), y(b) {}
        mockVect2d operator + (const mockVect2d& u) {
            return mockVect2d(x + u.x, y + u.y);
        }
    };

    struct MockPoly
    {
        MockPoly() {}
        mockVect2d*    m_Vrts;
        double  m_Area;
        int     m_Convex;
        bool    m_ParClear;

        void ClearPar()  { m_Area = -1.; m_Convex = 0; m_ParClear = true; }

        MockPoly(int len) { m_Vrts = new mockVect2d[len]; }

        mockVect2d& Vrt(int i) {
            if (!m_ParClear) ClearPar();
            return m_Vrts[i];
        }

        const mockVect2d& GetCenter() { return m_Vrts[0]; }
    };


    struct MockItem
    {
        MockItem() : Contour(1) {}
        MockPoly Contour;
    };

    struct Mock
    {
        Mock() {}
        MockItem m_item;
        virtual int GetCount()                  { return 2; }
        virtual mockVect2d GetCenter()  { return mockVect2d(1.0, 2.0); }
        virtual MockItem GetItem(int i) { return m_item; }
    };

    void testInner(int a)
    {
        int c = 8;
        printf("%d", c);
        Mock* pMock = new Mock;
        int Flag = true;
        int nlr = pMock->GetCount();

        if (nlr == 0)
            return;

        int flr = 1;
        if (flr == nlr)
            return;

        if (Flag)
        {
            if (flr < nlr && flr>0) {
                int c = 8;
                printf("%d", c);

                MockPoly pol(2);
                mockVect2d ctr = pMock->GetItem(0).Contour.GetCenter();

                // The mess happens here:
                //          ; 74   :            pol.Vrt(1) = ctr + mockVect2d(0., 1.0);
                // 
                //          call ? Vrt@MockPoly@@QAEAAUmockVect2d@@H@Z; MockPoly::Vrt
                //              movdqa  xmm0, XMMWORD PTR $T4[ebp]
                //              unpcklpd xmm0, QWORD PTR tv190[ebp]      **** crash!
                //              movdqu  XMMWORD PTR[eax], xmm0

                pol.Vrt(0) = ctr + mockVect2d(1.0, 0.);
                pol.Vrt(1) = ctr + mockVect2d(0., 1.0);
            }
        }
    }

    void main()
    {
        testInner(2);
        return;
    }

如果您愿意,请下载一个现成的 vcxproj,其中包含 here 中设置的所有开关。这也包括完整的 ASM。

既然没有人站出来,那我就来一炮。

1) 如果地址是相对于栈帧对齐的则不能强制。这真的是编译器错误吗?

我不确定您是否不能强制堆栈变量对齐。考虑这段代码:

struct foo
{
    char a;
    int b;
    unsigned long long c;
};

int wmain(int argc, wchar_t* argv[])
{
    foo moo;
    moo.a = 1;
    moo.b = 2;
    moo.c = 3;
}

查看 main 的启动代码,我们看到:

00E31AB0  push        ebp  
00E31AB1  mov         ebp,esp  
00E31AB3  sub         esp,0DCh  
00E31AB9  push        ebx  
00E31ABA  push        esi  
00E31ABB  push        edi  
00E31ABC  lea         edi,[ebp-0DCh]  
00E31AC2  mov         ecx,37h  
00E31AC7  mov         eax,0CCCCCCCCh  
00E31ACC  rep stos    dword ptr es:[edi]  
00E31ACE  mov         eax,dword ptr [___security_cookie (0E440CCh)]  
00E31AD3  xor         eax,ebp  
00E31AD5  mov         dword ptr [ebp-4],eax  

将 __declspec(align(16)) 添加到 moo 得到

01291AB0  push        ebx  
01291AB1  mov         ebx,esp  
01291AB3  sub         esp,8  
01291AB6  and         esp,0FFFFFFF0h  <------------------------
01291AB9  add         esp,4  
01291ABC  push        ebp  
01291ABD  mov         ebp,dword ptr [ebx+4]  
01291AC0  mov         dword ptr [esp+4],ebp  
01291AC4  mov         ebp,esp  
01291AC6  sub         esp,0E8h  
01291ACC  push        esi  
01291ACD  push        edi  
01291ACE  lea         edi,[ebp-0E8h]  
01291AD4  mov         ecx,3Ah  
01291AD9  mov         eax,0CCCCCCCCh  
01291ADE  rep stos    dword ptr es:[edi]  
01291AE0  mov         eax,dword ptr [___security_cookie (12A40CCh)]  
01291AE5  xor         eax,ebp  
01291AE7  mov         dword ptr [ebp-4],eax  

显然,编译器(VS2010 为 Win32 编译调试)认识到我们需要对代码进行特定对齐,因此采取措施确保它可以提供。

2) 仅当 运行 来自调试器时,才会在执行此指令时抛出异常,即使如此也并非总是如此。即使附加到进程并执行此代码也不会抛出。怎么会这样??

所以,有几点想法:

  • "and even then not always" - 当你 运行 这个的时候不站在你的肩膀上,我不能肯定地说。然而,看起来似乎有道理的是,只是偶然的机会,可以根据您需要的对齐方式创建堆栈。默认情况下,x86 使用 4 字节堆栈对齐。如果你需要 16 字节对齐,你有 1 in 4 shot。

  • 至于其余的(来自https://msdn.microsoft.com/en-us/library/aa290049%28v=vs.71%29.aspx#ia64alignment_topic4):

在x86架构上,操作系统不会让对齐错误对应用程序可见。 ...您也会因对齐错误而遭受性能下降,但它会比 Itanium 严重得多,因为硬件将多次访问内存以检索未对齐的数据。

TLDR:使用 __declspec(align(16)) 应该可以为您提供所需的对齐方式,即使对于堆栈变量也是如此。对于未对齐的访问,OS 将捕获异常并为您处理(以性能为代价)。

Edit1:回复下面的前 2 条评论:

根据MS的docs,你对堆栈参数的对齐是正确的,但他们也提出了解决方案:

You cannot specify alignment for function parameters. When data that has an alignment attribute is passed by value on the stack, its alignment is controlled by the calling convention. If data alignment is important in the called function, copy the parameter into correctly aligned memory before use.

您在 Microsoft connect 上的示例和有关的代码都没有为我生成相同的代码(我只使用 vs2010),所以我无法对此进行测试。但是鉴于您的样本中的这段代码:

struct mockVect2d
{
    double x, y;
    mockVect2d(double a, double b) : x(a), y(b) {}

似乎对齐 mockVect2d 或 2 个双打可能会有帮助。

更新:现在是 confirmed VC++ compiler bug,希望在 VS2015 RTM 中得到解决。


编辑:与许多其他报告一样,连接报告现在是垃圾。然而,编译器错误似乎在 VS2017 中得到解决 - not in 2015 update 3.