Pregunta ¿Por qué no concatenar los archivos fuente C antes de la compilación? [duplicar]


Esta pregunta ya tiene una respuesta aquí:

Vengo de un fondo de secuencias de comandos y el preprocesador en C siempre me ha parecido feo. Sin embargo, lo he abrazado mientras aprendo a escribir pequeños programas en C. Solo estoy usando el preprocesador para incluir las bibliotecas estándar y los archivos de encabezado que he escrito para mis propias funciones.

Mi pregunta es ¿por qué los programadores C no omiten todos los includes y simplemente concatenan sus archivos fuente C y luego compilan? Si coloca todas sus inclusiones en un solo lugar, solo deberá definir lo que necesita una vez, en lugar de todos sus archivos fuente.

Aquí hay un ejemplo de lo que estoy describiendo. Aquí tengo tres archivos:

// includes.c
#include <stdio.h>
// main.c
int main() {
    foo();
    printf("world\n");
    return 0;
}
// foo.c
void foo() {
    printf("Hello ");
}

Haciendo algo como cat *.c > to_compile.c && gcc -o myprogram to_compile.c en mi Makefile puedo reducir la cantidad de código que escribo.

Esto significa que no tengo que escribir un archivo de cabecera para cada función que creo (porque ya están en el archivo fuente principal) y también significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo. ¡Esto me parece una gran idea!

Sin embargo, me doy cuenta de que C es un lenguaje de programación muy maduro y me estoy imaginando que alguien más inteligente que yo ya tuvo esta idea y decidió no usarla. Por qué no?


74
2018-02-09 11:28


origen


Respuestas:


Algunos software están construidos de esa manera.

Un ejemplo típico es SQLite. A veces se compila como un amalgamación (hecho en tiempo de compilación a partir de muchos archivos fuente).

Pero ese enfoque tiene pros y contras.

Obviamente, el tiempo de compilación aumentará bastante. Entonces es práctico solo si compilas esas cosas raramente.

Tal vez, el compilador podría optimizar un poco más. Pero con optimizaciones de tiempo de enlace (por ejemplo, si se usa reciente GCC, compila y enlaza con gcc -flto -O2) puede obtener el mismo efecto (por supuesto, a expensas de un mayor tiempo de compilación).

No tengo que escribir un archivo de cabecera para cada función

Ese es un enfoque equivocado (de tener un archivo de encabezado por función). Para un proyecto de una sola persona (de menos de cien mil líneas de código, a.k.a. KLOC = kilo línea de código), es bastante razonable, al menos para proyectos pequeños, tener un soltero archivo de encabezado común (que podría precompilación si usas GCC), que contendrá declaraciones de todas las funciones y tipos públicos, y tal vez definiciones de static inline funciones (las suficientemente pequeñas y llamadas con la frecuencia suficiente para beneficiarse de en línea) Por ejemplo, el sash cáscara está organizado de esa manera (y también lo es el lout formateador, con 52 KLOC).

Es posible que también tenga algunos archivos de encabezado y quizás tenga algún encabezado de "agrupación" individual que #include-stodos ellos (y que usted podría precompilar). Ver por ejemplo jansson (que en realidad tiene un solo público archivo de encabezado) y GTK (que tiene un montón de encabezados internos, pero la mayoría de las aplicaciones lo usan tener solo uno #include <gtk/gtk.h> que a su vez incluyen todos los encabezados internos). En el lado opuesto, POSIX tiene una gran cantidad de archivos de encabezado, y documenta cuáles deben incluirse y en qué orden.

Algunas personas prefieren tener muchos archivos de encabezado (y algunos incluso prefieren colocar una sola declaración de función en su propio encabezado). Yo no (para proyectos personales, o proyectos pequeños en los que solo dos o tres personas podrían codificar), pero es una cuestión de gusto. Por cierto, cuando un proyecto crece mucho, sucede con bastante frecuencia que el conjunto de archivos de encabezado (y de unidades de traducción) cambia significativamente. Mira también en REDIS (tiene 139 .h archivos de encabezado y 214 .c archivos, es decir, unidades de traducción que totalizan 126 KLOC).

