La Irrazonable Efectividad de C

Me tomé el atrevimiento de traducir un artículo publicado por Damien Katz, cuyo titulo original es The Unreasonable Effectiveness of C, en donde da a conocer sus puntos de vista acerca de dicho lenguaje y el desarrollo de CouchDB.

Por años he intentado hasta lo peor para deshacerme de C. Demasiado simple, demasiados detalles a considerar, demasiado viejo y enrevesado, demasiado bajo nivel. He tenido intensos y tórridos romances con Java, C++ y Erlang. He construido cosas que me enorgullecen con todos estos, y aún así cada uno me ha roto el corazón. Hicieron promesas que no pudieron cumplir, crearon culturas que se enfocaron en cosas equivocadas, e hicieron mañas que eventualmente te hacen sufrir dolorosamente. Y me regreso gateando a C.

C es todo el paquete. Es el único lenguaje altamente productivo, extremadamente rápido, tiene buenas herramientas en todos lados, una gran comunidad, una cultura altamente profesional, y es muy honesto acerca de sus mañas.

Otros lenguajes te llevan a un estado funcional más rápido, pero a la larga, cuando el rendimiento y confiabilidad son importantes, C te ahorrará tiempo y dolores de cabeza. Estoy aprendiendo dolorosamente esa lección nuevamente.

Simple y Expresivo

C es un lenguaje de alto nivel fantástico. Lo repetiré. C es un lenguaje de alto nivel fantástico. No es de nivel tan alto como Java o C#, y ciertamente no tan cercano al alto nivel de Erlang, Python o Javascript. Pero es de tan alto nivel como C++, y mucho más simple. C++ ofrece más abstracción, pero no representa un nivel de abstracción mayor que C. Con C++ debes de conocer todo acerca de C, más otro tanto de ridícula mierda.

“Cuando alguien dice: ‘Quiero un lenguaje de programación en el cual solo tenga que decir lo que deseo que se haga’, dale un caramelo.”

Alan J. Perlis

No hemos tenido que pasar un mal rato pensando en qué lenguajes de bajo nivel podríamos usar en vez de C, porque C es un lenguaje de bajo nivel. Es porque C es tan exitoso en su abstracción sobre la máquina y hacerlo de alto nivel, que hizo a la mayoría de los lenguajes de bajo nivel irrelevantes. C es muy bueno en lo que hace.

La sintaxis y semántica de C es sorprendentemente poderosa y expresiva. Hace fácil el razonamiento de algoritmos de alto nivel y el hardware en bajo nivel al mismo tiempo. Su semántica es tan simple y la sintaxis tan poderosa que disminuye la carga cognitiva substancialmente, dejando al programador enfocarse en lo que es importante.

Hizo a un lado todo lo demás al punto que subió la barra y redefinió lo que creemos que es un lenguaje de bajo nivel. Eso es muy impresionante.

Código más simple, tipos más simples

C es un lenguaje débil, con tipos estáticos, y su sistema de tipos es simple. Al contrario de C++ o Java, no tienes clases donde definas todo tipo de comportamientos de ejecución de los tipos. Estás muy limitado a las estructuras y uniones y todas las llamadas deben ser muy explícitas acerca de como usar los tipos, las llamadas consiguen muy poco de a gratis.

“Querías una banana pero lo que conseguiste fue un gorila sosteniendo la banana y la selva entera.”

Joe Armstrong

Lo que suena como debilidad termina siendo una virtud: el área de superficie de las APIs de C tienden a ser simples y pequeñas. En lugar de frameworks masivos, hay una fuerte tendencia y cultura de crear bibliotecas pequeñas que son abstracciones ligeras sobre tipos más simples.

En contraste con los lenguajes orientados a objetos donde la base de código tiende a evolucionar interfaces interdependientes masivas de tipos complejos, donde los argumentos y tipos de retorno son tipos más complejos y que la complejidad es fractal, cada tipo es una clase definida en términos y métodos con argumentos y tipos de retorno o solo tipos de retorno más complejos.

No es que los sistemas de tipo orientados a objetos forcen que pase la complejidad fractal, pero lo animan, facilitando hacer cosas erróneas. C no lo hace imposible, pero lo hace más difícil. C tiende a engendrar tipos más simples y más superficiales con pocas dependencias que son más fáciles de entender y depurar.

Rey de la Velocidad

C es el lenguaje más rápido disponible, tanto en mediciones de pila pequeña como en uso de pila completa. Y no solo es el más rápido en ejecución, también es consistentemente el más eficiente en consumo de memoria y tiempo de arranque. Y si tienes que lidiar entre espacio y tiempo, C no te esconde los detalles, es fácil razonas en ambos aspectos.

“Tratar de burlar a un compilador elimina el propósito de usar uno.”

Kernighan & Plauger, Los Elementos de Estilo de Programación

Cada vez que alguien clama un rendimiento cercano a C de un lenguaje de mayor nivel como Java o Haskell, se convierte en una broma pesada cuando ves los detalles. Tienes que hacer extraños movimientos de sintaxis, un uso de conocimiento especial de compiladores inteligentes y las entrañas de VM para conseguir ese rendimiento, al punto que la naturaleza de expresión simple del lenguaje se pierde en extrañas optimizaciones que son específicicas de la versión, y usualmente sólo resaltan en micro-mediciones.

Cuendo escribes algo que deba ser rápido en C, sabes por qué es rápido, y no degrada significativamente con diferentes compiladores o entornos como ocurre con las VMs, la forma en que la configuración del recolector de basura pueda radicalmente afectar el rendimiento y las pausas, o la manera en que la interacción de un pedazo de código de una aplicación fuera cambiar totalmente el perfil de recolección de basura para el resto.

