Por qué las pruebas A/B no son solo para la web y el marketing
\nCuando escuchamos "pruebas A/B", nos vienen a la mente landing pages, botones de "Comprar" y conversiones en Google Analytics. Pero, ¿qué pasa si tu producto no es un sitio web, sino un servidor de alta carga en C++, un sistema embebido o un motor de juegos? ¿Se pueden aplicar pruebas A/B aquí? ¡Por supuesto que sí! Y esto no es solo un término de moda, sino una herramienta real para tomar decisiones basadas en datos, no en la intuición.
\nEn C++, las pruebas A/B ayudan a responder preguntas como: "¿El nuevo algoritmo de ordenación es realmente más rápido?", "¿Vale la pena introducir un pool de hilos?", "¿Mejorará el nuevo formato de serialización el tiempo de respuesta de la API?". Sin una prueba A/B, corres el riesgo de implementar una "optimización" que en realidad resulte ser una regresión. En este artículo, veremos cómo integrar correctamente un mecanismo de experimentos en código C++, sin convertirlo en un espagueti de if-else.
\n\nArquitectura de una prueba A/B en C++: sin dolor ni sufrimiento
\nEl principal peligro de las pruebas A/B en C++ es ensuciar el código de producción con operadores condicionales. Para evitarlo, debemos separar la lógica del experimento de la lógica de negocio. Usamos el patrón Feature Flag (bandera de funcionalidad) y una estrategia. Veamos la arquitectura básica.
\n\nPaso 1: Configuración centralizada
\nTodas las banderas y parámetros de los experimentos se almacenan en un solo lugar. Puede ser un archivo JSON, una base de datos o un servidor de configuración remoto. Para simplificar, usaremos JSON.
\n// experiment_config.json\n{\n "experiments": {\n "sort_algorithm_v2": {\n "enabled": true,\n "variants": {\n "control": 50,\n "treatment": 50\n }\n },\n "memory_pool_size": {\n "enabled": true,\n "variants": {\n "default": 33,\n "pool_256": 33,\n "pool_512": 34\n }\n }\n }\n}\nLa división del tráfico (traffic split) se define en porcentajes. En C++, cargamos esta configuración una vez al iniciar la aplicación.
\n\nPaso 2: Clase gestora de experimentos
\nCrearemos una clase segura para subprocesos llamada ExperimentManager, que, según una clave (por ejemplo, ID de usuario o transacción), determina en qué grupo cae el objeto.
#include <string>\n#include <unordered_map>\n#include <random>\n#include <shared_mutex>\n#include <nlohmann/json.hpp> // para parsear JSON\n\nclass ExperimentManager {\npublic:\n explicit ExperimentManager(const std::string& config_path);\n \n // Devuelve el nombre de la variante para el experimento y clave dados\n std::string getVariant(const std::string& experiment_name, \n const std::string& user_id) const;\n\nprivate:\n struct Experiment {\n bool enabled;\n std::vector<std::pair<std::string, int>> variants; // nombre, peso\n int total_weight;\n };\n \n std::unordered_map<std::string, Experiment> experiments_;\n mutable std::shared_mutex mutex_;\n \n // Hashing determinista para una distribución estable\n size_t hashAndMod(const std::string& key, int mod) const;\n};\n\n// Implementación (simplificada)\nstd::string ExperimentManager::getVariant(const std::string& exp_name,\n const std::string& user_id) const {\n std::shared_lock lock(mutex_);\n auto it = experiments_.find(exp_name);\n if (it == experiments_.end() || !it->second.enabled) {\n return "control"; // fallback\n }\n const auto& exp = it->second;\n size_t bucket = hashAndMod(user_id, exp.total_weight);\n int cumulative = 0;\n for (const auto& [name, weight] : exp.variants) {\n cumulative += weight;\n if (bucket < cumulative) {\n return name;\n }\n }\n return "control";\n}\nImportante: Usamos hashing determinista: el mismo usuario siempre cae en el mismo grupo. Esto es crítico para la validez de la prueba.
\n\n