LINUX.ORG.RU

Небольшой тест try_catch на C++, C, Vala

 , ,


1

4

Решил поделиться результатами.

Процессор, AMD FX 8350, 4Ghz

Компиляторы:

gcc version 7.2.0 (Ubuntu 7.2.0-1ubuntu1~16.04)
Vala 0.36.8

C/C++, без try catch

const char* noexcept_thrower_c(int i)
{
    if (i == 0)
        return "error";
    return NULL;
}

int noexcept_try(int i)
{
    int res;
    if (noexcept_thrower_c(i) == NULL)
    {
        res = 0;
    }
    else
    {
        res = 1;
    }
    return res;
}
C++ try..catch
void thrower_cpp(int i)
{
    if (i == 0)
        throw std::runtime_error("error");
}

int cpp_try(int i)
{
    int res;
    try
    {
        thrower_cpp(i);
        res = 0;
    }
    catch(const std::exception&)
    {
        res = 1;
    }
    return res;
}
Cexception Try..Catch
void thrower_c(int i)
{
    if (i == 0)
        Throw("error");
}

int cexception_try(int i)
{
    int res;
    CEXCEPTION_T e = CEXCEPTION_NONE;
    Try
    {
        thrower_c(i);
        res = 0;
    }
    Catch(e)
    {
        (void)e;
        res = 1;
    }
    return res;
}
Vala try..catch
public errordomain Error
{
    Thrower,
}

void thrower(int i)  throws Error
{
    if (i == 0)
        throw new Error.Thrower ("error");
}

int vala_try(int i)
{
    int res;
    try
    {
        thrower(i);
        res = 0;
    }
    catch(Error e)
    {
        (void)e;
        res = 1;
    }
    return res;
}
Получившиеся результаты:
vala_try                                 3 ns/op
vala_catch                             252 ns/op
c++_try                                  1 ns/op
c++_catch                             2382 ns/op
noexcept_try                             1 ns/op
noexcept_catch                           1 ns/op
сexception_try                           9 ns/op
сexception_catch                        25 ns/op
Ничего неожиданного, но меня порадовал с++ когда входим только в try, действительно zero-cost exception. Так же несколько удивило время vala, при входе в catch.

Добавил исходники на github: https://github.com/fsb4000/try_bench

Ответ на: комментарий от xaizek

Да Vala вызывает деструкторы, если они есть(за памятью Vala следит и так, так что для удаления писать свой деструктор не нужно)

В генерированном С это выглядит как if / else и goto.

Для этого примера Vala сгенерировал код с 5 if, и 2 goto.

goto были для catch и для finally

А if были для проверки типа исключения, и так же для проверки uncaught исключений.

Для исключения создавался объект GError

fsb4000 ★★ ()
Ответ на: комментарий от xaizek

Вот так vala_thrower выглядит:

void thrower (gint i, GError** error) {
	gint _tmp0_;
	GError * _inner_error_ = NULL;
	_tmp0_ = i;
	if (_tmp0_ == 0) {
		GError* _tmp1_;
		_tmp1_ = g_error_new_literal (ERROR, ERROR_Thrower, "error");
		_inner_error_ = _tmp1_;
		if (_inner_error_->domain == ERROR) {
			g_propagate_error (error, _inner_error_);
			return;
		} else {
			g_critical ("file %s: line %d: uncaught error: %s (%s, %d)", __FILE__, __LINE__, _inner_error_->message, g_quark_to_string (_inner_error_->domain), _inner_error_->code);
			g_clear_error (&_inner_error_);
			return;
		}
	}
}
А так vala_try
gint vala_try (gint i) {
	gint result = 0;
	gint res = 0;
	gint i = 0;
	GError * _inner_error_ = NULL;
	res = 0;
	{
		thrower (i, &_inner_error_);
		if (G_UNLIKELY (_inner_error_ != NULL)) {
			gint _tmp0_ = 0;
			if (_inner_error_->domain == ERROR) {
				goto __catch0_error;
			}
			g_critical ("file %s: line %d: unexpected error: %s (%s, %d)", __FILE__, __LINE__, _inner_error_->message, g_quark_to_string (_inner_error_->domain), _inner_error_->code);
			g_clear_error (&_inner_error_);
			return _tmp0_;
		}
	}
	goto __finally0;
	__catch0_error:
	{
		GError* e = NULL;
		GError* _tmp1_;
		e = _inner_error_;
		_inner_error_ = NULL;
		_tmp1_ = e;
		res = 1;
		_g_error_free0 (e);
	}
	__finally0:
	if (G_UNLIKELY (_inner_error_ != NULL)) {
		gint _tmp2_ = 0;
		g_critical ("file %s: line %d: uncaught error: %s (%s, %d)", __FILE__, __LINE__, _inner_error_->message, g_quark_to_string (_inner_error_->domain), _inner_error_->code);
		g_clear_error (&_inner_error_);
		return _tmp2_;
	}
	result = res;
	return result;
}