La ruta de optimización en C es directa y simple, y cuando no, hay herramientas de perfilado que te ayudarán a entender la razón sin tener que entender las tripas de una VM o del compilador suficientemente inteligente. Al usar perfiladores para CPU, Memoria y de IO, C es mejor al no esconder lo que está pasando de verdad. Las mediciones, de pilas micro o completa, consistentemente prueban que C es el rey.

Ciclos de Construcción-Ejecución-Depuración más rápidos

Algo que es criticamente importante para el desarrollador en cuando a eficiencia y productividad es el ciclo de construcción, ejecución y depuración. Entre más rápido sea el ciclo, lo más interactivo es el desarrollo, y te encuentras mejor situado en el estado de flujo y enfocado. C tiene la interactividad de desarrollo más rápida sobre cualquier lenguaje popular de tipos estáticos.

“El optimismo es un peligro ocupacional de la programación; la retroalimentación es el tratamiento.”

Kent Beck

Ya que el ciclo de construcción, ejecución y depuración no es una característica principal de un lenguaje, sino de las herramientas alrededor de este, el ciclo es algo que tiende a pasar desapercibido. Es difícil recalcar la importancia del ciclo para la productividad. Tristemente es algo que se descarta en la mayoría de las discusiones de lenguajes de programación, donde el foco tiene a ser sólo sobre líneas de código y sobre legibilidad y facilidad de escritura en el mismo. La realidad es que el ciclo de instrumentación e interactividad de C es el más rápido que cualquier otro lenguaje comparable.

Depuradores ubicuos y Volcados de caída útiles

Para casi cualquier sistema al que quieras portar tu aplicación, existen disponibles depuradores de C y herramientas para volcados. Estos son invaluables y permiten encontrar el origen del problema. Y si, habrá problemas.

Error, teclado no encontrado. Presione F1 para continuar.

Con cualquier otro lenguaje puede que no haya un depurador disponible y mucho menos una herramienta para volcado, y haya una posibilidad de que exista un problema potencial si tienes una interfaz a código en C. Entonces tienes que depurar la interfaz entre el otro lenguaje y el código en C, y probablemente pierdas mucho de contexto, haciéndolo un proceso incómodo, propenso a error, y muchas veces completamente inútil en la práctica.

Con código C puro, puedes ver las pilas de llamadas, variables, argumentos, hilos locales, globales, y básicamente todos los datos en memoria. Esto es ridículamente útil especialmente cuando tienes algo que falló después de varios días de ejecución de un proceso de servidor y que no pueda ser reproducible. Si pierdes este contexto en un lenguaje de más alto nivel, prepárate para sufrir mucho dolor.

Disponible desde cualquier lugar

C tiene una interfaz de binario de aplicación (Application Binary Interface, ABI) estándar que es soportada en todos los Sistemas Operativos, lenguajes y plataformas existentes. Y no requiere de alguna capa de ejecución o algo inherente. Esto significa que el código que escribas en C no sólo es útil para llamadas desde código en C, sino para cualquier biblioteca, lenguaje y entornos existentes.

“Portabilidad es el resultado de unos cuantos conceptos y una definición completa”

J. Palme

Puedes usar código en C en ejecutables únicos, lenguajes de script, código de kernel, código embebido, como una biblioteca dinámica, incluso desde SQL. Es la Lingua Franca de programación de sistemas y bibliotecas conectables. Si quieres escribir algo una vez y tenerlo a la mano para la mayoría de entornos y casos de uso posibles, C es la elección más sana.

Si. Tiene sus debilidades

Hay muchas debilidades en C. No tiene chequeo de límites, es fácil de corromper cualquier cosa en memoria, hay punteros colgados y fugar de recursos y memoria, soporte escueto de concurrencia, no tiene módulos ni espacios con nombre. El manejo de errores puede ser dolorosamente incómodo y verboso. Es fácil hacer toda una clase de errores cuando la pila de llamada es aplastada y entradas hostiles se apoderan de tu proceso. ¿Cierres? ¡También!

“Cuando todo falla, lee las instrucciones.”

L. Lasellio

Sus debilidades son bien conocidas, y eso es una virtud. Todos los lenguajes e implementaciones tienen gotchas y hangups. C es más bien serio al respecto. Y hay un montón de herramientas de ejecución y análisis que ayudan con los errores más comunes y peligrosos. La mayoría del software más usado y confiable en el mundo está escrito en C, siendo pruebas de que las debilidades son sobrevaloradas, y fáciles de detectar y arreglar.

En Couchbase recientemente se usaron aproximadamente 2 meses trabajando con una falla en la VM de Erlang. Se gastó mucho tiempo rastreando algo que estaba en el núcleo de la implementación de Erlang, sin estar seguros de qué estaba pasando ni por qué, pensando que a lo mejor la falla fue algo en un código de C propio, esperando que se pudiera encontrar y arreglar. Pero no lo fue, fue un error de condición de carrera en el núcleo de Erlang. Este es un problema fundamental en cualquier lenguaje que se abstrae mucho de la computadora.

Inicialmente por razones de rendimiento, se empezó incrementalmente a reescribir mucho del código de Couchbase a C, y eligiéndolo como primera opción para nuevas características. Está sorprendentemente probado que es más predecible cuando encontramos problemas y cómo depurarlos y arreglarlos. A la larga, es más productivo.

Siempre tengo en el fondo de mi cabeza de que quiero hacer un mejor C. Solo limpiar un poco en los bordes y arreglar unos de los problemas más escandalosos. Pero al juntarlo todo, de principio a fin, sintaxis, semántica, herramientas, etcétera; puede que no sea posible o incluso que no valga la pena el esfuerzo. Hasta hoy, C es irrazonablemente efectivo, y no creo que eso cambie pronto.