首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >汽车与混凝土的性能差异:参考文献与混凝土类型

汽车与混凝土的性能差异:参考文献与混凝土类型
EN

Stack Overflow用户
提问于 2022-06-29 21:02:07
回答 2查看 106关注 0票数 0

我正在试图了解Eigen::Ref是如何工作的,看看我是否可以在代码中利用它。

我设计了一个这样的基准

代码语言:javascript
复制
static void value(benchmark::State &state) {
  for (auto _ : state) {
    const Eigen::Matrix<double, Eigen::Dynamic, 1> vs =
        Eigen::Matrix<double, 9, 1>::Random();
    auto start = std::chrono::high_resolution_clock::now();

    const Eigen::Vector3d v0 = vs.segment<3>(0);
    const Eigen::Vector3d v1 = vs.segment<3>(3);
    const Eigen::Vector3d v2 = vs.segment<3>(6);
    const Eigen::Vector3d vt = v0 + v1 + v2;
    const Eigen::Vector3d v = vt.transpose() * vt * vt + vt;

    benchmark::DoNotOptimize(v);
    auto end = std::chrono::high_resolution_clock::now();

    auto elapsed_seconds =
        std::chrono::duration_cast<std::chrono::duration<double>>(end - start);
    state.SetIterationTime(elapsed_seconds.count());
  }
}

我还有两个类似thise的测试,一个使用const Eigen::Ref<const Eigen::Vector3D>auto作为v0, v1, v2, vt

这些基准的结果如下

代码语言:javascript
复制
Benchmark                          Time             CPU   Iterations
--------------------------------------------------------------------
value/manual_time               23.4 ns          113 ns     29974946
ref/manual_time                 23.0 ns          111 ns     29934053
with_auto/manual_time           23.6 ns          112 ns     29891056

正如您所看到的,所有的测试都是完全一样的。因此,我想,也许编译器正在发挥它的魔力,并决定用-O0进行测试。以下是研究结果:

代码语言:javascript
复制
--------------------------------------------------------------------
Benchmark                          Time             CPU   Iterations
--------------------------------------------------------------------
value/manual_time               2475 ns         3070 ns       291032
ref/manual_time                 2482 ns         3077 ns       289258
with_auto/manual_time           2436 ns         3012 ns       263170

同样,这三种情况也是一样的。

