Comparando el mismo proyecto en Rust, Haskell, C ++, Python, Scala y OCaml

En el último semestre de la universidad, elegí el curso del compilador CS444 . Allí, cada grupo de 1-3 personas tuvo que escribir un compilador de un subconjunto sustancial de Java en x86. Idioma para elegir un grupo. Esta fue una rara oportunidad para comparar implementaciones de grandes programas de la misma funcionalidad, escritos por programadores muy competentes en diferentes idiomas, y para comparar la diferencia en el diseño y la elección del idioma. Tal comparación dio lugar a muchos pensamientos interesantes. Tal comparación controlada de idiomas rara vez se ve. No es perfecto, pero es mucho mejor que la mayoría de las historias subjetivas en las que se basan las opiniones de las personas sobre los lenguajes de programación.

Creamos nuestro compilador Rust, y primero lo comparé con el proyecto del equipo Haskell. Esperaba que su programa fuera mucho más corto, pero resultó ser del mismo tamaño o más grande. Lo mismo vale para OCaml. Luego lo comparé con el compilador de C ++, y allí era bastante esperado que el compilador fuera aproximadamente un 30% más grande, principalmente debido a los encabezados, la falta de tipos de suma y la coincidencia de patrones. La siguiente comparación fue con mi novia, que hizo el compilador por su cuenta en Python y usó menos de la mitad del código en comparación con nosotros, debido al poder de la metaprogramación y los tipos dinámicos. Otro amigo tenía un programa Scala más pequeño. Lo que más me sorprendió fue la comparación con otro equipo que también usó Rust, pero resultaron tener tres veces más código debido a las diferentes decisiones de diseño. Al final, ¡la mayor diferencia en la cantidad de código estaba dentro del mismo idioma!

Explicaré por qué considero que es una buena comparación, proporcionaré información sobre cada proyecto y explicaré algunas razones para las diferencias en el tamaño del compilador. También sacaré conclusiones de cada comparación. Siéntase libre de usar estos enlaces para ir a la sección de interés:

Contenido


  • ¿Por qué lo encuentro significativo?
  • Óxido (base para la comparación)
  • Haskell : 1.0-1.6 tamaños, dependiendo de cómo cuente, por razones interesantes
  • C ++ : 1.4 tamaños por razones obvias
  • Python : ¡tamaño 0.5 debido a la metaprogramación elegante!
  • Rust (otro grupo) : ¡tres veces el tamaño debido a un diseño diferente!
  • Scala : 0.7 tamaños
  • OCaml : 1.0-1.6 tamaño dependiendo de cómo cuente, similar a Haskell

¿Por qué lo encuentro significativo?


Antes de decir que la cantidad de código (comparé cadenas y bytes) es una métrica terrible, quiero señalar que en este caso puede proporcionar una buena comprensión. Al menos este es el ejemplo mejor controlado en el que diferentes equipos escriben el mismo gran programa del que he oído o leído.

  • Nadie (incluyéndome a mí) sabía que mediría este parámetro, por lo que nadie intentó reproducir las métricas, todos intentaron terminar el proyecto de forma rápida y correcta.
  • Todos (con la excepción del proyecto Python, que analizaré más adelante) implementaron el programa con el único propósito de pasar el mismo conjunto de pruebas automatizadas al mismo tiempo, por lo que los resultados no pueden ser distorsionados en gran medida por grupos que resuelven diferentes problemas.
  • El proyecto se completó en unos pocos meses, con el equipo, y se suponía que se expandiría gradualmente y pasaría las pruebas conocidas y desconocidas. Esto significa que fue útil escribir código limpio y claro.
  • Además de pasar las pruebas del curso, el código no se usará para nada más, nadie lo leerá y, al ser un compilador para un subconjunto limitado de Java en ensamblador de texto, no será útil.
  • No se permiten bibliotecas que no sean la biblioteca estándar, y no hay ayudantes para el análisis, incluso si están en la biblioteca estándar. Esto significa que la comparación no puede ser distorsionada por las poderosas bibliotecas de compilación que solo tienen algunos comandos.
  • No solo hubo pruebas públicas, sino también secretas. Comenzaron una vez después de la entrega final. Esto significaba que había un incentivo para escribir su propio código de prueba y asegurarse de que el compilador sea confiable, correcto y maneje situaciones complejas de borde.
  • Aunque todos los participantes son estudiantes, los considero programadores bastante competentes. Cada uno de ellos realizó prácticas durante al menos dos años, principalmente en empresas de alta tecnología, a veces incluso trabajando en compiladores. Casi todos han estado programando durante 7-13 años y son entusiastas que leen mucho en Internet fuera de sus cursos.
  • El código generado no se tuvo en cuenta, pero se tuvieron en cuenta los archivos de gramática y el código que generó el otro código.

