en Tutoriales

Python en aplicaciones C++: mejoras

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;
}
py_interpreter_nok

¡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!

py_interpreter_ok

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

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!

Escribe un comentario

Comentario