Tener uno o varios unidades de traducción también es una cuestión de gusto (y de conveniencia y hábitos y convenciones). Mi preferencia es tener archivos fuente (es decir, unidades de traducción) que no sean demasiado pequeños, generalmente varios miles de líneas cada uno, y que a menudo tienen (para un proyecto pequeño de menos de 60 KLOC) un único archivo de encabezado común. No te olvides de usar algunos automatización de construcción herramienta como GNU hace (a menudo con un paralela construir a través de make -j; entonces tendrás varios procesos de compilación que se ejecutan simultáneamente). La ventaja de tener una organización de archivos fuente de este tipo es que la compilación es razonablemente rápida. Por cierto, en algunos casos, metaprogramación enfoque vale la pena: algunos de sus (archivos internos de cabecera o unidades de traducción) C archivos "fuente" podrían ser generado por otra cosa (por ejemplo, algún script en AWK, algunos programas especializados de C como bisonte o lo tuyo)

Recuerde que C fue diseñado en la década de 1970, para computadoras mucho más pequeñas y lentas que su computadora portátil favorita de hoy (por lo general, la memoria era en ese momento un megabyte como máximo, o incluso unos cientos de kilobytes, y la computadora era al menos mil veces más lenta que tu teléfono móvil hoy).

Sugiero fuertemente a estudiar el código fuente y construir algunos existente  software libre proyectos (por ejemplo, aquellos en GitHub o SourceForge o su distribución de Linux favorita). Aprenderá que son enfoques diferentes. Recuerda eso convenciones y hábitos importa mucho en la práctica, asi que existen diferente formas de organizar su proyecto en .c y .h archivos. Lea sobre el Preprocesador C.

También significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo

Incluyes archivos de encabezado, no bibliotecas (pero deberías enlazar bibliotecas). Pero podrías incluirlos en cada .c archivos (y muchos proyectos lo están haciendo), o podría incluirlos en un único encabezado y precompilar ese encabezado, o podría tener una docena de encabezados e incluirlos después de los encabezados del sistema en cada unidad de compilación. YMMV. Tenga en cuenta que el tiempo de preprocesamiento es rápido en las computadoras de hoy (al menos, cuando le pide al compilador que optimice, ya que las optimizaciones toman más tiempo que el análisis y el preprocesamiento).

Tenga en cuenta que lo que entra en algunos #include-d archivo es convencional (y no está definido por la especificación C). Algunos programas tienen algunos de sus códigos en algunos de esos archivos (que luego no deberían llamarse "encabezado", solo algunos "archivos incluidos", y que luego no deberían tener un .h  sufijo, pero algo más como .inc) Mira por ejemplo en XPM archivos. En el otro extremo, es posible que en principio no tenga ninguno de sus propios archivos de encabezado (aún necesita archivos de encabezado de la implementación, como <stdio.h> o <dlfcn.h> de su sistema POSIX) y copiar y pegar código duplicado en su .c archivos, por ejemplo tener la linea int foo(void); en cada .c archivo, pero esa es una práctica muy mala y está mal visto. Sin embargo, algunos programas son generando C archivos que comparten contenido común.

Por cierto, C o C ++ 14 no tienen módulos (como OCaml tiene). En otras palabras, en C un módulo es principalmente un convención.

(note que tiene muchos miles de muy pequeña  .h y .c los archivos de solo unas pocas docenas de líneas pueden ralentizar el tiempo de creación dramáticamente; tener cientos de archivos de unos cientos de líneas cada uno es más razonable, en términos de tiempo de construcción).

