Cómo tradujimos 10 millones de líneas de código C++ al estándar C++14 (y luego a C++17)

Hace algún tiempo (en el otoño de 2016), durante el desarrollo de la próxima versión de la plataforma tecnológica 1C:Enterprise, surgió la pregunta dentro del equipo de desarrollo sobre el soporte del nuevo estándar. C ++ 14 en nuestro código. La transición a un nuevo estándar, como asumimos, nos permitiría escribir muchas cosas de manera más elegante, simple y confiable, y simplificaría el soporte y mantenimiento del código. Y no parece haber nada extraordinario en la traducción, si no fuera por la escala de la base del código y las características específicas de nuestro código.

Para aquellos que no lo saben, 1C:Enterprise es un entorno para el rápido desarrollo de aplicaciones empresariales multiplataforma y tiempo de ejecución para su ejecución en diferentes sistemas operativos y DBMS. En términos generales el producto contiene:

Intentamos escribir el mismo código para diferentes sistemas operativos tanto como sea posible: la base del código del servidor es 99% común, la base del código del cliente es aproximadamente el 95%. La plataforma tecnológica 1C:Enterprise está escrita principalmente en C++ y las características aproximadas del código se detallan a continuación:

  • 10 millones de líneas de código C++,
  • 14 mil archivos,
  • 60 mil clases,
  • Medio millón de métodos.

Y todo esto tuvo que traducirse a C++14. Hoy te contamos cómo hicimos esto y qué encontramos en el proceso.

Cómo tradujimos 10 millones de líneas de código C++ al estándar C++14 (y luego a C++17)

Descargo de responsabilidad

Todo lo escrito a continuación sobre el trabajo lento/rápido y (no) el gran consumo de memoria por parte de las implementaciones de clases estándar en varias bibliotecas significa una cosa: esto es cierto PARA NOSOTROS. Es muy posible que las implementaciones estándar sean las más adecuadas para sus tareas. Partimos de nuestras propias tareas: tomamos datos típicos de nuestros clientes, ejecutamos escenarios típicos en ellos, analizamos el rendimiento, la cantidad de memoria consumida, etc., y analizamos si nosotros y nuestros clientes estábamos satisfechos con dichos resultados o no. . Y actuaron dependiendo de.

Lo que teníamos

Inicialmente, escribimos el código para la plataforma 1C:Enterprise 8 usando Microsoft Visual Studio. El proyecto comenzó a principios de la década de 2000 y teníamos una versión solo para Windows. Naturalmente, desde entonces el código se ha desarrollado activamente y muchos mecanismos se han reescrito por completo. Pero el código fue escrito de acuerdo con el estándar de 1998 y, por ejemplo, nuestros corchetes angulares estaban separados por espacios para que la compilación fuera exitosa, así:

vector<vector<int> > IntV;

En 2006, con el lanzamiento de la versión 8.1 de la plataforma, comenzamos a admitir Linux y cambiamos a una biblioteca estándar de terceros. Puerto STL. Una de las razones de la transición fue trabajar con líneas amplias. En nuestro código, utilizamos std::wstring, que se basa en el tipo wchar_t. Su tamaño en Windows es de 2 bytes y en Linux el predeterminado es de 4 bytes. Esto provocó incompatibilidad de nuestros protocolos binarios entre el cliente y el servidor, así como varios datos persistentes. Usando las opciones de gcc, puede especificar que el tamaño de wchar_t durante la compilación también sea de 2 bytes, pero luego puede olvidarse de usar la biblioteca estándar del compilador, porque utiliza glibc, que a su vez se compila para un wchar_t de 4 bytes. Otras razones fueron una mejor implementación de clases estándar, soporte para tablas hash e incluso la emulación de la semántica de movimiento dentro de contenedores, que utilizamos activamente. Y una razón más, como dicen por último pero no menos importante, fue el rendimiento de las cuerdas. Teníamos nuestra propia clase de cuerdas, porque... Debido a las características específicas de nuestro software, las operaciones con cadenas se utilizan ampliamente y para nosotros esto es fundamental.

Nuestra cadena se basa en ideas de optimización de cadenas expresadas a principios de la década de 2000. Andrei Alexandrescu. Más tarde, cuando Alexandrescu trabajó en Facebook, por sugerencia suya, se utilizó una línea en el motor de Facebook que funcionaba con principios similares (ver biblioteca locura).

Nuestra línea utilizó dos tecnologías de optimización principales:

  1. Para valores cortos, se utiliza un búfer interno en el propio objeto de cadena (no requiere asignación de memoria adicional).
  2. Para todos los demás, se utiliza la mecánica. Copiar en escrito. El valor de la cadena se almacena en un lugar y se utiliza un contador de referencia durante la asignación/modificación.

Para acelerar la compilación de la plataforma, excluimos la implementación de flujo de nuestra variante STLPort (que no usamos), esto nos dio una compilación aproximadamente un 20% más rápida. Posteriormente tuvimos que hacer un uso limitado Boost. Boost hace un uso intensivo de Stream, particularmente en sus API de servicio (por ejemplo, para el registro), por lo que tuvimos que modificarlo para eliminar el uso de Stream. Esto, a su vez, nos dificultó la migración a nuevas versiones de Boost.

La tercera forma

Al pasar al estándar C++14, consideramos las siguientes opciones:

  1. Actualice el STLPort que modificamos al estándar C++14. La opción es muy difícil, porque... El soporte para STLPort se suspendió en 2010 y tendríamos que crear todo su código nosotros mismos.
  2. Transición a otra implementación STL compatible con C++14. Es muy deseable que esta implementación sea para Windows y Linux.
  3. Al compilar para cada sistema operativo, utilice la biblioteca integrada en el compilador correspondiente.

La primera opción fue rechazada de plano por demasiado trabajo.

Pensamos durante algún tiempo en la segunda opción; considerado como candidato libc ++, pero en ese momento no funcionaba en Windows. Para portar libc++ a Windows, tendría que trabajar mucho; por ejemplo, escribir usted mismo todo lo que tiene que ver con subprocesos, sincronización de subprocesos y atomicidad, ya que libc++ se usa en estas áreas. API POSIX.

Y elegimos la tercera vía.

Переход

Entonces, tuvimos que reemplazar el uso de STLPort con las bibliotecas de los compiladores correspondientes (Visual Studio 2015 para Windows, gcc 7 para Linux, clang 8 para macOS).

Afortunadamente, nuestro código fue escrito principalmente de acuerdo con pautas y no utilizó todo tipo de trucos ingeniosos, por lo que la migración a nuevas bibliotecas se desarrolló relativamente sin problemas, con la ayuda de scripts que reemplazaron los nombres de tipos, clases, espacios de nombres e inclusiones en el código fuente. archivos. La migración afectó a 10 archivos fuente (de 000). wchar_t fue reemplazado por char14_t; decidimos abandonar el uso de wchar_t, porque char000_t ocupa 16 bytes en todos los sistemas operativos y no arruina la compatibilidad del código entre Windows y Linux.

Hubo algunas pequeñas aventuras. Por ejemplo, en STLPort un iterador podría convertirse implícitamente en un puntero a un elemento, y en algunos lugares de nuestro código se usó esto. En las bibliotecas nuevas ya no era posible hacer esto y estos pasajes debían analizarse y reescribirse manualmente.

Entonces, la migración del código está completa, el código se compila para todos los sistemas operativos. Es hora de hacer pruebas.

Las pruebas posteriores a la transición mostraron una caída en el rendimiento (en algunos lugares hasta un 20-30%) y un aumento en el consumo de memoria (hasta un 10-15%) en comparación con la versión anterior del código. Esto se debió, en particular, al rendimiento subóptimo de las cuerdas estándar. Por lo tanto, nuevamente tuvimos que utilizar nuestra propia línea, ligeramente modificada.

También se reveló una característica interesante de la implementación de contenedores en bibliotecas integradas: los std::map y std::set vacíos (sin elementos) de las bibliotecas integradas asignan memoria. Y debido a las características de implementación, en algunos lugares del código se crean bastantes contenedores vacíos de este tipo. Los contenedores de memoria estándar se asignan poco, para un elemento raíz, pero para nosotros esto resultó ser crítico: en varios escenarios, nuestro rendimiento disminuyó significativamente y el consumo de memoria aumentó (en comparación con STLPort). Por lo tanto, en nuestro código reemplazamos estos dos tipos de contenedores de las bibliotecas integradas con su implementación de Boost, donde estos contenedores no tenían esta característica, y esto resolvió el problema de desaceleración y mayor consumo de memoria.

Como suele suceder después de cambios a gran escala en grandes proyectos, la primera iteración del código fuente no funcionó sin problemas, y aquí, en particular, la compatibilidad con la depuración de iteradores en la implementación de Windows resultó útil. Avanzamos paso a paso y en la primavera de 2017 (versión 8.3.11 1C:Enterprise) se completó la migración.

resultados

La transición al estándar C++14 nos llevó unos 6 meses. La mayor parte del tiempo, un desarrollador (pero muy altamente calificado) trabajó en el proyecto, y en la etapa final se unieron representantes de los equipos responsables de áreas específicas: interfaz de usuario, clúster de servidores, herramientas de desarrollo y administración, etc.

La transición simplificó enormemente nuestro trabajo de migración a las últimas versiones del estándar. Así, la versión 1C:Enterprise 8.3.14 (en desarrollo, lanzamiento previsto para principios del próximo año) ya ha sido transferida al estándar. C++17.

Después de la migración, los desarrolladores tienen más opciones. Si antes teníamos nuestra propia versión modificada de STL y un espacio de nombres estándar, ahora tenemos clases estándar de las bibliotecas del compilador integradas en el espacio de nombres estándar, en el espacio de nombres stdx - nuestras líneas y contenedores optimizados para nuestras tareas, en boost - el última versión de impulso. Y el desarrollador utiliza aquellas clases que son óptimas para resolver sus problemas.

La implementación "nativa" de constructores de movimientos también ayuda en el desarrollo (mover constructores) para varias clases. Si una clase tiene un constructor de movimiento y esta clase se coloca en un contenedor, entonces STL optimiza la copia de elementos dentro del contenedor (por ejemplo, cuando el contenedor se expande y es necesario cambiar la capacidad y reasignar memoria).

Mosca en la sopa

Quizás la consecuencia más desagradable (pero no crítica) de la migración es que nos enfrentamos a un aumento en el volumen. archivos obj, y el resultado completo de la compilación con todos los archivos intermedios comenzó a ocupar entre 60 y 70 GB. Este comportamiento se debe a las peculiaridades de las bibliotecas estándar modernas, que se han vuelto menos críticas con el tamaño de los archivos de servicio generados. Esto no afecta el funcionamiento de la aplicación compilada, pero sí provoca una serie de inconvenientes durante el desarrollo, en particular, aumenta el tiempo de compilación. Los requisitos de espacio libre en disco en los servidores de compilación y en las máquinas de desarrollo también están aumentando. Nuestros desarrolladores trabajan en varias versiones de la plataforma en paralelo y cientos de gigabytes de archivos intermedios a veces crean dificultades en su trabajo. El problema es desagradable, pero no crítico; por ahora hemos pospuesto su solución. Consideramos la tecnología como una de las opciones para solucionarlo unidad de construcción (En particular, Google lo utiliza al desarrollar el navegador Chrome).

Fuente: habr.com

Añadir un comentario