Por lo tanto, creo que la cantidad de código proporciona una comprensión decente de cuánto esfuerzo se requerirá para apoyar cada proyecto si fuera a largo plazo. Creo que no demasiada diferencia entre los proyectos también le permite refutar algunas declaraciones extraordinarias que leí, por ejemplo, que el compilador de Haskell tendrá más de la mitad del tamaño de C ++ debido al lenguaje.

Óxido (base para la comparación)


Yo y uno de mis camaradas escribimos más de 10 mil líneas en Rust antes, y el tercer colega escribió, quizás, 500 líneas en algunos hackatones. Nuestro compilador salió en 6806 líneas de wc -l , 5900 líneas de fuente (sin espacios y comentarios) y 220 KB wc -c .

Descubrí que en otros proyectos estas proporciones se respetan más o menos, con algunas excepciones, que señalaré. Para el resto del artículo, cuando me refiero a cadenas o sumas, quiero decir wc -l , pero eso no importa (a menos que note la diferencia), y puede convertir con un coeficiente.

Escribí otro artículo describiendo nuestro diseño , que pasó todas las pruebas públicas y secretas. También contiene algunas características adicionales que hicimos para divertirnos, no para aprobar las pruebas, que probablemente agregaron alrededor de 400 líneas. También tiene alrededor de 500 líneas de nuestras pruebas unitarias.

Haskell


El equipo de Haskell incluyó a dos de mis amigos que escribieron quizás un par de miles de líneas de Haskell cada uno, además de leer mucho contenido en línea sobre Haskell y otros lenguajes funcionales similares, como OCaml y Lean. Tenían otro compañero de equipo a quien no conocía muy bien, pero parece que un programador fuerte usó Haskell antes.

Su compilador totalizó 9,750 líneas de wc -l , 357 KB y 7777 líneas de código (SLOC). Este equipo también tiene las únicas diferencias significativas entre estas proporciones: su compilador es 1.4 veces más grande que el nuestro en filas, 1.3 veces en SLOC y 1.6 veces en bytes. No implementaron ninguna función adicional, pasaron el 100% de las pruebas públicas y secretas.

Es importante tener en cuenta que la inclusión de pruebas afectó a este equipo sobre todo. Como se acercaron cuidadosamente a la exactitud del código, incluyeron 1.600 líneas de pruebas. Capturaron varias situaciones límite que nuestro equipo no captó, pero estos casos simplemente no fueron verificados por las pruebas del curso. Entonces, sin pruebas en ambos lados (6.3 mil líneas versus 8.1 mil líneas) su compilador es solo un 30% más que el nuestro.

Aquí tiendo a los bytes como una medida más razonable de comparación de volumen, porque en un proyecto de Haskell, en promedio, hay líneas más largas, ya que no tiene una gran cantidad de líneas de un paréntesis de cierre, y rustfmt no rustfmt cadenas de función de una sola línea en varias líneas.

