【问题标题】:Expression templates vs. hand-written code表达式模板与手写代码
【发布时间】:2012-04-06 23:10:28
【问题描述】:

我目前正在编写一个 C++ 模板表达式库,并将一些实例化与汇编级别的手写代码进行比较。手写功能如下:

spinor multiply(vector const& a, vector const& b)
{
        spinor result = {
                a.at<1>() * b.at<1>() - a.at<2>() * b.at<2>()
                          - a.at<4>() * b.at<4>() - a.at<8>() * b.at<8>(),
                a.at<1>() * b.at<2>() - a.at<2>() * b.at<1>(),
                a.at<1>() * b.at<4>() - a.at<4>() * b.at<1>(),
                a.at<1>() * b.at<8>() - a.at<8>() * b.at<1>(),
                a.at<2>() * b.at<4>() - a.at<4>() * b.at<2>(),
                a.at<2>() * b.at<8>() - a.at<8>() * b.at<2>(),
                a.at<4>() * b.at<8>() - a.at<8>() * b.at<4>()
        };

        return result;
}

vector 类只是四个双精度的包装器,可以使用 at&lt;index&gt;() 成员函数读取。由于设计决策,四个组件的索引是1, 2, 4, 8,使用at&lt;index&gt;() 而不是通常的0, 1, 2, 3 访问。

此函数的目的是返回两个向量相乘的结果(在 Minkowski 空间中)。如果您熟悉几何代数,您将看到点积(result 的第一个分量,ab 交换下对称)和楔积(其余分量,@ 交换下反对称987654331@ 和 b)。如果你不熟悉几何代数,可以把这个函数当作向量相乘的处方。

如果我用 GCC 4.7 编译上面的函数并查看 objdump -SC a.out 给出的反汇编,这会给我以下输出:

400bc0: movsd  0x8(%rsi),%xmm6
400bc5: mov    %rdi,%rax
400bc8: movsd  (%rsi),%xmm8
400bcd: movsd  0x8(%rdx),%xmm5
400bd2: movapd %xmm6,%xmm9
400bd7: movsd  (%rdx),%xmm7
400bdb: movapd %xmm8,%xmm0
400be0: mulsd  %xmm5,%xmm9
400be5: movsd  0x10(%rsi),%xmm4
400bea: mulsd  %xmm7,%xmm0
400bee: movsd  0x10(%rdx),%xmm1
400bf3: movsd  0x18(%rdx),%xmm3
400bf8: movsd  0x18(%rsi),%xmm2
400bfd: subsd  %xmm9,%xmm0
400c02: movapd %xmm4,%xmm9
400c07: mulsd  %xmm1,%xmm9
400c0c: subsd  %xmm9,%xmm0
400c11: movapd %xmm3,%xmm9
400c16: mulsd  %xmm2,%xmm9
400c1b: subsd  %xmm9,%xmm0
400c20: movapd %xmm6,%xmm9
400c25: mulsd  %xmm7,%xmm9
400c2a: movsd  %xmm0,(%rdi)
400c2e: movapd %xmm5,%xmm0
400c32: mulsd  %xmm8,%xmm0
400c37: subsd  %xmm9,%xmm0
400c3c: movapd %xmm4,%xmm9
400c41: mulsd  %xmm7,%xmm9
400c46: mulsd  %xmm2,%xmm7
400c4a: movsd  %xmm0,0x8(%rdi)
400c4f: movapd %xmm1,%xmm0
400c53: mulsd  %xmm8,%xmm0
400c58: mulsd  %xmm3,%xmm8
400c5d: subsd  %xmm9,%xmm0
400c62: subsd  %xmm7,%xmm8
400c67: movapd %xmm4,%xmm7
400c6b: mulsd  %xmm5,%xmm7
400c6f: movsd  %xmm0,0x10(%rdi)
400c74: mulsd  %xmm2,%xmm5
400c78: movapd %xmm1,%xmm0
400c7c: mulsd  %xmm6,%xmm0
400c80: movsd  %xmm8,0x18(%rdi)
400c86: mulsd  %xmm3,%xmm6
400c8a: mulsd  %xmm2,%xmm1
400c8e: mulsd  %xmm4,%xmm3
400c92: subsd  %xmm7,%xmm0
400c96: subsd  %xmm5,%xmm6
400c9a: subsd  %xmm1,%xmm3
400c9e: movsd  %xmm0,0x20(%rdi)
400ca3: movsd  %xmm6,0x28(%rdi)
400ca8: movsd  %xmm3,0x30(%rdi)
400cad: retq   
400cae: nop
400caf: nop

