最快的`finally` for C ++

时间:2017-06-13 11:54:09

标签: c++ performance raii try-catch-finally finally

到目前为止,

C ++(遗憾的是)不支持finally语句的try子句。这导致了对如何释放资源的猜测。在互联网上研究了这个问题之后,虽然我找到了一些解决方案,但我并没有明白他们的表现(如果表现并不重要,我会使用Java)。所以我不得不进行基准测试。

选项包括:

  1. CodeProject提议的基于Functor的finally课程。它很强大,但很慢。并且反汇编表明外部函数局部变量被非常低效地捕获:逐个推送到堆栈,而不是仅仅将帧指针传递给内部(lambda)函数。

  2. RAII:堆栈上的手动清理器对象:缺点是手动键入并为每个使用的位置定制它。另一个缺点是需要将资源释放所需的所有变量复制到其中。

  3. MSVC ++特定__try / __finally statement。缺点是它显然不便携。

  4. 我创建了这个小基准来比较这些方法的运行时性能:

    #include <chrono>
    #include <functional>
    #include <cstdio>
    
    class Finally1 {
      std::function<void(void)> _functor;
    public:
      Finally1(const std::function<void(void)> &functor) : _functor(functor) {}
      ~Finally1() {
        _functor();
      }
    };
    
    void BenchmarkFunctor() {
      volatile int64_t var = 0;
      const int64_t nIterations = 234567890;
      auto start = std::chrono::high_resolution_clock::now();
      for (int64_t i = 0; i < nIterations; i++) {
        Finally1 doFinally([&] {
          var++;
        });
      }
      auto elapsed = std::chrono::high_resolution_clock::now() - start;
      double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
      printf("Functor: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
    }
    
    void BenchmarkObject() {
      volatile int64_t var = 0;
      const int64_t nIterations = 234567890;
      auto start = std::chrono::high_resolution_clock::now();
      for (int64_t i = 0; i < nIterations; i++) {
          class Cleaner {
            volatile int64_t* _pVar;
          public:
            Cleaner(volatile int64_t& var) : _pVar(&var) { }
            ~Cleaner() { (*_pVar)++; }
          } c(var);
      }
      auto elapsed = std::chrono::high_resolution_clock::now() - start;
      double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
      printf("Object: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
    }
    
    void BenchmarkMSVCpp() {
      volatile int64_t var = 0;
      const int64_t nIterations = 234567890;
      auto start = std::chrono::high_resolution_clock::now();
      for (int64_t i = 0; i < nIterations; i++) {
        __try {
        }
        __finally {
          var++;
        }
      }
      auto elapsed = std::chrono::high_resolution_clock::now() - start;
      double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
      printf("__finally: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
    }
    
    template <typename Func> class Finally4 {
      Func f;
    public:
      Finally4(Func&& func) : f(std::forward<Func>(func)) {}
      ~Finally4() { f(); }
    };
    
    template <typename F> Finally4<F> MakeFinally4(F&& f) {
      return Finally4<F>(std::forward<F>(f));
    }
    
    void BenchmarkTemplate() {
      volatile int64_t var = 0;
      const int64_t nIterations = 234567890;
      auto start = std::chrono::high_resolution_clock::now();
      for (int64_t i = 0; i < nIterations; i++) {
        auto doFinally = MakeFinally4([&] { var++; });
        //Finally4 doFinally{ [&] { var++; } };
      }
      auto elapsed = std::chrono::high_resolution_clock::now() - start;
      double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
      printf("Template: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
    }
    
    void BenchmarkEmpty() {
      volatile int64_t var = 0;
      const int64_t nIterations = 234567890;
      auto start = std::chrono::high_resolution_clock::now();
      for (int64_t i = 0; i < nIterations; i++) {
        var++;
      }
      auto elapsed = std::chrono::high_resolution_clock::now() - start;
      double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
      printf("Empty: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
    }
    
    int __cdecl main() {
      BenchmarkFunctor();
      BenchmarkObject();
      BenchmarkMSVCpp();
      BenchmarkTemplate();
      BenchmarkEmpty();
      return 0;
    }
    

    我的Ryzen 1800X @ 3.9Ghz与DDR4 @ 2.6Ghz CL13的结果是:

    Functor: 175148825.946 Ops/sec, var=234567890
    Object: 553446751.181 Ops/sec, var=234567890
    __finally: 553832236.221 Ops/sec, var=234567890
    Template: 554964345.876 Ops/sec, var=234567890
    Empty: 554468478.903 Ops/sec, var=234567890
    

    显然,除了functor-base(#1)之外的所有选项都和空循环一样快。

    那么有一个快速而强大的C ++替代finally,它是可移植的,并且需要从外部函数的堆栈中进行最少的复制吗?

    更新:我已经对@ Jarod42解决方案进行了基准测试,所以这里的问题是更新代码和输出。虽然正如@Sopel所提到的,如果没有执行复制省略,它可能会中断。

    UPDATE2:澄清我要求的是一种方便快捷的C ++方法,即使抛出异常也可以执行代码块。由于问题中提到的原因,某些方法很慢或不方便。

2 个答案:

答案 0 :(得分:11)

您可以实现Finally,而无需删除std::function

的类型擦除和开销
template <typename F>
class Finally {
    F f;
public:
    template <typename Func>
    Finally(Func&& func) : f(std::forward<Func>(func)) {}
    ~Finally() { f(); }

    Finally(const Finally&) = delete;
    Finally(Finally&&) = delete;
    Finally& operator =(const Finally&) = delete;
    Finally& operator =(Finally&&) = delete;
};

template <typename F>
Finally<F> make_finally(F&& f)
{
    return { std::forward<F>(f) };
}

并使用它:

auto&& doFinally = make_finally([&] { var++; });

Demo

答案 1 :(得分:0)

嗯,这是你的基准破坏了:它实际上并没有抛出,所以你只能看到非异常路径。这是非常糟糕的,因为优化器可以证明你没有抛出,所以它可以丢弃实际处理执行清理的所有代码,但是在飞行中有异常。

我认为,您应该重复您的基准测试,将exceptionThrower()nonthrowingThrower()调用到您的try{}块中。这两个函数应编译为单独的转换单元,并且只与基准代码链接在一起。这将迫使编译器实际生成异常处理代码,无论您是调用exceptionThrower()还是nonthrowingThrower()。 (确保您没有打开链接时间优化,这可能会破坏效果。)

这也可以让您轻松比较异常和非抛出执行路径之间的性能影响。

除了基准问题,C ++中的异常很慢。你永远不会在一秒钟内抛出数以亿计的异常。它最多只有一位数百万,可能更少。我希望不同finally实现之间的任何性能差异在抛出案例中完全无关紧要。您可以优化的是非投掷路径,其中您的成本只是构建/销毁finally实现对象,无论是什么。