fsb4000 ★★ ()
Ответ на: комментарий от fsb4000

И всё это компилируется в нативный код, так что работы по обработке им меньше чем в C++, где надо найди соответствующий DSO в памяти, потом найти секции с DWARF, которые возможно остались лежать на диске, и выполнить байт-код DWARF, чтобы это всё обработать.

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

Решил посмотреть во что преобразуется пример с деструктором в Vala

public errordomain Error
{
    Thrower,
}

public class Number
{
    private int m_number;

    public Number() 
    {
        this.m_number = 42;
    }
    ~Number() 
    {
        stdout.printf("destructor\n");
    }
}

void thrower(int i)  throws Error
{
    var num = new Number();
    if (i == 0)
        throw new Error.Thrower ("error");
}
C
// тут ещё много функций, но остальные не стал приводить, 
// кто хочет может сам собрать, 
// достаточно valac передать флаг --save-temps,
// тогда после сборки останется временный c файл
#define _number_unref0(var) ((var == NULL) ? NULL : (var = (number_unref (var), NULL)))

static void number_finalize (Number * obj) {
	Number * self;
	FILE* _tmp0_;
	self = G_TYPE_CHECK_INSTANCE_CAST (obj, TYPE_NUMBER, Number);
	g_signal_handlers_destroy (self);
	_tmp0_ = stdout;
	fprintf (_tmp0_, "destructor\n");
}

void number_unref (gpointer instance) {
	Number * self;
	self = instance;
	if (g_atomic_int_dec_and_test (&self->ref_count)) {
		NUMBER_GET_CLASS (self)->finalize (self);
		g_type_free_instance ((GTypeInstance *) self);
	}
}

void thrower (gint i, GError** error) {
	Number* num = NULL;
	Number* _tmp0_;
	gint _tmp1_;
	GError * _inner_error_ = NULL;
	_tmp0_ = number_new ();
	num = _tmp0_;
	_tmp1_ = i;
	if (_tmp1_ == 0) {
		GError* _tmp2_;
		_tmp2_ = g_error_new_literal (ERROR, ERROR_Thrower, "error");
		_inner_error_ = _tmp2_;
		if (_inner_error_->domain == ERROR) {
			g_propagate_error (error, _inner_error_);
			_number_unref0 (num);
			return;
		} else {
			_number_unref0 (num);
			g_critical ("file %s: line %d: uncaught error: %s (%s, %d)", __FILE__, __LINE__, _inner_error_->message, g_quark_to_string (_inner_error_->domain), _inner_error_->code);
			g_clear_error (&_inner_error_);
			return;
		}
	}
	_number_unref0 (num);
}

fsb4000 ★★ ()
Ответ на: комментарий от xaizek

Ээ, какой еще байткод dwarf. Там просто cfi-директивами отмечается начало и конец функций, чтобы _Unwind_Backtrace не обломалась об инлайнинг и всякие omit-frame-pointer

annulen ★★★★★ ()
Ответ на: комментарий от annulen

А деструкторы вызывать? Оно же кодируется в виде выражений на основе состояния регистров. (Хотя в этом случае деструкторов нету, тут согласен.)

