diff --git a/test_perf_error_already_set.cpp b/test_perf_error_already_set.cpp new file mode 100644 index 0000000..cbbd7ce --- /dev/null +++ b/test_perf_error_already_set.cpp @@ -0,0 +1,205 @@ +#include "pybind11_tests.h" + +namespace test_perf_error_already_set { + +struct boost_python_error_already_set { + virtual ~boost_python_error_already_set() {} +}; + +void pure_unwind(std::size_t num_iterations) { + while (num_iterations) { + try { + throw boost_python_error_already_set(); + } catch (const boost_python_error_already_set &) { + } + num_iterations--; + } +} + +void generate_python_exception_with_traceback(const py::object &callable_raising_exception) { + try { + callable_raising_exception(); + } catch (py::error_already_set &e) { + e.restore(); + } +} + +void do_real_work(std::size_t num_iterations) { + while (num_iterations) { + std::sqrt(static_cast(num_iterations % 1000000)); + num_iterations--; + } +} + +void err_set_unwind_err_clear(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + while (num_iterations) { + do_real_work(real_work); + try { + generate_python_exception_with_traceback(callable_raising_exception); + throw boost_python_error_already_set(); + } catch (const boost_python_error_already_set &) { + if (call_error_string) { + py::detail::error_string(); + } + PyErr_Clear(); + } + num_iterations--; + } +} + +void err_set_err_clear(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + while (num_iterations) { + do_real_work(real_work); + generate_python_exception_with_traceback(callable_raising_exception); + if (call_error_string) { + py::detail::error_string(); + } + PyErr_Clear(); + num_iterations--; + } +} + +void err_set_error_already_set(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + while (num_iterations) { + do_real_work(real_work); + try { + generate_python_exception_with_traceback(callable_raising_exception); + throw py::error_already_set(); + } catch (const py::error_already_set &e) { + if (call_error_string) { + e.what(); + } + } + num_iterations--; + } +} + +void err_set_err_fetch(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + PyObject *exc_type, *exc_value, *exc_trace; + while (num_iterations) { + do_real_work(real_work); + generate_python_exception_with_traceback(callable_raising_exception); + PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + if (call_error_string) { + py::detail::error_string(exc_type, exc_value, exc_trace); + } + num_iterations--; + } +} + +void error_already_set_restore(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + generate_python_exception_with_traceback(callable_raising_exception); + while (num_iterations) { + do_real_work(real_work); + try { + throw py::error_already_set(); + } catch (py::error_already_set &e) { + if (call_error_string) { + e.what(); + } + e.restore(); + } + num_iterations--; + } + PyErr_Clear(); +} + +void err_fetch_err_restore(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + generate_python_exception_with_traceback(callable_raising_exception); + PyObject *exc_type, *exc_value, *exc_trace; + while (num_iterations) { + do_real_work(real_work); + PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + if (call_error_string) { + py::detail::error_string(exc_type, exc_value, exc_trace); + } + PyErr_Restore(exc_type, exc_value, exc_trace); + num_iterations--; + } + PyErr_Clear(); +} + +// https://github.com/pybind/pybind11/pull/1895 original PR description. +py::int_ pr1895_original_foo() { + py::dict d; + try { + return d["foo"]; + } catch (const py::error_already_set &) { + return py::int_(42); + } +} + +} // namespace test_perf_error_already_set + +TEST_SUBMODULE(perf_error_already_set, m) { + using namespace test_perf_error_already_set; + m.def("pure_unwind", pure_unwind); + m.def("err_set_unwind_err_clear", + // Is there an easier way to get an exception with traceback? + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_unwind_err_clear( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_err_clear", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_err_clear( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_error_already_set", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_error_already_set( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_err_fetch", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_err_fetch( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("error_already_set_restore", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + error_already_set_restore( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_fetch_err_restore", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_fetch_err_restore( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("pr1895_original_foo", pr1895_original_foo); +} diff --git a/test_perf_error_already_set.py b/test_perf_error_already_set.py new file mode 100644 index 0000000..4c591e2 --- /dev/null +++ b/test_perf_error_already_set.py @@ -0,0 +1,117 @@ +import time + +import pytest + +from pybind11_tests import perf_error_already_set as m + + +def raise_runtime_error_from_python(): + raise RuntimeError("Raised from Python.") + + +def recurse_first_then_call( + depth, callable, call_repetitions, call_error_string, real_work +): + if depth: + recurse_first_then_call( + depth - 1, callable, call_repetitions, call_error_string, real_work + ) + else: + if call_error_string is None: + callable(call_repetitions) + else: + callable( + raise_runtime_error_from_python, + call_repetitions, + call_error_string, + real_work, + ) + + +def find_call_repetitions( + recursion_depth, + callable, + call_error_string, + real_work, + time_delta_floor=1.0e-6, + target_elapsed_secs_multiplier=1.05, # Empirical. + target_elapsed_secs_tolerance=0.05, + max_iterations=100, + call_repetitions_first_pass=100, + call_repetitions_target_elapsed_secs=0.1, # 1.0 for real benchmarking. +): + td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier + crd = call_repetitions_first_pass + for _ in range(max_iterations): + t0 = time.time() + recurse_first_then_call( + recursion_depth, callable, crd, call_error_string, real_work + ) + td = time.time() - t0 + crd = max(1, int(td_target * crd / max(td, time_delta_floor))) + if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: + return crd + raise RuntimeError("find_call_repetitions failure: max_iterations exceeded.") + + +def pr1895_original_foo(num_iterations): + assert num_iterations >= 0 + while num_iterations: + m.pr1895_original_foo() + num_iterations -= 1 + + +@pytest.mark.parametrize( + "perf_name", + [ + "pure_unwind", + "err_set_unwind_err_clear", + "err_set_err_clear", + "err_set_error_already_set", + "err_set_err_fetch", + "error_already_set_restore", + "err_fetch_err_restore", + "pr1895_original_foo", + ], +) +def test_perf(perf_name): + print(flush=True) + if perf_name == "pr1895_original_foo": + callable = pr1895_original_foo + else: + callable = getattr(m, perf_name) + if perf_name in ("pure_unwind", "pr1895_original_foo"): + real_work_list = [None] + call_error_string_list = [None] + else: + real_work_list = [0, 10000] + call_error_string_list = [False, True] + for real_work in real_work_list: + for recursion_depth in [0, 100]: + first_per_call = None + for call_error_string in call_error_string_list: + call_repetitions = find_call_repetitions( + recursion_depth, callable, call_error_string, real_work + ) + t0 = time.time() + recurse_first_then_call( + recursion_depth, + callable, + call_repetitions, + call_error_string, + real_work, + ) + secs = time.time() - t0 + u_secs = secs * 10e6 + per_call = u_secs / call_repetitions + if first_per_call is None: + relative_to_first = "" + first_per_call = per_call + else: + rel_percent = first_per_call / per_call * 100 + relative_to_first = f",{rel_percent:.2f}%" + print( + f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," + f"{real_work},{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", + flush=True, + )