Hace un tiempo (en otoño de 2016), durante el desarrollo de una nueva versión de la plataforma tecnológica 1C:Enterprise, surgió dentro del equipo de desarrollo la cuestión del soporte para el nuevo estándar en nuestro código. La transición al nuevo estándar, como suponíamos, nos permitiría escribir muchas cosas de manera más elegante, sencilla y confiable, facilitando el soporte y mantenimiento del código. Y en la traducción no parece haber nada extraordinario, si no fuera por la magnitud de la base de código y las características específicas de nuestro código.
Para aquellos que no lo sepan, 1C:Enterprise es un entorno para el desarrollo rápido de aplicaciones empresariales multiplataforma y un runtime para su ejecución en diferentes sistemas operativos y bases de datos. En términos generales, el producto incluye:
- , que funciona en Windows y Linux
- , que se comunica con el servidor a través de http(s) o mediante su propio protocolo binario, funcionando en Windows, Linux, macOS
- , que opera en navegadores como Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (escrito en JavaScript)
- Un entorno de desarrollo (), que opera en Windows, Linux, macOS
- de servidores de aplicaciones, que funcionan en Windows, Linux, macOS
- , que se conecta al servidor a través de http(s), operativo en dispositivos móviles que funcionan con Android, iOS, Windows
- — un marco para crear aplicaciones móviles offline con capacidad de sincronización, que funcionan en Android, iOS, Windows
- Entorno de desarrollo , escrita en Java
- Servidor
Nos esforzamos por escribir un solo código para diferentes sistemas operativos: la base de código del servidor es común en un 99%, la del cliente — aproximadamente un 95%. La plataforma tecnológica 1C:Enterprise está predominantemente escrita en C++ y a continuación se presentan características aproximadas del código:
- 10 millones de líneas de código C++,
- 14 mil archivos,
- 60 mil clases,
- medio millón de métodos.
Y todo este conjunto debía ser migrado a C++14. Sobre cómo lo hicimos y con qué nos encontramos en el proceso, hoy les contaremos.

Descargo de responsabilidad
Todo lo que se ha escrito a continuación sobre el funcionamiento lento/rápido, el bajo consumo de memoria de las implementaciones de las clases estándar en diversas bibliotecas significa una cosa: esto es válido PARA NOSOTROS. Es posible que las implementaciones estándar se adapten perfectamente a sus necesidades. Nosotros, en cambio, partimos de nuestras propias tareas: tomamos datos típicos de nuestros clientes, los sometimos a escenarios típicos, observamos el rendimiento, el volumen de memoria consumida, etc. y analizamos si estos resultados son satisfactorios para nosotros y nuestros clientes o no. Y actuamos en consecuencia.
Lo que teníamos
Inicialmente, escribíamos el código de la plataforma 1C:Enterprise 8 en Microsoft Visual Studio. El proyecto comenzó a principios de los años 2000 y solo teníamos una versión para Windows. Por supuesto, desde entonces el código ha evolucionado significativamente y muchos mecanismos han sido reescritos por completo. Sin embargo, el código se escribió según el estándar de 1998, y, por ejemplo, los signos de mayor estaban separados por espacios para que la compilación fuera exitosa, así:
vector<vector > IntV;En 2006, con el lanzamiento de la versión 8.1 de la plataforma, comenzamos a soportar Linux y transicionamos a una biblioteca estándar de terceros . Una de las razones para la transición fue trabajar con cadenas anchas. En nuestro código utilizamos universalmente std::wstring, basado en el tipo wchar_t. Su tamaño en Windows es de 2 bytes, mientras que en Linux, por defecto, es de 4 bytes. Esto llevó a incompatibilidades en nuestros protocolos binarios entre cliente y servidor, así como con varios datos persistentes. Se pueden especificar opciones de gcc para que el tamaño de wchar_t al compilar sea también de 2 bytes, pero entonces no se puede pensar en usar la biblioteca estándar del compilador, ya que utiliza glibc, que a su vez está compilada para wchar_t de 4 bytes. Otras razones incluían una implementación de las clases estándar de mayor calidad, soporte para tablas hash e incluso emulación de semántica de movimiento dentro de los contenedores, que utilizábamos ampliamente. Y, como se dice, la última pero no menos importante, fue el rendimiento de las cadenas. Teníamos nuestra propia clase para cadenas, ya que debido a la especificidad de nuestro software, las operaciones con cadenas se utilizan de manera muy amplia y esto es crítico para nosotros.
Nuestra cadena se basa en ideas de optimización de cadenas expresadas a principios de la década de 2000 . Más tarde, cuando Alexandrescu trabajaba en Facebook, se implementó en el motor de Facebook una cadena que funcionaba bajo principios similares (ver la biblioteca ).
En nuestra cadena se utilizaron dos tecnologías principales de optimización:
- Para valores cortos, se utiliza un búfer interno en el propio objeto de la cadena (que no requiere asignación de memoria adicional).
- Para todos los demás se utiliza la mecánica . El valor de la cadena se almacena en un solo lugar; al asignarse/modificarse, se usa un contador de referencias.
Para acelerar la compilación de la plataforma, excluimos de nuestra variante de STLPort la implementación de stream (que no utilizamos), lo que nos dio una aceleración de compilación de aproximadamente el 20%. Posteriormente, tuvimos que utilizar. . Boost utiliza intensivamente stream, en particular, en sus API de servicio (por ejemplo, para el registro), por lo que tuvimos que modificarlo, excluyendo su uso de stream. Esto, a su vez, complicaba nuestra transición a nuevas versiones de Boost.
El tercer camino
Al migrar al estándar C++14, consideramos las siguientes opciones:
- Elevar nuestra versión modificada de STLPort al estándar C++14. Esta opción es muy difícil, ya que el soporte para STLPort se detuvo en 2010, y tendríamos que elevar todo su código nosotros mismos.
- Migrar a otra implementación de STL compatible con C++14. Es extremadamente deseable que esta implementación esté disponible para Windows y Linux.
- Utilizar la biblioteca integrada en el compilador correspondiente para la compilación en cada SO.
La primera opción fue descartada de inmediato por el alto volumen de trabajo que implicaba.
Pensamos un tiempo sobre la segunda opción; estábamos considerando , pero en ese momento no funcionaba en Windows. Para portar libc++ a Windows, sería necesario hacer mucho trabajo, como escribir todo lo relacionado con hilos, sincronización de hilos y atomicidad, ya que en libc++ se utilizaban .
Y elegimos el tercer camino.
Transición
Así que teníamos que reemplazar el uso de STLPort por las bibliotecas de los compiladores correspondientes (Visual Studio 2015 para Windows, gcc 7 para Linux, clang 8 para macOS).
Afortunadamente, nuestro código se escribió principalmente según las pautas y no usó trucos complicados, por lo que la migración a las nuevas bibliotecas se llevó a cabo de manera relativamente suave, con la ayuda de scripts que reemplazaban en los archivos originales los nombres de tipos, clases, espacios de nombres e inclusiones. La migración afectó a 10,000 archivos fuente (de 14,000). wchar_t fue reemplazado por char16_t; decidimos abandonar el uso de wchar_t, ya que char16_t ocupa 2 bytes en todos los sistemas operativos y no interfiere con la compatibilidad del código entre Windows y Linux.
No estuvo exenta de pequeñas aventuras. Por ejemplo, en STLPort se podía castear implícitamente un iterador a un puntero a un elemento, y en algunos lugares de nuestro código se utilizaba. En las nuevas bibliotecas esto ya no era posible, y tuvimos que analizar y reescribir manualmente esos lugares.
Así que la migración del código ha terminado, el código se compila para todos los sistemas operativos. Es hora de las pruebas.
Las pruebas posteriores a la transición mostraron una disminución en el rendimiento (en algunos casos hasta 20-30%) y un aumento en el consumo de memoria (hasta 10-15%) en comparación con la versión anterior del código. Esto estaba, en particular, relacionado con el funcionamiento subóptimo de las cadenas estándar. Por lo tanto, una vez más tuvimos que utilizar nuestra propia cadena, ligeramente modificada.
También se reveló una característica interesante de la implementación de contenedores en las bibliotecas embebidas: los std::map y std::set vacíos (sin elementos) de las bibliotecas embebidas asignan memoria. Y en nuestro caso, debido a las características de la implementación, se crean bastantes contenedores vacíos de este tipo en algunos lugares del código. Los contenedores estándar asignan un poco de memoria, para un elemento raíz, pero para nosotros esto resultó crítico: en varios escenarios experimentamos una notable disminución del rendimiento y un aumento en el consumo de memoria (en comparación con STLPort). Por lo tanto, reemplazamos en nuestro código estos dos tipos de contenedores de las bibliotecas embebidas por su implementación de Boost, donde estos contenedores no tenían tal característica, y esto resolvió el problema de la desaceleración y el aumento del consumo de memoria.
Como suele suceder después de cambios a gran escala en grandes proyectos, la primera iteración de los fuentes no funcionó sin problemas, y aquí nos fue muy útil, en particular, el soporte de iteradores de depuración en la implementación de Windows. Paso a paso avanzamos, y para la primavera de 2017 (versión 8.3.11 de 1C:Enterprise) la migración fue completada.
Resultados
La transición al estándar C++14 nos tomó alrededor de 6 meses. La mayor parte del tiempo, un solo desarrollador (muy cualificado) trabajó en el proyecto, y en la fase final se unieron representantes de equipos responsables de áreas específicas: UI, clúster de servidores, herramientas de desarrollo y administración, etc.
La transición facilitó mucho nuestro trabajo de migración a las versiones más recientes del estándar. Así, la versión 1C:Empresa 8.3.14 (en desarrollo, con lanzamiento programado para principios del próximo año) ya ha sido traducida al estándar .
Tras la migración, los desarrolladores tienen más oportunidades. Si antes teníamos nuestra propia versión mejorada de STL y un solo namespace std, ahora en el namespace std se encuentran las clases estándar de las bibliotecas incorporadas del compilador, en el namespace stdx – nuestras cadenas y contenedores optimizados para nuestras tareas, y en boost – la versión más reciente de boost. Y el desarrollador utiliza las clases que se ajustan de manera óptima a la solución de sus problemas.
También ayuda en el desarrollo la implementación 'nativa' de los constructores de movimiento () para una serie de clases. Si una clase tiene un constructor de movimiento y esta clase se coloca en un contenedor, STL optimiza la copia de elementos dentro del contenedor (por ejemplo, cuando el contenedor se expande y es necesario cambiar la capacidad y realocar la memoria).
Una cucharada de alquitrán
Quizás el resultado más desagradable (pero no crítico) de la migración es que nos enfrentamos a un aumento en el tamaño de , y el resultado completo de la compilación con todos los archivos intermedios ahora ocupa entre 60 y 70 GB. Este comportamiento está relacionado con las características de las bibliotecas estándar modernas, que han comenzado a preocuparse menos por el volumen de archivos de servicio generados. Esto no afecta al funcionamiento de la aplicación compilada, pero presenta una serie de inconvenientes en el desarrollo, especialmente aumenta el tiempo de compilación. También aumentan los requisitos de espacio libre en el disco en los servidores de compilación y en las máquinas de los desarrolladores. Nuestros desarrolladores trabajan paralelamente en varias versiones de la plataforma, y cientos de gigabytes de archivos intermedios a veces crean dificultades en el trabajo. El problema es desagradable, pero no crítico, y por el momento hemos pospuesto su solución. Como una de las opciones para resolverlo, estamos considerando la técnica (esto, en particular, lo utiliza Google al desarrollar el navegador Chrome).
Fuente: habr.com
