Índice
7 min de lectura

570,000 líneas de código generado por un LLM compilaron sin errores. Era 20,171 veces más lento que SQLite.

Alguien hizo un benchmark de una reimplementación de SQLite en Rust escrita por un LLM. La brecha entre código que parece correcto y código que es correcto resultó ser de cinco órdenes de magnitud.

Alguien reimplementó SQLite en Rust usando exclusivamente un LLM, y hace poco le hicieron un benchmark. Compiló sin problemas. Las pruebas pasaron. El código era limpio, bien estructurado y Rust idiomático. En una búsqueda básica por primary key, era 20,171 veces más lento que SQLite.

Ese número me detuvo. No porque que el código generado por LLMs sea lento resulte sorprendente, sino por el origen de esa lentitud. El código no tenía ningún error que un compilador o una suite de tests pudiera detectar. El B-tree estaba correctamente implementado. Existía un query planner. El storage engine funcionaba. Cada pieza era individualmente defendible. El sistema en conjunto era prácticamente inutilizable.

Dediqué tiempo a leer el análisis del benchmark y el código fuente. Los patrones que encontré siguen apareciendo en proyectos generados por LLMs, y creo que apuntan a algo fundamental sobre cómo estos modelos escriben código.

El B-tree estaba ahí. El query planner lo ignoraba.

En SQLite, una búsqueda por PRIMARY KEY toma el camino del B-tree y termina en tiempo O(log n). Cuatro líneas en where.c verifican el iPKey y enrutan la consulta directamente al árbol. Esta es una de esas micro-optimizaciones que solo tiene sentido si entendés cómo encaja el sistema completo.

La versión generada por el LLM también tenía una implementación de B-tree. Funcionaba correctamente en aislamiento. El problema era que el query planner nunca lo llamaba para búsquedas por primary key. La función is_rowid_ref() solo reconocía tres strings literales: “rowid”, “rowid” y “oid”. Si declarabas una columna como id INTEGER PRIMARY KEY, el planner no la reconocía como un alias de rowid. Cada consulta terminaba haciendo un full table scan.

La matemática de esto es brutal. Para 100 filas consultadas 100 veces, el camino del B-tree requiere aproximadamente 700 pasos de comparación. El full scan requiere más de 10,000. Pero el daño real viene de la complejidad algorítmica: O(log n) por búsqueda se convierte en O(n), y a lo largo del benchmark completo eso se acumula hasta llegar a la brecha de 20,171x.

Este es el tipo de bug que ningún unit test detecta a menos que escribas específicamente un benchmark. El B-tree funciona. El scan funciona. El planner elige el incorrecto. Todo pasa.

Los defaults seguros se componen como el interés

Esto es lo que hizo que este caso fuera más interesante que un simple bug de routing. Incluso después de solucionar el problema del query planner, la reimplementación seguía siendo unas 2,900 veces más lenta. La brecha restante vino de una acumulación de decisiones individualmente razonables.

Cada ejecución de query clonaba el AST completo y lo recompilaba a bytecode. SQLite reutiliza los prepared statement handles. Ambos enfoques son válidos, pero clonar un AST en cada ejecución es costoso a escala.

Cada lectura de página allocaba un buffer nuevo de 4KB en el heap. El page cache de SQLite devuelve un puntero directo a la memoria ya cargada. La versión del LLM eligió el camino seguro y obvio: allocar, leer, retornar. Funciona. Solo que es órdenes de magnitud más lento cuando estás leyendo miles de páginas por query.

Cada commit reconstruía el esquema completo desde cero. SQLite compara un único valor entero de cookie. Si el cookie no cambió, el esquema sigue siendo válido. La reimplementación no tenía este concepto, así que hacía todo el trabajo cada vez.

Cada statement disparaba una llamada sync_all() para flushear todos los metadatos del archivo a disco. SQLite usa fdatasync(), que solo flushea los datos del archivo y omite el sync de metadatos. La diferencia es enorme en workloads con muchas escrituras.

Quiero llamar a esto el efecto compuesto de los defaults defensivos. Cada elección en aislamiento tiene una justificación razonable. Clonar el AST evita la complejidad de ownership en Rust. Allocar buffers nuevos previene bugs de use-after-free. Reconstruir el esquema evita problemas de caché obsoleta. Llamar sync_all() da la garantía de durabilidad más fuerte.