Después de hurgar con uno de mis compañeros de equipo, se nos ocurrió la siguiente explicación para esta diferencia:

  • Utilizamos un analizador léxico escrito a mano y un método de descenso recursivo, y utilizaron un generador NFA y DFA y un analizador LR , y luego un pase para convertir el árbol de análisis a AST ( árbol de sintaxis abstracta , representación más conveniente del código). Esto les dio significativamente más código: 2677 líneas en comparación con nuestro 1705, alrededor de 1000 líneas más.
  • Utilizaron el fantástico AST genérico, que pasó a varios parámetros de tipo a medida que se agregaba más información en cada pasada. Esta y más funciones auxiliares para la reescritura probablemente explican por qué su código AST es aproximadamente 500 líneas más largo que nuestra implementación, donde recopilamos literales de estructura y mutamos los campos de Option<_> para agregar información a medida que avanzamos.
  • Todavía tienen alrededor de 400 líneas de código durante la generación, que se asocian principalmente con la mayor abstracción necesaria para generar y combinar el código de una manera puramente funcional, donde simplemente usamos líneas de mutación y escritura.

Estas diferencias más pruebas explican todas las diferencias en volumen. De hecho, nuestros archivos para constantes plegables y resolución de contexto tienen un tamaño muy cercano. Pero aún así, hay alguna diferencia en los bytes debido a las líneas más largas: probablemente porque se necesita más código para reescribir todo el árbol en cada pasada.

Como resultado, dejando de lado las decisiones de diseño, en mi opinión, Rust y Haskell son igualmente expresivos, tal vez con una ligera ventaja Rust debido a la capacidad de usar fácilmente la mutación cuando sea conveniente. También fue interesante saber que mi elección del método de descenso recursivo y el analizador léxico escrito a mano valió la pena: era un riesgo que contradecía las recomendaciones e instrucciones del profesor, pero decidí que era más fácil y correcto.

Los fanáticos de Haskell argumentarán que ese equipo probablemente no aprovechó al máximo Haskell, y si supieran mejor el idioma, podrían haber hecho un proyecto con menos código. Estoy de acuerdo, alguien como Edward Kmett puede escribir el mismo compilador en una cantidad mucho menor. De hecho, el equipo de mi amigo no usó muchas abstracciones súper avanzadas sofisticadas y bibliotecas combinadas elegantes como lentes . Sin embargo, todo esto afecta la legibilidad del código. Todas las personas en el equipo son programadores experimentados, sabían que Haskell era capaz de hacer cosas muy extrañas, pero decidieron no usarlas porque decidieron que comprenderlas tomaría más tiempo del que ahorrarían y haría que el código fuera más difícil de entender para otros. Esto parece un compromiso real, y la afirmación de que Haskell es mágicamente adecuado para compiladores entra en algo como "Haskell requiere una habilidad extremadamente alta en la redacción de compiladores si no te importa el soporte de código para personas que tampoco son muy expertas en Haskell".

También es interesante notar que al comienzo de cada proyecto, el profesor dice que los estudiantes pueden usar cualquier idioma que se ejecute en un servidor universitario, pero advierte que los equipos en Haskell son diferentes del resto: tienen la mayor dispersión en las calificaciones. Muchas personas sobreestiman sus habilidades y los equipos de Haskell tienen las mejores calificaciones, aunque a otros les va muy bien como a mis amigos.

C ++


Luego hablé con mi amigo del equipo de C ++. Conocía solo a una persona en este equipo, pero C ++ se usa en varios cursos en nuestra universidad, por lo que probablemente todos en el equipo tenían experiencia en C ++.

Su proyecto consistió en 8733 líneas y 280 KB, sin incluir el código de prueba, pero incluyendo alrededor de 500 líneas de funciones adicionales. Lo que lo hace 1,4 veces más grande que nuestro código sin pruebas, que también tiene alrededor de 500 líneas de funciones adicionales. Pasaron el 100% de las pruebas públicas, pero solo el 90% de las pruebas secretas. Presumiblemente porque no implementaron los arreglos vtables elegantes requeridos por la especificación, que ocupan quizás 50-100 líneas de código.

