Por que alguns programas C são gravados em um grande arquivo de origem?

88

Por exemplo, a ferramenta SysInternals "FileMon" do passado possui um driver no modo kernel cujo código fonte está inteiramente em um arquivo de 4.000 linhas. O mesmo para o primeiro programa de ping já escrito (~ 2.000 LOC).

Farelo
fonte

Respostas:

143

O uso de vários arquivos sempre exige sobrecarga administrativa adicional. É necessário configurar um script de construção e / ou arquivo de criação com estágios separados de compilação e vinculação, garantir que as dependências entre os diferentes arquivos sejam gerenciados corretamente, escrever um script "zip" para facilitar a distribuição do código-fonte por email ou download, e assim em. Hoje em dia, os IDEs modernos normalmente exigem muito desse trabalho, mas tenho certeza de que, no momento em que o primeiro programa de ping foi gravado, esse IDE não estava disponível. E para arquivos tão pequenos quanto ~ 4000 LOC, sem um IDE que gere vários arquivos para você, a troca entre a sobrecarga mencionada e os benefícios do uso de vários arquivos pode permitir que as pessoas tomem uma decisão pela abordagem de arquivo único.

Doc Brown
fonte
9
"E para arquivos tão pequenos quanto ~ 4000 LOC ..." Estou trabalhando como desenvolvedor JS agora. Quando tenho um arquivo com apenas 400 linhas de código, fico nervoso com o tamanho dele! (Mas temos dúzias e dezenas de arquivos em nosso projeto.) #
213 Kevin Kevin
36
@ Kevin: um fio na minha cabeça é muito pouco, um fio na minha sopa é demais ;-) AFAIK em vários arquivos JS não causa tanta sobrecarga administrativa como em "C sem um IDE moderno".
Doc Brown
4
@ Kevin JS é um animal bastante diferente. O JS é transmitido para um usuário final toda vez que um usuário carrega um site e ainda não o tem em cache pelo navegador. C só precisa ter o código transmitido uma vez, a pessoa do outro lado o compila e permanece compilado (obviamente há exceções, mas esse é o caso de uso geral esperado). Também o material C tende a ser um código legado, pois muitos dos projetos '4000 linhas são normais' que as pessoas estão descrevendo nos comentários.
Pharap
5
@ Kevin Agora, veja como underscore.js (1700 loc, um arquivo) e uma infinidade de outras bibliotecas que são distribuídas são gravadas. Javascript é quase tão ruim quanto C em relação à modularização e implantação.
Voo
2
@ Sharap Acho que ele quis dizer usar algo como o Webpack antes de implantar o código. Com o Webpack, você pode trabalhar em vários arquivos e compilá-los em um único pacote.
precisa saber é o seguinte
81