这对我来说看起来很不错 - 第一个 (%rsi) 和第二个 (%rdx) 向量的组件仅被访问一次,并且实际计算仅在寄存器中完成。最后,将结果写入寄存器%rdi 中的地址。由于这是第一个参数寄存器,我认为这里采用了返回值优化。

将此与上述函数的表达式模板版本的以下清单进行比较:

400cb0: mov    (%rsi),%rdx
400cb3: mov    0x8(%rsi),%rax
400cb7: movsd  0x1f1(%rip),%xmm4        # 400eb0 <_IO_stdin_used+0x10>
400cbe: 
400cbf: movsd  0x10(%rdx),%xmm3
400cc4: movsd  0x18(%rdx),%xmm0
400cc9: mulsd  0x10(%rax),%xmm3
400cce: xorpd  %xmm4,%xmm0
400cd2: mulsd  0x18(%rax),%xmm0
400cd7: movsd  0x8(%rdx),%xmm2
400cdc: movsd  (%rdx),%xmm1
400ce0: mulsd  0x8(%rax),%xmm2
400ce5: mulsd  (%rax),%xmm1
400ce9: subsd  %xmm3,%xmm0
400ced: subsd  %xmm2,%xmm0
400cf1: addsd  %xmm0,%xmm1
400cf5: movsd  %xmm1,(%rdi)
400cf9: movsd  (%rdx),%xmm0
400cfd: movsd  0x8(%rdx),%xmm1
400d02: mulsd  0x8(%rax),%xmm0
400d07: mulsd  (%rax),%xmm1
400d0b: subsd  %xmm1,%xmm0
400d0f: movsd  %xmm0,0x8(%rdi)
400d14: movsd  (%rdx),%xmm0
400d18: movsd  0x10(%rdx),%xmm1
400d1d: mulsd  0x10(%rax),%xmm0
400d22: mulsd  (%rax),%xmm1
400d26: subsd  %xmm1,%xmm0
400d2a: movsd  %xmm0,0x10(%rdi)
400d2f: movsd  0x8(%rdx),%xmm0
400d34: movsd  0x10(%rdx),%xmm1
400d39: mulsd  0x10(%rax),%xmm0
400d3e: mulsd  0x8(%rax),%xmm1
400d43: subsd  %xmm1,%xmm0
400d47: movsd  %xmm0,0x18(%rdi)
400d4c: movsd  (%rdx),%xmm0
400d50: movsd  0x18(%rdx),%xmm1
400d55: mulsd  0x18(%rax),%xmm0
400d5a: mulsd  (%rax),%xmm1
400d5e: subsd  %xmm1,%xmm0
400d62: movsd  %xmm0,0x20(%rdi)
400d67: movsd  0x8(%rdx),%xmm0
400d6c: movsd  0x18(%rdx),%xmm1
400d71: mulsd  0x18(%rax),%xmm0
400d76: mulsd  0x8(%rax),%xmm1
400d7b: subsd  %xmm1,%xmm0
400d7f: movsd  %xmm0,0x28(%rdi)
400d84: movsd  0x10(%rdx),%xmm0
400d89: movsd  0x18(%rdx),%xmm1
400d8e: mulsd  0x18(%rax),%xmm0
400d93: mulsd  0x10(%rax),%xmm1
400d98: subsd  %xmm1,%xmm0
400d9c: movsd  %xmm0,0x30(%rdi)
400da1: retq   

