Bagaimana kami menerjemahkan 10 juta baris kode C++ ke standar C++14 (lalu ke C++17)

Beberapa waktu lalu (pada musim gugur 2016), selama pengembangan versi berikutnya dari platform teknologi 1C:Enterprise, muncul pertanyaan dalam tim pengembangan tentang dukungan standar baru. C ++ 14 dalam kode kita. Transisi ke standar baru, seperti yang kami asumsikan, akan memungkinkan kami menulis banyak hal dengan lebih elegan, sederhana, dan andal, serta menyederhanakan dukungan dan pemeliharaan kode. Dan sepertinya tidak ada yang luar biasa dalam terjemahannya, jika bukan karena skala basis kode dan fitur spesifik kode kita.

Bagi yang belum tahu, 1C:Enterprise adalah lingkungan untuk perkembangan pesat aplikasi bisnis lintas platform dan runtime untuk eksekusinya pada OS dan DBMS yang berbeda. Secara umum, produk tersebut mengandung:

  • Kluster Server Aplikasi, berjalan di Windows dan Linux
  • Klien, bekerja dengan server melalui http(s) atau protokol binernya sendiri, berfungsi di Windows, Linux, macOS
  • klien web, berjalan di browser Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (ditulis dalam JavaScript)
  • Pengembangan lingkungan (Pengonfigurasi), berfungsi di Windows, Linux, macOS
  • Alat administrasi server aplikasi, berjalan di Windows, Linux, macOS
  • Klien seluler, menghubungkan ke server melalui http(s), berfungsi pada perangkat seluler yang menjalankan Android, iOS, Windows
  • Platform seluler β€” kerangka kerja untuk membuat aplikasi seluler offline dengan kemampuan sinkronisasi, berjalan di Android, iOS, Windows
  • Lingkungan pengembangan 1C:Alat Pengembangan Perusahaan, ditulis dalam bahasa Jawa
  • Server Sistem Interaksi

Kami mencoba menulis kode yang sama untuk sistem operasi yang berbeda sebanyak mungkin - basis kode server 99% umum, basis kode klien sekitar 95%. Platform teknologi 1C:Enterprise sebagian besar ditulis dalam C++ dan perkiraan karakteristik kode diberikan di bawah ini:

  • 10 juta baris kode C++,
  • 14 ribu file,
  • 60 ribu kelas,
  • setengah juta metode.

Dan semua hal ini harus diterjemahkan ke dalam C++14. Hari ini kami akan memberi tahu Anda bagaimana kami melakukan ini dan apa yang kami temui dalam prosesnya.

Bagaimana kami menerjemahkan 10 juta baris kode C++ ke standar C++14 (lalu ke C++17)

Π΅ΠΉΠΌΠ΅Ρ€

Segala sesuatu yang ditulis di bawah ini tentang kerja lambat/cepat, (bukan) konsumsi memori yang besar oleh implementasi kelas standar di berbagai perpustakaan berarti satu hal: ini berlaku UNTUK KITA. Sangat mungkin bahwa penerapan standar akan paling sesuai untuk tugas Anda. Kami memulai dari tugas kami sendiri: kami mengambil data yang khas untuk klien kami, menjalankan skenario umum pada data tersebut, melihat kinerja, jumlah memori yang dikonsumsi, dll., dan menganalisis apakah kami dan klien kami puas dengan hasil tersebut atau tidak. . Dan mereka bertindak tergantung pada.

Apa yang kami punya

Awalnya, kami menulis kode untuk platform 1C:Enterprise 8 di Microsoft Visual Studio. Proyek ini dimulai pada awal tahun 2000an dan kami memiliki versi khusus Windows. Tentu saja, sejak itu kode tersebut dikembangkan secara aktif, banyak mekanisme telah ditulis ulang sepenuhnya. Namun kodenya ditulis sesuai standar tahun 1998, dan misalnya tanda kurung siku kanan kita dipisahkan dengan spasi agar kompilasi berhasil, seperti ini:

vector<vector<int> > IntV;