No profundicé demasiado en estas diferencias de tamaño. Supongo que esto se debe principalmente a:

  • Utilizan el analizador LR y la reescritura de árboles en lugar del método de descenso recursivo.
  • La falta de tipos de suma y comparaciones de patrones en C ++, que hemos utilizado ampliamente y que fueron muy útiles.
  • La necesidad de duplicar todas las firmas en los archivos de encabezado, que no es el caso en Rust.

También comparamos el tiempo de compilación. En mi computadora portátil, la compilación de depuración limpia de nuestro compilador tarda 9.7 s, la versión limpia de 12.5 sy la compilación incremental de depuración de 3.5 s. Mi amigo no tenía los tiempos disponibles para su compilación C ++ (usando make paralelo), pero dijo que los números son similares, con la advertencia de que ponen implementaciones de muchas funciones pequeñas en los archivos de encabezado para reducir la duplicación de firmas a costa de un tiempo más largo (es decir por lo tanto, no puedo medir la sobrecarga de la línea neta en los archivos de encabezado).

Pitón


Mi amigo, un muy buen programador, decidió hacer el proyecto solo en Python. También implementó características más avanzadas (por diversión) que cualquier otro equipo, incluida una vista intermedia de SSA con asignación de registros y otras optimizaciones. Por otro lado, dado que funcionó solo e implementó muchas funciones adicionales, prestó la menor atención a la calidad del código, por ejemplo, lanzando excepciones indiferenciadas para todos los errores (confiando en las trazas inversas para la depuración) en lugar de implementar tipos de error y mensajes correspondientes, como nosotros

Su compilador constaba de 4581 líneas y pasaba todas las pruebas públicas y secretas. También implementó funciones más avanzadas que cualquier otro comando, pero es difícil determinar cuánto código adicional tomó, porque muchas de las funciones adicionales eran versiones más potentes de cosas simples que todos necesitaban implementar, como plegar constantes y generar código. Las funciones adicionales son probablemente de 1000 a 2000 líneas, al menos, así que estoy seguro de que su código es al menos dos veces más expresivo que el nuestro.

Una gran parte de esta diferencia es probablemente la escritura dinámica. Solo en nuestros ast.rs 500 líneas de definiciones de tipos y muchos más tipos definidos en otra parte del compilador. También siempre estamos limitados al sistema de tipos en sí. Por ejemplo, necesitamos una infraestructura para agregar ergonómicamente nueva información al AST a medida que pasamos y accedemos a ella más tarde. Mientras esté en Python, puede establecer nuevos campos en los nodos AST.

Potente metaprogramación también explica parte de la diferencia. Por ejemplo, aunque usó un analizador LR en lugar de un método de descenso recursivo, en mi caso creo que tomó menos código porque en lugar de pasar por una reescritura de árbol, su gramática LR incluía fragmentos de código Python para construir el AST, que el generador podría convertir en funciones de Python usando eval . Parte de la razón por la que no usamos el analizador LR es que construir un AST sin reescribir el árbol requerirá mucha ceremonia (creando archivos Rust o macros de procedimiento) para asociar la gramática con fragmentos de código Rust.

Otro ejemplo del poder de la metaprogramación y la tipificación dinámica es el archivo visit.rs 400 líneas, que es básicamente un código repetitivo repetitivo que implementa un visitante en un montón de estructuras AST. En Python, esta puede ser una función corta de aproximadamente 10 líneas que introspectivamente recurrentemente examina los campos de un nodo AST y los visita (usando el atributo __dict__ ).

Como fanático de Rust y de los lenguajes estáticamente escritos en general, me inclino a notar que el sistema de tipos es muy útil para prevenir errores y para el rendimiento. La metaprogramación inusual también puede dificultar la comprensión del funcionamiento del código. Sin embargo, esta comparación me sorprendió por el hecho de que no esperaba que la diferencia en la cantidad de código fuera tan grande. Si la diferencia en su conjunto está realmente cerca de tener que escribir el doble de código, sigo pensando que Rust es un compromiso adecuado, pero aún así la mitad de ese código es un argumento, y en el futuro tiendo a hacer algo en Ruby / Python si solo necesita construir algo rápidamente solo, y luego tirarlo a la basura.

