En un tutorial anterior vimos como incluir un interprete de Python en una aplicación C++. Ahora veremos cómo con unos pequeños cambios podemos incrementar su rendimiento
Introducción
Mejoras de rendimiento
Empezaremos partiendo con nuestra función calculate_cosine
float calculate_cosine() { py::scoped_interpreter guard{}; py::module_ math_module = py::module_::import("math"); py::object result = math_module.attr("cos")(0.5); return py::cast<float>(result); }
Cada vez que llamanamos a esta función una instancia del intérprete de Python es inicializada y su módulo math es cargado antes de llamar a la función cos. Pongamos esta función en una clase para hacer un pequeño test de rendimiento
PythonInterpreter.cpp
#include "PythonInterpreter.h" namespace py = pybind11; PythonInterpreter::PythonInterpreter() { } PythonInterpreter::~PythonInterpreter() { } float PythonInterpreter::calculate_cosine() { py::scoped_interpreter guard{}; py::module_ math_module = py::module_::import("math"); py::object result = math_module.attr("cos")(0.5); return py::cast<float>(result); }
Veamos cuando tiempo nos lleva ejecutar solo 10 veces la función. En nuestro main.cpp instanciamos un objeto de la anterior clase y ponemos unas métricas de tiempo alrededor del bloque que hace las llamadas a calculate_cosine
main.cpp
#include <iostream> #include <chrono> #include "PythonInterpreter.h" using namespace std::chrono; int main() { { PythonInterpreter python_interpreter; float dumpvalue = 0.0; auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 10; ++i) { dumpvalue += python_interpreter.calculate_cosine(); } auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<milliseconds>(stop - start); std::cout << "Time: (10 iterations) : " << duration.count() << " ms" << std::endl; } return 0; }
¡Nos lleva unos 300 ms calcular solo 10 cosenos! Es múcho tiempo para solo 10 cáculos muy simples
Por lo tanto vamos a mover la inicialización del intérprete de Python al constructor de nuestra clase, sin olvidar poner también la finalización del intérprete en el destructor
Queremos reemplazar el scoped_interpreter guard con un sistema de inicialización-finalize y enlazar el tiempo de vida del intérprete con el tiempo de vida de nuestra clase. Con esto cuando el objeto de la clase es instanciado el intérprete es inicializado y cuando el objeto es destruido el intérprete es finalizado
PythonInterpreter.cpp
#include "PythonInterpreter.h" namespace py = pybind11; PythonInterpreter::PythonInterpreter() { py::initialize_interpreter(); } PythonInterpreter::~PythonInterpreter() { py::finalize_interpreter(); } float PythonInterpreter::calculate_cosine() { py::module_ math_module = py::module_::import("math"); py::object result = math_module.attr("cos")(0.5); return py::cast<float>(result); }
Con este cambio cuando llamamos a la función calculate_cosine el intérprete ya está inicializado
Podemos ver cuanto tiempo lleva la inicialización del intérprete mirando el tiempo que nos lleva instanciar el objeto de nuestra clase
main.cpp
#include <iostream> #include <chrono> #include "PythonInterpreter.h" using namespace std::chrono; int main() { { auto start = std::chrono::high_resolution_clock::now(); PythonInterpreter python_interpreter; auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<milliseconds>(stop - start); std::cout << "Initialize interpreter: " << duration.count() << " ms" << std::endl; float dumpvalue = 0.0; start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 10; ++i) { dumpvalue += python_interpreter.calculate_cosine(); } stop = std::chrono::high_resolution_clock::now(); duration = std::chrono::duration_cast<milliseconds>(stop - start); std::cout << "Time (10 iterations) : " << duration.count() << " ms" << std::endl; } return 0; }
Vemos que el tiempo utilizado para inicializar el intérprete es de unos 31ms, y el tiempo para ejecutar los 10 cosenos es de algún microsegundo. Esto explica el resultado anterior de 316ms ~= 310ms = (31ms + 0ms) * 10
¡Y con este cambio el tiempo de ejecutar todo el programa es ahora de tan solo 31ms!
Conclusion
Con este tutorial hemos visto como tenemos que gestionar el intérprete de Python embebido en una aplicación C++ para incrementar su rendimiento cuando ejecutamos código Python. Ha sido algo realmente sencillo pero aún así puede pasarse por alto, especialmente cuando es nuestra primera vez embebiendo un intérprete de Python
Te puede interesar:
Ayudanos con este blog!
El último año he estado dedicando cada vez más tiempo a la creación de tutoriales, en su mayoria sobre desarrollo de videojuegos. Si crees que estos posts te han ayudado de alguna manera o incluso inspirado, por favor considera ayudarnos a mantener este blog con alguna de estas opciones. Gracias por hacerlo posible!