Sistema flexível para testar e coletar métricas de programas usando o conjunto de testes LLVM como exemplo

1. Introdução


A maioria dos desenvolvedores ouviu claramente alguns desenvolvimentos de código aberto bastante significativos, como o sistema LLVM e o compilador clang. No entanto, o LLVM agora não é apenas o sistema propriamente dito para a criação de compiladores, mas também um grande ecossistema que inclui muitos projetos para solucionar vários problemas que surgem durante qualquer estágio da criação do compilador (geralmente cada um desses projetos tem seu próprio repositório separado). Parte da infraestrutura inclui naturalmente ferramentas de teste e benchmarking, como ao desenvolver um compilador, sua eficácia é um indicador muito importante. Um desses projetos individuais de infraestrutura de teste do LLVM é o conjunto de testes ( documentação oficial ).

Conjunto de teste LLVM


À primeira vista, no repositório do conjunto de testes, parece que este é apenas um conjunto de benchmarks em C / C ++, mas isso não é inteiramente verdade. Além do código fonte dos programas nos quais as medições de desempenho serão realizadas, o conjunto de testes inclui uma infraestrutura flexível para criar, executar e coletar métricas. Por padrão, ele coleta as seguintes métricas: tempo de compilação, tempo de execução, tempo do link, tamanho do código (em seções).

O conjunto de testes é naturalmente útil para compiladores de teste e de benchmarking, mas também pode ser usado para outras tarefas de pesquisa em que é necessária alguma base de código C / C ++. Aqueles que fizeram tentativas de fazer algo no campo da análise de dados, acho, enfrentaram o problema da falta e fragmentação dos dados de origem. Um conjunto de testes, embora não seja composto por um grande número de aplicativos, mas possui um mecanismo unificado de coleta de dados. Adicionando seus próprios aplicativos à coleção, é muito simples coletar as métricas necessárias para sua tarefa específica. Portanto, na minha opinião, o conjunto de testes (além das principais tarefas de teste e benchmarking) é uma boa opção para um projeto básico, com base no qual você pode criar sua própria coleta de dados para tarefas nas quais é necessário analisar alguns recursos do código do programa ou algumas características dos programas.

Estrutura do conjunto de testes LLVM


test-suite |----CMakeLists.txt //  CMake ,   ,  | //   .. | |---- cmake | |---- .modules //        , | //   API    | |---- litsupport //  Python,      test-suite, | //    lit (  LLVM) | |---- tools //   :    | //     (    | // ),    .. | | //     | |---- SingleSource //   ,       | // .        . | |---- MultiSource //   ,      | //  .        | //  . | |---- MicroBenchmarks // ,   google-benchmark.   | //  ,    ,  | //       | |---- External //    ,     test-suite,  | // ,     (  ) | // -    

A estrutura é simples e direta.

Princípio de funcionamento


Como você pode ver, o CMake e um formato especial de lit-test são responsáveis ​​por todo o trabalho de descrição da montagem, lançamento e coleta de métricas.

Se considerarmos de uma maneira muito abstrata, fica claro que o processo de benchmarking usando esse sistema parece simples e muito previsível:


Como isso se parece com mais detalhes? Neste artigo, gostaria de me debruçar sobre exatamente qual papel o CMake desempenha em todo o sistema e qual é o único arquivo que você deve escrever se desejar adicionar algo a este sistema.

1. Construindo aplicativos de teste.

Como um sistema de compilação, tornou-se o padrão de fato para os programas C / C ++ CMake. O CMake configura o projeto e gera arquivos make, ninja etc., dependendo das preferências do usuário. para construção direta.
No entanto, no conjunto de testes, o CMake gera não apenas regras sobre como criar aplicativos, mas também configura os próprios testes.

Depois de iniciar o CMake, outros arquivos (com a extensão .test) serão gravados no diretório build com uma descrição de como o aplicativo deve ser executado e verificado se está correto.

Exemplo do arquivo .test mais padrão

 RUN: cd <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football ; <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football/football VERIFY: cd <some_path_to_build_directory>/MultiSource/Benchmarks/Prolangs-C/football ; <some_path_to_build_directory>/tools/fpcmp %o football.reference_output 

O arquivo com a extensão .test pode conter as seguintes seções:

  • PREPARE - descreve todas as ações que devem ser executadas antes do lançamento do aplicativo, muito semelhante ao método Before existente em diferentes estruturas de teste de unidade;
  • EXECUTAR - descreve como executar o aplicativo;
  • VERIFICAR - descreve como verificar o funcionamento correto do aplicativo;
  • METRIC - descreve as métricas que precisam ser coletadas adicionalmente no padrão.

Qualquer uma dessas seções pode ser omitida.

Porém, como esse arquivo é gerado automaticamente, ele está no arquivo CMake da referência que descreve: como obter os arquivos de objeto, como montá-los em um aplicativo e o que fazer com esse aplicativo.

Para uma melhor compreensão do comportamento padrão e como isso é descrito, considere um exemplo de alguns CMakeLists.txt

 list(APPEND CFLAGS -DBREAK_HANDLER -DUNICODE-pthread) #      (         ..     CMak,       ) list(APPEND LDFLAGS -lstdc++ -pthread) #       

Os sinalizadores podem ser configurados dependendo da plataforma, o arquivo DetectArchitecture é incluído nos módulos cmake da suíte de testes, que determina a plataforma de destino na qual os benchmarks são executados, para que você possa simplesmente usar os dados já coletados. Outros dados também estão disponíveis: sistema operacional, ordem de bytes, etc.

 if(TARGET_OS STREQUAL "Linux") list(APPEND CPPFLAGS -DC_LINUX) endif() if(NOT ARCH STREQUAL "ARM") if(ENDIAN STREQUAL "little") list(APPEND CPPFLAGS -DFPU_WORDS_BIGENDIAN=0) endif() if(ENDIAN STREQUAL "big") list(APPEND CPPFLAGS -DFPU_WORDS_BIGENDIAN=1) endif() endif() 

Em princípio, esta parte não deve ser novidade para pessoas que viram ou gravaram pelo menos uma vez um simples arquivo CMake. Naturalmente, você pode usar as bibliotecas, construí-las por conta própria, em geral, usar todos os meios fornecidos pelo CMake para descrever o processo de criação de seu aplicativo.

E então você precisa garantir a geração do arquivo .test. Quais ferramentas a interface tets-suite fornece para isso?

Existem 2 macros básicas llvm_multisource e llvm_singlesource , suficientes para a maioria dos casos triviais.

  • llvm_multisource será usado se o aplicativo consistir em vários arquivos. Se você não passar os arquivos de código-fonte como parâmetros ao chamar essa macro no seu CMake, todos os arquivos de código-fonte localizados no diretório atual serão usados ​​como base para a construção. De fato, atualmente estão ocorrendo alterações na interface dessa macro no conjunto de testes, e o método descrito para transferir arquivos de origem como parâmetros de macro é a versão atual localizada na ramificação principal. Anteriormente, havia outro sistema: os arquivos com código fonte tinham que ser gravados na variável Source (como na versão 7.0), e a macro não aceitava nenhum parâmetro. Mas a lógica básica da implementação permaneceu a mesma.
  • O llvm_singlesource considera que cada arquivo .c / .cpp é uma referência separada e, para cada um, coleta um arquivo executável separado.

Por padrão, as duas macros descritas acima para iniciar um aplicativo criado geram um comando que simplesmente chama esse aplicativo. E a verificação é realizada comparando-se com a saída esperada, que está no arquivo com a extensão .reference_output (também com os possíveis sufixos .reference_output.little-endian, .reference_output.big-endian).

Se isso lhe convém, é ótimo, uma linha extra (chamando llvm_multisource ou llvm_singlesource) é suficiente para você iniciar o aplicativo e obter as seguintes métricas: tamanho do código (em seções), tempo de compilação, tempo do link, tempo de execução.

Mas, é claro, isso raramente acontece dessa maneira. Pode ser necessário alterar um ou mais estágios. E isso também é possível com a ajuda de ações simples. A única coisa que você precisa lembrar é que, se você redefinir um determinado estágio, precisará descrever todos os outros (mesmo que o algoritmo padrão do trabalho deles seja satisfeito, o que, é claro, é um pouco perturbador).

Existem macros na API para descrever ações em cada estágio.

Não há muito o que escrever sobre a macro llvm_test_prepare para o estágio preparatório; os comandos que você precisa executar são simplesmente passados ​​para lá como parâmetro.

O que pode ser necessário na seção de lançamento? O caso mais previsível é que o aplicativo aceita alguns argumentos, arquivos de entrada. Para isso, existe a macro llvm_test_run , que aceita apenas os argumentos de inicialização do aplicativo (sem o nome do arquivo executável) como parâmetros.

 llvm_test_run(--fixed 400 --cpu 1 --num 200000 --seed 1158818515 run.hmm) 

Para alterar as ações no estágio de validação, é usada a macro llvm_test_verify , que aceita todos os comandos como parâmetros. Obviamente, para verificar a correção, é melhor usar as ferramentas incluídas na pasta de ferramentas. Eles fornecem boas oportunidades para comparar a saída gerada com a esperada (existe um processamento separado para comparar números reais com algum erro, etc.). Mas você pode em algum lugar e basta verificar se o aplicativo foi concluído com êxito etc.

 llvm_test_verify("cat %o | grep -q 'exit 0'") # %o -   placeholder   ,   lit.          lit,    ,    .    lit (  ,   LLVM)      (   <a href="https://llvm.org/docs/CommandGuide/lit.html"> </a>) 

Mas e se houver a necessidade de coletar algumas métricas adicionais? Existe uma macro llvm_test_metric para isso .

 llvm_test_metric(METRIC < > <,   >) 

Por exemplo, para dhrystone, uma métrica específica pode ser obtida.

 llvm_test_metric(METRIC dhry_score grep 'Dhrystones per Second' %o | awk '{print $4}') 

Obviamente, se você precisar coletar métricas adicionais para todos os testes, esse método será um pouco inconveniente. Você precisa adicionar a chamada llvm_test_metric às macros de nível superior fornecidas pela interface ou pode usar TEST_SUITE_RUN_UNDER (a variável CMake) e um script específico para coletar métricas. A variável TEST_SUITE_RUN_UNDER é bastante útil e pode ser usada, por exemplo, para rodar em simuladores, etc. De fato, um comando é gravado nele que aceitará o aplicativo com seus argumentos como uma entrada.

Como resultado, obtemos alguns CMakeLists.txt do formulário

 #       llvm_test_run(--fixed 400 --cpu 1 --num 200000 --seed 1158818515 run.hmm) llvm_test_verify("cat %o | grep -q 'exit 0'") llvm_test_metric(METRIC score grep 'Score' %o | awk '{print $4}') llvm_multisource() # llvm_multisource(my_application)    

A integração não requer esforços adicionais, se o aplicativo já estiver criado usando o CMake, em CMakeList.txt no conjunto de teste, você poderá incluir o CMake existente para montagem e adicionar algumas chamadas de macro simples.

2. Executando testes

Como resultado de seu trabalho, o CMake gerou um arquivo de teste especial de acordo com a descrição especificada. Mas como esse arquivo é executado?

lit sempre usa algum arquivo de configuração lit.cfg, que existe, portanto, no conjunto de teste. Nesse arquivo de configuração, são indicadas várias configurações para a execução de testes, incluindo o formato dos testes executáveis. O conjunto de testes usa seu próprio formato, localizado na pasta litsupport.

 config.test_format = litsupport.test.TestSuiteTest() 