Rust (otro grupo)


La comparación más interesante para mí fue con mi amigo, que también estaba haciendo un proyecto en Rust con un compañero de equipo (a quien no conocía). Mi amigo tuvo una buena experiencia de Rust. Contribuyó al desarrollo del compilador Rust y leyó mucho. No sé nada de su compañero.

Su proyecto consistió en 17.211 líneas sin procesar, 15.000 líneas de origen y 637 KB, sin incluir el código de prueba y el código generado. No tenía funciones adicionales, y solo pasó 4 de 10 pruebas secretas y el 90% de las pruebas públicas para la generación de código, porque no tenían suficiente tiempo antes de la fecha límite para implementar partes más extrañas de la especificación. ¡Su programa es tres veces más grande que el nuestro, escrito en el mismo idioma y con menos funcionalidad!

Este resultado fue realmente sorprendente para mí y eclipsó todas las diferencias entre los idiomas que he estudiado hasta ahora. Por lo tanto, comparamos las listas de tamaños de archivo wc -l , y también verificamos cómo cada uno de nosotros implementó algunas cosas específicas que dieron como resultado diferentes tamaños de código.

Parece que todo se reduce a la adopción constante de varias decisiones de diseño. Por ejemplo, su interfaz (análisis léxico, análisis, creación de AST) toma 7597 líneas contra nuestro 2164. Usaron el analizador léxico DFA y el analizador LALR (1), pero otros grupos hicieron cosas similares sin tanto código. Al mirar su archivo de eliminación, noté una serie de decisiones de diseño que eran diferentes a las nuestras:

  • Decidieron usar un árbol de análisis completamente tipado en lugar de un árbol de análisis estándar, uniforme y basado en cadenas. Esto probablemente requirió muchas más definiciones de tipo y código de conversión adicional en la etapa de análisis o un analizador más complejo.
  • Utilizaron implementaciones de prueba tryfrom para convertir entre tipos de árbol de análisis y tipos AST para validarlos. Esto lleva a muchos bloques impl 10-20 líneas. Para hacer esto, utilizamos funciones que devuelven tipos de Result , lo que genera menos líneas, y también nos libera un poco de la estructura de tipos, simplificando la parametrización y la reutilización. Algunas de las cosas que, para nosotros, eran ramas de match sola línea, tenían bloques impl 10 líneas.
  • Nuestros tipos están estructurados para reducir copiar y pegar. Por ejemplo, utilizaron campos separados is_abstract , is_native e is_static , donde el código de verificación de restricciones tuvo que copiarse dos veces: una vez para métodos de tipo vacío y otra para métodos con tipo de retorno, con ligeras modificaciones. Si bien nuestro void era solo un tipo especial, se nos ocurrió una taxonomía de modificadores con mode y visibility que aplicaban restricciones de nivel de tipo, y se generaban errores de restricción por defecto para el operador de coincidencia, que traducía los conjuntos de modificadores en mode y visibility .

No miré el código de los pases del análisis de su compilador, pero también son geniales. Hablé con mi amigo, y parece que no implementaron nada similar a la infraestructura de los visitantes, como la nuestra. Supongo que, junto con algunas otras diferencias de diseño más pequeñas, explica la diferencia de tamaño de esta parte. El visitante permite que nuestros pases de análisis se centren solo en las partes del AST que necesitaban, en lugar de hacer coincidir el patrón en toda la estructura del AST. Esto ahorra mucho código.

