Sin embargo, C es un lenguaje de bajo nivel.


Durante la última década desde la llegada del lenguaje C, se han creado muchos lenguajes de programación interesantes. Algunos de ellos todavía se usan, otros han influido en la próxima generación de idiomas, la popularidad del tercero se ha desvanecido en silencio. Mientras tanto, arcaico, controvertido, primitivo, hecho en las peores tradiciones de su generación de lenguajes C (y sus herederos) más vivos que todos los seres vivos.


La crítica C es un género epistolar clásico para nuestra industria. Suena más fuerte, luego más tranquilo, pero últimamente ha sido literalmente impresionante. Un ejemplo es una traducción del artículo de David Ciswell "C no es un lenguaje de bajo nivel", publicado en nuestro blog hace algún tiempo. Puedes decir cosas diferentes sobre C, realmente hay muchos errores desagradables en el diseño del lenguaje, ¡pero rechazar C en "nivel bajo" es demasiado!


Para no tolerar tal injusticia, tomé coraje e intenté decidir qué era un lenguaje de programación de bajo nivel y qué prácticas querían de él, después de lo cual repasé los argumentos de los críticos C. Así es como resultó este artículo.


Contenido



Argumentos de crítica C


Estos son algunos de los argumentos de los críticos de C, incluidos los del artículo de David Chiznell:


  1. La máquina abstracta del lenguaje C es demasiado similar a la arquitectura PDP-11 obsoleta, que ha dejado de corresponder durante mucho tiempo al dispositivo de los procesadores modernos populares.
  2. La falta de coincidencia entre una máquina abstracta y el dispositivo de máquinas reales complica el desarrollo de la optimización de compiladores de lenguaje.
  3. La incompletitud y complejidad del lenguaje estándar lleva a discrepancias en las implementaciones estándar.
  4. El dominio de los lenguajes tipo C no permite explorar arquitecturas de procesador alternativas.

Primero determinemos los requisitos para un lenguaje de bajo nivel, después de lo cual volveremos a los argumentos dados.


Lenguaje de programación de bajo nivel


No existe una definición universalmente aceptada de lenguaje de bajo nivel. Pero antes de discutir temas controvertidos, es deseable tener al menos algunos requisitos iniciales para el tema de la disputa.


Nadie discutirá que el lenguaje ensamblador está en el nivel más bajo. Pero en cada plataforma es único, por lo que el código en dicho lenguaje no puede ser portátil. Incluso en una plataforma compatible con versiones anteriores, es posible que deba usar algunas instrucciones nuevas.


A partir de aquí sigue el primer requisito para un lenguaje de bajo nivel: debe conservar características comunes para plataformas populares . En pocas palabras, el compilador debe ser portátil. La portabilidad del compilador simplifica el desarrollo de compiladores de lenguaje para nuevas plataformas, y la variedad de plataformas soportadas por los compiladores elimina la necesidad de que los desarrolladores reescriban programas de aplicación para cada nueva máquina.


El primer requisito entra en conflicto con los deseos de los desarrolladores de programas especiales: lenguajes de programación, controladores, sistemas operativos y bases de datos de alto rendimiento. Los programadores que escriben estos programas quieren poder optimizar manualmente, trabajar directamente con la memoria, etc. En una palabra, un lenguaje de bajo nivel debería permitir trabajar con los detalles de la implementación de la plataforma .


Encontrar un equilibrio entre estos dos requisitos (identificar aspectos comunes a las plataformas y acceder a tantos detalles como sea posible) es una razón fundamental de la dificultad de desarrollar un lenguaje de bajo nivel.


Tenga en cuenta que las abstracciones de alto nivel no son tan importantes para ese lenguaje; es más importante que sirva como un contrato entre la plataforma, el compilador y el desarrollador. Y si hay un contrato, entonces se necesita un lenguaje independiente del estándar de implementación particular .


Nuestro primer requisito, características comunes a las plataformas de destino, se expresa en una máquina de lenguaje abstracto, por lo que comenzaremos la discusión con C.


No se trata solo de PDP-11


La plataforma en la que apareció el lenguaje C es PDP-11. Se basa en la arquitectura tradicional de von Neumann , en la cual los programas son ejecutados secuencialmente por el procesador central, y la memoria es una cinta plana, donde se almacenan tanto los datos como los programas. Dicha arquitectura se implementa fácilmente en hardware, y con el tiempo, todas las computadoras de uso general comenzaron a usarla.


Las mejoras modernas a la arquitectura de von Neumann tienen como objetivo eliminar su principal cuello de botella: retrasos en el intercambio de datos entre el procesador y la memoria ( cuello de botella de von Neuman en inglés). La diferencia en el rendimiento de la memoria y la CPU condujo a la aparición de subsistemas de procesadores de almacenamiento en caché (de un solo nivel y más tarde de varios niveles).