Si comienza a trabajar en un proyecto de una sola persona en C, le sugiero que primero tenga un archivo de encabezado (y precompilarlo) y varios .c unidades de traducción. En la práctica, cambiarás .c archivos mucho más a menudo que .h unos. Una vez que tenga más de 10 KLOC, puede refactorizarlo en varios archivos de encabezado. Tal refactorización es difícil de diseñar, pero fácil de hacer (solo mucha copia y pegado de códigos). Otras personas tendrían diferentes sugerencias y consejos (¡y eso está bien!). Pero no olvide habilitar todas las advertencias e información de depuración al compilar (para compilar con gcc -Wall -g, quizás poniendo CFLAGS= -Wall -g en tus Makefile) Utilizar el gdb depurador (y valgrind...). Pedir optimizaciones (-O2) cuando compara un programa ya depurado. También use un sistema de control de versiones como Git.

Por el contrario, si está diseñando un proyecto más grande en el cual varias personas funcionaría, podría ser mejor tener varios archivos -incluso varios archivos de encabezado- (intuitivamente, cada archivo tiene una sola persona principalmente responsable de él, y otros hacen contribuciones menores a ese archivo).

En un comentario, agregas:

Estoy hablando de escribir mi código en muchos archivos diferentes pero usando un Makefile para concatenarlos

No veo por qué eso sería útil (excepto en casos muy extraños). Es mucho mejor (y práctica muy habitual y común) compilar cada unidad de traducción (por ejemplo, cada .c archivo) en su archivo de objeto (un .o  DUENDE archivo en Linux) y enlazar ellos más tarde. Esto es fácil con make (en la práctica, cuando cambiarás solo una .c archivo, p. ej. para arreglar un error, solo ese archivo se compila y la compilación incremental es realmente rápida), y puede pedirle que compile archivos de objetos en paralela utilizando make -j (y luego su compilación va muy rápido en su procesador multi-core).


103
2018-02-09 11:32



podría hacer eso, pero nos gusta separar los programas C en separados unidades de traducción, principalmente porque:

  1. Acelera las construcciones. Solo necesita reconstruir los archivos que han cambiado, y esos pueden ser vinculado con otros archivos compilados para formar el programa final.

  2. La biblioteca estándar de C consiste en componentes precompilados. ¿De verdad quieres tener que recompilar todo eso?

  3. Es más fácil colaborar con otros programadores si la base de código se divide en diferentes archivos.


26
2018-02-09 11:32



  • Con la modularidad, puede compartir su biblioteca sin compartir el código.
  • Para proyectos grandes, si cambia un solo archivo, terminaría compilando el proyecto completo.
  • Es posible que se quede sin memoria más fácilmente cuando intente compilar proyectos grandes.
  • Puede tener dependencias circulares en los módulos, la modularidad ayuda a mantenerlos.

Puede haber algunos avances en su enfoque, pero para lenguajes como C, la compilación de cada módulo tiene más sentido.


16
2018-02-09 11:32



Su enfoque de concatenar archivos .c está completamente roto:

  • Aunque el comando cat *.c > to_compile.c pondrá todas las funciones en un solo archivo, el orden importa: debe tener cada función declarada antes de su primer uso.

    Es decir, tiene dependencias entre sus archivos .c que fuerzan cierto orden. Si su comando de concatenación no cumple con este orden, no podrá compilar el resultado.

    Además, si tiene dos funciones que se usan recursivamente, no hay forma de evitar la escritura de una declaración directa para al menos uno de los dos. También puede colocar esas declaraciones avanzadas en un archivo de encabezado donde las personas esperan encontrarlas.

  • Cuando concatena todo en un solo archivo, fuerza una reconstrucción completa cada vez que cambia una sola línea en su proyecto.

    Con el enfoque clásico de compilación dividida en .c / .h, un cambio en la implementación de una función requiere la recompilación de exactamente un archivo, mientras que un cambio en un encabezado requiere la recompilación de los archivos que realmente incluyen este encabezado. Esto puede acelerar fácilmente la reconstrucción después de un pequeño cambio por un factor de 100 o más (dependiendo de la cantidad de archivos .c).

  • Pierdes toda la capacidad de compilación paralelacuando concatena todo en un solo archivo.

    ¿Tiene un gran procesador de 12 núcleos con hyper-threading habilitado? Lástima, tu archivo fuente concatenado se compila con un solo hilo. Acabas de perder una aceleración de un factor mayor a 20 ... Ok, este es un ejemplo extremo, pero tengo un software de compilación con make -j16 ya, y te digo, puede marcar una gran diferencia.

  • Los tiempos de compilación son generalmente no lineal.

    Por lo general, los compiladores contienen al menos algunos algoritmos que tienen un comportamiento de tiempo de ejecución cuadrático. En consecuencia, generalmente hay algún umbral a partir del cual la compilación agregada es en realidad más lenta que la compilación de las partes independientes.

    Obviamente, la ubicación precisa de este umbral depende del compilador y de los indicadores de optimización que le pase, pero he visto que un compilador tarda más de media hora en un único archivo fuente enorme. No querrás tener tal obstáculo en tu ciclo change-compile-test.

