重复间接执行

Performance of repetitive indirection

本文关键字:执行      更新时间:2023-10-16

我发现自己在争论我是否想像代码 1 和代码 2 那样写。在我看来,代码 1 看起来更干净,但从理论上讲,与代码 2 相比,由于其额外的间接性,我可以预期性能损失吗? 这里有任何相关的编译器优化吗? 如果 bar() 返回 Bar*,有什么变化吗?

代码 1:

foo.bar().method1();
foo.bar().method2();
foo.bar().method3();
foo.bar().method4();

代码 2:

Bar& bar = foo.bar(); //Java programmers: ignore ampersand
bar.method1();
bar.method2();
bar.method3();
bar.method4();

编辑:我认为有太多的变量可以问这样一个一般性的问题(例如,常量与非常量方法,编译器是否内联方法,编译器如何处理引用等)。在汇编中分析我的特定代码可能是要走的路。

Code_2相比,Code_1似乎有性能损失。

但请记住鲁棒C++设计的最基本规则:- 过早的优化是万恶之源。为了清楚起见,首先制作代码,然后指定一个好的分析器作为您的"大师"。

第二种选择,Bar bar = foo.bar()肯定更有效率,尽管多少取决于重量条的重量。差异很可能是微不足道的;尝试基准测试。

至于可读性,

我认为第二个选项更具可读性,但这正在进入个人风格。我认为你真正想要的是一个在内部调用所有四个方法的method5。因此,您可以拥有

foo.bar().method5();

仅此而已。

根据bar实际执行的操作,可能会或可能不会(明显的)性能损失。一个更有趣的问题是,是什么让你认为你的第一种方法是"更干净"的。

在不知道实现的任何细节的情况下,我实际上倾向于相反的想法:后一种方法不仅更短(短是好的,因为更少的代码就是更少的错误,更少的内容),而且更干净,更具可读性。

它清楚地反映了作者的意图,并没有让读者想知道实施bar的细节,这可能会导致意想不到的副作用,而这些副作用可能是有意的,也可能不是有意的和/或期望的。

不要这样做,除非你有很好的理由。

参考测试

我进行了一个简单的测试。 在没有优化的情况下编译时,在我的机器上Test_1需要 1272 毫秒,Test_2 1108 毫秒(我运行了几次测试,结果在几毫秒内)。 通过 O2/O3 优化,两个测试似乎花费的时间相同:946 毫秒。

    #include <iostream>
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    #include <chrono>
    using namespace std;
    class Foo
    {
    public:
      Foo() : x_(0) {}
      void add(unsigned amt)
      {
        x_ += amt;
      }
      unsigned x_;
    };
    class Bar
    {
    public:
      Foo& get()
      {
        return foo_;
      }
    private:
      Foo foo_;
    };
    int main()
    {
      srand(time(NULL));
      Bar bar;
      constexpr int N = 100000000;
      //Foo& foo = bar.get(); //TEST_2
      auto start_time = chrono::high_resolution_clock::now();
      for (int i = 0; i < N; ++i)
      {
        bar.get().add(rand()); //TEST_1
        //foo.add(rand()); //TEST_2
      }
      auto end_time = chrono::high_resolution_clock::now();
      cout << bar.get().x_ << endl;
      cout << "Time: ";
      cout << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << endl;
    }

指针测试

我重新运行了测试,但这次是类成员是一个指针。 在没有优化的情况下编译时,在我的机器上Test_3需要 1285-1340 毫秒,Test_4 1110 毫秒。 通过 O2/O3 优化,两个测试似乎花费的时间相同:915 毫秒(令人惊讶的是,比上面的参考测试时间短)。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <chrono>
using namespace std;
class Foo
{
public:
  Foo() : x_(0) {}
  void add(unsigned amt)
  {
    x_ += amt;
  }
  unsigned x_;
};
class Bar
{
public:
  ~Bar()
  {
    delete foo_;
  }
  Foo* get()
  {
    return foo_;
  }
private:
  Foo* foo_ = new Foo;
};
int main()
{
  srand(time(NULL));
  Bar bar;
  constexpr int N = 100000000;
  //Foo* foo = bar.get(); //TEST_4
  auto start_time = chrono::high_resolution_clock::now();
  for (int i = 0; i < N; ++i)
  {
    bar.get()->add(rand()); //TEST_3
    //foo->add(rand()); //TEST_4
  }
  auto end_time = chrono::high_resolution_clock::now();
  cout << bar.get()->x_ << endl;
  cout << "C++ Time: ";
  cout << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << endl;
}

结论

根据我机器上的这些简单测试,当启用优化时,Code 2 样式的速度略快约 ~15%,但启用优化后,性能没有差异。

我认为

在大多数情况下,这两种选择都不是一个好的选择。两者都通过引用公开内部结构,从而破坏封装。我认为Foo对象的抽象级别太低,无法使用它,它应该提供更多类似服务的功能来使用它。

而不是

Bar& bar = foo.bar(); //Java programmers: ignore ampersand
bar.method1(); 
bar.method2(); 
bar.method3(); 
bar.method4();

Foo中应该有一些更高级的方法

class Foo
{
public:
    void doSomething()
    {
        Bar& bar = foo.bar(); //Java programmers: ignore ampersand
        bar.method1(); 
        bar.method2(); 
        bar.method3(); 
        bar.method4();
    }
private:
    Bar& bar();
};

切勿向客户端提供对内部的非常量引用。