xaizek ★★★★★ ()
Последнее исправление: xaizek (всего исправлений: 1)

Чё прям 2us/op ?

А чего - возвращаешь в варианте без исключений чиселко, а в варианте с исключениями кидаешь комплексный обьект с выделением памяти на куче?

pon4ik ★★★★★ ()

Реквестирую так же методику подсчёта результатов, а в идеале и код бенчмарков.

А то согласно твоим подсчётам, обработка catch блока стоит как пакет по сети послать принять на некоторых сетевухах...

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

https://github.com/fsb4000/try_bench

Ещё раз затестил перед отправкой

vala_try                2 ns/op
vala_catch            257 ns/op
c++_try                 1 ns/op
c++_catch            2046 ns/op
noexcept_try            1 ns/op
noexcept_catch          1 ns/op
Cexception_try          8 ns/op
Cexception_catch       24 ns/op
Некоторая погрешность есть, но всё же повторяемость хорошая

fsb4000 ★★ ()

vala == c++ без try/catch, потому что язык vala совсем другой, понятие объекта по другому конструируется, а в c++ размотка деструкторов стоит, вообщем пытаетесь сравнивать теплое и мягкое

anonymous ()
Ответ на: комментарий от fsb4000

Ок, спасибо.

Я к тому, что для честного сравнения броска/поимки, необходимо заменить бросок std::runtime_error, на

...
{
throw 42;
} catch (int) {}

или сделать кастомного наследника std::excpeption, или использовать что-то в духе std::system_error.

Т.к. std::runtime_error это последовательные аллокации деаллокации + копирование памяти, да ещё и с неким подсчётом ссылок (который скорее всего блокирует шину памяти при каждом обращении). А в остальных вариантах мы имеем тупо бросок числа размером не более указателя.

Второй момент - умеет этот фреймворк распределение показать? Я не верю, что в 99.9% случаев даже обработка такого тяжёлого исключения может стоить 2 микросекунды :)

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Спасибо, добавил бенч с бросанием const char*

c++_try_const_char             1 ns/op
c++_catch_const_char        1843 ns/op

Справедливости ради в vala тоже были 2 аллокации/2 деалокации. Исключение в Vala это такая структура:

struct _GError
{
  GQuark       domain;
  gint         code;
  gchar       *message;
};
и строка передающиеся в исключение, тоже копируется с помощью strdup

в Cexception действительно передаётся только указатель, и нет аллокаций.

fsb4000 ★★ ()

Обычное заключение:

Да даже родной плюсовый try-with-catch можно использовать. Просто использовать его действительно целесообразно только для обработки ошибочной ситуации, а не для обработки логики.

Ну и целесообразно там, где ошибки идут часто, переписать исключения на другую обработку логики.

Вроде ничего нестандартного нет.

Deleted ()
Ответ на: комментарий от Deleted

Да, у меня такие же выводы, и ещё что нужно править функции такого вида:

T foo_throw(Params p)
{
...
}

std::optional<T> foo_noexcept(Params p) noexcept
{
    std::optional<T> res;
    try
    {
       res = foo_throw(p);
    }
    catch(...)
    {
       res = {};
    }
    return res;
}
На такого вида функции:
enum class exc_type
{ 
     RUNTIME_ERROR,
     LOGIC_ERROR,
     ......
};

struct T_with_errors
{
    exc_type e;
    const char* error_message;
    T result;
};

static T_with_errors foo_noexcept_with_errors(Params p) noexcept
{
....
}

T foo_throw(Params p)
{
    T_with_errors res = foo_noexcept_with_errors(p);
    if (res.error_message != nullptr)
    {
       switch(res.e)
       {
        case exc_type::RUNTIME_ERROR: throw std::runtime_error(res.message);
        ...
       }
    }
    return res.result;
}

std::optional<T> foo_noexcept(Params p) noexcept
{
    T_with_errors res = foo_noexcept_with_errors(p);
    if (res.error_message != nullptr)
    {
       return {};
    }
    return res.result;
}

