Há algum tempo atrás (no outono de 2016), durante o desenvolvimento de mais uma versão da plataforma tecnológica 1C:Enterprise, surgiu a questão dentro da equipe de desenvolvimento sobre o suporte a um novo padrão em nosso código. A transição para o novo padrão, como supúnhamos, nos permitiria escrever muitas coisas de forma mais elegante, simples e confiável, facilitando a manutenção e o suporte do código. E, em tradução, não parece haver nada extraordinário, se não fossem as dimensões da base de código e as características específicas do nosso código.
Para quem não sabe, 1C:Enterprise é um ambiente para o rápido desenvolvimento de aplicativos de negócios multiplataforma e runtime para sua execução em diferentes sistemas operacionais e bancos de dados. Em termos gerais, o produto inclui:
- , que funciona em Windows e Linux
- , se comunicando com o servidor via http(s) ou através de seu próprio protocolo binário, funcionando em Windows, Linux, macOS
- , que opera nos navegadores Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (escrito em JavaScript)
- Um ambiente de desenvolvimento (), funcionando em Windows, Linux, macOS
- de servidores de aplicativos, que funcionam em Windows, Linux, macOS
- , que se conecta ao servidor via http(s), funciona em dispositivos móveis com Android, iOS, Windows
- — um framework para a criação de aplicativos móveis offline com possibilidade de sincronização, que operam em Android, iOS, Windows
- Ambiente de desenvolvimento , escrito em Java
- Servidor
Nos esforçamos ao máximo para escrever um único código para diferentes sistemas operacionais — a base de código do servidor é comum em 99%, a do cliente — cerca de 95%. A plataforma tecnológica 1C:Enterprise é predominantemente escrita em C++ e abaixo estão as características aproximadas do código:
- 10 milhões de linhas de código C++,
- 14 mil arquivos,
- 60 mil classes,
- meia milhão de métodos.
E tudo isso precisava ser migrado para C++14. Sobre como fizemos isso e com o que encontramos no processo, é o que iremos compartilhar hoje.

