производительность — самый быстрый `наконец` для переполнения стека

C ++ до сих пор (к сожалению) не поддерживает finally пункт для try заявление. Это приводит к размышлениям о том, как освободить ресурсы. Изучив этот вопрос в Интернете, хотя я нашел некоторые решения, я не получил четкого представления об их производительности (и я бы использовал Java, если бы производительность не имела большого значения). Так что мне пришлось тестировать.

Варианты:

  1. Functor основе finally класс предлагается в CodeProject. Это мощно, но медленно. А разборка предполагает, что локальные переменные внешней функции захватываются очень неэффективно: помещаются в стек по очереди, а не просто передают указатель кадра на внутреннюю (лямбда) функцию.

  2. RAII: Ручной уборщик объекта в стеке: недостатком является ручная печать и адаптация его для каждого используемого места. Еще одним недостатком является необходимость копирования в него всех переменных, необходимых для освобождения ресурса.

  3. Специфичный для MSVC ++ __try / __finally заявление. Недостаток в том, что он явно не переносимый.

Я создал этот небольшой тест для сравнения производительности во время выполнения этих подходов:

#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,9 ГГц с DDR4 при 2,6 ГГц 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, он может сломаться, если копирование не выполнено.

ОБНОВЛЕНИЕ 2: Чтобы уточнить, что я спрашиваю, это удобный быстрый способ в C ++ выполнить блок кода, даже если выдается исключение. По причинам, указанным в вопросе, некоторые способы являются медленными или неудобными.

5

Решение

Вы можете реализовать 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++; });

демонстрация

11

Другие решения

Ну, это ваш тест, который сломан: он на самом деле не выбрасывает, так что вы видите только путь без исключения. Это очень плохо, так как оптимизатор может доказать, что вы не генерируете, поэтому он может выбросить весь код, который фактически выполняет очистку, за исключением исключения в полете.

Я думаю, вы должны повторить свой тест, позвонив exceptionThrower() или же nonthrowingThrower() в ваш try{} блок. Эти две функции должны быть скомпилированы как отдельный модуль перевода и связаны только вместе с кодом эталонного теста. Это заставит компилятор фактически генерировать код обработки исключений независимо от того, вызываете ли вы exceptionThrower() или же nonthrowingThrower(), (Убедитесь, что вы не включаете оптимизацию времени соединения, это может испортить эффект.)

Это также позволит вам легко сравнивать влияние производительности на исключительную ситуацию и неисполняемые пути выполнения.


Помимо проблем с тестами, исключения в C ++ медленные. Вы никогда не получите сотни миллионов исключений в течение секунды. В лучшем случае это больше миллионов цифр, а может быть, и меньше. Я ожидаю, что любые различия в производительности между различными finally реализации не имеют никакого отношения к метанию. То, что вы можете оптимизировать, это путь без бросков, где ваши затраты — это просто строительство / разрушение вашего finally объект реализации, что бы это ни было.

0

По вопросам рекламы [email protected]