Década de 1970: Mejorando el ensamblaje
Viajemos a los orígenes de los sistemas informáticos modernos para entender cómo evolucionó el término. No sé quién acuñó la frase originalmente, pero mis búsquedas sugieren que los esfuerzos serios por definir los «sistemas informáticos» comenzaron alrededor de los primeros años de la década de 1970. En Systems Programming Languages (Bergeron1 et al. 1972), los autores dicen:
Un programa de sistema es un conjunto integrado de subprogramas, que juntos forman un todo mayor que la suma de sus partes, y que supera algún umbral de tamaño y/o complejidad. Ejemplos típicos son los sistemas de multiprogramación, de traducción, de simulación, de gestión de la información y de tiempo compartido. Lo siguiente es un conjunto parcial de propiedades, algunas de las cuales se encuentran en los no-sistemas, no todos los cuales tienen que estar presentes en un sistema dado.
- El problema a resolver es de una amplia naturaleza que consiste en muchos, y por lo general bastante variados, subproblemas.
- El programa del sistema es probable que se utilice para apoyar a otros programas de software y aplicaciones, pero también puede ser un paquete de aplicaciones completo en sí mismo.
- Está diseñado para el uso continuo de «producción» en lugar de una solución de una sola vez a un problema de aplicaciones.
- Es probable que evolucione continuamente en el número y los tipos de características que admite.
- Un programa de sistema requiere una cierta disciplina o estructura, tanto dentro de los módulos como entre ellos (es decir, «comunicación»), y suele ser diseñado e implementado por más de una persona.
Esta definición es bastante aceptable: los sistemas informáticos son a gran escala, se utilizan durante mucho tiempo y varían en el tiempo. Sin embargo, aunque esta definición es en gran medida descriptiva, una idea clave del documento es prescriptiva: abogar por la separación de los lenguajes de bajo nivel de los lenguajes de sistemas (en ese momento, contrastando el ensamblador con el FORTRAN).
El objetivo de un lenguaje de programación de sistemas es proporcionar un lenguaje que pueda utilizarse sin preocuparse excesivamente por consideraciones de «manipulación de bits», pero que genere un código que no sea apreciablemente peor que el generado a mano. Este lenguaje debe combinar la concisión y la legibilidad de los lenguajes de alto nivel con la eficiencia de espacio y tiempo y la capacidad de «llegar» a las instalaciones de la máquina y del sistema operativo que se obtienen en el lenguaje ensamblador. El tiempo de diseño, escritura y depuración debe minimizarse sin imponer una sobrecarga innecesaria en los recursos del sistema.
Al mismo tiempo, los investigadores de la CMU publicaron BLISS: A Language for Systems Programming (Wulf et al. 1972), describiéndolo como:
Nos referimos a BLISS como un «lenguaje de implementación», aunque admitimos que el término es algo ambiguo ya que, presumiblemente, todos los lenguajes informáticos se utilizan para implementar algo. Para nosotros la frase connota un lenguaje de propósito general, de alto nivel, en el que el énfasis principal se ha puesto en una aplicación específica, a saber, la escritura de grandes sistemas de software de producción para una máquina específica. Los lenguajes de propósito especial, como los compiladores, no entran en esta categoría, ni suponemos necesariamente que estos lenguajes deban ser independientes de la máquina. Destacamos la palabra «implementación» en nuestra definición y no hemos utilizado palabras como «diseño» y «documentación». No esperamos necesariamente que un lenguaje de implementación sea un vehículo apropiado para expresar el diseño inicial de un gran sistema ni para la documentación exclusiva de ese sistema. Conceptos como la independencia de la máquina, la expresión del diseño y la implementación en la misma notación, la autodocumentación, y otros, son objetivos claramente deseables y son criterios por los que evaluamos varios lenguajes.
Aquí, los autores contraponen la idea de un «lenguaje de implementación» como de mayor nivel que el ensamblador, pero de menor nivel que un «lenguaje de diseño». Esto se resiste a la definición del artículo anterior, que defiende que el diseño de un sistema y la implementación de un sistema deben tener lenguajes separados.
Ambos artículos son artefactos de investigación o defensas. La última entrada a considerar (también de 1972, ¡un año productivo!) es Systems Programming (Donovan 1972), un texto educativo para el aprendizaje de la programación de sistemas.
¿Qué es la programación de sistemas? Usted puede visualizar un ordenador como una especie de bestia que obedece todas las órdenes. Se ha dicho que los ordenadores son básicamente personas hechas de metal o, por el contrario, las personas son ordenadores hechos de carne y hueso. Sin embargo, una vez que nos acercamos a los ordenadores, vemos que son básicamente máquinas que siguen instrucciones muy específicas y primitivas. En los primeros tiempos de los ordenadores, la gente se comunicaba con ellos mediante interruptores de encendido y apagado que indicaban instrucciones primitivas. Pronto la gente quiso dar instrucciones más complejas. Por ejemplo, querían poder decir X = 30 * Y; dado que Y = 10, ¿cuál es X? Los ordenadores actuales no pueden entender ese lenguaje sin la ayuda de programas de sistemas. Los programas de sistemas (por ejemplo, compiladores, cargadores, macroprocesadores, sistemas operativos) se desarrollaron para que los ordenadores se adaptaran mejor a las necesidades de sus usuarios. Además, la gente quería más ayuda en la mecánica de preparación de sus programas.
Me gusta que esta definición nos recuerde que los sistemas están al servicio de las personas, aunque sólo sean infraestructuras no expuestas directamente al usuario final.
Década de 1990: El auge del scripting
En los años 70 y 80, parece que la mayoría de los investigadores veían la programación de sistemas normalmente como un contraste con la programación en ensamblador. Simplemente no había otras buenas herramientas para construir sistemas. (No estoy seguro de qué lugar ocupaba Lisp en todo esto. Ninguno de los recursos que leí citaba a Lisp, aunque soy vagamente consciente de que las máquinas Lisp existieron aunque brevemente.)
Sin embargo, a mediados de los 90, se produjo un gran cambio en los lenguajes de programación con el auge de los lenguajes de scripting de tipado dinámico. Mejorando los anteriores sistemas de scripts de shell como Bash, lenguajes como Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) y Javascript (1995) se abrieron paso en la corriente principal. Esto culminó en el influyente artículo «Scripting: Programación de alto nivel para el siglo XXI» (Ousterhout 1998). En él se articulaba la «dicotomía de Ousterhout» entre los «lenguajes de programación de sistemas» y los «lenguajes de scripting»
Los lenguajes de scripting están diseñados para tareas diferentes a las de los lenguajes de programación de sistemas, y esto lleva a diferencias fundamentales en los lenguajes. Los lenguajes de programación de sistemas se diseñaron para construir estructuras de datos y algoritmos desde cero, partiendo de los elementos informáticos más primitivos, como las palabras de memoria. En cambio, los lenguajes de scripting están diseñados para pegar: asumen la existencia de un conjunto de componentes potentes y están pensados principalmente para conectar componentes entre sí. Los lenguajes de programación de sistemas están fuertemente tipados para ayudar a gestionar la complejidad, mientras que los lenguajes de scripting no están tipados para simplificar las conexiones entre componentes y proporcionar un rápido desarrollo de aplicaciones. Varias tendencias recientes, como máquinas más rápidas, mejores lenguajes de scripting, la creciente importancia de las interfaces gráficas de usuario y las arquitecturas de componentes, y el crecimiento de Internet, han aumentado en gran medida la aplicabilidad de los lenguajes de scripting.
A nivel técnico, Ousterhout contrastó los lenguajes de scripting frente a los de sistemas a lo largo de los ejes de seguridad de tipos e instrucciones por declaración, como se muestra arriba. A nivel de diseño, caracterizó las nuevas funciones de cada clase de lenguaje: la programación de sistemas sirve para crear componentes, y el scripting para pegarlos.
Por esta época, los lenguajes de tipado estático pero con recolección de basura también empezaron a ganar popularidad. Java (1995) y C# (2000) se convirtieron en los titanes que hoy conocemos. Aunque estos dos no se consideran tradicionalmente «lenguajes de programación de sistemas», se han utilizado para diseñar muchos de los mayores sistemas de software del mundo. Ousterhout incluso mencionó explícitamente que «en el mundo de Internet que está tomando forma ahora, Java se utiliza para la programación de sistemas».
La década de 2010: Los límites se difuminan
En la última década, la línea entre los lenguajes de scripting y los lenguajes de programación de sistemas ha empezado a difuminarse. Empresas como Dropbox fueron capaces de construir sistemas sorprendentemente grandes y escalables con sólo Python. Javascript se utiliza para renderizar interfaces de usuario complejas en tiempo real en miles de millones de páginas web. La tipificación gradual ha ganado fuerza en Python, Javascript y otros lenguajes de scripting, permitiendo una transición de código «prototipo» a código «de producción» mediante la adición incremental de información de tipo estático.
Al mismo tiempo, los enormes recursos de ingeniería invertidos en compiladores JIT tanto para lenguajes estáticos (por ejemplo, HotSpot de Java) como para lenguajes dinámicos (por ejemplo, LuaJIT de Lua, V8 de Javascript, PyPy de Python) han hecho que su rendimiento sea competitivo con los lenguajes de programación de sistemas tradicionales (C, C++). Los sistemas distribuidos a gran escala, como Spark, están escritos en Scala2. Nuevos lenguajes de programación como Julia, Swift y Go siguen superando los límites de rendimiento de los lenguajes de recolección de basura.
Un panel llamado Systems Programming in 2014 and Beyond (Programación de sistemas en 2014 y más allá) contó con las mayores mentes detrás de los lenguajes de sistemas autoidentificados de hoy en día: Bjarne Stroustrup (creador de C++), Rob Pike (creador de Go), Andrei Alexandrescu (desarrollador de D) y Niko Matsakis (desarrollador de Rust). Cuando se les preguntó «¿qué es un lenguaje de programación de sistemas en 2014?», dijeron (transcripción editada):
- Niko Matsakis: Escribir aplicaciones del lado del cliente. El polo opuesto de lo que Go está diseñado para. En estas aplicaciones, tienes necesidades de alta latencia, requisitos de alta seguridad, un montón de requisitos que no aparecen en el lado del servidor.
- Bjarne Stroustrup: La programación de sistemas salió del campo en el que tenías que lidiar con el hardware, y luego las aplicaciones se volvieron más complicadas. Tienes que lidiar con la complejidad. Si tienes problemas de limitaciones de recursos importantes, estás en el ámbito de la programación de sistemas. Si necesitas un control más detallado, también estás en el ámbito de la programación de sistemas. Son las restricciones las que determinan si se trata de programación de sistemas. ¿Te estás quedando sin memoria? ¿Te estás quedando sin tiempo?
- Rob Pike: Cuando anunciamos Go por primera vez, lo llamamos lenguaje de programación de sistemas, y me arrepiento un poco de ello porque mucha gente asumió que era un lenguaje de escritura de sistemas operativos. Lo que deberíamos haber llamado es un lenguaje de escritura para servidores, que es lo que realmente pensamos que era. Ahora entiendo que lo que tenemos es un lenguaje de infraestructura en la nube. Otra definición de programación de sistemas es el material que se ejecuta en la nube.
- Andrei Alexandrescu: Tengo algunas pruebas de fuego para comprobar si algo es un lenguaje de programación de sistemas. Un lenguaje de programación de sistemas debe ser capaz de permitirte escribir tu propio asignador de memoria en él. Debe ser capaz de forjar un número en un puntero, ya que así es como funciona el hardware.
¿La programación de sistemas es entonces de alto rendimiento? ¿Limitaciones de recursos? ¿Control de hardware? ¿Infraestructura de la nube? Parece que, a grandes rasgos, los lenguajes de la categoría de C, C++, Rust y D se distinguen por su nivel de abstracción de la máquina. Estos lenguajes exponen detalles del hardware subyacente como la asignación/distribución de memoria y la gestión de recursos de grano fino.
Otra manera de pensar en ello: cuando tienes un problema de eficiencia, ¿cuánta libertad tienes para resolverlo? La parte maravillosa de los lenguajes de programación de bajo nivel es que cuando identificas una ineficiencia, está a tu alcance eliminar el cuello de botella mediante un cuidadoso control de los detalles de la máquina. Vectorizar esta instrucción, redimensionar esa estructura de datos para mantenerla en la caché, y así sucesivamente. De la misma manera que los tipos estáticos proporcionan más confianza3 como «estas dos cosas que estoy tratando de añadir son definitivamente enteros», los lenguajes de bajo nivel proporcionan más confianza en que «este código se ejecutará en la máquina como he especificado».
Por el contrario, la optimización de los lenguajes interpretados es una jungla absoluta. Es increíblemente difícil saber si el tiempo de ejecución ejecutará consistentemente tu código de la manera que esperas. Este es exactamente el mismo problema con los compiladores autoparalelizantes: «la autovectorización no es un modelo de programación» (ver La historia de ispc). Es como escribir una interfaz en Python, pensando «bueno, ciertamente espero que quien llame a esta función me dé un int.»
Hoy: …¿entonces qué es la programación de sistemas?
Esto me lleva de nuevo a mi queja original. Lo que mucha gente llama programación de sistemas, yo lo veo como una programación de bajo nivel que expone detalles de la máquina. Pero, ¿qué pasa entonces con los sistemas? Recordemos nuestra definición de 1972:
- El problema que hay que resolver es de naturaleza amplia y consiste en muchos, y normalmente muy variados, subproblemas.
- El programa del sistema es probable que se utilice para apoyar a otros programas de software y aplicaciones, pero también puede ser un paquete de aplicaciones completo en sí mismo.
- Está diseñado para un uso continuado de «producción» en lugar de una solución de una sola vez para un único problema de aplicaciones.
- Es probable que evolucione continuamente en el número y los tipos de características que soporta.
- Un programa de sistema requiere una cierta disciplina o estructura, tanto dentro de los módulos como entre ellos (es decir, «comunicación»), y suele ser diseñado e implementado por más de una persona.
Estos parecen mucho más problemas de ingeniería de software (modularidad, reutilización, evolución del código) que problemas de rendimiento de bajo nivel. Lo que significa que cualquier lenguaje de programación que dé prioridad a estos problemas es un lenguaje de programación de sistemas. Eso no significa que todos los lenguajes sean lenguajes de programación de sistemas. Los lenguajes de programación dinámicos están todavía lejos de ser lenguajes de sistemas, ya que los tipos dinámicos y los modismos como «pedir perdón, no permiso» no conducen a una buena calidad de código.
¿Qué nos aporta esta definición, entonces? He aquí una idea candente: los lenguajes funcionales como OCaml y Haskell están mucho más orientados a los sistemas que los lenguajes de bajo nivel como C o C++. Cuando enseñamos programación de sistemas a los estudiantes universitarios, deberíamos incluir principios de programación funcional como el valor de la inmutabilidad, el impacto de los sistemas de tipos ricos en la mejora del diseño de interfaces y la utilidad de las funciones de orden superior. Las escuelas deberían enseñar tanto la programación de sistemas como la programación de bajo nivel.
Según se defiende, ¿hay una distinción entre la programación de sistemas y la buena ingeniería de software? En realidad no, pero un problema aquí es que la ingeniería de software y la programación de bajo nivel a menudo se enseñan de forma aislada. Mientras que la mayoría de las clases de ingeniería de software suelen estar centradas en Java «escribir buenas interfaces y pruebas», también deberíamos enseñar a los estudiantes cómo diseñar sistemas que tienen importantes limitaciones de recursos. Quizá llamemos «sistemas» a la programación de bajo nivel porque muchos de los sistemas de software más interesantes son de bajo nivel (por ejemplo, bases de datos, redes, sistemas operativos, etc.). Dado que los sistemas de bajo nivel tienen muchas restricciones, requieren que sus diseñadores piensen de forma creativa.
Otro marco es que los programadores de bajo nivel deberían tratar de entender qué ideas en el diseño de sistemas podrían adaptarse para hacer frente a la realidad del hardware moderno. Creo que la comunidad de Rust ha sido excesivamente innovadora en este sentido, buscando cómo los buenos principios de diseño de software/programación funcional pueden aplicarse a los problemas de bajo nivel (por ejemplo, los futuros, el manejo de errores o, por supuesto, la seguridad de la memoria).
Para resumir, lo que llamamos «programación de sistemas» creo que debería llamarse «programación de bajo nivel». El diseño de sistemas informáticos como campo es demasiado importante como para no tener su propio nombre. Separar claramente estas dos ideas proporciona una mayor claridad conceptual en el espacio del diseño de lenguajes de programación, y también abre la puerta a compartir ideas a través de los dos espacios: ¿cómo podemos diseñar el sistema alrededor de la máquina, y viceversa?
Por favor, dirija los comentarios a mi bandeja de entrada en [email protected] o Hacker News.
-
Dato curioso: dos de los autores de este trabajo, R. Bergeron y Andy Van Dam, son miembros fundadores de la comunidad gráfica y de la conferencia SIGGRAPH. Parte de un patrón continuo en el que los investigadores de gráficos marcan la tendencia en el diseño de sistemas, c.f. el origen de la GPGPU.
-
¡Enlace obligatorio a la escalabilidad! Pero ¿a qué COSTE?.
-
En realidad los tipos estáticos son garantías al 100% (o te devuelven el dinero), pero en la práctica la mayoría de los lenguajes permiten cierta cantidad de Obj.magic.