Pero incluso los cachés en estos días no son suficientes. Los procesadores modernos se han convertido en superescalares. Los retrasos cuando las instrucciones reciben datos de la memoria son parcialmente compensados ​​por la ejecución extraordinaria ( paralelismo a nivel de instrucción) de las instrucciones, junto con el predictor de rama .


La máquina abstracta secuencial C (y muchos otros lenguajes) imita el trabajo no tanto específicamente de PDP-11, sino de cualquier computadora dispuesta de acuerdo con el principio de la arquitectura von Neumann. Incluye arquitecturas construidas alrededor de procesadores con un solo núcleo: escritorio y servidor x86, ARM móvil, proveniente de la escena de Sun / Oracle SPARC e IBM POWER.


Con el tiempo, varios núcleos de procesamiento comenzaron a integrarse en un procesador, como resultado de lo cual se hizo necesario mantener la coherencia de las memorias caché de cada núcleo y los protocolos de interacción internuclear requeridos. La arquitectura de von Neumann se escaló a varios núcleos.


La versión original de la máquina abstracta C era secuencial, no reflejaba la presencia de hilos de ejecución del programa que interactuaban a través de la memoria. La aparición del modelo de memoria en el estándar expandió las capacidades de la máquina abstracta al paralelo.


Por lo tanto, la afirmación de que la máquina C abstracta ha sido inconsistente durante mucho tiempo con la estructura de los procesadores modernos no se refiere tanto a un lenguaje específico, sino a las computadoras que usan la arquitectura von Neumann, incluso en ejecución paralela.


Pero como profesional, quiero señalar lo siguiente: podemos suponer que el enfoque de Fonneimann está desactualizado, podemos suponer que es relevante, pero esto no cancela el hecho de que las arquitecturas populares de uso general de hoy en día usan derivados de los enfoques tradicionales.


La realización estandarizada y portátil de la arquitectura von Neumann, la máquina abstracta C, se implementa convenientemente en todas las plataformas principales y, por lo tanto, goza de su popularidad como ensamblador portátil.


Optimizando compiladores y lenguaje de bajo nivel


Nuestro segundo requisito para un lenguaje de bajo nivel es el acceso a los detalles de implementación de bajo nivel de cada una de las plataformas populares. En el caso de C, este es un trabajo directo con memoria y objetos como una matriz de bytes, la capacidad de trabajar directamente con direcciones de bytes y aritmética avanzada de puntero.


Los críticos de C señalan que el estándar de lenguaje ofrece demasiadas garantías con respecto, por ejemplo, a la ubicación de campos individuales en estructuras y asociaciones. Junto con punteros y mecanismos primitivos de bucles, esto complica el trabajo del optimizador.


De hecho, un enfoque más declarativo permitiría al compilador resolver de forma independiente los problemas de alineación de datos en la memoria o el orden óptimo de los campos en las estructuras; y los ciclos de nivel superior le dan la libertad que necesita al vectorizar.


La posición de los desarrolladores de C en este caso es la siguiente: un lenguaje de bajo nivel debería permitirle trabajar a un nivel lo suficientemente bajo como para que el programador resuelva de forma independiente los problemas de optimización. Dentro de C, es posible trabajar como compilador, eligiendo, por ejemplo, instrucciones SIMD y colocando correctamente los datos en la memoria.


En otras palabras, nuestro requisito de acceso a los detalles de implementación de cada plataforma entra en conflicto con los deseos de los desarrolladores de optimizar los compiladores precisamente debido a la presencia de herramientas de bajo nivel.


Curiosamente, Chiznell en un artículo titulado "C no es un lenguaje de bajo nivel" paradójicamente afirma que C es de muy bajo nivel, lo que indica la ausencia de herramientas de alto nivel. Pero los profesionales necesitan exactamente herramientas de bajo nivel; de lo contrario, el lenguaje no puede usarse para desarrollar sistemas operativos y otros programas de bajo nivel, es decir, no satisfará el segundo de nuestros requisitos.