Pada tahun 2006, dengan dirilisnya platform versi 8.1, kami mulai mendukung Linux dan beralih ke perpustakaan standar pihak ketiga STLPort. Salah satu alasan transisi adalah bekerja dengan jalur lebar. Dalam kode kami, kami menggunakan std::wstring, yang didasarkan pada tipe wchar_t, secara keseluruhan. Ukurannya di Windows adalah 2 byte, dan di Linux defaultnya adalah 4 byte. Hal ini menyebabkan ketidakcocokan protokol biner kami antara klien dan server, serta berbagai data persisten. Dengan menggunakan opsi gcc, Anda dapat menentukan bahwa ukuran wchar_t selama kompilasi juga 2 byte, tetapi kemudian Anda dapat melupakan penggunaan perpustakaan standar dari kompiler, karena ia menggunakan glibc, yang kemudian dikompilasi untuk wchar_t 4-byte. Alasan lainnya adalah penerapan kelas standar yang lebih baik, dukungan untuk tabel hash, dan bahkan emulasi semantik pergerakan di dalam container, yang kami gunakan secara aktif. Dan satu alasan lagi, seperti yang mereka katakan terakhir, adalah performa string. Kami memiliki kelas string sendiri, karena... Karena spesifikasi perangkat lunak kami, operasi string digunakan secara luas dan bagi kami ini sangat penting.

String kami didasarkan pada ide pengoptimalan string yang diungkapkan pada awal tahun 2000an Andrey Alexandrescu. Kemudian, ketika Alexandrescu bekerja di Facebook, atas sarannya, sebuah baris digunakan di mesin Facebook yang bekerja dengan prinsip serupa (lihat perpustakaan kebodohan).

Jalur kami menggunakan dua teknologi pengoptimalan utama:

  1. Untuk nilai pendek, buffer internal dalam objek string itu sendiri digunakan (tidak memerlukan alokasi memori tambahan).
  2. Untuk yang lainnya, mekanik digunakan Salin Saat Menulis. Nilai string disimpan di satu tempat, dan penghitung referensi digunakan selama penugasan/modifikasi.

Untuk mempercepat kompilasi platform, kami mengecualikan implementasi streaming dari varian STLPort kami (yang tidak kami gunakan), ini memberi kami kompilasi sekitar 20% lebih cepat. Selanjutnya kami harus memanfaatkannya secara terbatas Mendorong. Boost banyak menggunakan streaming, khususnya di API layanannya (misalnya, untuk logging), jadi kami harus memodifikasinya untuk menghapus penggunaan streaming. Hal ini, pada gilirannya, mempersulit kami untuk bermigrasi ke Boost versi baru.

Jalan yang ketiga

Saat berpindah ke standar C++14, kami mempertimbangkan opsi berikut:

  1. Tingkatkan STTLPort yang kami modifikasi ke standar C++14. Pilihannya sangat sulit, karena... dukungan untuk STLPort dihentikan pada tahun 2010, dan kami harus membuat semua kodenya sendiri.
  2. Transisi ke implementasi STL lain yang kompatibel dengan C++14. Sangat diharapkan penerapan ini dilakukan untuk Windows dan Linux.
  3. Saat mengkompilasi untuk setiap OS, gunakan perpustakaan yang ada di dalam kompiler yang sesuai.

Opsi pertama ditolak mentah-mentah karena terlalu banyak pekerjaan.

Kami memikirkan pilihan kedua selama beberapa waktu; dianggap sebagai kandidat libc++, tetapi pada saat itu tidak berfungsi pada Windows. Untuk mem-porting libc++ ke Windows, Anda harus melakukan banyak pekerjaan - misalnya, menulis sendiri segala sesuatu yang berkaitan dengan thread, sinkronisasi thread, dan atomisitas, karena libc++ digunakan di area ini API POSIX.

Dan kami memilih cara ketiga.

Transisi

Jadi, kami harus mengganti penggunaan STLPort dengan perpustakaan kompiler yang sesuai (Visual Studio 2015 untuk Windows, gcc 7 untuk Linux, clang 8 untuk macOS).

Untungnya, kode kami ditulis terutama sesuai dengan pedoman dan tidak menggunakan segala macam trik cerdas, sehingga migrasi ke perpustakaan baru berjalan relatif lancar, dengan bantuan skrip yang menggantikan nama tipe, kelas, ruang nama, dan penyertaan dalam sumbernya. file. Migrasi tersebut memengaruhi 10 file sumber (dari 000). wchar_t digantikan oleh char14_t; kami memutuskan untuk meninggalkan penggunaan wchar_t, karena char000_t membutuhkan 16 byte di semua OS dan tidak merusak kompatibilitas kode antara Windows dan Linux.

Ada beberapa petualangan kecil. Misalnya, di STLPort, sebuah iterator dapat secara implisit dilemparkan ke sebuah pointer ke suatu elemen, dan di beberapa tempat dalam kode kita ini digunakan. Di perpustakaan baru, hal ini tidak mungkin lagi dilakukan, dan bagian-bagian ini harus dianalisis dan ditulis ulang secara manual.

Jadi, migrasi kode selesai, kode dikompilasi untuk semua sistem operasi. Sudah waktunya untuk tes.

