-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Copy of files from pybind/pybind11#1895 (at commit pybind/pybind11@e5…
…f028f).
- Loading branch information
Ralf W. Grosse-Kunstleve
committed
May 18, 2022
1 parent
7b63b9f
commit 7765113
Showing
2 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<double>(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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |