Como traducimos 10 millóns de liñas de código C++ ao estándar C++14 (e despois a C++17)

Hai algún tempo (no outono de 2016), durante o desenvolvemento da próxima versión da plataforma tecnolóxica 1C:Enterprise, xurdiu a pregunta no equipo de desenvolvemento sobre a compatibilidade do novo estándar. C ++ 14 no noso código. A transición a un novo estándar, como supoñíamos, permitiríanos escribir moitas cousas de forma máis elegante, sinxela e fiable, e simplificaría o soporte e o mantemento do código. E parece que non hai nada extraordinario na tradución, se non pola escala do código base e as características específicas do noso código.

Para quen non o saiba, 1C:Enterprise é un ambiente para o desenvolvemento rápido de aplicacións empresariais multiplataforma e tempo de execución para a súa execución en diferentes sistemas operativos e DBMS. En termos xerais, o produto contén:

Tentamos escribir o mesmo código para diferentes sistemas operativos o máximo posible: a base de código do servidor é 99% común, a base de código do cliente é de aproximadamente o 95%. A plataforma tecnolóxica 1C:Enterprise está escrita principalmente en C++ e as características aproximadas do código móstranse a continuación:

  • 10 millóns de liñas de código C++,
  • 14 mil ficheiros,
  • 60 mil clases,
  • medio millón de métodos.

E todo este material tiña que ser traducido a C++14. Hoxe imos contarvos como fixemos isto e o que nos atopamos no proceso.

Como traducimos 10 millóns de liñas de código C++ ao estándar C++14 (e despois a C++17)

Exención de responsabilidade

Todo o escrito a continuación sobre o traballo lento/rápido, (non) gran consumo de memoria por implementacións de clases estándar en varias bibliotecas significa unha cousa: isto é certo PARA NÓS. É moi posible que as implementacións estándar sexan as máis adecuadas para as súas tarefas. Partimos das nosas propias tarefas: tomamos datos típicos dos nosos clientes, realizamos escenarios típicos neles, analizamos o rendemento, a cantidade de memoria consumida, etc., e analizamos se nós e os nosos clientes quedamos satisfeitos con tales resultados ou non. . E actuaron en función de.

O que tiñamos

Inicialmente, escribimos o código para a plataforma 1C:Enterprise 8 usando Microsoft Visual Studio. O proxecto comezou a principios dos anos 2000 e tiñamos unha versión só para Windows. Por suposto, desde entón o código foi desenvolvido activamente, moitos mecanismos foron completamente reescritos. Pero o código foi escrito segundo o estándar de 1998 e, por exemplo, os nosos corchetes en ángulo recto estaban separados por espazos para que a compilación tivese éxito, así:

vector<vector<int> > IntV;

En 2006, co lanzamento da versión 8.1 da plataforma, comezamos a admitir Linux e cambiamos a unha biblioteca estándar de terceiros. STLPort. Un dos motivos da transición foi traballar con liñas anchas. No noso código, usamos std::wstring, que se basea no tipo wchar_t. O seu tamaño en Windows é de 2 bytes, e en Linux o predeterminado é de 4 bytes. Isto provocou a incompatibilidade dos nosos protocolos binarios entre o cliente e o servidor, así como varios datos persistentes. Usando as opcións gcc, pode especificar que o tamaño de wchar_t durante a compilación tamén é de 2 bytes, pero entón pode esquecerse de usar a biblioteca estándar do compilador, porque usa glibc, que á súa vez se compila para un wchar_t de 4 bytes. Outros motivos foron unha mellor implementación de clases estándar, soporte para táboas hash e mesmo a emulación da semántica de moverse dentro dos contedores, que utilizamos activamente. E un motivo máis, como din por último, pero non menos importante, foi a interpretación de cordas. Tivemos a nosa propia clase de cordas, porque... Debido ás características específicas do noso software, as operacións de cadeas úsanse moi amplamente e para nós isto é fundamental.

A nosa cadea baséase en ideas de optimización de cadeas expresadas a principios dos anos 2000 Andrei Alexandrescu. Máis tarde, cando Alexandrescu traballou en Facebook, por suxestión súa, utilizouse unha liña no motor de Facebook que funcionaba con principios similares (ver a biblioteca insensatez).

A nosa liña utilizou dúas tecnoloxías de optimización principais:

  1. Para valores curtos, utilízase un búfer interno no propio obxecto de cadea (non esixe asignación de memoria adicional).
  2. Para todos os demais, utilízase a mecánica Copiar en escritura. O valor da cadea gárdase nun só lugar e úsase un contador de referencia durante a asignación/modificación.

Para acelerar a compilación da plataforma, excluímos a implementación do fluxo da nosa variante STLPort (que non usamos), isto deunos un 20% máis rápido de compilación. Posteriormente tivemos que facer un uso limitado Impulsar. Boost fai un gran uso do fluxo, especialmente nas súas API de servizo (por exemplo, para o rexistro), polo que tivemos que modificalo para eliminar o uso do fluxo. Isto, á súa vez, dificultou a migración a novas versións de Boost.

Terceiro camiño

Ao pasar ao estándar C++14, consideramos as seguintes opcións:

  1. Actualiza o STLPort que modificamos ao estándar C++14. A opción é moi difícil, porque... O soporte para STLPort descontinuouse en 2010 e teriamos que construír todo o seu código nós mesmos.
  2. Transición a outra implementación STL compatible con C++14. É moi desexable que esta implementación sexa para Windows e Linux.
  3. Ao compilar para cada sistema operativo, use a biblioteca integrada no compilador correspondente.

A primeira opción foi rexeitada por mor de moito traballo.

Pensamos na segunda opción durante algún tempo; considerado como candidato libc++, pero naquel momento non funcionaba en Windows. Para portar libc++ a Windows, terías que traballar moito; por exemplo, escribir todo o que teña que ver con fíos, sincronización de fíos e atomicidade, xa que libc++ se usa nestas áreas. API POSIX.

E escollemos a terceira vía.

Transición

Entón, tivemos que substituír o uso de STLPort polas bibliotecas dos compiladores correspondentes (Visual Studio 2015 para Windows, gcc 7 para Linux, clang 8 para macOS).

Afortunadamente, o noso código foi escrito principalmente de acordo coas directrices e non utilizou todo tipo de trucos intelixentes, polo que a migración a novas bibliotecas foi relativamente suave, coa axuda de scripts que substituíron os nomes de tipos, clases, espazos de nomes e inclúe na fonte. arquivos. A migración afectou a 10 ficheiros fonte (de 000). wchar_t foi substituído por char14_t; decidimos abandonar o uso de wchar_t, porque char000_t leva 16 bytes en todos os SO e non estraga a compatibilidade do código entre Windows e Linux.

Houbo algunhas pequenas aventuras. Por exemplo, en STLPort un iterador podería ser enviado implícitamente a un punteiro a un elemento, e nalgúns lugares do noso código utilizouse isto. Nas novas bibliotecas xa non era posible facelo, e estas pasaxes debían ser analizadas e reescritas manualmente.

Entón, a migración do código está completa, o código está compilado para todos os sistemas operativos. Chegou a hora das probas.

As probas posteriores á transición mostraron unha caída no rendemento (nalgúns lugares ata un 20-30%) e un aumento no consumo de memoria (ata un 10-15%) en comparación coa versión antiga do código. Isto foi, en particular, debido ao rendemento subóptimo das cordas estándar. Polo tanto, de novo tivemos que usar a nosa propia liña lixeiramente modificada.

Tamén se revelou unha característica interesante da implementación de contedores en bibliotecas incorporadas: baleiro (sen elementos) std::map e std::set desde bibliotecas integradas asignar memoria. E debido ás características de implementación, nalgúns lugares do código créanse moitos recipientes baleiros deste tipo. Os contedores de memoria estándar están asignados un pouco para un elemento raíz, pero para nós isto resultou ser crítico: en varios escenarios, o noso rendemento baixou significativamente e o consumo de memoria aumentou (en comparación con STLPort). Polo tanto, no noso código substituímos estes dous tipos de contedores das bibliotecas integradas pola súa implementación desde Boost, onde estes contenedores non tiñan esta función, e isto resolveu o problema de ralentización e aumento do consumo de memoria.

Como adoita ocorrer despois de cambios a gran escala en grandes proxectos, a primeira iteración do código fonte non funcionou sen problemas, e aquí, en particular, foi útil o soporte para depurar iteradores na implementación de Windows. Paso a paso avanzamos e na primavera de 2017 (versión 8.3.11 1C:Enterprise) a migración completouse.

Resultados de

A transición ao estándar C++14 levounos uns 6 meses. Na maioría das veces, un desenvolvedor (pero moi altamente cualificado) traballou no proxecto, e na fase final uníronse representantes de equipos responsables de áreas específicas: interface de usuario, clúster de servidores, ferramentas de desenvolvemento e administración, etc.

A transición simplificou moito o noso traballo na migración ás últimas versións do estándar. Así, a versión 1C:Enterprise 8.3.14 (en desenvolvemento, lanzamento previsto para principios do próximo ano) xa foi transferida ao estándar C++17.

Despois da migración, os desenvolvedores teñen máis opcións. Se antes tiñamos a nosa propia versión modificada de STL e un espazo de nomes std, agora temos clases estándar das bibliotecas do compilador incorporados no espazo de nomes std, no espazo de nomes stdx -as nosas liñas e contedores optimizados para as nosas tarefas, en boost- o última versión de boost. E o programador usa aquelas clases que son adecuadas para resolver os seus problemas.

A implementación "nativa" dos construtores de movementos tamén axuda no desenvolvemento (mover os construtores) para varias clases. Se unha clase ten un constructor de movemento e esta clase se coloca nun contenedor, entón o STL optimiza a copia de elementos dentro do contenedor (por exemplo, cando o contenedor se expande e é necesario cambiar a capacidade e reasignar memoria).

Voar na pomada

Quizais a consecuencia máis desagradable (pero non crítica) da migración é que estamos ante un aumento do volume ficheiros obj, e o resultado completo da compilación con todos os ficheiros intermedios comezou a ocupar entre 60 e 70 GB. Este comportamento débese ás peculiaridades das bibliotecas estándar modernas, que se fixeron menos críticas co tamaño dos ficheiros de servizo xerados. Isto non afecta o funcionamento da aplicación compilada, pero si causa unha serie de inconvenientes no desenvolvemento, en particular, aumenta o tempo de compilación. Tamén están aumentando os requisitos de espazo libre en disco nos servidores de compilación e nas máquinas de desenvolvemento. Os nosos desenvolvedores traballan en varias versións da plataforma en paralelo, e centos de gigabytes de ficheiros intermedios ás veces crean dificultades no seu traballo. O problema é desagradable, pero non crítico; por agora aprazamos a súa solución. Estamos considerando a tecnoloxía como unha das opcións para solucionalo construción de unidade (en particular, Google úsao cando desenvolve o navegador Chrome).

Fonte: www.habr.com

Engadir un comentario