Skip to content

Commit bc61683

Browse files
committed
Add test_perf_accessors (to be merged into test_pytypes).
1 parent 8d14e66 commit bc61683

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

include/pybind11/pytypes.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ class object_api : public pyobject_tag {
180180

181181
PYBIND11_NAMESPACE_END(detail)
182182

183+
#if !defined(PYBIND11_HANDLE_REF_DEBUG) && !defined(NDEBUG)
184+
# define PYBIND11_HANDLE_REF_DEBUG
185+
#endif
186+
183187
/** \rst
184188
Holds a reference to a Python object (no reference counting)
185189
@@ -209,6 +213,9 @@ class handle : public detail::object_api<handle> {
209213
this function automatically. Returns a reference to itself.
210214
\endrst */
211215
const handle &inc_ref() const & {
216+
#ifdef PYBIND11_HANDLE_REF_DEBUG
217+
inc_ref_counter(1);
218+
#endif
212219
Py_XINCREF(m_ptr);
213220
return *this;
214221
}
@@ -244,6 +251,18 @@ class handle : public detail::object_api<handle> {
244251

245252
protected:
246253
PyObject *m_ptr = nullptr;
254+
255+
#ifdef PYBIND11_HANDLE_REF_DEBUG
256+
private:
257+
std::size_t static inc_ref_counter(std::size_t add) {
258+
static std::size_t counter = 0;
259+
counter += add;
260+
return counter;
261+
}
262+
263+
public:
264+
std::size_t static inc_ref_counter() { return inc_ref_counter(0); }
265+
#endif
247266
};
248267

249268
/** \rst

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ set(PYBIND11_TEST_FILES
145145
test_numpy_vectorize
146146
test_opaque_types
147147
test_operator_overloading
148+
test_perf_accessors
148149
test_pickling
149150
test_pytypes
150151
test_sequences_and_iterators

tests/test_perf_accessors.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#include "pybind11_tests.h"
2+
3+
namespace test_perf_accessors {
4+
5+
py::object perf_list_accessor(std::size_t num_iterations, int test_id) {
6+
py::list l;
7+
l.append(0);
8+
auto var = l[0]; // Type of var is list accessor.
9+
py::int_ answer(42);
10+
var = answer; // Detach var from list, which releases a reference.
11+
#ifdef PYBIND11_HANDLE_REF_DEBUG
12+
if (num_iterations == 0) {
13+
py::list return_list;
14+
std::size_t inc_refs = py::handle::inc_ref_counter();
15+
var = answer;
16+
inc_refs = py::handle::inc_ref_counter() - inc_refs;
17+
return_list.append(inc_refs);
18+
inc_refs = py::handle::inc_ref_counter();
19+
var = 42;
20+
inc_refs = py::handle::inc_ref_counter() - inc_refs;
21+
return_list.append(inc_refs);
22+
return return_list;
23+
}
24+
#endif
25+
if (test_id == 0) {
26+
while (num_iterations != 0u) {
27+
var = answer;
28+
num_iterations--;
29+
}
30+
} else {
31+
while (num_iterations != 0u) {
32+
var = 42;
33+
num_iterations--;
34+
}
35+
}
36+
return py::none();
37+
}
38+
39+
} // namespace test_perf_accessors
40+
41+
TEST_SUBMODULE(perf_accessors, m) {
42+
using namespace test_perf_accessors;
43+
m.def("perf_list_accessor", perf_list_accessor);
44+
}

tests/test_perf_accessors.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import time
2+
3+
from pybind11_tests import perf_accessors as m
4+
5+
6+
def find_num_iterations(
7+
callable,
8+
callable_args,
9+
num_iterations_first_pass,
10+
num_iterations_target_elapsed_secs,
11+
time_delta_floor=1.0e-6,
12+
target_elapsed_secs_multiplier=1.05, # Empirical.
13+
target_elapsed_secs_tolerance=0.05,
14+
search_max_iterations=100,
15+
):
16+
td_target = num_iterations_target_elapsed_secs * target_elapsed_secs_multiplier
17+
num_iterations = num_iterations_first_pass
18+
for _ in range(search_max_iterations):
19+
t0 = time.time()
20+
callable(num_iterations, *callable_args)
21+
td = time.time() - t0
22+
num_iterations = max(
23+
1, int(td_target * num_iterations / max(td, time_delta_floor))
24+
)
25+
if abs(td - td_target) / td_target < target_elapsed_secs_tolerance:
26+
return num_iterations
27+
raise RuntimeError("find_num_iterations failure: search_max_iterations exceeded.")
28+
29+
30+
def test_perf_list_accessor():
31+
print(flush=True)
32+
inc_refs = m.perf_list_accessor(0, 0)
33+
print(f"{inc_refs=}", flush=True)
34+
if inc_refs is not None:
35+
assert inc_refs == [1, 1]
36+
num_iterations = find_num_iterations(
37+
callable=m.perf_list_accessor,
38+
callable_args=(0,),
39+
num_iterations_first_pass=100000,
40+
num_iterations_target_elapsed_secs=2.0,
41+
)
42+
for test_id in range(2):
43+
t0 = time.time()
44+
m.perf_list_accessor(num_iterations, test_id)
45+
secs = time.time() - t0
46+
u_secs = secs * 10e6
47+
per_call = u_secs / num_iterations
48+
print(
49+
f"PERF {test_id},{num_iterations},{secs:.3f}s,{per_call:.6f}μs",
50+
flush=True,
51+
)

0 commit comments

Comments
 (0)