Su parte para la generación de código consiste en 3594 líneas, y la nuestra: 1560. Observé su código y parece que casi toda la diferencia es que eligieron una estructura de datos intermedia para las instrucciones del ensamblador, donde solo usamos el formato de cadena para la salida del ensamblador directo. . Tenían que definir tipos y funciones de salida para todas las instrucciones usadas y tipos de operandos. También significaba que las instrucciones de montaje del edificio tomaban más código. Cuando teníamos un operador de formato con instrucciones breves, como mov ecx, [edx] , necesitaban un operador rustfmt gigante, dividido en 6 líneas, que construía una instrucción con un grupo de tipos anidados intermedios para operandos que incluyen hasta 6 niveles de paréntesis anidados. También podríamos generar bloques de instrucciones relacionadas, como un preámbulo de función, en una declaración de formato único, donde tenían que especificar la construcción completa para cada instrucción.

Nuestro equipo estaba considerando usar una abstracción como la de ellos. Era más fácil poder generar un conjunto de texto o emitir directamente el código de la máquina, sin embargo, este no era un requisito del curso. Lo mismo podría hacerse con menos código y un mejor rendimiento utilizando el X86Writer X86Writer con métodos como push(reg: Register) . También tomamos en cuenta que esto podría simplificar la depuración y las pruebas, pero nos dimos cuenta de que ver el ensamblador de texto generado es realmente más fácil de leer y probar usando la prueba de instantáneas , si inserta comentarios libremente. Pero (aparentemente correctamente) predijimos que tomaría mucho código adicional, y no había un beneficio real, dadas nuestras necesidades reales, por lo que no nos preocupamos.

Es bueno comparar esto con la representación intermedia que el equipo de C ++ usó como una función adicional, que solo les tomó 500 líneas adicionales. Usaron una estructura muy simple (para definiciones de tipo simples y código de compilación) que usaba operaciones cercanas a lo que Java requería. Esto significaba que su representación intermedia era mucho más pequeña (y por lo tanto requería menos código de compilación) que el ensamblador resultante, ya que muchas operaciones de lenguaje, como llamadas y conversiones, se expandieron en muchas instrucciones de ensamblador. También dicen que realmente ayudó a la depuración, ya que eliminó mucha basura y mejoró la legibilidad. Una presentación de nivel superior también permitió hacer algunas optimizaciones simples en su representación intermedia. El equipo de C ++ ideó un diseño realmente bueno que les hizo mucho más bien con mucho menos código.

En general, parece que la razón común de la triple diferencia en volumen se debe a la adopción constante de varias decisiones de diseño, tanto grandes como pequeñas, en la dirección de más código. Implementaron una serie de abstracciones, lo que no hicimos: agregaron más código y omitieron algunas de nuestras abstracciones, que reducen la cantidad de código.

Este resultado realmente me sorprendió. Sabía que las decisiones de diseño son importantes, pero no habría adivinado de antemano que conducirían a diferencias de este tamaño, dado que solo examiné a las personas que considero programadores competentes. De todos los resultados de comparación, este es el más significativo para mí.Probablemente me ayudó que leí mucho sobre cómo escribir compiladores antes de tomar este curso, para poder usar proyectos inteligentes que otras personas inventaron y descubrieron que funcionaban bien, como los visitantes de AST y el método de descenso recursivo, aunque no se les enseñó en nuestro curso

Lo que realmente me hizo pensar es el costo de la abstracción. Las abstracciones pueden facilitar la expansión futura o proteger contra algunos tipos de errores, pero deben tenerse en cuenta, dado que puede obtener el triple de código para comprender y refactorizar, el triple de lugares posibles para errores y menos tiempo para pruebas y más desarrollo Nuestro curso de capacitación era diferente del mundo real: sabíamos con certeza que nunca tocaríamos el código después del desarrollo, esto elimina los beneficios de la abstracción proactiva. Sin embargo, si tuviera que elegir qué compilador ampliar con una función arbitraria, que dirás más tarde, elegiría el nuestro, sin siquiera considerar mi familiaridad con él. Solo porque tiene mucho menos código para entender, y podría elegir la mejor abstracción para los requisitos (p. Ej.representación intermedia del comando C ++) cuando conozco los requisitos específicos.