Distrayendo de la descripción de los problemas de optimización, es decir, C, quiero señalar que en este momento no se invierte menos esfuerzo en optimizar los compiladores de lenguajes de alto nivel (el mismo C # y Java) que en GCC o Clang. Los lenguajes funcionales también tienen suficientes compiladores efectivos: MLTon, OCaml y otros. Pero los desarrolladores del mismo OCaml todavía pueden presumir de rendimiento en el mejor de los casos a la mitad de la velocidad del código C ...


Estándar como un bien absoluto


En su artículo, Chiznell cita los resultados de una encuesta realizada en 2015: muchos programadores cometieron errores al resolver problemas de comprensión de los estándares C.


Supongo que uno de los lectores estaba tratando con el estándar C. Tengo una versión en papel de C99, una especie de 900 páginas. Esta no es una especificación de esquema lacónico con un volumen de menos de 100 páginas y no un ML estándar lamido, que consta de 300. Diversión desde el trabajo nadie obtiene el estándar C: ni los desarrolladores de compiladores, ni los desarrolladores de documentos, ni los programadores.


Pero debemos entender que el estándar C se desarrolló después del hecho, después de la aparición de muchos dialectos compatibles de "casi lugares". Los autores de ANSI C han hecho un gran trabajo resumiendo las implementaciones existentes y cubriéndolas con innumerables "muletas" de no ortogonalidad en el diseño del lenguaje.


Puede parecer extraño que alguien se haya comprometido a implementar dicho documento. Pero C ha sido implementado por muchos compiladores. No volveré a contar las historias de otros sobre el zoológico del mundo UNIX de finales de los 80, especialmente porque en ese momento yo mismo no lo consideraba con mucha confianza y solo hasta las cinco. Pero, obviamente, todos en la industria realmente necesitaban un estándar.


Lo bueno es que existe y está implementado por al menos tres compiladores grandes y muchos compiladores más pequeños, que en conjunto son compatibles con cientos de plataformas. Ninguno de los idiomas de la competencia C, que reclama la corona del rey de los idiomas de bajo nivel, puede presumir de tanta diversidad y versatilidad.


En realidad, el estándar C actual no es tan malo. Un programador más o menos experimentado puede desarrollar un compilador de C no optimizador en un tiempo razonable, lo que se confirma por la existencia de muchas implementaciones semi-amateur (el mismo TCC, LCC y 8cc).


Tener un estándar generalmente aceptado significa que C satisface el último de nuestros requisitos para un lenguaje de bajo nivel: este lenguaje se basa en una especificación, no en una implementación específica.


Arquitecturas alternativas - informática especial


Pero Lifewell cita otro argumento, volviendo al dispositivo de los procesadores modernos de uso general que implementan las opciones de arquitectura von Neumann. Afirma que tiene sentido cambiar los principios del procesador central. Una vez más, esta crítica no es específica de C, sino del modelo más básico de programación imperativa.


De hecho, hay muchas alternativas al enfoque tradicional con ejecución secuencial de programas: modelos SIMD en el estilo GPU, modelos en el estilo de una máquina Erlang abstracta, y otros. Pero cada uno de estos enfoques tiene una aplicabilidad limitada cuando se usa en un procesador central.


Las GPU, por ejemplo, multiplican notablemente las matrices en los juegos y el aprendizaje automático, pero son difíciles de usar para el trazado de rayos. En otras palabras, este modelo es adecuado para aceleradores especializados, pero no funciona para procesadores de propósito general.


Erlang funciona muy bien en un clúster, pero es difícil hacer una clasificación rápida eficiente o una tabla hash rápida. El modelo de actores independientes se usa mejor en un nivel superior, en un grupo grande, donde cada nodo sigue siendo la misma máquina de alto rendimiento con un procesador tradicional.


Mientras tanto, los procesadores modernos compatibles con x86 han incluido durante mucho tiempo un conjunto de instrucciones vectoriales similares a la GPU en cuanto a propósito y principios operativos, pero conservando el circuito del procesador general en el estilo de von Neumann en su conjunto. No tengo dudas de que cualquier enfoque bastante general de la informática se incluirá en los procesadores populares.


Existe una opinión tan autorizada : el futuro reside en aceleradores programables especializados. Bajo piezas de hierro tan extraordinarias, realmente tiene sentido desarrollar lenguajes con semántica especial. Pero una computadora de uso general era y sigue siendo similar a la PDP-11 para la cual los lenguajes imperativos tipo C son muy adecuados.


C vivirá


Hay una contradicción fundamental en el artículo de Chiznell. Él escribe que para garantizar la velocidad de los programas en C, los procesadores imitan la máquina C abstracta (y el PDP-11 olvidado hace mucho tiempo), después de lo cual señala las limitaciones de dicha máquina. Pero no entiendo por qué esto significa que "C no es un lenguaje de bajo nivel".


En general, no se trata de los defectos de C como lenguaje, sino de la crítica de las arquitecturas comunes de estilo von Neumann y el modelo de programación que se deriva de ellas. Pero hasta ahora no parece que la industria esté lista para abandonar la arquitectura familiar (al menos no en los procesadores de uso general).


A pesar de la disponibilidad de muchos procesadores especializados como GPU y TPU, la arquitectura von Neumann está actualmente en control y la industria necesita un lenguaje que le permita operar al nivel más bajo posible dentro del marco de la arquitectura más popular. Un lenguaje bastante simple, portado a docenas de plataformas y programación estandarizada es C (y su familia inmediata).


Por todo eso, C tiene suficientes deficiencias: una biblioteca arcaica de funciones, un estándar intrincado e inconsistente y errores de diseño groseros. Pero, aparentemente, los creadores del lenguaje todavía hicieron algo bien.


De una forma u otra, todavía necesitamos un lenguaje de bajo nivel, y fue construido específicamente para computadoras Fonneimann populares. Y dejemos que C esté desactualizado, pero aparentemente, cualquier sucesor tendrá que basarse en los mismos principios.

Source: https://habr.com/ru/post/465429/


All Articles