Como traduzimos 10 milhões de linhas de código C++ para o padrão C++ 14 (e depois para C++ 17)

Há algum tempo (no outono de 2016), durante o desenvolvimento da próxima versão da plataforma tecnológica 1C:Enterprise, surgiu a questão dentro da equipe de desenvolvimento sobre o suporte ao novo padrão C ++ 14 em nosso código. A transição para um novo padrão, como presumimos, nos permitiria escrever muitas coisas de maneira mais elegante, simples e confiável, e simplificaria o suporte e a manutenção do código. E parece não haver nada de extraordinário na tradução, se não fosse pela escala da base de código e pelas características específicas do nosso código.

Para quem não sabe, 1C:Enterprise é um ambiente para o rápido desenvolvimento de aplicações de negócios multiplataforma e tempo de execução para sua execução em diferentes sistemas operacionais e SGBDs. Em termos gerais, o produto contém:

Tentamos escrever o mesmo código para diferentes sistemas operacionais, tanto quanto possível - a base de código do servidor é 99% comum, a base de código do cliente é de cerca de 95%. A plataforma de tecnologia 1C:Enterprise é escrita principalmente em C++ e as características aproximadas do código são fornecidas abaixo:

  • 10 milhões de linhas de código C++,
  • 14 mil arquivos,
  • 60 mil aulas,
  • meio milhão de métodos.

E tudo isso teve que ser traduzido para C++14. Hoje contaremos como fizemos isso e o que encontramos no processo.

Como traduzimos 10 milhões de linhas de código C++ para o padrão C++ 14 (e depois para C++ 17)

Isenção de responsabilidade

Tudo o que está escrito abaixo sobre trabalho lento/rápido, (não) grande consumo de memória por implementações de classes padrão em várias bibliotecas significa uma coisa: isso é verdade PARA NÓS. É bem possível que as implementações padrão sejam mais adequadas às suas tarefas. Começamos com nossas próprias tarefas: pegamos dados típicos de nossos clientes, executamos cenários típicos neles, analisamos o desempenho, a quantidade de memória consumida, etc., e analisamos se nós e nossos clientes estávamos satisfeitos com tais resultados ou não . E eles agiram dependendo.

O que tínhamos

Inicialmente, escrevemos o código para a plataforma 1C:Enterprise 8 usando o Microsoft Visual Studio. O projeto começou no início dos anos 2000 e tínhamos uma versão somente para Windows. Naturalmente, desde então o código foi desenvolvido ativamente, muitos mecanismos foram completamente reescritos. Mas o código foi escrito de acordo com o padrão de 1998 e, por exemplo, nossos colchetes foram separados por espaços para que a compilação fosse bem-sucedida, assim:

vector<vector<int> > IntV;

Em 2006, com o lançamento da versão 8.1 da plataforma, começamos a oferecer suporte ao Linux e mudamos para uma biblioteca padrão de terceiros Porta STL. Um dos motivos da transição foi trabalhar com linhas largas. Em nosso código, usamos std::wstring, que é baseado no tipo wchar_t. Seu tamanho no Windows é de 2 bytes e no Linux o padrão é de 4 bytes. Isso levou à incompatibilidade de nossos protocolos binários entre cliente e servidor, bem como a vários dados persistentes. Usando as opções do gcc, você pode especificar que o tamanho de wchar_t durante a compilação também é de 2 bytes, mas você pode esquecer de usar a biblioteca padrão do compilador, porque ele usa glibc, que por sua vez é compilado para um wchar_t de 4 bytes. Outros motivos foram uma melhor implementação de classes padrão, suporte para tabelas hash e até emulação da semântica de movimentação dentro de contêineres, que usamos ativamente. E mais um motivo, como dizem por último mas não menos importante, foi o desempenho das cordas. Tínhamos nossa própria aula de cordas, porque... Devido às especificidades do nosso software, as operações com strings são amplamente utilizadas e para nós isso é fundamental.

Nossa string é baseada em ideias de otimização de strings expressas no início dos anos 2000 Andrei Alexandrescu. Mais tarde, quando Alexandrescu trabalhava no Facebook, por sugestão dele, foi usada uma linha no mecanismo do Facebook que funcionava com princípios semelhantes (ver biblioteca loucura).

Nossa linha utilizou duas tecnologias principais de otimização:

  1. Para valores curtos, um buffer interno no próprio objeto string é usado (não requer alocação de memória adicional).
  2. Para todos os outros, a mecânica é usada Copiar na gravação. O valor da string é armazenado em um local e um contador de referência é usado durante a atribuição/modificação.

Para acelerar a compilação da plataforma, excluímos a implementação de stream da nossa variante STLPort (que não usamos), o que nos proporcionou uma compilação cerca de 20% mais rápida. Posteriormente tivemos que fazer uso limitado Boost . O Boost faz uso intenso de stream, principalmente em suas APIs de serviço (por exemplo, para registro), então tivemos que modificá-lo para remover o uso de stream. Isso, por sua vez, dificultou a migração para novas versões do Boost.

O Terceiro Caminho

Ao migrar para o padrão C++14, consideramos as seguintes opções:

  1. Atualize o STLPort que modificamos para o padrão C++14. A opção é muito difícil, porque... o suporte para STLPort foi descontinuado em 2010 e teríamos que construir nós mesmos todo o seu código.
  2. Transição para outra implementação STL compatível com C++14. É altamente desejável que esta implementação seja para Windows e Linux.
  3. Ao compilar para cada sistema operacional, use a biblioteca incorporada no compilador correspondente.

A primeira opção foi rejeitada liminarmente devido ao excesso de trabalho.

Pensamos há algum tempo na segunda opção; considerado como candidato libc ++, mas naquela época não funcionava no Windows. Para portar libc++ para Windows, você teria que fazer muito trabalho - por exemplo, escrever tudo sozinho que tenha a ver com threads, sincronização de threads e atomicidade, já que libc++ é usado nessas áreas API POSIX.

E escolhemos a terceira via.

Transição

Assim, tivemos que substituir o uso do STLPort pelas bibliotecas dos compiladores correspondentes (Visual Studio 2015 para Windows, gcc 7 para Linux, clang 8 para macOS).

Felizmente, nosso código foi escrito principalmente de acordo com as diretrizes e não usou todos os tipos de truques inteligentes, então a migração para novas bibliotecas ocorreu de forma relativamente tranquila, com a ajuda de scripts que substituíram os nomes de tipos, classes, namespaces e inclusões no código-fonte arquivos. A migração afetou 10 arquivos de origem (de 000). wchar_t foi substituído por char14_t; decidimos abandonar o uso de wchar_t, porque char000_t ocupa 16 bytes em todos os sistemas operacionais e não prejudica a compatibilidade do código entre Windows e Linux.

Houve algumas pequenas aventuras. Por exemplo, em STLPort, um iterador poderia ser convertido implicitamente em um ponteiro para um elemento e, em alguns lugares de nosso código, isso foi usado. Nas novas bibliotecas já não era possível fazer isso, e essas passagens tiveram que ser analisadas e reescritas manualmente.

Assim, a migração do código está completa, o código está compilado para todos os sistemas operacionais. É hora de testes.

Os testes após a transição mostraram uma queda no desempenho (em alguns lugares até 20-30%) e um aumento no consumo de memória (até 10-15%) em comparação com a versão antiga do código. Isto se deveu, em particular, ao desempenho abaixo do ideal das strings padrão. Portanto, novamente tivemos que usar nossa própria linha ligeiramente modificada.

Uma característica interessante da implementação de contêineres em bibliotecas incorporadas também foi revelada: std::map e std::set vazios (sem elementos) de bibliotecas integradas alocam memória. E devido aos recursos de implementação, em alguns lugares do código são criados muitos contêineres vazios desse tipo. Os contêineres de memória padrão são alocados um pouco, para um elemento raiz, mas para nós isso acabou sendo crítico - em vários cenários, nosso desempenho caiu significativamente e o consumo de memória aumentou (em comparação com STLPort). Portanto, em nosso código substituímos esses dois tipos de containers das bibliotecas embutidas pela sua implementação do Boost, onde esses containers não possuíam esse recurso, e isso resolveu o problema de lentidão e aumento do consumo de memória.

Como costuma acontecer após mudanças em grande escala em grandes projetos, a primeira iteração do código-fonte não funcionou sem problemas e, aqui, em particular, o suporte para depuração de iteradores na implementação do Windows foi útil. Avançamos passo a passo e, na primavera de 2017 (versão 8.3.11 1C:Enterprise), a migração foi concluída.

Resultados de

A transição para o padrão C++14 levou cerca de 6 meses. Na maioria das vezes, um desenvolvedor (mas altamente qualificado) trabalhava no projeto e, na fase final, juntavam-se representantes de equipes responsáveis ​​por áreas específicas - UI, cluster de servidores, ferramentas de desenvolvimento e administração, etc.

A transição simplificou bastante nosso trabalho de migração para as versões mais recentes do padrão. Assim, a versão 1C:Enterprise 8.3.14 (em desenvolvimento, lançamento previsto para o início do próximo ano) já foi transferida para o padrão C++17.

Após a migração, os desenvolvedores têm mais opções. Se antes tínhamos nossa própria versão modificada do STL e um namespace std, agora temos classes padrão das bibliotecas integradas do compilador no namespace std, no namespace stdx - nossas linhas e contêineres otimizados para nossas tarefas, em boost - o versão mais recente do impulso. E o desenvolvedor usa as classes mais adequadas para resolver seus problemas.

A implementação “nativa” dos construtores de movimento também ajuda no desenvolvimento (mover construtores) para várias aulas. Se uma classe possui um construtor de movimento e esta classe é colocada em um contêiner, então o STL otimiza a cópia dos elementos dentro do contêiner (por exemplo, quando o contêiner é expandido e é necessário alterar a capacidade e realocar a memória).

Mosca na sopa

Talvez a consequência mais desagradável (mas não crítica) da migração seja o facto de nos depararmos com um aumento no volume arquivos obj, e o resultado completo da compilação com todos os arquivos intermediários começou a ocupar 60–70 GB. Esse comportamento se deve às peculiaridades das bibliotecas padrão modernas, que se tornaram menos críticas quanto ao tamanho dos arquivos de serviço gerados. Isto não afeta o funcionamento da aplicação compilada, mas causa uma série de inconvenientes no desenvolvimento, em particular, aumenta o tempo de compilação. Os requisitos de espaço livre em disco em servidores de construção e em máquinas de desenvolvedores também estão aumentando. Nossos desenvolvedores trabalham em várias versões da plataforma em paralelo, e centenas de gigabytes de arquivos intermediários às vezes criam dificuldades em seu trabalho. O problema é desagradável, mas não crítico; por enquanto adiamos a sua solução. Estamos considerando a tecnologia como uma das opções para resolvê-lo construção de unidade (em particular, o Google o utiliza ao desenvolver o navegador Chrome).

Fonte: habr.com

Adicionar um comentário