No se equivoquen: aunque se trata de todos estos problemas, hay personas que usan la concatenación de archivos .c en la práctica, y algunos programadores de C ++ consiguen casi el mismo punto moviendo todo en plantillas (para que la implementación se encuentre en el archivo .hpp y no hay ningún archivo .cpp asociado), permitiendo que el preprocesador haga la concatenación. No veo cómo pueden ignorar estos problemas, pero lo hacen.

También tenga en cuenta que muchos de estos problemas solo se hacen evidentes con proyectos de mayor tamaño. Si su proyecto tiene menos de 5000 líneas de código, es relativamente irrelevante cómo lo compila. Pero cuando tiene más de 50000 líneas de código, definitivamente desea un sistema de compilación que admita compilaciones incrementales y paralelas. De lo contrario, estás perdiendo tu tiempo de trabajo.


16
2018-02-10 11:32



Porque dividir las cosas es un buen diseño del programa. Un buen diseño de programa tiene que ver con la modularidad, los módulos de código autónomo y la reutilización del código. Como resultado, el sentido común te llevará muy lejos al hacer el diseño del programa: las cosas que no deben estar juntas no deberían juntarse.

Colocar el código no relacionado en diferentes unidades de traducción significa que puede localizar el alcance de las variables y funciones tanto como sea posible.

Fusionar cosas crea acoplamiento apretado, lo que significa dependencias incómodas entre los archivos de código que realmente ni siquiera deberían saber sobre la existencia del otro. Esta es la razón por la cual un "global.h" que contiene todas las inclusiones en un proyecto es algo malo, porque crea un acoplamiento apretado entre cada archivo no relacionado en todo su proyecto.

Supongamos que está escribiendo firmware para controlar un automóvil. Un módulo en el programa controla la radio FM del automóvil. Luego, vuelve a utilizar el código de radio en otro proyecto para controlar la radio FM en un teléfono inteligente. Y luego su código de radio no se compilará porque no puede encontrar los frenos, las ruedas, los engranajes, etc. Cosas que no tienen el menor sentido para la radio FM, y mucho menos para el teléfono inteligente.

Lo que es aún peor es que si tiene un acoplamiento estricto, los errores escalan a lo largo de todo el programa, en lugar de permanecer localmente en el módulo donde se encuentra el error. Esto hace que las consecuencias de los errores sean mucho más graves. Usted escribe un error en su código de radio FM y de repente los frenos del automóvil dejan de funcionar. Aunque no haya tocado el código de freno con su actualización que contenía el error.

Si un error en un módulo rompe cosas completamente no relacionadas, es casi seguro debido al diseño deficiente del programa. Y una cierta forma de lograr un diseño deficiente del programa es fusionar todo en su proyecto en una sola burbuja.


15
2018-02-09 12:23



Los archivos de encabezado deben definir interfaces: es una convención deseable a seguir. No están destinados a declarar todo lo que está en un correspondiente .c archivo, o un grupo de .c archivos. En su lugar, declaran toda la funcionalidad en el .c archivo (s) que está disponible para sus usuarios. Un bien diseñado .h archivo comprende un documento básico de la interfaz expuesta por el código en el .c archivo incluso si no hay un solo comentario en él. Una forma de abordar el diseño de un módulo C es escribir primero el archivo de encabezado y luego implementarlo en uno o más .c archivos.