Esse formato é descrito como uma classe de teste herdada do teste padrão aceso e substituindo o método principal da interface de execução. Também componentes importantes do litsupport são uma classe com uma descrição do plano de execução de teste do TestPlan, que armazena todos os comandos que devem ser executados em diferentes estágios e conhece a ordem dos estágios. Para fornecer a flexibilidade necessária, os módulos também foram introduzidos na arquitetura que deve fornecer o método mutatePlan, dentro do qual eles podem alterar o plano de teste, apenas introduzindo uma descrição da coleção das métricas necessárias, adicionando comandos adicionais para medir o tempo para iniciar o aplicativo, etc. Devido a essa solução, a arquitetura está se expandindo bem.



Um exemplo da operação de teste do conjunto de testes (com exceção dos detalhes na forma de classes TestContext, várias configurações acesas e os próprios testes, etc.) é apresentado abaixo.



Aceso faz com que o tipo de teste especificado no arquivo de configuração seja executado. TestSuiteTest analisa o arquivo de teste CMake gerado, recebendo uma descrição dos estágios principais. Então, todos os módulos encontrados são chamados para alterar o plano de teste atual, o lançamento é instrumentado. Em seguida, o plano de teste recebido é executado: eles são executados na ordem do estágio de preparação, lançamento e validação. Se necessário, a criação de perfil pode ser executada (adicionada por um dos módulos, se uma variável foi configurada durante a configuração que indica a necessidade de criação de perfil). A próxima etapa é coletar métricas, as funções de coleta adicionadas pelos módulos padrão no campo metric_collectors no TestPlan e, em seguida, as métricas adicionais descritas pelo usuário no CMake.

3. Executando o conjunto de testes

Existem duas maneiras de executar o conjunto de testes:

  • Manual, ou seja, invocação seqüencial de comandos.
     cmake -DCMAKE_CXX_COMPILER:FILEPATH=clang++ -DCMAKE_C_COMPILER:FILEPATH=clang test-suite #  make #   llvm-lit . -o <output> #   
  • usando o LNT (outro sistema do ecossistema LLVM que permite executar benchmarks, salvar resultados no banco de dados, analisar os resultados na interface da web). O LNT, dentro de sua equipe de execução de teste, executa as mesmas etapas do parágrafo anterior.
     lnt runtest test-suite --sandbox SANDBOX --cc clang --cxx clang++ --test-suite test-suite 

O resultado para cada teste é exibido como

 PASS: test-suite :: MultiSource/Benchmarks/Prolangs-C/football/football.test (m of n) ********** TEST 'test-suite :: MultiSource/Benchmarks/Prolangs-C/football/football.test' RESULTS ********** compile_time: 1.1120 exec_time: 0.0014 hash: "38254c7947642d1adb9d2f1200dbddf7" link_time: 0.0240 size: 59784 size..bss: 99800 … size..text: 37778 ********** 

Os resultados de diferentes lançamentos podem ser comparados sem o LNT (embora essa estrutura forneça grandes oportunidades para analisar informações usando diferentes ferramentas, mas precise de uma revisão separada), usando o script incluído no conjunto de teste

 test-suite/utils/compare.py results_a.json results_b.json 

Um exemplo de comparação do tamanho do código de um e o mesmo benchmark de dois lançamentos: com os sinalizadores -O3 e -Os

 test-suite/utils/compare.py -m size SANDBOX1/build/O3.json SANDBOX/build/Os.json Tests: 1 Metric: size Program O3 Os diff test-suite...langs-C/football/football.test 59784 47496 -20.6% 

Conclusão


A infra-estrutura para descrever e executar benchmarks implementados no conjunto de testes é fácil de usar e oferecer suporte, dimensiona-se bem e, em princípio, na minha opinião, ele usa soluções bastante elegantes em sua arquitetura, o que, é claro, torna o conjunto de testes uma ferramenta muito útil para desenvolvedores compiladores, bem como esse sistema, podem ser modificados para uso em algumas tarefas de análise de dados.

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


All Articles