Llegamos a ustedes gracias a:



Reportajes y análisis

Por qué el lenguaje de programación C todavía reina

[08/10/2022] El lenguaje de programación C ha estado vivo y coleando desde 1972, y aún reina como uno de los pilares fundamentales de nuestro mundo repleto de software. Pero ¿qué pasa con las docenas de nuevos lenguajes que han surgido en las últimas décadas? Algunos fueron diseñados explícitamente para desafiar el dominio de C, mientras que otros lo socavan como un subproducto de su propia popularidad.

[Reciba lo último de CIO Perú suscribiéndose a nuestro newsletter semanal]

Es difícil superar a C en cuanto a rendimiento, compatibilidad en bare metal y ubicuidad. Aun así, vale la pena ver cómo se compara con algunos de los competidores de lenguajes de renombre.

C versus C++

C se compara con frecuencia con C++, el lenguaje que -como su nombre lo indica- se creó como una extensión de C. Las diferencias entre C++ y C podrían caracterizarse como extensas o excesivas, según a quién se le pregunte.

Si bien sigue siendo similar a C en su sintaxis y enfoque, C++ proporciona muchas características realmente útiles que no están disponibles de forma nativa en C: espacios de nombres, plantillas, excepciones, administración automática de memoria, etcétera. Los proyectos que exigen un rendimiento de primer nivel -como las bases de datos y los sistemas de aprendizaje automático- se escriben con frecuencia en C++ y utilizan esas funciones para exprimir cada gota de rendimiento del sistema.

Además, C++ continúa expandiéndose mucho más agresivamente que C. El próximo C++ 23 trae aún más a la mesa, incluyendo módulos, rutinas y una biblioteca estándar en módulos para una compilación más rápida y más código exitoso. Por el contrario, la próxima versión planificada del estándar C, C2x, agrega poco y se enfoca en mantener la compatibilidad con versiones anteriores.

La cuestión es que todas las ventajas de C++ también pueden funcionar como desventajas (grandes). Cuantas más funciones de C++ utilice, más complejidad introducirá y más difícil será controlar los resultados. Los desarrolladores que se limitan a un subconjunto de C++ pueden evitar muchas de sus peores trampas. Pero algunos usuarios quieren protegerse contra esa complejidad por completo. El equipo de desarrollo del kernel de Linux, por ejemplo, evita C++, y aunque ahora considera a Rust como lenguaje para futuras adiciones al kernel, la mayoría de Linux aún se escribirá en C.

Elegir C, en lugar de C++, es una forma para que los desarrolladores y aquellos que mantienen su código adopten el minimalismo forzado y eviten enredarse con los excesos de C++. Por supuesto, C++ tiene un amplio conjunto de funciones de alto nivel por una buena razón. Pero si el minimalismo se adapta mejor a los proyectos actuales y futuros -y a los equipos de proyectos- entonces C tiene más sentido.

C versus Java

Después de décadas, Java sigue siendo un elemento básico del desarrollo de software empresarial -y un elemento básico del desarrollo en general. La sintaxis de Java toma mucho de C y C++. Sin embargo, a diferencia de C, Java no se compila de forma predeterminada en código nativo. En su lugar, el compilador JIT (just-in-time) de Java compila el código Java para ejecutarlo en el ambiente de destino. El motor JIT optimiza las rutinas en tiempo de ejecución en función del comportamiento del programa, lo que permite muchas clases de optimización que no son posibles con C compilado con anticipación. En las circunstancias adecuadas, el código Java compilado JIT puede acercarse o incluso superar el rendimiento de C.

Y, aunque el tiempo de ejecución de Java automatiza la gestión de la memoria, es posible solucionarlo. Por ejemplo, Apache Spark optimiza el procesamiento en memoria, parcialmente, mediante el uso de partes "no seguras del tiempo de ejecución de Java para asignar y administrar directamente la memoria y evitar la sobrecarga del sistema de recolección de elementos no utilizados de JVM.

La filosofía de Java de "escribir una vez, ejecutar en cualquier lugar también hace posible que los programas de Java se ejecuten con relativamente pocos ajustes para una arquitectura de destino. Por el contrario, aunque C se ha portado a una gran cantidad de arquitecturas, es posible que cualquier programa C dado aún requiera personalización para ejecutarse correctamente, por ejemplo, en Windows versus Linux.

Esta combinación de portabilidad y rendimiento sólido, junto con un ecosistema masivo de bibliotecas y marcos de software, hace de Java un lenguaje y tiempo de ejecución de referencia para crear aplicaciones empresariales. Donde no llega al nivel de C es en el área en la que el lenguaje nunca tuvo la intención de competir: correr cerca del metal o trabajar directamente con hardware.

El código C se compila en código de máquina, que es ejecutado por el proceso directamente. Java se compila en bytecode, que es un código intermedio que el intérprete de JVM luego convierte en código de máquina. Además, aunque la administración automática de memoria de Java es una bendición en la mayoría de las circunstancias, C es más adecuado para programas que deben hacer un uso óptimo de recursos de memoria limitados, debido a su pequeña huella inicial.

