Ninja: Opção para usar características de arquivo em vez de carimbos de data/hora

Criado em 14 ago. 2018  ·  15Comentários  ·  Fonte: ninja-build/ninja

Conforme observado em outros problemas, o uso de carimbos de data/hora para determinar se é necessário reconstruir algo pode ser problemático. Embora os carimbos de data e hora sejam convenientes e relativamente rápidos, muitas vezes é desejável tomar decisões de construção chave em alguma característica intrínseca ao próprio arquivo, como um hash de seu conteúdo.

Eu uso muito o git e é chato que simplesmente mudar de ramificações desencadeia recompilações. Idealmente, eu seria capaz de mudar da minha ramificação de trabalho atual para outra ramificação, não tocar em nenhum arquivo e depois voltar para a ramificação original e não precisar reconstruir nada. Até onde eu sei, isso não é possível se o sistema de compilação usar timestamps. O uso de hashes de arquivo resolveria esse problema específico.

Eu entendo que o uso de hashes de arquivos ou outras características de arquivos intrínsecos podem diminuir a velocidade do Ninja, portanto, usá-los deve ser uma opção.

feature

Comentários muito úteis

Eu também usei hash no trabalho, com grande sucesso. É baseado no #929, mas com vários patches, como pode ser visto em https://github.com/moroten/ninja/commits/hashed. hash_input = 1 nas regras selecionadas é muito conveniente. Minha ramificação ainda contém um bug em que os arquivos são stat com muita frequência, O(n^2) em vez de O(n) . O bug está relacionado a bordas falsas.

Um problema é como lidar com bordas falsas. Eu uso bordas falsas para agrupar, por exemplo, arquivos de cabeçalho. Portanto, minha implementação itera recursivamente através de bordas falsas. Também está relacionado ao bug em #1021.

Outro pensamento é fazer o hashing membro de primeira classe do ninja, ou seja, mover os hashes para o log de construção. Usar o SHA256 seria um passo para adicionar suporte à API de execução remota do Bazel. Uma implementação C++ pode ser encontrada em https://gitlab.com/bloomberg/recc/. Não seria muito legal?

Infelizmente, classificar a semântica para bordas falsas provavelmente quebrará a compatibilidade com versões anteriores.

Todos 15 comentários

929 tem uma implementação. Embora seja usado com sucesso (em uma bifurcação) para milhares de compilações diariamente, não foi considerado para mesclagem.

929 é single-thread e, portanto, pode ser mais lento como ccache ou outras soluções. Também acho que isso deve ser um sinalizador de linha de comando, para que não exija alterações nas definições de compilação.

Não pode (ou pelo menos não deveria) ser um sinalizador de linha de comando, pois o hash se aplicaria a todas as regras. Hashing, por exemplo, todas as entradas de regras de link é caro e, portanto, não desejado, enquanto os arquivos de origem e suas dependências conhecidas são bons candidatos. Para essa distinção, ela deve fazer parte da descrição da compilação. Além disso, para fazer uso do recurso, você deve usar o sinalizador de forma consistente o tempo todo. Não apenas às vezes.

Respondendo ao argumento de thread único: Sim, adiciona instruções ao loop de thread único. Na realidade, isso só importa se o único thread tiver mais trabalho do que pode trabalhar (ou seja, mais regras terminam do que um thread pode processar (depslog+hashlog+...)). Só então o hash dói. Caso contrário, o loop de thread único espera que os trabalhos terminem de qualquer maneira. Nunca vimos um ninja ocupado com hash, mesmo com experimentos -j1000. (E para regras de acabamento rápido, o hash não é interessante para garantir o tempo.)

Considere também: o hashing com murmur hash é consideravelmente rápido e mesmo arquivos de origem grandes levam apenas alguns milissegundos para serem hash. Além disso, o hash acontece logo após o arquivo fonte (e as dependências) serem vistos pelo compilador. Portanto, eles geralmente são lidos do cache do sistema de arquivos.
Como o hash ocorre durante a compilação (em paralelo às regras executadas), o tempo geral de compilação geralmente não é afetado de forma mensurável.

