diff --git a/CMakeLists.txt b/CMakeLists.txt index ba5f665a2f..a6d619bbdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,7 @@ if(PYBIND11_MASTER_PROJECT) endif() set(PYBIND11_HEADERS + include/pybind11/detail/argument_vector.h include/pybind11/detail/class.h include/pybind11/detail/common.h include/pybind11/detail/cpp_conduit.h diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index c635791fee..4dcbf26236 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -10,6 +10,7 @@ #pragma once +#include "detail/argument_vector.h" #include "detail/common.h" #include "detail/descr.h" #include "detail/native_enum_data.h" @@ -2037,6 +2038,10 @@ using is_pos_only = std::is_same, pos_only>; // forward declaration (definition in attr.h) struct function_record; +/// (Inline size chosen mostly arbitrarily; 6 should pad function_call out to two cache lines +/// (16 pointers) in size.) +constexpr std::size_t arg_vector_small_size = 6; + /// Internal data associated with a single function call struct function_call { function_call(const function_record &f, handle p); // Implementation in attr.h @@ -2045,10 +2050,10 @@ struct function_call { const function_record &func; /// Arguments passed to the function: - std::vector args; + argument_vector args; /// The `convert` value the arguments should be loaded with - std::vector args_convert; + args_convert_vector args_convert; /// Extra references for the optional `py::args` and/or `py::kwargs` arguments (which, if /// present, are also in `args` but without a reference). diff --git a/include/pybind11/detail/argument_vector.h b/include/pybind11/detail/argument_vector.h new file mode 100644 index 0000000000..e9bfe064d4 --- /dev/null +++ b/include/pybind11/detail/argument_vector.h @@ -0,0 +1,330 @@ +/* + pybind11/detail/argument_vector.h: small_vector-like containers to + avoid heap allocation of arguments during function call dispatch. + + Copyright (c) Meta Platforms, Inc. and affiliates. + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +#include "common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +PYBIND11_WARNING_DISABLE_MSVC(4127) + +PYBIND11_NAMESPACE_BEGIN(detail) + +// Shared implementation utility for our small_vector-like containers. +// We support C++11 and C++14, so we cannot use +// std::variant. Union with the tag packed next to the inline +// array's size is smaller anyway, allowing 1 extra handle of +// inline storage for free. Compare the layouts (1 line per +// size_t/void*, assuming a 64-bit machine): +// With variant, total is N + 2 for N >= 2: +// - variant tag (cannot be packed with the array size) +// - array size (or first pointer of 3 in std::vector) +// - N pointers of inline storage (or 2 remaining pointers of std::vector) +// Custom union, total is N + 1 for N >= 3: +// - variant tag & array size if applicable +// - N pointers of inline storage (or 3 pointers of std::vector) +// +// NOTE: this is a low-level representational convenience; the two +// use cases of this union are materially different and in particular +// have different semantics for inline_array::size. All that is being +// shared is the memory management behavior. +template +union inline_array_or_vector { + struct inline_array { + bool is_inline = true; + std::uint32_t size = 0; + std::array arr; + }; + struct heap_vector { + bool is_inline = false; + std::vector vec; + + heap_vector() = default; + heap_vector(std::size_t count, VectorT value) : vec(count, value) {} + }; + + inline_array iarray; + heap_vector hvector; + + static_assert(std::is_trivially_move_constructible::value, + "ArrayT must be trivially move constructible"); + static_assert(std::is_trivially_destructible::value, + "ArrayT must be trivially destructible"); + + inline_array_or_vector() : iarray() {} + ~inline_array_or_vector() { + if (!is_inline()) { + hvector.~heap_vector(); + } + } + // Disable copy ctor and assignment. + inline_array_or_vector(const inline_array_or_vector &) = delete; + inline_array_or_vector &operator=(const inline_array_or_vector &) = delete; + + inline_array_or_vector(inline_array_or_vector &&rhs) noexcept { + if (rhs.is_inline()) { + std::memcpy(&iarray, &rhs.iarray, sizeof(iarray)); + } else { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } + assert(is_inline() == rhs.is_inline()); + } + + inline_array_or_vector &operator=(inline_array_or_vector &&rhs) noexcept { + if (this == &rhs) { + return *this; + } + + if (rhs.is_inline()) { + if (!is_inline()) { + hvector.~heap_vector(); + } + std::memcpy(&iarray, &rhs.iarray, sizeof(iarray)); + } else { + if (is_inline()) { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } else { + hvector = std::move(rhs.hvector); + } + } + return *this; + } + + bool is_inline() const { + // It is undefined behavior to access the inactive member of a + // union directly. However, it is well-defined to reinterpret_cast any + // pointer into a pointer to char and examine it as an array + // of bytes. See + // https://dev-discuss.pytorch.org/t/unionizing-for-profit-how-to-exploit-the-power-of-unions-in-c/444#the-memcpy-loophole-4 + bool result = false; + static_assert(offsetof(inline_array, is_inline) == 0, + "untagged union implementation relies on this"); + static_assert(offsetof(heap_vector, is_inline) == 0, + "untagged union implementation relies on this"); + std::memcpy(&result, reinterpret_cast(this), sizeof(bool)); + return result; + } +}; + +// small_vector-like container to avoid heap allocation for N or fewer +// arguments. +template +struct argument_vector { +public: + argument_vector() = default; + + // Disable copy ctor and assignment. + argument_vector(const argument_vector &) = delete; + argument_vector &operator=(const argument_vector &) = delete; + argument_vector(argument_vector &&) noexcept = default; + argument_vector &operator=(argument_vector &&) noexcept = default; + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + handle &operator[](std::size_t idx) { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + handle operator[](std::size_t idx) const { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + void push_back(handle x) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == N) { + move_to_heap_vector_with_reserved_size(N + 1); + push_back_slow_path(x); + } else { + ha.arr[ha.size++] = x; + } + } else { + push_back_slow_path(x); + } + } + + template + void emplace_back(Arg &&x) { + push_back(handle(x)); + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > N) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + reserve_slow_path(sz); + } + } + +private: + using repr_type = inline_array_or_vector; + repr_type m_repr; + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + assert(is_inline()); + auto &ha = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + std::copy(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec)); + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void push_back_slow_path(handle x) { m_repr.hvector.vec.push_back(x); } + + PYBIND11_NOINLINE void reserve_slow_path(std::size_t sz) { m_repr.hvector.vec.reserve(sz); } + + bool is_inline() const { return m_repr.is_inline(); } +}; + +// small_vector-like container to avoid heap allocation for N or fewer +// arguments. +template +struct args_convert_vector { +private: +public: + args_convert_vector() = default; + + // Disable copy ctor and assignment. + args_convert_vector(const args_convert_vector &) = delete; + args_convert_vector &operator=(const args_convert_vector &) = delete; + args_convert_vector(args_convert_vector &&) noexcept = default; + args_convert_vector &operator=(args_convert_vector &&) noexcept = default; + + args_convert_vector(std::size_t count, bool value) { + if (count > kInlineSize) { + new (&m_repr.hvector) typename repr_type::heap_vector(count, value); + } else { + auto &inline_arr = m_repr.iarray; + inline_arr.arr.fill(value ? std::size_t(-1) : 0); + inline_arr.size = static_cast(count); + } + } + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > kInlineSize) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + m_repr.hvector.vec.reserve(sz); + } + } + + bool operator[](std::size_t idx) const { + if (is_inline()) { + return inline_index(idx); + } + assert(idx < m_repr.hvector.vec.size()); + return m_repr.hvector.vec[idx]; + } + + void push_back(bool b) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == kInlineSize) { + move_to_heap_vector_with_reserved_size(kInlineSize + 1); + push_back_slow_path(b); + } else { + assert(ha.size < kInlineSize); + const auto wbi = word_and_bit_index(ha.size++); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + if (b) { + ha.arr[wbi.word] |= (std::size_t(1) << wbi.bit); + } else { + ha.arr[wbi.word] &= ~(std::size_t(1) << wbi.bit); + } + assert(operator[](ha.size - 1) == b); + } + } else { + push_back_slow_path(b); + } + } + + void swap(args_convert_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); } + +private: + struct WordAndBitIndex { + std::size_t word; + std::size_t bit; + }; + + static WordAndBitIndex word_and_bit_index(std::size_t idx) { + return WordAndBitIndex{idx / kBitsPerWord, idx % kBitsPerWord}; + } + + bool inline_index(std::size_t idx) const { + const auto wbi = word_and_bit_index(idx); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + return m_repr.iarray.arr[wbi.word] & (std::size_t(1) << wbi.bit); + } + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + auto &inline_arr = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + for (std::size_t ii = 0; ii < inline_arr.size; ++ii) { + hv.vec.push_back(inline_index(ii)); + } + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void push_back_slow_path(bool b) { m_repr.hvector.vec.push_back(b); } + + static constexpr auto kBitsPerWord = 8 * sizeof(std::size_t); + static constexpr auto kWords = (kRequestedInlineSize + kBitsPerWord - 1) / kBitsPerWord; + static constexpr auto kInlineSize = kWords * kBitsPerWord; + + using repr_type = inline_array_or_vector; + repr_type m_repr; + + bool is_inline() const { return m_repr.is_inline(); } +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 117fecabf0..515aab1414 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1048,13 +1048,14 @@ class cpp_function : public function { } #endif - std::vector second_pass_convert; + args_convert_vector second_pass_convert; if (overloaded) { // We're in the first no-convert pass, so swap out the conversion flags for a // set of all-false flags. If the call fails, we'll swap the flags back in for // the conversion-allowed call below. - second_pass_convert.resize(func.nargs, false); - call.args_convert.swap(second_pass_convert); + second_pass_convert = std::move(call.args_convert); + call.args_convert + = args_convert_vector(func.nargs, false); } // 6. Call the function. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 34b22a57ae..8ac236d404 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -647,8 +647,8 @@ if(NOT PYBIND11_CUDA_TESTS) # Test pure C++ code (not depending on Python). Provides the `test_pure_cpp` target. add_subdirectory(pure_cpp) - # Test embedding the interpreter. Provides the `cpptest` target. - add_subdirectory(test_embed) + # Test C++ code that depends on Python, such as embedding the interpreter. Provides the `cpptest` target. + add_subdirectory(test_with_catch) # Test CMake build using functions and targets from subdirectory or installed location add_subdirectory(test_cmake_build) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 63e59f65a6..d3452aa383 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -76,6 +76,7 @@ } detail_headers = { + "include/pybind11/detail/argument_vector.h", "include/pybind11/detail/class.h", "include/pybind11/detail/common.h", "include/pybind11/detail/cpp_conduit.h", diff --git a/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt b/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt index d84916ea77..bb83a6e6ef 100644 --- a/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt +++ b/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt @@ -26,11 +26,11 @@ add_custom_target( DEPENDS test_subdirectory_embed) # Test custom export group -- PYBIND11_EXPORT_NAME -add_library(test_embed_lib ../embed.cpp) -target_link_libraries(test_embed_lib PRIVATE pybind11::embed) +add_library(test_with_catch_lib ../embed.cpp) +target_link_libraries(test_with_catch_lib PRIVATE pybind11::embed) install( - TARGETS test_embed_lib + TARGETS test_with_catch_lib EXPORT test_export ARCHIVE DESTINATION bin LIBRARY DESTINATION lib diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_with_catch/CMakeLists.txt similarity index 84% rename from tests/test_embed/CMakeLists.txt rename to tests/test_with_catch/CMakeLists.txt index 23c3336437..136537e67f 100644 --- a/tests/test_embed/CMakeLists.txt +++ b/tests/test_with_catch/CMakeLists.txt @@ -33,10 +33,11 @@ if(PYBIND11_TEST_SMART_HOLDER) -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) endif() -add_executable(test_embed catch.cpp test_interpreter.cpp test_subinterpreter.cpp) -pybind11_enable_warnings(test_embed) +add_executable(test_with_catch catch.cpp test_args_convert_vector.cpp test_argument_vector.cpp + test_interpreter.cpp test_subinterpreter.cpp) +pybind11_enable_warnings(test_with_catch) -target_link_libraries(test_embed PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) +target_link_libraries(test_with_catch PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) file(COPY test_interpreter.py test_trampoline.py DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") @@ -44,8 +45,8 @@ endif() add_custom_target( cpptest - COMMAND "$" - DEPENDS test_embed + COMMAND "$" + DEPENDS test_with_catch WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") pybind11_add_module(external_module THIN_LTO external_module.cpp) diff --git a/tests/test_embed/catch.cpp b/tests/test_with_catch/catch.cpp similarity index 93% rename from tests/test_embed/catch.cpp rename to tests/test_with_catch/catch.cpp index 558a7a35e5..5bd8b3880e 100644 --- a/tests/test_embed/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -19,7 +19,7 @@ namespace py = pybind11; int main(int argc, char *argv[]) { // Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number: - std::string updated_pythonpath("pybind11_test_embed_PYTHONPATH_2099743835476552"); + std::string updated_pythonpath("pybind11_test_with_catch_PYTHONPATH_2099743835476552"); const char *preexisting_pythonpath = getenv("PYTHONPATH"); if (preexisting_pythonpath != nullptr) { #if defined(_WIN32) diff --git a/tests/test_embed/external_module.cpp b/tests/test_with_catch/external_module.cpp similarity index 100% rename from tests/test_embed/external_module.cpp rename to tests/test_with_catch/external_module.cpp diff --git a/tests/test_with_catch/test_args_convert_vector.cpp b/tests/test_with_catch/test_args_convert_vector.cpp new file mode 100644 index 0000000000..7ce2d713fa --- /dev/null +++ b/tests/test_with_catch/test_args_convert_vector.cpp @@ -0,0 +1,80 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +using args_convert_vector = py::detail::args_convert_vector; + +namespace { +template +std::vector get_sample_vectors() { + std::vector result; + result.emplace_back(); + for (const auto sz : {0, 4, 5, 6, 31, 32, 33, 63, 64, 65}) { + for (const bool b : {false, true}) { + result.emplace_back(static_cast(sz), b); + } + } + return result; +} + +void require_vector_matches_sample(const args_convert_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii] == expected[ii]); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_vectors>(); + auto samples = get_sample_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](args_convert_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample args_convert_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("args_convert_vector push_back") { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(b, { vec.push_back(b); })); + } +} + +TEST_CASE("args_convert_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("args_convert_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(=, { + vec.reserve(ii); + vec.push_back(b); + })); + } + } +} diff --git a/tests/test_with_catch/test_argument_vector.cpp b/tests/test_with_catch/test_argument_vector.cpp new file mode 100644 index 0000000000..9cf302a9b1 --- /dev/null +++ b/tests/test_with_catch/test_argument_vector.cpp @@ -0,0 +1,94 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +// 2 is chosen because it is the smallest number (keeping tests short) +// where we can create non-empty vectors whose size is the inline size +// plus or minus 1. +using argument_vector = py::detail::argument_vector<2>; + +namespace { +argument_vector to_argument_vector(const std::vector &v) { + argument_vector result; + result.reserve(v.size()); + for (const auto x : v) { + result.push_back(x); + } + return result; +} + +std::vector> get_sample_argument_vector_contents() { + return std::vector>{ + {}, + {py::handle(Py_None)}, + {py::handle(Py_None), py::handle(Py_False)}, + {py::handle(Py_None), py::handle(Py_False), py::handle(Py_True)}, + }; +} + +std::vector get_sample_argument_vectors() { + std::vector result; + for (const auto &vec : get_sample_argument_vector_contents()) { + result.push_back(to_argument_vector(vec)); + } + return result; +} + +void require_vector_matches_sample(const argument_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii].ptr() == expected[ii].ptr()); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_argument_vector_contents(); + auto samples = get_sample_argument_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} + +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](argument_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample argument_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("argument_vector push_back") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { vec.emplace_back(Py_None); })); +} + +TEST_CASE("argument_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("argument_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { + vec.reserve(ii); + vec.emplace_back(Py_True); + })); + } +} diff --git a/tests/test_embed/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp similarity index 99% rename from tests/test_embed/test_interpreter.cpp rename to tests/test_with_catch/test_interpreter.cpp index 0e6c17a777..d227eecddc 100644 --- a/tests/test_embed/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -94,8 +94,9 @@ PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { TEST_CASE("PYTHONPATH is used to update sys.path") { // The setup for this TEST_CASE is in catch.cpp! auto sys_path = py::str(py::module_::import("sys").attr("path")).cast(); - REQUIRE_THAT(sys_path, - Catch::Matchers::Contains("pybind11_test_embed_PYTHONPATH_2099743835476552")); + REQUIRE_THAT( + sys_path, + Catch::Matchers::Contains("pybind11_test_with_catch_PYTHONPATH_2099743835476552")); } TEST_CASE("Pass classes and data between modules defined in C++ and Python") { diff --git a/tests/test_embed/test_interpreter.py b/tests/test_with_catch/test_interpreter.py similarity index 100% rename from tests/test_embed/test_interpreter.py rename to tests/test_with_catch/test_interpreter.py diff --git a/tests/test_embed/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp similarity index 100% rename from tests/test_embed/test_subinterpreter.cpp rename to tests/test_with_catch/test_subinterpreter.cpp diff --git a/tests/test_embed/test_trampoline.py b/tests/test_with_catch/test_trampoline.py similarity index 100% rename from tests/test_embed/test_trampoline.py rename to tests/test_with_catch/test_trampoline.py