C versus C# y .NET

Casi dos décadas después de su introducción, C# y .NET siguen siendo partes importantes del mundo del software empresarial. Se ha dicho que C# y .NET fueron la respuesta de Microsoft a Java -un sistema compilador de código administrado y un tiempo de ejecución universal- y tantas comparaciones entre C y Java también son válidas para C y C#/.NET.

Al igual que Java (y hasta cierto punto Python), .NET ofrece portabilidad en una variedad de plataformas, así como un vasto ecosistema de software integrado. Estas no son ventajas pequeñas dada la cantidad de desarrollo orientado a la empresa que se lleva a cabo en el mundo .NET. Cuando usted desarrolla un programa en C#, o cualquier otro lenguaje .NET, puede aprovechar un universo de herramientas y bibliotecas escritas para el tiempo de ejecución de .NET.

Otra ventaja de .NET similar a Java es la optimización JIT. Los programas C# y .NET se pueden compilar con anticipación según C, pero principalmente se compilan justo a tiempo mediante el tiempo de ejecución de .NET y se optimizan con información de tiempo de ejecución. La compilación JIT permite todo tipo de optimizaciones en el lugar para un programa .NET en ejecución que no se puede hacer en C.

Al igual que C (y Java, hasta cierto punto), C# y .NET proporcionan varios mecanismos para acceder a la memoria directamente. A través de las API y los objetos de .NET, se puede acceder al heap, stack y a la memoria del sistema no administrada. Y los desarrolladores pueden usar el modo no seguro en .NET para lograr un rendimiento aún mayor.

Sin embargo, nada de esto es gratis. Los objetos administrados y los objetos no seguros no se pueden intercambiar arbitrariamente, y la clasificación entre ellos incurre en un costo de rendimiento. Por lo tanto, maximizar el rendimiento de las aplicaciones .NET significa mantener al mínimo el movimiento entre objetos administrados y no administrados.

Cuando no puede permitirse el lujo de pagar la penalidad por la memoria administrada frente a la no administrada, o cuando el tiempo de ejecución de .NET es una mala elección para el ambiente de destino (por ejemplo, el espacio del kernel) o puede no estar disponible en absoluto, entonces C es lo que necesita. Y a diferencia de C# y .NET, C desbloquea el acceso directo a la memoria de manera predeterminada.

C versus Go

La sintaxis de Go le debe mucho a C -las llaves como delimitadores y las declaraciones terminadas con punto y coma son solo dos ejemplos. Los desarrolladores que dominan C normalmente pueden pasar directamente a Go sin mucha dificultad, incluso teniendo en cuenta las nuevas funciones de Go, como los espacios de nombres y la gestión de paquetes.

El código legible fue uno de los objetivos rectores del diseño de Go: facilitar que los desarrolladores se pongan al día con cualquier proyecto de Go y dominen la base de código en poco tiempo. Las bases de código C pueden ser difíciles de asimilar, ya que tienden a convertirse en un nido de ratas de macros y #ifdefs específicos tanto para un proyecto como para un equipo determinado. La sintaxis de Go, y sus herramientas integradas de formato de código y gestión de proyectos, están destinadas a mantener a raya ese tipo de problemas institucionales.

Go también presenta extras como goroutines y canales, herramientas a nivel de lenguaje para manejar la concurrencia y el paso de mensajes entre componentes. C requeriría que estas cosas se desarrollen a mano o que sean abastecidas por una biblioteca externa, pero Go las proporciona listas para usar, lo que hace que sea mucho más fácil construir software que las necesite.

Donde Go difiere más de C, en su interior, es en la gestión de la memoria. Los objetos Go se administran automáticamente y se recolectan como elementos no utilizados de forma predeterminada. Para la mayoría de los trabajos de programación, esto es tremendamente conveniente. Pero también significa que cualquier programa que requiera un manejo determinista de la memoria será más difícil de escribir.

Go incluye el inseguro paquete para eludir algunas de las seguridades de manejo de tipos de Go, como leer y escribir memoria arbitraria con un tipo de Pointer. Pero el paquete inseguro viene con una advertencia de que los programas escritos con él "pueden no ser portables y no estar protegidos por las pautas de compatibilidad de Go 1.

Go es adecuado para crear programas como utilidades de línea de comandos y servicios de red, porque rara vez necesitan manipulaciones tan detalladas. Pero los controladores de dispositivos de bajo nivel, los componentes del sistema operativo del espacio del kernel y otras tareas que exigen un control riguroso sobre el diseño y la administración de la memoria se crean mejor en C.

C versus Rust

De alguna manera, Rust es una respuesta a los acertijos de administración de memoria creados por C y C++, y también a muchas otras deficiencias de estos lenguajes. Rust compila en código de máquina nativo, por lo que se considera a la par con C en cuanto a rendimiento. Sin embargo, la seguridad de la memoria, por defecto, es el principal punto de venta de Rust.

