Cách chúng tôi dịch 10 triệu dòng mã C++ sang tiêu chuẩn C++14 (và sau đó sang C++17)

Cách đây một thời gian (vào mùa thu năm 2016), trong quá trình phát triển phiên bản tiếp theo của nền tảng công nghệ 1C:Enterprise, nhóm phát triển đã đặt ra câu hỏi về việc hỗ trợ tiêu chuẩn mới C ++ 14 trong mã của chúng tôi. Việc chuyển đổi sang một tiêu chuẩn mới, như chúng tôi giả định, sẽ cho phép chúng tôi viết nhiều thứ một cách trang nhã hơn, đơn giản và đáng tin cậy hơn, đồng thời sẽ đơn giản hóa việc hỗ trợ và bảo trì mã. Và dường như không có gì bất thường trong bản dịch, nếu không nói đến quy mô của cơ sở mã và các tính năng cụ thể trong mã của chúng tôi.

Dành cho những ai chưa biết, 1C:Enterprise là môi trường để phát triển nhanh chóng các ứng dụng kinh doanh đa nền tảng và thời gian chạy để thực thi chúng trên các hệ điều hành và DBMS khác nhau. Nói chung, sản phẩm có chứa:

Chúng tôi cố gắng viết cùng một mã cho các hệ điều hành khác nhau nhiều nhất có thể - cơ sở mã máy chủ phổ biến là 99%, cơ sở mã máy khách là khoảng 95%. Nền tảng công nghệ 1C:Enterprise chủ yếu được viết bằng C++ và các đặc điểm mã gần đúng được đưa ra dưới đây:

  • 10 triệu dòng mã C++,
  • 14 nghìn tập tin,
  • 60 nghìn lớp học,
  • nửa triệu phương pháp.

Và tất cả những thứ này phải được dịch sang C++14. Hôm nay chúng tôi sẽ cho bạn biết chúng tôi đã thực hiện điều này như thế nào và chúng tôi đã gặp phải những gì trong quá trình này.

Cách chúng tôi dịch 10 triệu dòng mã C++ sang tiêu chuẩn C++14 (và sau đó sang C++17)

Tuyên bố từ chối trách nhiệm

Mọi thứ được viết bên dưới về công việc chậm/nhanh, (không phải) mức tiêu thụ bộ nhớ lớn khi triển khai các lớp tiêu chuẩn trong các thư viện khác nhau đều có một ý nghĩa: điều này đúng CHO CHÚNG TÔI. Rất có thể việc triển khai tiêu chuẩn sẽ phù hợp nhất cho nhiệm vụ của bạn. Chúng tôi bắt đầu từ nhiệm vụ của riêng mình: chúng tôi lấy dữ liệu điển hình cho khách hàng của mình, chạy các kịch bản điển hình trên chúng, xem xét hiệu suất, lượng bộ nhớ tiêu thụ, v.v. và phân tích xem chúng tôi và khách hàng của mình có hài lòng với kết quả đó hay không . Và họ hành động tùy theo.

Những gì chúng tôi đã có

Ban đầu, chúng tôi viết mã cho nền tảng 1C:Enterprise 8 trong Microsoft Visual Studio. Dự án bắt đầu vào đầu những năm 2000 và chúng tôi có phiên bản chỉ dành cho Windows. Đương nhiên, kể từ đó mã đã được phát triển tích cực, nhiều cơ chế đã được viết lại hoàn toàn. Nhưng mã được viết theo tiêu chuẩn năm 1998, và ví dụ, dấu ngoặc nhọn bên phải của chúng tôi được phân tách bằng dấu cách để quá trình biên dịch thành công, như thế này:

vector<vector<int> > IntV;

Năm 2006, với việc phát hành phiên bản nền tảng 8.1, chúng tôi bắt đầu hỗ trợ Linux và chuyển sang thư viện tiêu chuẩn của bên thứ ba Cổng STL. Một trong những lý do cho sự chuyển đổi là để làm việc với các đường nét rộng. Trong mã của chúng tôi, chúng tôi sử dụng std::wstring, dựa trên loại wchar_t. Kích thước của nó trong Windows là 2 byte và trong Linux theo mặc định là 4 byte. Điều này dẫn đến sự không tương thích của các giao thức nhị phân của chúng tôi giữa máy khách và máy chủ, cũng như các dữ liệu liên tục khác nhau. Bằng cách sử dụng các tùy chọn gcc, bạn có thể chỉ định rằng kích thước của wchar_t trong quá trình biên dịch cũng là 2 byte, nhưng sau đó bạn có thể quên việc sử dụng thư viện chuẩn từ trình biên dịch, bởi vì nó sử dụng glibc, do đó được biên dịch cho wchar_t 4 byte. Các lý do khác là việc triển khai tốt hơn các lớp tiêu chuẩn, hỗ trợ cho bảng băm và thậm chí là mô phỏng ngữ nghĩa của việc di chuyển bên trong các vùng chứa mà chúng tôi đã tích cực sử dụng. Và một lý do nữa, như họ nói cuối cùng nhưng không kém phần quan trọng, là hiệu suất của dây. Chúng tôi có lớp học riêng về chuỗi, bởi vì... Do đặc thù của phần mềm của chúng tôi, các thao tác chuỗi được sử dụng rất rộng rãi và đối với chúng tôi, điều này rất quan trọng.

