包含内在函数的简单C++表达式模板会产生不同的指令

kei*_*ith 9 c++ intrinsics

我正在测试一个非常简单的程序,该程序使用C++表达式模板来简化编写在值数组上运行的SSE2和AVX代码.

我有一个svec代表一组值的类.

我有一个sreg代表SSE2双重寄存器的类.

我有expradd_expr代表添加svec数组.

与手动代码相比,编译器为每个循环生成三个额外的指令用于表达式模板测试用例.我想知道是否有这样的原因,或者我可以做任何改变让他编译器产生相同的输出?

完整的测试工具是:

#include <iostream>
#include <emmintrin.h>

struct sreg
{
    __m128d reg_;

    sreg() {}

    sreg(const __m128d& r) :
        reg_(r)
    {
    }

    sreg operator+(const sreg& b) const
    {
        return _mm_add_pd(reg_, b.reg_);
    }
};

template <typename T>
struct expr
{
    sreg operator[](std::size_t i) const
    {
        return static_cast<const T&>(*this).operator[](i);
    }

    operator const T&() const
    {
        return static_cast<const T&>(*this);
    }
};

template <typename A, typename B>
struct add_expr : public expr<add_expr<A, B>>
{
    const A& a_;
    const B& b_;

    add_expr(const A& a, const B& b) :
        a_{ a }, b_{ b }
    {
    }

    sreg operator[](std::size_t i) const
    {
        return a_[i] + b_[i];
    }
};

template <typename A, typename B>
inline auto operator+(const expr<A>& a, const expr<B>& b)
{
    return add_expr<A, B>(a, b);
}

struct svec : public expr<svec>
{
    sreg* regs_;
    std::size_t size_;

    svec(std::size_t size) :
        size_{ size }
    {
        regs_ = static_cast<sreg*>(_aligned_malloc(size * 32, 32));
    }

    ~svec()
    {
        _aligned_free(regs_);
    }

    template <typename T>
    svec& operator=(const T& expression)
    {
        for (std::size_t i = 0; i < size(); i++)
        {
            regs_[i] = expression[i];
        }

        return *this;
    }

    const sreg& operator[](std::size_t index) const
    {
        return regs_[index];
    }

    sreg& operator[](std::size_t index)
    {
        return regs_[index];
    }

    std::size_t size() const
    {
        return size_;
    }
};

static constexpr std::size_t size = 64;

int main()
{
    svec a(size);
    svec b(size);
    svec c(size);
    svec d(size);
    svec vec(size);

    //hand rolled loop
    for (std::size_t j = 0; j < size; j++)
    {
        vec[j] = a[j] + b[j] + c[j] + d[j];
    }

    //expression templates version of hand rolled loop
    vec = a + b + c + d;

    std::cout << "Done...";

    std::getchar();

    return EXIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)

对于手动循环,说明如下:

00007FF621CD1B70  mov         r8,qword ptr [c]  
00007FF621CD1B75  mov         rdx,qword ptr [b]  
00007FF621CD1B7A  mov         rax,qword ptr [a]  
00007FF621CD1B7F  vmovupd     xmm0,xmmword ptr [rcx+rax]  
00007FF621CD1B84  vaddpd      xmm1,xmm0,xmmword ptr [rdx+rcx]  
00007FF621CD1B89  vaddpd      xmm3,xmm1,xmmword ptr [r8+rcx]  
00007FF621CD1B8F  lea         rax,[rcx+rbx]  
00007FF621CD1B93  vaddpd      xmm1,xmm3,xmmword ptr [r10+rax]  
00007FF621CD1B99  vmovupd     xmmword ptr [rax],xmm1  
00007FF621CD1B9D  add         rcx,10h  
00007FF621CD1BA1  cmp         rcx,400h  
00007FF621CD1BA8  jb          main+0C0h (07FF621CD1B70h)  
Run Code Online (Sandbox Code Playgroud)

对于表达式模板版本:

00007FF621CD1BC0  mov         rdx,qword ptr [c]  
00007FF621CD1BC5  mov         rcx,qword ptr [rcx]  
00007FF621CD1BC8  mov         rax,qword ptr [r8]  
00007FF621CD1BCB  vmovupd     xmm0,xmmword ptr [r9+rax]  
00007FF621CD1BD1  vaddpd      xmm1,xmm0,xmmword ptr [rcx+r9]  
00007FF621CD1BD7  vaddpd      xmm0,xmm1,xmmword ptr [rdx+r9]  
00007FF621CD1BDD  lea         rax,[r9+rbx]  
00007FF621CD1BE1  vaddpd      xmm0,xmm0,xmmword ptr [rax+r10]  
00007FF621CD1BE7  vmovupd     xmmword ptr [rax],xmm0  
00007FF621CD1BEB  add         r9,10h  
00007FF621CD1BEF  cmp         r9,400h  
00007FF621CD1BF6  jae         main+154h (07FF621CD1C04h)  # extra instruction 1
00007FF621CD1BF8  mov         rcx,qword ptr [rsp+60h]     # extra instruction 2
00007FF621CD1BFD  mov         r8,qword ptr [rsp+58h]      # extra instruction 3
00007FF621CD1C02  jmp         main+110h (07FF621CD1BC0h)
Run Code Online (Sandbox Code Playgroud)

请注意,这是专门用于演示问题的最低可验证代码.代码是使用Visual Studio 2015 Update 3中的默认版本构建设置编译的.

我打折的想法:

  • 循环的顺序(我已经切换了手动循环和表达式模板循环以检查编译器是否仍然插入了额外的指令,它确实如此)

  • 编译器正在基于constexpr size(我已经尝试过的测试代码size来优化手动循环,以防止编译器推断为常量以更好地优化手动循环,并且它对手动循环的指令没有任何影响).

Pet*_*des 3

两个循环似乎每次迭代都会重新加载数组指针。(例如mov r8, [c]在第一个循环中)。第二个版本的效率更低,有两个间接级别。其中之一出现在循环末尾,在条件分支之后跳出循环。

请注意,您未将其标识为“新”的已更改指令之一是mov rcx, [rcx]。循环之间的寄存器分配不同,但这些是数组起始指针。它(以及rcx,[rsp+60h]商店之后)正在取代mov rax,qword ptr [a]. 我认为a这也是 RSP 的偏移量,实际上并不是静态存储的标签。


发生这种情况可能是因为 MSVC++ 未能成功进行别名分析来证明存储到vec[j]不能修改任何指针。我没有仔细查看您的模板,但如果您引入了您希望优化掉的额外间接级别,那么问题是事实并非如此。

显而易见的解决方案是使用优化得更好的编译器。clang3.9 做得很好(自动向量化,无需重新加载指针),并且 gcc 完全优化了它,因为结果没有被使用。

但如果您坚持使用 MSVC,请查看是否有任何严格别名选项或无别名关键字或声明,这会有所帮助。例如,GNU C++ 扩展包括__restrict__获得与 C99restrict关键字相同的“这不别名”行为。我不知道 MSVC 是否支持类似的东西。


挑剔:

jae称其为“额外”指令并不完全正确。它只是 的相反谓词jb,所以现在它是一个while(true){ ... if() break; reload; }循环,而不是一个更高效的do{...}while()循环。(我使用 C 语法来显示 asm 循环结构。显然,如果您实际编译了这些 C 循环,编译器可以优化它们。)因此,如果有的话,“额外指令”就是无条件分支 JMP。