Pruebas A/B en C++: cómo no romper la producción y encontrar la verdad

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Por qué las pruebas A/B no son solo para la web y el marketing

\n

Cuando 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.

\n

En 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\n

Arquitectura de una prueba A/B en C++: sin dolor ni sufrimiento

\n

El 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\n

Paso 1: Configuración centralizada

\n

Todas 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}
\n

La 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\n

Paso 2: Clase gestora de experimentos

\n

Crearemos 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.

\n
#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}
\n

Importante: Usamos hashing determinista: el mismo usuario siempre cae en el mismo grupo. Esto es crítico para la validez de la prueba.

\n\n

Ejemplo práctico: Prueba A/B de un nuevo alg

Blogs

Book Recommendations