Porque C não é bom em modularização. Fica confuso (arquivos de cabeçalho e #includes, funções externas, erros de tempo de link, etc.) e quanto mais módulos você inserir, mais complicado será.

As linguagens mais modernas têm melhores recursos de modularização em parte porque aprenderam com os erros de C e facilitam a decomposição da base de código em unidades menores e mais simples. Mas com C, pode ser benéfico evitar ou minimizar todo esse problema, mesmo que isso signifique agrupar o que de outra forma seria considerado muito código em um único arquivo.

Mason Wheeler
fonte
38
Eu acho que é injusto descrever a abordagem C como 'erros'; eram decisões perfeitamente sensatas e razoáveis ​​no momento em que foram tomadas.
precisa saber é o seguinte
14
Nenhuma dessas coisas de modularização é particularmente complicada. Ela pode ser feita complicada por estilo de codificação ruim, mas não é difícil de compreender ou implementar, e nenhum dos que poderiam ser classificados como "erros". A verdadeira razão, conforme a resposta de Snowman, é que a otimização de vários arquivos de origem não era tão boa no passado e que o driver FileMon requer alto desempenho. Além disso, ao contrário da opinião do OP, esses arquivos não são particularmente grandes.
Graham
8
@ Graham Qualquer arquivo com mais de 1000 linhas de código deve ser tratado como um cheiro de código.
Mason Wheeler
11
@JackAidley não é injusto em tudo , ter algo ser um erro não é mútuo exclusivo com a dizer que foi uma decisão razoável no momento. Erros são inevitáveis, dadas informações imperfeitas e tempo limitado, e devem ser aprendidos com não vergonhosamente ocultos ou reclassificados para salvar a face.
Jared Smith
8
Qualquer pessoa que afirme que a abordagem de C não é um erro deixa de entender como um arquivo C aparentemente com dez linhas pode realmente ser um arquivo com dez mil linhas com todos os cabeçalhos #include: d. Isso significa que todos os arquivos do seu projeto são efetivamente de pelo menos dez mil linhas, independentemente da quantidade de linhas fornecida por "wc -l". Um melhor suporte à modularidade reduziria facilmente os tempos de análise e compilação em uma pequena fração.
21717 juhist
37

Além dos motivos históricos, há um motivo para usá-lo em softwares modernos sensíveis ao desempenho. Quando todo o código está em uma unidade de compilação, o compilador pode executar otimizações de todo o programa. Com unidades de compilação separadas, o compilador não pode otimizar o programa inteiro de determinadas maneiras (por exemplo, embutir determinado código).

O vinculador certamente pode executar algumas otimizações além do que o compilador pode fazer, mas não todas. Por exemplo: vinculadores modernos são realmente bons em eliminar funções não referenciadas, mesmo em vários arquivos de objetos. Eles podem executar outras otimizações, mas nada como o que um compilador pode fazer dentro de uma função.

Um exemplo bem conhecido de um módulo de código de fonte única é o SQLite. Você pode ler mais sobre isso na página Amalgamação SQLite .

1. Sumário Executivo

Mais de 100 arquivos de origem separados são concatenados em um único arquivo grande de código C chamado "sqlite3.c" e chamado "a fusão". A fusão contém tudo o que um aplicativo precisa para incorporar o SQLite. O arquivo de amálgama tem mais de 180.000 linhas e mais de 6 megabytes de tamanho.

A combinação de todo o código do SQLite em um arquivo grande facilita a implantação do SQLite - há apenas um arquivo para acompanhar. E como todo o código está em uma única unidade de tradução, os compiladores podem otimizar melhor a inter-procedimentos, resultando em um código de máquina entre 5% e 10% mais rápido.


fonte
15
Mas observe que os compiladores C modernos podem otimizar todo o programa de vários arquivos de origem (embora não se você os compilar primeiro em arquivos de objeto individuais).
Davislor
10
@ Davidislor Veja o script típico de compilação: os compiladores não farão isso de maneira realista.
4
É significativamente mais fácil alterar um script de construção $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)do que mover tudo para um único arquivo de som. Você pode até fazer a compilação do programa inteiro como um destino alternativo ao script de construção tradicional que ignora a recompilação de arquivos de origem que não foram alterados, semelhante à maneira como as pessoas podem desativar a criação de perfil e a depuração para o destino de produção. Você não tem essa opção se tudo estiver em uma grande fonte de heap. Não é o que as pessoas estão acostumadas, mas não há nada complicado nisso.
Davislor
9
O @Davislor otimização de programa inteiro / otimização de tempo de link (LTO) também funciona quando você "compila" o código em arquivos de objetos individuais (dependendo do que "compilar" significa para você). Por exemplo, o LTO do GCC adicionará sua representação de código analisada aos arquivos de objetos individuais no tempo de compilação e, no momento do link, usará aquele em vez do código de objeto (também presente) para recompilar e criar o programa inteiro. Portanto, isso funciona com configurações de compilação que são compiladas primeiro para arquivos de objetos individuais, embora o código de máquina gerado pela compilação inicial seja ignorado.
Dreamer
8
JsonCpp faz isso hoje em dia também. A chave é que os arquivos não são assim durante o desenvolvimento.
Lightness Races em Órbita
15

Além do fator de simplicidade mencionado pelo outro entrevistado, muitos programas em C são escritos por um indivíduo.

Quando você tem uma equipe de indivíduos, torna-se desejável dividir o aplicativo entre vários arquivos de origem para evitar conflitos gratuitos nas alterações de código. Especialmente quando há programadores avançados e muito juniores trabalhando no projeto.