fsb4000 ★★ ()
Ответ на: комментарий от anonymous

Тогда не хватает сравнения со стандартным в Си errno.h

https://ru.m.wikipedia.org/wiki/Errno.h

Его тоже можно дергать вручную. Ну или написать собственный модуль с аналогичным поведением для своей программы.

Deleted ()
Последнее исправление: merhalak (всего исправлений: 1)

Там, где надо считать наносекунды, экскпшоны не используют. В других местах время выполнения не имеет значения. О чем этот пост?

Deleted ()
Ответ на: комментарий от CYB3R

Так а тестировалось что? Компиляторы-то разные.

Ага, вообще ничего общего. Ну кроме разве что бэкэнда, мидлэнда и большей части сишного фронтэнда, общей для C/С++/Obj-C. Так что не переживай, разницы не будет, да и глупо ее искать при таких результатах и тестах. Хотя ты ведь просто хотел придраться к написанию С/C++, не так ли?

anonymous ()

действительно zero-cost exception.

Нет - это понятие не отражает тех выводов, что ты из него делаешь.

Причины просты - реальный мир сложнее, и отсутствие прямого влияние не то же самое, что его отсутствие. В реальном мире вообще редко что-то на что-то влияет явно/прямо.

И тут точно так же. zero-const значит лишь то, что try ничего не стоит( и то, даже этого они не значит), но это лишь try - это не значит, что бесплатно то, о чём все думают - об экспешенах без throw.

Ты учёл в своих расчётах то, сколько стоит exception safety? Хотя exception safety - это лишь вершина айсберга, на самом деле исключения порождают куда больше зависимостей в логике, которые без них могли быть реализованы лучше и быстрее.

Это вообще типичная для сравнений ситуация. Мы берём две реализации и сравниваем их, но кто тебе сказал, что они оптимальны? Кто тебе сказал, что их оптимальность находится на одном уровне?

Типичный тому пример - сравнения языков. Приходит ламерок на хабр и начинает сравнивать кресты и раст, пишет дерьмо. Разницы нет. Ты приходишь на хабр и делаешь крестовый пример в 10раз быстрее, и ламерок, как и все его защитники - тут же сливаются.

В конечном итоге проблема сводится к тому, что не надо распространять узкие понятия «try бесплатен» на более широкие «цена использования исключений», либо «цена реализации с ними всегда будет аналогична цене без них, при условии одинакового качества реализации», что, естественно, неверно.

vcerloman ()
Ответ на: комментарий от fsb4000

А чего с распределением? Можно посмотреть какой процент замеров - больше чем эти 1.8us?

Пока что - всё равно выглядит странно. Что считает этот бенчмарк? Среднее арифметическое?

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Да, бенчмарк считает среднее арифметическое.

Добавил другую либу для сравнения

Только для c++ throw const char*.

Вот какие результаты у меня выдаёт:

bin/nodius_bench 
clock resolution: mean is 59.191 ns (10240002 iterations)

benchmarking c++_try_const_char
collecting 100 samples, 87096 iterations each, in estimated 0 ns
mean: 0.697393 ns, lb 0.696394 ns, ub 0.700704 ns, ci 0.95
std dev: 0.00760513 ns, lb 0.000209451 ns, ub 0.0168954 ns, ci 0.95
found 6 outliers among 100 samples (6%)
variance is unaffected by outliers

benchmarking c++_catch_const_char
collecting 100 samples, 25 iterations each, in estimated 5.955 ms
mean: 2.40424 μs, lb 2.39725 μs, ub 2.43265 μs, ci 0.95
std dev: 61.6843 ns, lb 7.1206 ns, ub 143.279 ns, ci 0.95
found 2 outliers among 100 samples (2%)
variance is moderately inflated by outliers
коммит добавил на гитхаб.

fsb4000 ★★ ()
Ответ на: комментарий от CYB3R

Хрень сморозил. Дело не в том, что подмножество или супермножество, а в том, что в С - нет throw/catch. Соответственно надпись С/С++ в заголовке попросту безграмотна.

next_time ★★★★ ()