Isenção de responsabilidade
Tudo o que está escrito abaixo sobre o desempenho lento/rápido e o (não) alto consumo de memória das implementações de classes padrão em várias bibliotecas é válido PARA NÓS. É possível que, para suas tarefas, as implementações padrão funcionem da melhor maneira. Nós nos baseamos nas nossas necessidades: pegamos dados típicos de nossos clientes, executamos cenários comuns, observamos o desempenho, o volume de memória consumido, etc., e analisamos se esses resultados nos satisfazem e aos nossos clientes ou não. E agimos de acordo.
O que tínhamos
Inicialmente, escrevemos o código da plataforma 1C:Enterprise 8 no Microsoft Visual Studio. O projeto começou no início dos anos 2000 e tinha apenas uma versão para Windows. É claro que, desde então, o código evoluiu ativamente, muitos mecanismos foram reescritos completamente. Mas o código era escrito segundo o padrão de 1998, e, por exemplo, os sinais de maior e menor estavam separados por espaços para que a compilação fosse bem-sucedida, assim:
vector<vector > IntV;Em 2006, com o lançamento da versão 8.1 da plataforma, começamos a oferecer suporte ao Linux e migramos para uma biblioteca padrão de terceiros . Uma das razões para a migração foi o trabalho com strings largas. Em nosso código, usamos amplamente std::wstring, baseado no tipo wchar_t. Seu tamanho no Windows é de 2 bytes, enquanto no Linux é de 4 bytes por padrão. Isso levava a incompatibilidades nos nossos protocolos binários entre cliente e servidor, bem como nos diversos dados persistentes. As opções do gcc permitem especificar que o tamanho de wchar_t durante a compilação deve ser também de 2 bytes, mas então não se pode contar com a biblioteca padrão do compilador, já que ela usa glibc, que por sua vez é compilada para wchar_t de 4 bytes. Outras razões incluíam uma implementação de classes padrão de melhor qualidade, suporte a tabelas hash e até mesmo a emulação da semântica de movimentação dentro de contêineres, da qual usamos ativamente. E outra razão, como se diz, last but not least, era o desempenho das strings. Tínhamos nossa própria classe para strings, pois, devido à especificidade do nosso software, as operações com strings são amplamente utilizadas e isso é crítico para nós.
Nossa string é baseada nas ideias de otimização de strings propostas no início dos anos 2000 . Mais tarde, quando Alexandrescu trabalhava no Facebook, a partir de sua sugestão, uma linha de código com princípios semelhantes foi utilizada no motor do Facebook (veja a biblioteca ).
Em nossa implementação, utilizamos duas tecnologias principais de otimização:
- Para valores curtos, é utilizado um buffer interno dentro do próprio objeto string (sem necessidade de alocação adicional de memória).
- Para todos os demais, é usada a mecânica . O valor da string é armazenado em um único local, e ao atribuir/modificar é utilizado um contador de referências.
Para acelerar a compilação da plataforma, excluímos a implementação de stream do nosso STLPort modificado (que não utilizávamos), o que resultou em um ganho de aproximadamente 20% na compilação. Posteriormente, tivemos que usar de forma limitada . O Boost utiliza ativamente streams, especialmente em suas APIs de serviços (por exemplo, para log), então tivemos que modificar isso, excluindo o uso de streams. Isso, por sua vez, dificultou nossa migração para novas versões do Boost.
A terceira abordagem
Ao migrar para o padrão C++14, consideramos as seguintes opções:
- Subir nosso STLPort modificado para o padrão C++14. Essa opção é bastante complexa, pois o suporte ao STLPort foi descontinuado em 2010, e teríamos que portar todo o seu código sozinhos.
- Migrar para outra implementação do STL compatível com C++14. É altamente desejável que essa implementação funcione em Windows e Linux.
- Usar a biblioteca embutida no compilador correspondente para cada sistema operacional durante a compilação.
A primeira opção foi descartada imediatamente devido ao volume excessivo de trabalho.
Pensamos por um tempo na segunda opção; consideramos como candidato , mas na época não funcionava em Windows. Para portar o libc++ para Windows, seria necessário realizar muito trabalho — por exemplo, escrever tudo relacionado a threads, sincronização de threads e atomicidade, uma vez que o libc++ utilizava .
E escolhemos a terceira abordagem.
Transição
Assim, precisávamos substituir o uso do STLPort pelas bibliotecas correspondentes dos compiladores (Visual Studio 2015 para Windows, gcc 7 para Linux, clang 8 para macOS).
Felizmente, nosso código foi escrito principalmente seguindo as diretrizes e não utilizou truques complicados, então a migração para as novas bibliotecas ocorreu de forma relativamente suave, com scripts que substituíram nos arquivos de origem os nomes de tipos, classes, namespaces e includes. A migração afetou 10.000 arquivos de origem (de 14.000). wchar_t foi substituído por char16_t; decidimos abandonar o uso de wchar_t, pois char16_t ocupa 2 bytes em todos os sistemas operacionais e não compromete a compatibilidade do código entre Windows e Linux.
Não faltaram pequenas aventuras. Por exemplo, no STLPort, o iterador poderia ser implicitamente convertido em um ponteiro para o elemento, e em algumas partes do nosso código isso era utilizado. Nas novas bibliotecas, isso já não era possível, e essas partes precisavam ser analisadas e reescritas manualmente.
Assim, a migração do código foi concluída, o código compila para todos os sistemas operacionais. É hora dos testes.
Os testes após a migração mostraram uma queda de desempenho (em alguns casos, de 20 a 30%) e um aumento no consumo de memória (de 10 a 15%) em comparação com a antiga versão do código. Isso estava, em particular, relacionado ao desempenho subótimo das strings padrão. Portanto, tivemos que usar novamente a nossa própria string, que foi levemente aprimorada.
Também foi revelada uma característica interessante da implementação de contêineres nas bibliotecas embutidas: contêineres std::map e std::set vazios (sem elementos) das bibliotecas embutidas alocam memória. E em nosso código, devido a características da implementação, criamos, em alguns lugares, uma boa quantidade de contêineres vazios desse tipo. Os contêineres padrão alocam um pouco de memória, para um único elemento raiz, mas para nós isso se mostrou crítico – em vários cenários, percebemos uma queda significativa no desempenho e um aumento no consumo de memória (em comparação com o STLPort). Portanto, substituímos esses dois tipos de contêineres das bibliotecas embutidas por suas implementações do Boost, onde esses contêineres não apresentavam essa característica, e isso resolveu o problema de desaceleração e aumento do consumo de memória.
Como é comum após mudanças em larga escala em grandes projetos, a primeira iteração dos fontes não funcionou sem problemas, e aqui nos foi muito útil, em particular, o suporte a iteradores de depuração na implementação do Windows. Passo a passo, avançamos, e na primavera de 2017 (versão 8.3.11 1C:Enterprise), a migração foi concluída.
Resultados
A transição para o padrão C++14 levou cerca de 6 meses. A maior parte do tempo, um único desenvolvedor (mas muito qualificado) trabalhou no projeto, e na fase final, membros das equipes responsáveis por áreas específicas — UI, cluster de servidores, ferramentas de desenvolvimento e administração, etc. — foram envolvidos.
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 programado para o início do próximo ano) já foi traduzida para o padrão .
Após a migração, os desenvolvedores tiveram mais oportunidades. Se antes tínhamos nossa própria versão modificada da STL e um namespace std, agora temos no namespace std as classes padrão das bibliotecas integradas do compilador, no namespace stdx — nossas strings e contêineres otimizados para nossas necessidades, e no boost — a versão mais recente do boost. O desenvolvedor utiliza as classes que melhor se adequam à sua tarefa.
A implementação 'nativa' dos construtores de movimentação também auxilia no desenvolvimento () para várias classes. Se uma classe possui um construtor de movimentação e essa classe é colocada em um contêiner, a 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).
Uma colher de veneno
O que talvez seja a consequência mais desagradável (mas não crítica) da migração — encontramos um aumento no volume , e o resultado completo da compilação com todos os arquivos intermediários passou a ocupar entre 60 e 70 GB. Esse comportamento está relacionado às peculiaridades das bibliotecas padrão modernas, que se tornaram menos rigorosas em relação ao volume de arquivos auxiliares gerados. Isso não afeta o funcionamento do aplicativo compilado, mas traz vários incômodos durante o desenvolvimento, especialmente aumentam o tempo de compilação. Também aumentam os requisitos de espaço livre no disco dos servidores de compilação e nas máquinas dos desenvolvedores. Nossos desenvolvedores trabalham paralelamente em várias versões da plataforma, e centenas de gigabytes de arquivos intermediários às vezes criam dificuldades no trabalho. O problema é desagradável, mas não crítico, e sua solução foi adiada por enquanto. Como uma das possíveis soluções, estamos considerando a técnica (isso é usado pela Google no desenvolvimento do navegador Chrome).
Fonte: habr.com