Quando uma pessoa está trabalhando sozinha, isso não é um problema.

Pessoalmente, eu uso vários arquivos com base na função como uma coisa habitual. Mas sou só eu.

Ron Ruble
fonte
4
@OskarSkog Mas você nunca modificará um arquivo ao mesmo tempo que o seu futuro.
Loren Pechtel 5/03
2

Porque C89 não tinha inlinefunções. O que significava que dividir seu arquivo em funções causava a sobrecarga de empurrar valores na pilha e saltar. Isso adicionou um pouco de sobrecarga ao implementar o código em 1 instrução de chave grande (loop de evento). Mas um loop de eventos é sempre muito mais difícil de implementar com eficiência (ou mesmo corretamente) do que uma solução mais modularizada. Portanto, para projetos de grande porte, as pessoas ainda optariam por modularizar. Mas quando eles tinham o design pensado com antecedência e podiam controlar o estado em uma declaração de switch, eles optaram por isso.

Atualmente, mesmo em C, não é preciso sacrificar o desempenho para modularizar, porque mesmo em funções C podem ser incorporadas.

Dmitry Rubanovich
fonte
2
As funções C podem ser tão inline em 89 quanto nos dias de hoje, inline é algo que deve ser usado quase nunca - o compilador sabe melhor do que você em quase todas as situações. E a maioria desses arquivos LOC de 4k não é uma função gigantesca - esse é um estilo de codificação horrível que também não terá nenhum benefício perceptível no desempenho.
Voo
@ Voo, não sei por que você mencionou o estilo de codificação. Eu não estava defendendo isso. Na verdade, mencionei que, na maioria dos casos, garante uma solução menos eficiente devido a uma implementação incorreta. Eu também mencionei que é uma péssima ideia porque não é dimensionável (para projetos maiores). Dito isto, em loops muito restritos (que é o que acontece no código de rede próximo ao hardware), empurrar e acionar desnecessariamente os valores de ativação / desativação da pilha (ao chamar funções) aumentará o custo do programa em execução. Esta não foi uma ótima solução. Mas era o melhor disponível na época.
Dmitry Rubanovich
2
Nota obrigatória: a palavra-chave inline tem pouco a ver com a otimização embutida. Não é uma dica especial para o compilador fazer essa otimização, mas sim a vinculação com símbolos duplicados.
Hyde
@Dmitry O ponto é que alegar que, porque não havia inlinepalavra-chave nos compiladores C89, não era possível incorporar, e é por isso que você teve que escrever tudo em uma função gigante que está incorreta. Você nunca deve usar inlinecomo otimizações de desempenho - o compilador geralmente sabe melhor do que você (e pode ignorar a palavra-chave).
Voo
@Voo: Um programador e um compilador geralmente sabem algumas coisas que o outro não sabe. A inlinepalavra-chave possui semântica relacionada ao vinculador, que é mais importante do que a questão de executar ou não otimizações em linha, mas algumas implementações têm outras diretrizes para controlar o alinhamento interno e essas coisas às vezes podem ser muito importantes. Em alguns casos, uma função pode parecer muito grande para valer a pena, mas a dobragem constante pode reduzir o tamanho e o tempo de execução para quase nada. Um compilador que não recebe uma cutucada forte para incentivar o alinhamento
interno
1

Isso conta como um exemplo de evolução, que me surpreende ainda não ter sido mencionado.

Nos dias sombrios da programação, a compilação de um único arquivo pode levar alguns minutos. Se um programa fosse modularizado, a inclusão dos arquivos de cabeçalho necessários (sem opções de cabeçalho pré-compilados) seria uma causa adicional significativa de desaceleração. Além disso, o compilador pode escolher / precisar manter algumas informações no próprio disco, provavelmente sem o benefício de um arquivo de troca automática.

Os hábitos que esses fatores ambientais levaram a transitar para práticas de desenvolvimento contínuas e se adaptaram lentamente ao longo do tempo.

Naquele momento, o ganho do uso de um único arquivo seria semelhante ao obtido pelo uso de SSDs em vez de HDDs.

itj
fonte