Pero los costos de performance se multiplican, no se suman. Cuando cuatro penalizaciones de 10x se apilan, no obtenés 40x más lento. Obtenés 10,000x más lento. Un LLM no razona sobre esta composición porque genera cada función en relativo aislamiento. Optimiza localmente y paga globalmente.

82,000 líneas para reemplazar un one-liner de cron

El otro proyecto generado por LLM del mismo desarrollador mostró el mismo patrón de una manera diferente. El problema: los artefactos de build en el directorio target/ de Rust consumen espacio en disco con el tiempo. La solución del LLM: un daemon de Rust de 82,000 líneas con siete dashboards y un motor de scoring bayesiano para decidir qué artefactos limpiar.

La solución existente es find ./target -type f -atime +30 -delete, una sola línea en un cron job. Cero dependencias. O cargo-sweep, una herramienta oficial de la comunidad que ya existe y maneja edge cases que el daemon no maneja.

El proyecto generado por el LLM incorporó 192 dependencias. Como referencia, ripgrep, una de las herramientas de búsqueda más sofisticadas del ecosistema Rust, usa 61.

Este es un patrón que sigo viendo: los LLMs construyen lo que les pedís, no lo que necesitás. Si le pedís “construye un sistema que gestione inteligentemente los artefactos de build de Rust con monitoreo y scoring”, obtenés exactamente eso. El modelo no tiene mecanismo para dar un paso atrás y preguntarse si el problema requiere un sistema en absoluto. No sabe que el tamaño del directorio target/ es una queja perenne en la comunidad Rust con soluciones bien conocidas. No considera el costo de mantenimiento de 192 dependencias versus cero.

La investigación apunta en la misma dirección

Tenía curiosidad sobre si estos dos proyectos eran casos aislados, así que revisé la investigación más amplia. No lo son.

METR realizó un ensayo controlado aleatorizado con 16 desarrolladores open source experimentados. El grupo que usó herramientas de IA completó las tareas un 19% más lento que el grupo de control. Lo que me quedó dando vueltas: después de que terminó el experimento, el grupo de IA creía que había sido un 20% más rápido. La experiencia subjetiva de productividad era la inversa de la realidad medida.

GitClear analizó 210 millones de líneas de código y encontró que el código copiado y pegado superó al código refactorizado por primera vez. La tendencia correlaciona directamente con la adopción de herramientas de coding con IA. Se está agregando código más rápido de lo que se está mejorando.

El reporte DORA 2024 de Google encontró que un aumento del 25% en la adopción de IA correlacionó con una caída del 7.2% en la estabilidad de deployments. Más código generado por IA llegando a producción, más incidentes saliendo.

El benchmark Mercury de NeurIPS 2024 agregó métricas de eficiencia a los benchmarks estándar de coding. Cuando medís no solo “¿produce el output correcto?” sino “¿produce el output correcto sin desperdiciar recursos?”, las tasas de aprobación cayeron por debajo del 50%.

Nada de esto significa que los LLMs sean inútiles para programar. Los uso constantemente. Pero sí significa que “compila y pasa los tests” es una barra peligrosamente baja. La brecha entre código plausible y código correcto es donde ocurre la ingeniería real.

Lo que esto exige de los desarrolladores

El problema central no es que los LLMs escriban código malo. Escriben código que es localmente coherente y globalmente incoherente. Cada función tiene sentido. El sistema no. Este es exactamente el modo de falla que el testing tradicional no detecta, porque los tests verifican comportamiento local.

Lo que se necesita es evaluación que apunte a las brechas. Benchmarks, no solo tests. Presupuestos de performance en CI, no solo checks de correctness. Revisión arquitectónica que pregunte “¿por qué existe este módulo?” antes de verificar si funciona. Auditorías de dependencias que comparen la complejidad de la solución contra la complejidad del problema.

La pregunta no es “¿este código parece correcto?”. Es “¿cómo probamos que es correcto?”. Y probarlo requiere el tipo de pensamiento a nivel de sistemas que los LLMs actualmente no tienen.

La brecha entre lo que pediste y lo que producción exige es donde vive el criterio de ingeniería. Sin medición, la generación de código es solo generación de tokens.

Unite al boletín

Recibí actualizaciones sobre mis últimos proyectos, artículos y experimentos con IA y desarrollo web.