La sintaxis y las reglas de compilación de Rust ayudan a los desarrolladores a evitar errores comunes en la gestión de la memoria. Si un programa tiene un problema de administración de memoria que cruza la sintaxis de Rust, simplemente no se compilará. Los recién llegados al lenguaje --especialmente si provienen de un lenguaje como C, que brinda mucho espacio para este tipo de errores-- pasan la primera fase de su educación en Rust aprendiendo cómo apaciguar al compilador. Pero los defensores de Rust argumentan que este dolor a corto plazo tiene una recompensa a largo plazo: un código más seguro que no sacrifica la velocidad.

Las herramientas de Rust también mejoran con respecto a C. La gestión de proyectos y componentes son parte de la cadena de herramientas proporcionada con Rust de forma predeterminada, que es la misma que con Go. Hay una forma predeterminada y recomendada de administrar paquetes, organizar carpetas de proyectos y manejar muchas otras cosas que en C son ad-hoc en el mejor de los casos, y en las que cada proyecto y equipo las manejan de manera diferente.

Aun así, lo que se promociona como una ventaja en Rust puede no parecerlo para un desarrollador de C. Las funciones de seguridad en tiempo de compilación de Rust no se pueden desactivar, por lo que incluso el programa Rust más trivial debe cumplir con las restricciones de seguridad de la memoria de Rust. C puede ser menos seguro por defecto, pero es mucho más flexible y tolerante cuando es necesario.

Otro posible inconveniente es el tamaño del lenguaje Rust. C tiene relativamente pocas funciones, incluso si se tiene en cuenta la biblioteca estándar. El conjunto de características de Rust es extenso y continúa creciendo. Al igual que con C++, un conjunto de funciones más grande significa más potencia, pero también más complejidad. C es un lenguaje más pequeño, pero mucho más fácil de modelar mentalmente, por lo que quizás sea más adecuado para proyectos en los que Rust sería demasiado.

C versus Python

En estos días, cada vez que se habla de desarrollo de software, Python siempre parece entrar en la conversación. Después de todo, Python es "el segundo mejor lenguaje para todo y, sin duda, uno de los más versátiles, con miles de bibliotecas de terceros disponibles.

Lo que enfatiza Python, y donde más difiere de C, es que favorece la velocidad de desarrollo sobre la velocidad de ejecución. Un programa que podría tardar una hora en armarse en otro lenguaje --como C-- podría ensamblarse en Python en minutos. Por otro lado, ese programa puede tardar unos segundos en ejecutarse en C, pero un minuto en ejecutarse en Python. (Como regla general, los programas de Python habitualmente ejecutan un orden de magnitud más lento que sus contrapartes de C). Pero para muchos trabajos en hardware moderno, Python es lo suficientemente rápido y eso ha sido clave para su adopción.

Otra gran diferencia es la gestión de la memoria. El tiempo de ejecución de Python administra completamente la memoria de los programas de Python, por lo que los desarrolladores no tienen que preocuparse por el meollo de la asignación y la liberación de memoria. Pero aquí, nuevamente, la facilidad del desarrollador tiene un costo en el rendimiento del tiempo de ejecución. Escribir programas en C requiere una atención escrupulosa a la gestión de la memoria, pero los programas resultantes suelen ser el estándar de oro para la velocidad pura de la máquina.

Sin embargo, bajo la piel, Python y C comparten una conexión profunda: el tiempo de ejecución de referencia de Python está escrito en C. Esto permite que los programas de Python envuelvan bibliotecas escritas en C y C++. Partes significativas del ecosistema Python de bibliotecas de terceros, como el aprendizaje automático, tienen código C en su núcleo. En muchos casos, no es una cuestión de C versus Python, sino más bien una cuestión de qué partes de su aplicación deben escribirse en C y cuáles en Python.

Si la velocidad de desarrollo es más importante que la velocidad de ejecución, y si la mayoría de las partes de rendimiento del programa se pueden aislar en componentes independientes (en lugar de distribuirse por todo el código), ya sea Python puro o una combinación de bibliotecas de Python y C una mejor opción que C solo. De lo contrario, C todavía reina.

C versus Carbon

Otro posible competidor reciente tanto para C como para C++ es Carbon, un nuevo lenguaje que se encuentra actualmente en un intenso desarrollo.

El objetivo de Carbon es ser una alternativa moderna a C y C++, con una sintaxis sencilla, herramientas modernas y técnicas de organización de código, así como soluciones a los problemas que los programadores de C y C++ han enfrentado durante mucho tiempo. También está destinado a proporcionar interoperabilidad con bases de código C++, por lo que el código existente se puede migrar de forma incremental. Todo esto es un esfuerzo bienvenido, ya que C y C++ históricamente han tenido herramientas y procesos primitivos en comparación con los lenguajes desarrollados más recientemente.

Entonces, ¿cuál es el inconveniente? En este momento, Carbon es un proyecto experimental, que no está remotamente listo para su uso en producción. Ni siquiera hay un compilador que funcione; solo un explorador de código en línea. Pasará un tiempo antes de que Carbon se convierta en una alternativa práctica a C o C++, si es que alguna vez lo logra.

Crédito foto: Get Bullish / License: CC BY 2.0)