Chuỗi của chúng tôi dựa trên các ý tưởng tối ưu hóa chuỗi được thể hiện từ đầu những năm 2000 Andrei Alexandrescu. Sau này, khi Alexandrescu làm việc tại Facebook, theo gợi ý của ông, một dòng đã được sử dụng trong công cụ Facebook hoạt động theo các nguyên tắc tương tự (xem thư viện dại dột).

Dây chuyền của chúng tôi sử dụng hai công nghệ tối ưu hóa chính:

  1. Đối với các giá trị ngắn, bộ đệm bên trong trong chính đối tượng chuỗi sẽ được sử dụng (không yêu cầu cấp phát bộ nhớ bổ sung).
  2. Đối với tất cả những người khác, cơ học được sử dụng Sao chép khi viết. Giá trị chuỗi được lưu trữ ở một nơi và bộ đếm tham chiếu được sử dụng trong quá trình gán/sửa đổi.

Để tăng tốc độ biên dịch nền tảng, chúng tôi đã loại trừ việc triển khai luồng khỏi biến thể STLPort (mà chúng tôi không sử dụng), điều này giúp chúng tôi biên dịch nhanh hơn khoảng 20%. Sau đó chúng tôi phải hạn chế sử dụng Tăng. Boost sử dụng nhiều luồng, đặc biệt là trong các API dịch vụ của nó (ví dụ: để ghi nhật ký), vì vậy chúng tôi đã phải sửa đổi luồng này để loại bỏ việc sử dụng luồng. Chính điều này đã khiến chúng tôi gặp khó khăn khi chuyển sang các phiên bản Boost mới.

Cách thứ ba

Khi chuyển sang tiêu chuẩn C++ 14, chúng tôi đã xem xét các tùy chọn sau:

  1. Nâng cấp STLPort mà chúng tôi đã sửa đổi thành tiêu chuẩn C++ 14. Lựa chọn này rất khó khăn, bởi vì... hỗ trợ cho STLPort đã ngừng hoạt động vào năm 2010 và chúng tôi sẽ phải tự mình xây dựng tất cả mã của nó.
  2. Chuyển sang triển khai STL khác tương thích với C++ 14. Rất mong muốn việc triển khai này dành cho Windows và Linux.
  3. Khi biên dịch cho từng HĐH, hãy sử dụng thư viện được tích hợp trong trình biên dịch tương ứng.

Phương án đầu tiên đã bị từ chối hoàn toàn do phải làm quá nhiều việc.

Chúng tôi đã nghĩ về lựa chọn thứ hai một thời gian; được coi là ứng cử viên libc ++, nhưng tại thời điểm đó nó không hoạt động trong Windows. Để chuyển libc++ sang Windows, bạn sẽ phải thực hiện rất nhiều công việc - ví dụ: tự viết mọi thứ liên quan đến luồng, đồng bộ hóa luồng và tính nguyên tử, vì libc++ được sử dụng trong các lĩnh vực này API POSIX.

Và chúng tôi đã chọn cách thứ ba.

Chuyển tiếp

Vì vậy, chúng tôi đã phải thay thế việc sử dụng STLPort bằng thư viện của các trình biên dịch tương ứng (Visual Studio 2015 cho Windows, gcc 7 cho Linux, clang 8 cho macOS).

May mắn thay, mã của chúng tôi chủ yếu được viết theo hướng dẫn và không sử dụng bất kỳ loại thủ thuật thông minh nào, vì vậy việc di chuyển sang các thư viện mới diễn ra tương đối suôn sẻ, với sự trợ giúp của các tập lệnh thay thế tên loại, lớp, không gian tên và bao gồm trong nguồn các tập tin. Quá trình di chuyển đã ảnh hưởng đến 10 tệp nguồn (trong số 000). wchar_t đã được thay thế bằng char14_t; chúng tôi quyết định từ bỏ việc sử dụng wchar_t, bởi vì char000_t mất 16 byte trên tất cả các hệ điều hành và không làm hỏng khả năng tương thích mã giữa Windows và Linux.

Có một số cuộc phiêu lưu nhỏ. Ví dụ: trong STLPort, một trình vòng lặp có thể được chuyển ngầm thành một con trỏ tới một phần tử và ở một số vị trí trong mã của chúng tôi, điều này đã được sử dụng. Trong các thư viện mới, điều này không thể thực hiện được nữa và những đoạn văn này phải được phân tích và viết lại bằng tay.

Vậy là quá trình di chuyển mã đã hoàn tất, mã được biên dịch cho tất cả các hệ điều hành. Đã đến lúc kiểm tra.

Các thử nghiệm sau khi chuyển đổi cho thấy hiệu suất giảm (ở một số nơi lên tới 20-30%) và mức tiêu thụ bộ nhớ tăng (lên tới 10-15%) so với phiên bản mã cũ. Đặc biệt, điều này là do hiệu suất dưới mức tối ưu của các chuỗi tiêu chuẩn. Vì vậy, chúng tôi lại phải sử dụng dòng được sửa đổi một chút của riêng mình.