如果我正确理解,使用Eigen::Vector3d的第一个情况应该更慢,因为它必须保留, perform the v0+v1+v2`操作并保存它,然后执行另一个操作和保存。

auto案例应该是最快的,因为它应该跳过所有的文章。

关于ref的案子,我认为它应该和auto一样快。如果我正确理解,我的所有操作都可以存储在对const Eigen::Vector3d的引用中,所以应该跳过操作,对吗?

为什么结果是一样的?我是误解了什么,还是基准设计太糟糕了?

EN

回答 2

Stack Overflow用户

发布于 2022-06-29 22:16:55

基准测试的一个大问题是,您需要测量热基准测试循环中的时间。测量时间需要一些时间,它可能比实际计算要昂贵得多。事实上,我认为这就是在你的案子中发生的事情。实际上,在使用-O3的Clang 13上,下面是实际基准的组装代码(可在GodBolt上使用):

代码语言:javascript
复制
        mov     rbx, rax
        mov     rax, qword ptr [rsp + 24]
        cmp     rax, 2
        jle     .LBB0_17
        cmp     rax, 5
        jle     .LBB0_17
        cmp     rax, 8
        jle     .LBB0_17
        mov     rax, qword ptr [rsp + 16]
        movupd  xmm0, xmmword ptr [rax]
        movsd   xmm1, qword ptr [rax + 16]      # xmm1 = mem[0],zero
        movupd  xmm2, xmmword ptr [rax + 24]
        addpd   xmm2, xmm0
        movupd  xmm0, xmmword ptr [rax + 48]
        addsd   xmm1, qword ptr [rax + 40]
        addpd   xmm0, xmm2
        addsd   xmm1, qword ptr [rax + 64]
        movapd  xmm2, xmm0
        mulpd   xmm2, xmm0
        movapd  xmm3, xmm2
        unpckhpd        xmm3, xmm2                      # xmm3 = xmm3[1],xmm2[1]
        addsd   xmm3, xmm2
        movapd  xmm2, xmm1
        mulsd   xmm2, xmm1
        addsd   xmm2, xmm3
        movapd  xmm3, xmm1
        mulsd   xmm3, xmm2
        unpcklpd        xmm2, xmm2                      # xmm2 = xmm2[0,0]
        mulpd   xmm2, xmm0
        addpd   xmm2, xmm0
        movapd  xmmword ptr [rsp + 32], xmm2
        addsd   xmm3, xmm1
        movsd   qword ptr [rsp + 48], xmm3

这段代码可以在几十个周期内执行,所以在一个4~5 GHz的现代x86处理器上可能不到10-15 ns。同时,high_resolution_clock::now()应该使用一个RDTSC/RDTSCP指令,这个指令也需要几十个周期才能完成。例如,在Skylake处理器上,应该需要大约25个周期(类似于较新的Intel处理器)。在AMD Zen处理器上,它大约需要35-38个周期.此外,它还添加了一个可能不代表实际应用程序的同步。请考虑使用多次迭代来度量基准测试循环的时间。

票数 3
EN

Stack Overflow用户

发布于 2022-06-29 22:15:22

因为所有的事情都发生在函数内部,编译器可以进行转义分析,并将副本优化到向量中。

为了检查这一点,我将代码放入一个函数中,查看汇编程序:

代码语言:javascript
复制
Eigen::Vector3d foo(const Eigen::VectorXd& vs)
{
    const Eigen::Vector3d v0 = vs.segment<3>(0);
    const Eigen::Vector3d v1 = vs.segment<3>(3);
    const Eigen::Vector3d v2 = vs.segment<3>(6);
    const Eigen::Vector3d vt = v0 + v1 + v2;
    return vt.transpose() * vt * vt + vt;
}

它变成了这个汇编程序

代码语言:javascript
复制
        push    rax
        mov     rax, qword ptr [rsi + 8]
...
        mov     rax, qword ptr [rsi]
        movupd  xmm0, xmmword ptr [rax]
        movsd   xmm1, qword ptr [rax + 16]
        movupd  xmm2, xmmword ptr [rax + 24]
        addpd   xmm2, xmm0
        movupd  xmm0, xmmword ptr [rax + 48]
        addsd   xmm1, qword ptr [rax + 40]
        addpd   xmm0, xmm2
        addsd   xmm1, qword ptr [rax + 64]
...
        movupd  xmmword ptr [rdi], xmm2
        addsd   xmm3, xmm1
        movsd   qword ptr [rdi + 16], xmm3
        mov     rax, rdi
        pop     rcx
        ret

请注意,唯一的内存操作是两个GP寄存器加载,以获得开始指针和长度,然后在最后将结果写入内存之前,使用两个内存负载将向量内容输入寄存器。

这只是因为我们处理的是固定大小的向量。使用VectorXd,复制肯定会发生。

替代基准

Ref通常用于函数调用。为什么不尝试一个不能内联的函数呢?或者给出一个例子,在这个例子中,转义分析不能工作,而对象确实必须物化。就像这样:

代码语言:javascript
复制
struct Foo
{
public:
    Eigen::Vector3d v0;
    Eigen::Vector3d v1;
    Eigen::Vector3d v2;
    
    Foo(const Eigen::VectorXd& vs) __attribute__((noinline));
    Eigen::Vector3d operator()() const __attribute__((noinline));
};

Foo::Foo(const Eigen::VectorXd& vs)
: v0(vs.segment<3>(0)),
  v1(vs.segment<3>(3)),
  v2(vs.segment<3>(6))
{}
Eigen::Vector3d Foo::operator()() const
{
    const Eigen::Vector3d vt = v0 + v1 + v2;
    return vt.transpose() * vt * vt + vt;
}
Eigen::Vector3d bar(const Eigen::VectorXd& vs)
{
    Foo f(vs);
    return f();
}

通过将初始化和使用拆分为非内联函数,副本确实必须完成。当然,我们现在更改了整个用例。你必须决定这是否与你有关。

参考文献的目的

Ref存在的唯一目的是提供一个函数接口,它既可以接受完整的矩阵/向量,也可以接受一个完整的矩阵/向量。考虑到这一点:

代码语言:javascript
复制
Eigen::VectorXd foo(const Eigen::VectorXd&)

这个接口只能接受一个完整的向量作为输入。如果您想要调用foo(vector.head(10)),您必须分配一个新的向量来保存向量段。同样,它总是返回一个新分配的向量,如果要将其称为output.head(10) = foo(input),这是浪费的。所以我们可以写

代码语言:javascript
复制
void foo(Eigen::Ref<Eigen::VectorXd> out, const Eigen::Ref<const Eigen::VectorXd>& in);

并将其作为foo(output.head(10), input.head(10))使用,而不创建任何副本。这只有在编译单元中才有用。如果有一个cpp文件声明了在另一个cpp中使用的函数,Ref允许这种情况发生。在cpp文件中,只需使用模板即可。

代码语言:javascript
复制
template<class Derived1, class Derived2>
void foo(const Eigen::MatrixBase<Derived1>& out,
         const Eigen::MatrixBase<Derived2>& in)
{
    Eigen::MatrixBase<Derived1>& mutable_out =
          const_cast<Eigen::MatrixBase<Derived1>&>(out);
    mutable_out = ...;
}

模板总是更快,因为它可以使用具体的数据类型。例如,如果您传递整个向量,本征知道数组是正确对齐的。在一个完整的矩阵中,它知道列之间没有跨距。对于Ref,它都不知道这些。在这方面,Ref只是一个花哨的Eigen::Map<Type, Eigen::Unaligned, Eigen::OuterStride<>>包装器。

同样,在某些情况下,Ref必须创建临时副本。最常见的情况是内部步长不是1。例如,如果传递矩阵的一行(但不是列),就会发生这种情况。在默认情况下,特征是列-主)或复值矩阵的真实部分。您甚至不会收到这方面的警告,您的代码运行速度将比预期的要慢。

在单个cpp文件中使用Ref的唯一原因是

  1. 使代码更具可读性。上面显示的模板模式肯定没有告诉您多少有关预期类型的信息。
  2. 以减少代码大小,这可能会提高性能,但通常不会。

与固定大小的类型一起使用

由于您的用例似乎涉及固定大小的向量,所以让我们特别考虑这个案例并查看内部。

代码语言:javascript
复制
void foo(const Eigen::Vector3d&);
void bar(const Eigen::Ref<const Eigen::Vector3d>&);

int main()
{
    Eigen::VectorXd in = ...;
    foo(in.segment<3>(6));
    bar(in.segment<3>(6));
}

当您调用foo时,将发生以下情况:

  1. 我们将3个双倍从in[6]复制到堆栈。这需要4个指令(2个movapd,2个movsd)。
  2. 指向这些值的指针将传递给foo。(即使是固定大小的特征向量也声明一个析构函数,因此它们总是被传递到堆栈上,即使我们按值声明它们)
  3. 然后foo通过该指针加载值,接受2条指令(movsd+ movsd)

当我们调用bar时,会发生以下情况:

  1. 我们创建一个Ref<Vector>对象。为此,我们在堆栈上放置了指向in.data() + 6的指针
  2. 指向此指针的指针传递给条形图。
  3. 条从堆栈加载指针,然后加载值。

请注意,两者之间几乎没有任何差别。也许Ref保存了一些说明,但它也引入了一个间接的方向。与其他一切相比,这几乎没有什么意义。这当然是太少,无法衡量。

我们也进入了微优化的领域。这可能导致出现这样的情况,在这种情况下,仅仅是代码的排列就会导致不同的优化。特征:为什么这个模板表达式的Map比Vector3d慢?

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72807833

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档