Por fim, a implementação em #929 é opcional e não tem custo para pessoas que não usam o recurso (além da instrução if).

Hashing, por exemplo, todas as entradas de regras de link é caro e, portanto, não desejado, enquanto os arquivos de origem e suas dependências conhecidas são bons candidatos.

Eu diria que o hash de entradas para o vinculador é especialmente desejado, pois muitas vezes pode resultar em que a vinculação seja ignorada completamente (por exemplo, para alterações de formatação ou quando os comentários são alterados). À medida que a compilação dos arquivos de objetos termina peça por peça, o cálculo de hash pode acontecer enquanto a compilação está em execução (como você apontou).

Se for muito lento (por exemplo, com grandes bibliotecas estáticas), podemos pensar em implementar hashes apenas para entradas puras e não para arquivos intermediários. Isso resolveria pelo menos o caso "trocar ramificações do Git causa reconstruções completas".

Além disso, para fazer uso do recurso, você deve usar o sinalizador de forma consistente o tempo todo. Não apenas às vezes.

Eu diria que isso é uma vantagem: se estou trabalhando em uma única ramificação e quero iterar rapidamente, não usaria hashes. Se eu estiver comparando diferentes ramificações de recursos, usaria hashes.

Além disso, para fazer uso do recurso, você deve usar o sinalizador de forma consistente o tempo todo. Não apenas às vezes.

Eu diria que isso é uma vantagem: se estou trabalhando em uma única ramificação e quero iterar rapidamente, não usaria hashes. Se eu estiver comparando diferentes ramificações de recursos, usaria hashes.

Mas então precisaria haver uma maneira de alternar de não hash para hash, o que significa que o estado atual dos arquivos precisaria ser hash, para que a próxima reconstrução possa usar os hashes (que provavelmente não existiam ou estão fora de data se você não passou a bandeira).

Eu acho que o hash ainda usa o timestamp primeiro, para que, se os timestamps corresponderem, não haja necessidade de comparar os hashes. Isso significaria que as primeiras compilações podem recompilar desnecessariamente alguns arquivos, mas isso não deve acontecer com frequência (na maioria das vezes, a heurística de carimbo de data/hora está certa, afinal).

Eu também usei hash no trabalho, com grande sucesso. É baseado no #929, mas com vários patches, como pode ser visto em https://github.com/moroten/ninja/commits/hashed. hash_input = 1 nas regras selecionadas é muito conveniente. Minha ramificação ainda contém um bug em que os arquivos são stat com muita frequência, O(n^2) em vez de O(n) . O bug está relacionado a bordas falsas.

Um problema é como lidar com bordas falsas. Eu uso bordas falsas para agrupar, por exemplo, arquivos de cabeçalho. Portanto, minha implementação itera recursivamente através de bordas falsas. Também está relacionado ao bug em #1021.

Outro pensamento é fazer o hashing membro de primeira classe do ninja, ou seja, mover os hashes para o log de construção. Usar o SHA256 seria um passo para adicionar suporte à API de execução remota do Bazel. Uma implementação C++ pode ser encontrada em https://gitlab.com/bloomberg/recc/. Não seria muito legal?

Infelizmente, classificar a semântica para bordas falsas provavelmente quebrará a compatibilidade com versões anteriores.

Enquanto isso, inventei uma solução para o meu caso de uso de troca de ramificações no Chromium (o que é particularmente doloroso); o pequeno programa e script Go que eu uso está aqui: https://github.com/bromite/mtool

Sinta-se à vontade para adaptá-lo aos seus casos de uso, se funcionar para o Chromium, aposto que também funcionará para projetos de compilação menores (e leva um tempo insignificante para minhas execuções). A única desvantagem é que, se você usar o script que publiquei lá como está, ele espalhará arquivos .mtool em cada diretório pai do repositório git, mas nada que um gitignore global não possa curar.

