表达式模板实现不进行优化(Expression Template implementation no

2019-10-19 00:15发布

我想了解在C ++表达式模板的概念,因为这样我鹅卵石的示例代码等综合在一起,产生一个简单的载体和相关的表达式模板架构只支持二元运算符(+, - ,*)。

一切编译,但是我注意到标准手写循环与表达模板变种之间的性能差异是相当大的。 ET是因为手写的近两倍慢。 我期望的差异,但并不多。

一个完整的代码可以在这里找到:

https://gist.github.com/BernieWt/769a4a3ceb90bb0cae9e

(道歉乱码。)

总之,我基本上是比较以下两个循环:

AND:

for (std::size_t i = 0 ; i < rounds; ++i)
{
   v4 = ((v0 - v1) + (v2 * v3)) + v4;
   total += v4[0];
}

HW:

for (std::size_t i = 0 ; i < rounds; ++i)
{
   for (std::size_t x = 0; x < N; ++x)
   {
      v4[x] = (v0[x] - v1[x]) + (v2[x] * v3[x]) + v4[x];
   }
   total += v4[0];
}

当我拆开输出,以下是生产的,所不同的是清楚地表明,ET变体的返回期间发生的额外memcpy和几个64位的负载:

Standard Loop                           | Expression Template
----------------------------------------+--------------------------------
L26:                                    | L12:
xor   edx, edx                          | xor   edx, edx
jmp   .L27                              | jmp   .L13
L28:                                    | L14:
movsd xmm3, QWORD PTR [rsp+2064+rdx*8]  | movsd xmm3, QWORD PTR [rsp+2064+rdx*8]
L27:                                    | L13:
movsd xmm2, QWORD PTR [rsp+1040+rdx*8]  | movsd xmm1, QWORD PTR [rsp+1552+rdx*8]
movsd xmm1, QWORD PTR [rsp+16+rdx*8]    | movsd xmm2, QWORD PTR [rsp+16+rdx*8]
mulsd xmm2, QWORD PTR [rsp+1552+rdx*8]  | mulsd xmm1, QWORD PTR [rsp+1040+rdx*8]
subsd xmm1, QWORD PTR [rsp+528+rdx*8]   | subsd xmm2, QWORD PTR [rsp+528+rdx*8]
addsd xmm1, xmm2                        | addsd xmm1, xmm2
addsd xmm1, xmm3                        | addsd xmm1, xmm3
movsd QWORD PTR [rsp+2064+rdx*8], xmm1  | movsd QWORD PTR [rsp+2576+rdx*8], xmm1
add   rdx, 1                            | add   rdx, 1
cmp   rdx, 64                           | cmp   rdx, 64
jne   .L28                              | jne   .L14
                                        | mov   dx, 512
                                        | movsd QWORD PTR [rsp+8], xmm0
                                        | lea   rsi, [rsp+2576]
                                        | lea   rdi, [rsp+2064]
                                        | call  memcpy
movsd xmm3, QWORD PTR [rsp+2064]        | movsd xmm0, QWORD PTR [rsp+8]
sub   rcx, 1                            | sub   rbx, 1
                                        | movsd xmm3, QWORD PTR [rsp+2064]
addsd xmm0, xmm3                        | addsd xmm0, xmm3
jne   .L26                              | jne   .L12

我的问题是:在这一点上我在如何去消除副本,我基本上是想不复制更新到位V4。 如何去这样做的任何想法?

注1:我试过GCC 4.7 / 9,铛3.3,VS2010 / 2013 -我得到大致相同的性能配置中提到的所有的编译器。

注2:我也尝试着声明bin_exp的VEC,然后添加以下赋值运算符和删除bin_exp转换操作符, 但无济于事

template<typename LHS, typename RHS, typename Op>
inline vec<N>& operator=(const bin_exp<LHS,RHS,Op,N>& o)
{
   for (std::size_t i = 0; i < N; ++i)  { d[i] = o[i]; }
   return *this;
}

更新注2:提出的解决方案实际上是正确的。 和不会导致编译器产生几乎相同的手写循环代码。

在另一方面,如果我重写用例为ET变种如下:

auto expr = ((v0 - v1) + (v2 * v3)) + v4;

//auto& expr = ((v0 - v1) + (v2 * v3)) + v4;   same problem
//auto&& expr = ((v0 - v1) + (v2 * v3)) + v4;   same problem

for (std::size_t i = 0 ; i < rounds; ++i)
{
   v4 = expr
   total += v4[0];
}

发生崩溃的原因是该ET的实例化过程中产生的临时对象(右值)之前将分配破坏。 我在想,如果有使用C ++ 11导致编译器错误的任何方式。

Answer 1:

表达模板的一点是,子表达式的评估可能会导致会产生费用,并且提供任何好处的临时。 在你的代码是不是真的比较苹果和苹果。 另外两种比较是:

// Traditional
vector operator+(vector const& lhs, vector const& rhs);
vector operator-(vector const& lhs, vector const& rhs);
vector operator*(vector const& lhs, vector const& rhs);

随着这些定义的操作,要解决的表达式:

v4 = ((v0 - v1) + (v2 * v3)) + v4;

成为(提供名称到所有的临时):

auto __tmp1 = v0 - v1;
auto __tmp2 = v2 * v3;
auto __tmp3 = __tmp1 + __tmp2;
auto __tmp4 = __tmp3 + v4;
// assignment is not really part of the expression
v4 = __tmp4;

正如你看到有4名临时对象,如果你使用的表达模板,得到降低到最低限度:一个临时的,因为任何这些操作的产生外的地方的值。

在你手挽你没有执行相同操作的代码的版本,则是相当的了解,你会在年底分配展开整个循环,并采取完整的操作知识的优势,并没有真正相同的操作,因为表达式的元素中的一个的,则改变了表达成:

v4 += ((v0 - v1) + (v2 * v3));

现在考虑如果不是分配给这需要表达的部分载体之一,你创建一个新的载体,会发生什么v5 。 试着表达:

auto v5 = ((v0 - v1) + (v2 * v3)) + v4;

表达模板的神奇的是,你可以提供针对上是一样的手动实现高效模板的工作运营的实现,用户代码更简单,更容易出错(无需遍历所有的元素与发生错误的可能性,或维护的成本作为载体的内部表示需要在其中执行的算术运算的每个地方被称为)的矢量的

我基本上是想不复制更新到位V4

随着表达式模板和当前界面的载体,你要支付临时和副本。 其原因是,在表达的(概念)评估过程中一个新的载体创建,虽然它可能似乎很明显你v4 = ... + v4; 相当于v4 += ... ,这种转变不能由编译器或表达模板完成。 你可以,在另一方面,提供的重载vector::operator+= (甚至operator= ),其采用的表达模板,并执行到位的操作。


提供了从表达模板分配赋值运算符,并用克++构建4.7 -O2这是两个回路所产生的组件:

    call    __ZNSt6chrono12system_clock3nowEv   |    call    __ZNSt6chrono12system_clock3nowEv  
    movl    $5000000, %ecx                      |    movl    $5000000, %ecx                     
    xorpd   %xmm0, %xmm0                        |    xorpd   %xmm0, %xmm0                       
    movsd   2064(%rsp), %xmm3                   |    movsd   2064(%rsp), %xmm3                  
    movq    %rax, %rbx                          |    movq    %rax, %rbx                         
    .align 4                                    |    .align 4                                   
L9:                                             |L15:                                           
    xorl    %edx, %edx                          |    xorl    %edx, %edx                         
    jmp L8                                      |    jmp L18                                    
    .align 4                                    |    .align 4                                   
L32:                                            |L16:                                           
    movsd   2064(%rsp,%rdx,8), %xmm3            |    movsd   2064(%rsp,%rdx,8), %xmm3           
L8:                                             |L18:                                           
    movsd   1552(%rsp,%rdx,8), %xmm1            |    movsd   1040(%rsp,%rdx,8), %xmm2           
    movsd   16(%rsp,%rdx,8), %xmm2              |    movsd   16(%rsp,%rdx,8), %xmm1             
    mulsd   1040(%rsp,%rdx,8), %xmm1            |    mulsd   1552(%rsp,%rdx,8), %xmm2           
    subsd   528(%rsp,%rdx,8), %xmm2             |    subsd   528(%rsp,%rdx,8), %xmm1            
    addsd   %xmm2, %xmm1                        |    addsd   %xmm2, %xmm1                       
    addsd   %xmm3, %xmm1                        |    addsd   %xmm3, %xmm1                       
    movsd   %xmm1, 2064(%rsp,%rdx,8)            |    movsd   %xmm1, 2064(%rsp,%rdx,8)           
    addq    $1, %rdx                            |    addq    $1, %rdx                           
    cmpq    $64, %rdx                           |    cmpq    $64, %rdx                          
    jne L32                                     |    jne L16                                    
    movsd   2064(%rsp), %xmm3                   |    movsd   2064(%rsp), %xmm3                  
    subq    $1, %rcx                            |    subq    $1, %rcx                           
    addsd   %xmm3, %xmm0                        |    addsd   %xmm3, %xmm0                       
    jne L9                                      |    jne L15                                    
    movsd   %xmm0, (%rsp)                       |    movsd   %xmm0, (%rsp)                      
    call    __ZNSt6chrono12system_clock3nowEv   |    call    __ZNSt6chrono12system_clock3nowEv  


Answer 2:

C ++ 11个引入移动语义 ,以减少不必要的份数。

您的代码是相当模糊的,但我认为这应该做的伎俩

在您的struct vec更换

value_type d[N];

std::vector<value_type> d;

并加入d(N)的构造函数初始化列表。 std::array是显而易见的选择,但是这意味着移动每个元素(即拷贝你试图避免)。

然后添加一个移动构造函数:

vec(vec&& from): d(std::move(from.d))
{
}

此举构造让新对象“偷”旧的内容。 换言之,代替复制整个矢量(阵列)仅指向数组被复制。



文章来源: Expression Template implementation not being optimized