这个函数的签名是

spinor<product<vector, vector>>(product<vector, vector> const&)

我希望你相信我,这两个版本都给出了相同的结果。前两行提取作为引用存储在product 中的第一个和第二个向量。我想知道以下几点:

  • movsd 0x1f1(%rip),%xmm4xorpd %xmm4,%xmm0 结合有什么作用?我已经发现这被称为“RIP 相对寻址”,参见http://www.x86-64.org/documentation/assembly.html
  • 为什么 GCC 不使用更多的寄存器,例如缓存被读取四次的0x10(%rax)

我还通过生成 100000000 个随机向量并花费两个函数所需的时间来对这两个函数进行基准测试:

ET: 7.5 sec
HW: 6.8 sec

手写功能快10%左右。有没有人有表达模板的经验并且知道如何让它们的表现更接近他们的手写对应物?

【问题讨论】:

  • 与您的问题无关,但即使使用范围规则,我也会尝试不将新类命名为与某些标准容器类相同。如果你使用using namespace std;,事情可能会变得非常混乱。
  • @如果没有实际的 ET 代码,这将很难。愿意分享吗?
  • @JoachimPileborg:如果你使用using namespace std;,我不在乎你是否遇到问题(你应该更具体)......但我同意尽量避免名称冲突的一般想法.
  • 这是不使用 using namespace std 的完美理由 ;-)
  • 您能告诉我们您是如何实现表达式模板解决方案的吗?

标签: c++ gcc assembly c++11 expression-templates


【解决方案1】:

如果我们确实知道地址0x400eb0 的内容会很清楚,但我怀疑它是0x8000 0000 0000 0000 8000 0000 0000 0000 或类似的(可能带有前导0,因为代码未矢量化),写为128位整数。

在这种情况下,xorpd 确实会改变第二个操作数的符号。

为什么不缓存寄存器读取 - 最好在 gcc-help 邮件列表中询问。可能编译器无法证明这两个向量或一个中间结果没有别名。

但与普遍看法相反,编译器并不总是完美地优化,但仅优于所有程序员的 90%(或 99%?)(如果他们尝试编写汇编),有时(很少)他们会产生非常慢的代码.

但是你的方法非常好——如果你想优化,基准测试和查看生成的目标代码是正确的做法。

PS:他们的代码可能会通过使用向量指令(mulpd 而不是mulsd)来加速,它可以一次性乘以两个或四个双精度数),也就是 SSE 或 AVX。但是需要一些指令来将值洗牌到寄存器中的正确位置,因此增益总是慢于两倍或四倍。

【讨论】:

  • 我也不知道这个地址,它指向我的代码末尾之外(参见上面由 objdump 生成的注释)。这是完整的打印输出:
  • students.uni-mainz.de/cschwan/asm.out(旧版本,但情况仍然存在,见第 373 行)
  • 我也考虑过使用 SSE,但我认为这很难将表达式模板与 SSE 内在函数结合起来,特别是因为计算可能包含符号并且可能非常不同(比较第一个和第二个组件multiply).
  • 你可以试试objdump -s a.out,看看.data的内容,一般会重新定位到.text部分的末尾。使用objdump -r可以找到准确的重定位信息,找一个值0x400cba,也就是mov $1f1(%rip)...指令的偏移位置。
  • 我的代码中有一个类似的问题(也使用 gcc-4.7),128 位常量为零。关键是使用内部函数将变量声明为static const __m128i zero = _mm_set1_epi8(0),这是推荐的方式。但是编译器对包含零的内存块产生了负载。在声明它static const __m128i zero = {0} 后,它被正确折叠到xor 指令。但是您的另一个问题是:为什么 xor 没有完全优化掉?毕竟,如果源操作数是 0,它什么也不做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-15
  • 1970-01-01
  • 1970-01-01
  • 2018-11-15
  • 1970-01-01
  • 2012-05-23
相关资源
最近更新 更多