Pengujian setelah transisi menunjukkan penurunan kinerja (di beberapa tempat hingga 20-30%) dan peningkatan konsumsi memori (hingga 10-15%) dibandingkan dengan versi kode yang lama. Hal ini, khususnya, disebabkan oleh kinerja string standar yang kurang optimal. Oleh karena itu, kami kembali harus menggunakan jalur kami sendiri yang sedikit dimodifikasi.

Fitur menarik dari implementasi container di perpustakaan tertanam juga terungkap: kosong (tanpa elemen) std::map dan std::set dari perpustakaan bawaan mengalokasikan memori. Dan karena fitur implementasinya, di beberapa tempat dalam kode cukup banyak container kosong jenis ini yang dibuat. Wadah memori standar dialokasikan sedikit, untuk satu elemen root, tetapi bagi kami hal ini ternyata sangat penting - dalam beberapa skenario, kinerja kami turun secara signifikan dan konsumsi memori meningkat (dibandingkan dengan STLPort). Oleh karena itu, dalam kode kami, kami mengganti kedua jenis penampung ini dari perpustakaan bawaan dengan implementasinya dari Boost, di mana penampung ini tidak memiliki fitur ini, dan ini memecahkan masalah perlambatan dan peningkatan konsumsi memori.

Seperti yang sering terjadi setelah perubahan skala besar dalam proyek besar, iterasi pertama dari kode sumber tidak berfungsi tanpa masalah, dan di sini, khususnya, dukungan untuk debugging iterator dalam implementasi Windows sangat berguna. Selangkah demi selangkah kami bergerak maju, dan pada musim semi tahun 2017 (versi 8.3.11 1C:Enterprise) migrasi telah selesai.

Hasil

Transisi ke standar C++14 membutuhkan waktu sekitar 6 bulan. Seringkali, satu pengembang (tetapi sangat berkualifikasi tinggi) mengerjakan proyek tersebut, dan pada tahap akhir perwakilan tim yang bertanggung jawab untuk bidang tertentu bergabung - UI, cluster server, alat pengembangan dan administrasi, dll.

Transisi ini sangat menyederhanakan pekerjaan kami dalam bermigrasi ke versi standar terbaru. Dengan demikian, versi 1C:Enterprise 8.3.14 (dalam pengembangan, rilis dijadwalkan awal tahun depan) telah ditransfer ke standar C++17.

Setelah migrasi, pengembang memiliki lebih banyak pilihan. Jika sebelumnya kita memiliki versi STL yang dimodifikasi dan satu namespace std, sekarang kita memiliki kelas standar dari perpustakaan kompiler bawaan di namespace std, di namespace stdx - baris dan wadah kami dioptimalkan untuk tugas-tugas kami, dalam peningkatan - the peningkatan versi terbaru. Dan pengembang menggunakan kelas-kelas yang paling cocok untuk memecahkan masalahnya.

Implementasi konstruktor gerakan β€œasli” juga membantu dalam pengembangan (memindahkan konstruktor) untuk beberapa kelas. Jika suatu kelas memiliki konstruktor perpindahan dan kelas ini ditempatkan dalam sebuah wadah, maka STL mengoptimalkan penyalinan elemen di dalam wadah tersebut (misalnya, ketika wadah diperluas dan perlu mengubah kapasitas dan mengalokasikan kembali memori).

Terbang di Salep ini

Mungkin konsekuensi yang paling tidak menyenangkan (tetapi tidak kritis) dari migrasi adalah kita dihadapkan pada peningkatan volume migrasi file objek, dan hasil lengkap pembangunan dengan semua file perantara mulai memakan 60–70 GB. Perilaku ini disebabkan oleh kekhasan perpustakaan standar modern, yang menjadi kurang kritis terhadap ukuran file layanan yang dihasilkan. Hal ini tidak mempengaruhi pengoperasian aplikasi yang dikompilasi, namun menyebabkan sejumlah ketidaknyamanan dalam pengembangan, khususnya, meningkatkan waktu kompilasi. Persyaratan ruang disk kosong di server build dan mesin pengembang juga meningkat. Pengembang kami bekerja pada beberapa versi platform secara paralel, dan ratusan gigabyte file perantara terkadang menimbulkan kesulitan dalam pekerjaan mereka. Masalahnya tidak menyenangkan, tapi tidak kritis, kami telah menunda penyelesaiannya untuk saat ini. Kami mempertimbangkan teknologi sebagai salah satu opsi untuk menyelesaikannya membangun kesatuan (khususnya, Google menggunakannya saat mengembangkan browser Chrome).

Sumber: www.habr.com

Tambah komentar