Interessante notar que eu uso a saída git ls-files --stage para as necessidades de hash; é possível (mas menos eficiente) pedir ao git para fazer o hash também de arquivos não indexados se sua compilação depender deles.

Em termos de recursos, seria de se esperar que, para implementar o recurso discutido aqui, o ninja pudesse fazer o mesmo internamente (sem depender do git) e com resultados de desempenho semelhantes.

Depois de uma olhada rápida, esses patches ninja não parecem fazer o hash da linha de comando do compilador, além dos arquivos de entrada. Estou faltando alguma coisa?

A linha de comando já é hash pelo ninja e armazenada no log de compilação.

Eu acho que o hash ainda usa o timestamp primeiro, para que, se os timestamps corresponderem, não haja necessidade de comparar os hashes. Isso significaria que as primeiras compilações podem recompilar desnecessariamente alguns arquivos, mas isso não deve acontecer com frequência (na maioria das vezes, a heurística de carimbo de data/hora está certa, afinal).

Isso seria muito errado. A comparação de hashes pode ser eliminada se os carimbos de data/hora não corresponderem; o sistema de compilação pode assumir que a dependência foi modificada e, se não foi realmente modificada (apenas tocada), a compilação será subótima, mas correta. No entanto, se os carimbos de data/hora corresponderem, ainda é possível que a dependência tenha sido modificada e seu carimbo de data/hora tenha sido redefinido à força (EDIT: ou que o destino foi tocado e, portanto, mais recente que suas dependências). Sem verificação dupla comparando hashes, isso resultaria em uma compilação incorreta.

Acho que Ninja já assume e pula todo o trabalho quando os timestamps correspondem. Portanto, no seu exemplo, isso seria uma compilação incorreta de qualquer maneira.

Não estou ciente (provavelmente devido à minha ignorância) de qualquer ferramenta que produza uma saída diferente e mantenha o carimbo de data e hora anterior. Por que alguém faria isso?

IMHO, pular a verificação de hash quando os carimbos de data/hora correspondem é uma otimização muito válida.

A verificação de hash simplesmente evitaria algumas reconstruções "falsas sujas", mantendo a semântica existente já fornecida pelo Ninja.

O problema não são as reconstruções falsamente sujas, são as reconstruções falsamente limpas. Um check-out git toca em tudo que ele substitui. Ele pode tornar um destino mais recente do que uma dependência (sim, as pessoas confirmam o código gerado por vários motivos válidos). Uma verificação de hash impediria uma não reconstrução de limpeza falsa nesse caso.

@rulatir Acho que entendo principalmente o que você está dizendo :) Acho que essa é uma das razões pelas quais o Bazel e outros sistemas de compilação baseados em verificações de hash são realmente contra as saídas de destino na árvore.

No entanto, esse problema não seria resolvido se o sistema de compilação verificasse se os destinos são mais recentes do que o tempo conhecido anteriormente e reconstruísse, se necessário?

@rulatir Acho que entendo principalmente o que você está dizendo :) Acho que essa é uma das razões pelas quais o Bazel e outros sistemas de compilação baseados em verificações de hash são realmente contra as saídas de destino na árvore.

Como o hash depende de onde o arquivo está?

No entanto, esse problema não seria resolvido se o sistema de compilação verificasse se os destinos são mais recentes do que o tempo conhecido anteriormente e reconstruísse, se necessário?

Entendo que o principal benefício de usar carimbos de data/hora é evitar a necessidade de manter um banco de dados separado que rastreie as assinaturas de versão "conhecidas anteriormente". Se você está disposto a renunciar a esse benefício, por que essas assinaturas não deveriam ser hashes?

Esta página foi útil?
0 / 5 - 0 avaliações