Corolario: funciones y estructuras de datos internas para la implementación de un .c archivo normalmente no pertenecen en el archivo de encabezado. Es posible que tenga que enviar declaraciones, pero esas deben ser locales y todas las variables y funciones así declaradas y definidas deben ser static: si no forman parte de la interfaz, el vinculador no debería verlos.


11
2018-02-09 22:07



La razón principal es el tiempo de compilación. Compilar un archivo pequeño cuando lo cambie puede demorar poco tiempo. Sin embargo, si compilara todo el proyecto cada vez que cambiara una línea, compilaría, por ejemplo, 10.000 archivos cada vez, lo que podría llevar mucho más tiempo.

Si tiene, como en el ejemplo anterior, 10.000 archivos fuente y compila uno tarda 10 ms, entonces el proyecto completo se construye de forma incremental (después de cambiar un solo archivo) en (10 ms + tiempo de enlace) si compila solo este archivo modificado, o (10 ms * 10000 + tiempo de enlace corto) si compila todo como una sola burbuja concatenada.


8
2018-02-09 11:31



Si bien aún puede escribir su programa de forma modular y construirlo como una sola unidad de traducción, se perderá todo los mecanismos C proporcionan para hacer cumplir esa modularidad. Con múltiples unidades de traducción, usted tiene un buen control en las interfaces de sus módulos al usar, p. extern y static palabras clave.

Al fusionar su código en una sola unidad de traducción, se perderá cualquier problema de modularidad que pueda tener porque el compilador no lo advertirá sobre ellos. En un proyecto grande, esto eventualmente dará como resultado la expansión de dependencias involuntarias. Al final, tendrá problemas para cambiar cualquier módulo sin crear efectos secundarios globales en otros módulos.


7
2018-02-09 13:07



Si coloca todas sus inclusiones en un solo lugar, solo deberá definir lo que necesita una vez, en lugar de todos sus archivos fuente.

Ese es el propósito de .h archivos, para que pueda definir lo que necesita una vez e incluirlo en todas partes. Algunos proyectos incluso tienen un everything.h encabezado que incluye a cada individuo .h archivo. Entonces tus Pro se puede lograr con separado .c archivos también

Esto significa que no tengo que escribir un archivo de cabecera para cada función que creo [...]

No se supone que debes escribir un archivo de cabecera para cada función de todos modos. Se supone que debes tener un archivo de cabecera para un conjunto de funciones relacionadas. Entonces tus estafa tampoco es válido


4
2018-02-09 21:49



Esto significa que no tengo que escribir un archivo de cabecera para cada función que creo (porque ya están en el archivo fuente principal) y también significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo. ¡Esto me parece una gran idea!

Los profesionales que notó son en realidad una razón por la que esto a veces se hace en una escala más pequeña.

Para programas grandes, no es práctico. Al igual que otras buenas respuestas mencionadas, esto puede aumentar los tiempos de construcción sustancialmente.

Sin embargo, se puede utilizar para dividir una unidad de traducción en bits más pequeños, que comparten el acceso a las funciones de una manera que recuerda la accesibilidad del paquete de Java.

La forma en que se logra lo anterior implica cierta disciplina y ayuda del preprocesador.

Por ejemplo, puede dividir su unidad de traducción en dos archivos:

// a.c

static void utility() {
}

static void a_func() {
  utility();
}

// b.c

static void b_func() {
  utility();
}

Ahora agrega un archivo para su unidad de traducción:

// ab.c

static void utility();

#include "a.c"
#include "b.c"

Y tu sistema de compilación tampoco construye a.c o b.c, sino que solo crea ab.o fuera de ab.c.

Que hace ab.c ¿realizar?

Incluye ambos archivos para generar una sola unidad de traducción y proporciona un prototipo para la utilidad. Para que el código en ambos a.c y b.c podría verlo, independientemente del orden en el que están incluidos, y sin requerir que la función sea extern.


2
2018-02-09 11:57