Một tính năng thú vị của việc triển khai vùng chứa trong thư viện nhúng cũng được tiết lộ: trống (không có phần tử) std::map và std::set từ các thư viện tích hợp cấp phát bộ nhớ. Và do các tính năng triển khai, ở một số nơi trong mã có khá nhiều vùng chứa trống kiểu này được tạo ra. Vùng chứa bộ nhớ tiêu chuẩn được phân bổ một chút cho một phần tử gốc, nhưng đối với chúng tôi, điều này hóa ra lại rất quan trọng - trong một số trường hợp, hiệu suất của chúng tôi giảm đáng kể và mức tiêu thụ bộ nhớ tăng lên (so với STLPort). Do đó, trong mã của chúng tôi, chúng tôi đã thay thế hai loại vùng chứa này từ các thư viện tích hợp bằng cách triển khai chúng từ Boost, trong đó các vùng chứa này không có tính năng này và điều này đã giải quyết được vấn đề làm chậm và tăng mức tiêu thụ bộ nhớ.

Như thường xảy ra sau những thay đổi quy mô lớn trong các dự án lớn, lần lặp đầu tiên của mã nguồn không hoạt động mà không gặp sự cố và đặc biệt ở đây, việc hỗ trợ gỡ lỗi các trình vòng lặp trong quá trình triển khai Windows rất hữu ích. Chúng tôi đã từng bước tiến về phía trước và đến mùa xuân năm 2017 (phiên bản 8.3.11 1C:Enterprise), quá trình di chuyển đã hoàn tất.

Kết quả

Quá trình chuyển đổi sang tiêu chuẩn C++14 mất khoảng 6 tháng. Hầu hết thời gian, một nhà phát triển (nhưng có trình độ rất cao) đã làm việc trong dự án và ở giai đoạn cuối, đại diện của các nhóm chịu trách nhiệm về các lĩnh vực cụ thể đã tham gia - UI, cụm máy chủ, các công cụ phát triển và quản trị, v.v.

Quá trình chuyển đổi đã đơn giản hóa rất nhiều công việc của chúng tôi trong việc di chuyển sang các phiên bản mới nhất của tiêu chuẩn. Do đó, phiên bản 1C:Enterprise 8.3.14 (đang được phát triển, dự kiến ​​phát hành vào đầu năm sau) đã được chuyển sang tiêu chuẩn C++17.

Sau khi di chuyển, các nhà phát triển có nhiều lựa chọn hơn. Nếu trước đây chúng ta có phiên bản sửa đổi của STL và một không gian tên std thì bây giờ chúng ta có các lớp tiêu chuẩn từ thư viện trình biên dịch tích hợp trong không gian tên std, trong không gian tên stdx - các dòng và vùng chứa của chúng ta được tối ưu hóa cho các tác vụ của chúng ta, trong boost - phiên bản tăng cường mới nhất. Và nhà phát triển sử dụng những lớp phù hợp nhất để giải quyết vấn đề của mình.

Việc triển khai “bản địa” của các hàm tạo di chuyển cũng giúp phát triển (di chuyển hàm tạo) cho một số lớp Nếu một lớp có một hàm tạo di chuyển và lớp này được đặt trong một vùng chứa, thì STL sẽ tối ưu hóa việc sao chép các phần tử bên trong vùng chứa (ví dụ: khi vùng chứa được mở rộng và cần phải thay đổi dung lượng và phân bổ lại bộ nhớ).

Bay trong thuốc mỡ

Có lẽ hậu quả khó chịu nhất (nhưng không nghiêm trọng) của việc di cư là chúng ta phải đối mặt với sự gia tăng về số lượng tập tin objvà kết quả đầy đủ của quá trình xây dựng với tất cả các tệp trung gian bắt đầu chiếm 60–70 GB. Hành vi này là do đặc thù của các thư viện tiêu chuẩn hiện đại, vốn đã trở nên ít quan trọng hơn đối với kích thước của các tệp dịch vụ được tạo ra. Điều này không ảnh hưởng đến hoạt động của ứng dụng được biên dịch nhưng lại gây ra một số bất tiện trong quá trình phát triển, cụ thể là làm tăng thời gian biên dịch. Yêu cầu về dung lượng ổ đĩa trống trên máy chủ xây dựng và trên máy của nhà phát triển cũng ngày càng tăng. Các nhà phát triển của chúng tôi làm việc song song trên nhiều phiên bản của nền tảng và hàng trăm gigabyte tệp trung gian đôi khi gây khó khăn trong công việc của họ. Vấn đề này thật khó chịu, nhưng không nghiêm trọng; chúng tôi đã tạm dừng giải pháp cho vấn đề này. Chúng tôi đang coi công nghệ là một trong những lựa chọn để giải quyết nó đoàn kết xây dựng (đặc biệt Google sử dụng nó khi phát triển trình duyệt Chrome).

Nguồn: www.habr.com

Thêm một lời nhận xét