Además, en mi opinión, se fortaleció la taxonomía de las abstracciones: existen aquellas que reducen el código, teniendo en cuenta solo los requisitos actuales, como nuestra plantilla de visitante, y hay abstracciones que agregan código, pero proporcionan los beneficios de la extensibilidad, la depuración o la corrección.

Scala


También hablé con un amigo que hizo un proyecto en Scala en el semestre anterior, pero el proyecto y las pruebas fueron exactamente iguales. Su compilador consistía en 4141 líneas y ~ 160 KB de código, sin contar las pruebas. Pasaron 8 de 10 pruebas secretas y 100% pruebas abiertas y no implementaron ninguna función adicional. Por lo tanto, en comparación con nuestras líneas 5906 sin funciones y pruebas adicionales, su compilador es un 30% menor.

Uno de los pequeños factores de diseño fue un enfoque diferente para el análisis. El curso permitió el uso de una herramienta de línea de comando para el generador de tablas LR. Nadie lo usó excepto este equipo. Esto les salvó de tener que implementar el generador de tablas LR. También lograron evitar escribir gramática LR con un script Python de 150 líneas que borró la página web de gramática Java que encontraron en Internet y la tradujo al formato de entrada del generador. Todavía necesitaban hacer algún tipo de árbol en Scala, pero en general la etapa de análisis ascendió a 1073 líneas en comparación con nuestra 1443, aunque nuestro método de descenso de gradiente aquí dio una ventaja en volumen en comparación con todos los demás equipos.

El resto de su compilador también era más pequeño que el nuestro, sin grandes diferencias obvias de diseño, aunque no profundicé en el código. Sospecho que esto se debe a las diferencias en la expresividad de Scala y Rust. Scala y Rust tienen características de programación similares útiles para compiladores, como la coincidencia de patrones, pero la memoria administrada de Scala guarda el código necesario para que el verificador de préstamos funcione en Rust. Además, Scala tiene un azúcar sintáctico más variado que Rust.

OCaml


Como todos los miembros de nuestro equipo realizan una pasantía en Jane Street (empresa comercializadora de tecnología, aprox. Por persona), estaba especialmente interesado en ver el resultado de otros ex pasantes de Jane Street que eligieron OCaml para escribir el compilador.

Su compilador tenía 10.914 líneas y 377 KB, incluida una pequeña cantidad de código de prueba y sin características adicionales. Pasaron 9/10 pruebas secretas y todas las pruebas públicas.

Al igual que otros grupos, parece que la principal diferencia de tamaño se debe al uso del analizador LR y la reescritura de árboles para el análisis, así como la canalización de conversión regex-> NFA-> DFA para el análisis léxico. Su interfaz (análisis léxico, análisis, construcción AST) es de 5548 líneas, y la nuestra, 2164, con proporciones similares para bytes. También usaron las pruebas para su analizador con la expectativa de que fuera similar a nuestras pruebas de instantáneas, que colocaban la salida esperada fuera del código, por lo que sus pruebas de analizador hicieron ~ 600 líneas del total, y las nuestras, alrededor de 200.

Esto deja 5366 líneas para el resto del compilador (461 líneas de las cuales son archivos de interfaz con declaraciones de tipo) y 4642 para nosotros, la diferencia es solo del 15%, si contamos sus archivos de interfaz, y casi del mismo tamaño, si no cuenta. Parece que, aparte de nuestras soluciones de diseño de análisis, Rust y OCaml parecen igualmente expresivos, excepto que OCaml necesita archivos front-end y Rust no.

Conclusión


En general, estoy muy contento de haber hecho esta comparación, aprendí mucho y me sorprendí muchas veces. Creo que la conclusión general es que las decisiones de diseño son mucho más importantes que el lenguaje, pero el lenguaje es importante porque le brinda herramientas para implementar diferentes diseños.

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


All Articles