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

时间:2016-12-01 10:21:51

标签: c++ intrinsics

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

我有一个类svec,它代表一组值。

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

我有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;
}

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

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)  

对于表达式模板版本:

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)

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

我打折的想法:

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

  • 编译器正在根据constexpr size优化手卷循环(我已经尝试过测试代码,阻止编译器推断size是常量以更好地优化手卷循环,它与手卷循环的说明没有区别。)

1 个答案:

答案 0 :(得分:3)

两个循环似乎每次迭代都重新加载数组指针。 (例如,第一个循环中的mov r8, [c])。第二个版本只是更低效地执行它,间接有两个级别。其中一个在循环结束时,在一个条件分支后退出循环。

请注意,您未将其中的一条更改为“&#34; new&#34;是mov rcx, [rcx]。循环之间的寄存器分配是不同的,但那些是数组起始指针。它(以及商店后的rcx,[rsp+60h])正在替换mov rax,qword ptr [a]。我假设a也是RSP的偏移量,实际上并不是静态存储的标签。

可能这种情况正在发生,因为MSVC ++在别名分析方面没有成功,以证明vec[j]中的商店无法修改任何指针。我没有仔细查看您的模板,但如果您要引入一个额外的间接级别,您希望优化它,那么问题就是它不是。

显而易见的解决方案是使用更好地优化的编译器。 clang3.9运行良好(自动向量化,没有重新加载指针),gcc完全优化它,因为没有使用结果。

但是,如果您仍然坚持使用MSVC,请查看是否存在任何有用的严格别名选项或无别名关键字或声明。例如GNU C ++扩展包括__restrict__以获得相同的&#34;这不是别名&#34;行为为C99&n restrict个关键字。 IDK,如果MSVC支持这样的话。

挑剔:

拨打jae&#34;额外&#34;是不对的。指令。它只是来自jb的相反谓词,所以它现在是while(true){ ... if() break; reload; }循环而不是更高效的do{...}while()循环。 (我使用C语法来显示asm循环结构。显然,如果你真的编译了那些C循环,编译器可以优化它们。)所以,如果有的话,&#34;额外的指令&#34;是无条件的分支,JMP。