Nhóm Android Runtime (ART) đã giảm 18% thời gian biên dịch mà không ảnh hưởng đến mã đã biên dịch hoặc bất kỳ sự hồi quy bộ nhớ cao điểm nào. Cải tiến này là một phần trong sáng kiến năm 2025 của chúng tôi nhằm cải thiện thời gian biên dịch mà không làm ảnh hưởng đến mức sử dụng bộ nhớ hoặc chất lượng của mã được biên dịch.
Việc tối ưu hoá tốc độ thời gian biên dịch là rất quan trọng đối với ART. Ví dụ: khi biên dịch đúng lúc (JIT), việc này ảnh hưởng trực tiếp đến hiệu quả của ứng dụng và hiệu suất tổng thể của thiết bị. Quá trình biên dịch nhanh hơn giúp giảm thời gian trước khi các hoạt động tối ưu hoá bắt đầu, mang lại trải nghiệm người dùng mượt mà và thích ứng nhanh hơn. Hơn nữa, đối với cả JIT và biên dịch trước (AOT), việc cải thiện tốc độ thời gian biên dịch sẽ giúp giảm mức tiêu thụ tài nguyên trong quá trình biên dịch, mang lại lợi ích cho thời lượng pin và nhiệt độ thiết bị, đặc biệt là trên các thiết bị cấp thấp.
Một số điểm cải tiến về tốc độ thời gian biên dịch này đã ra mắt trong bản phát hành Android tháng 6 năm 2025, còn những điểm cải tiến khác sẽ có trong bản phát hành Android cuối năm. Hơn nữa, tất cả người dùng Android phiên bản 12 trở lên đều đủ điều kiện nhận những điểm cải tiến này thông qua các bản cập nhật chính.
Tối ưu hoá trình biên dịch tối ưu hoá
Tối ưu hoá trình biên dịch luôn là một trò chơi đánh đổi. Bạn không thể có được tốc độ mà không phải đánh đổi điều gì. Chúng tôi đặt ra một mục tiêu rất rõ ràng và đầy thách thức cho bản thân: tăng tốc trình biên dịch nhưng không gây ra tình trạng hồi quy bộ nhớ và quan trọng nhất là không làm giảm chất lượng của mã mà trình biên dịch tạo ra. Nếu trình biên dịch nhanh hơn nhưng ứng dụng chạy chậm hơn, thì chúng tôi đã thất bại.
Tài nguyên duy nhất mà chúng tôi sẵn sàng bỏ ra là thời gian phát triển của chính mình để tìm hiểu sâu, điều tra và tìm ra các giải pháp thông minh đáp ứng các tiêu chí nghiêm ngặt này. Hãy cùng tìm hiểu kỹ hơn về cách chúng tôi nỗ lực tìm ra những điểm cần cải thiện, cũng như tìm ra giải pháp phù hợp cho nhiều vấn đề.
Tìm các phương án tối ưu hoá khả thi và hữu ích
Trước khi có thể bắt đầu tối ưu hoá một chỉ số, bạn phải đo lường được chỉ số đó. Nếu không, bạn sẽ không bao giờ biết chắc liệu mình có cải thiện được hay không. May mắn là tốc độ thời gian biên dịch khá nhất quán, miễn là bạn thực hiện một số biện pháp phòng ngừa như sử dụng cùng một thiết bị để đo lường trước và sau khi thay đổi, đồng thời đảm bảo rằng bạn không điều tiết nhiệt cho thiết bị. Ngoài ra, chúng tôi cũng có các phép đo tất định như số liệu thống kê của trình biên dịch để giúp chúng tôi hiểu được những gì đang diễn ra trong hệ thống.
Vì tài nguyên mà chúng tôi phải đánh đổi để có được những điểm cải tiến này là thời gian phát triển, nên chúng tôi muốn có thể lặp lại nhanh nhất có thể. Điều này có nghĩa là chúng tôi đã chọn một số ứng dụng tiêu biểu (kết hợp giữa ứng dụng bên thứ nhất, ứng dụng bên thứ ba và chính hệ điều hành Android) để tạo nguyên mẫu cho các giải pháp. Sau đó, chúng tôi đã xác minh rằng việc triển khai cuối cùng là xứng đáng bằng cả thử nghiệm thủ công và tự động trên diện rộng.
Với tập hợp tệp APK được chọn lọc kỹ lưỡng đó, chúng tôi sẽ kích hoạt một bản biên dịch thủ công trên thiết bị, lấy hồ sơ của bản biên dịch và sử dụng pprof để hình dung nơi chúng tôi đang dành thời gian.
Ví dụ về biểu đồ dạng ngọn lửa của hồ sơ trong pprof
Công cụ pprof rất mạnh mẽ và cho phép chúng ta phân chia, lọc và sắp xếp dữ liệu để xem, ví dụ: giai đoạn hoặc phương thức nào của trình biên dịch đang chiếm nhiều thời gian nhất. Chúng ta sẽ không đi sâu vào chi tiết về pprof; chỉ cần biết rằng nếu thanh càng lớn thì có nghĩa là quá trình biên dịch càng mất nhiều thời gian.
Một trong những chế độ xem này là chế độ xem "từ dưới lên", nơi bạn có thể xem phương thức nào đang chiếm nhiều thời gian nhất. Trong hình ảnh bên dưới, chúng ta có thể thấy một phương thức có tên là Kill, chiếm hơn 1% thời gian biên dịch. Một số phương thức hàng đầu khác cũng sẽ được thảo luận ở phần sau của bài đăng trên blog.
Chế độ xem từ dưới lên của một hồ sơ
Trong trình biên dịch tối ưu hoá của chúng tôi, có một giai đoạn gọi là Đánh số giá trị chung (GVN). Bạn không cần phải lo lắng về chức năng tổng thể của lớp này, nhưng điều quan trọng là bạn cần biết rằng lớp này có một phương thức tên là `Kill` để xoá một số nút theo bộ lọc. Việc này tốn thời gian vì phải lặp lại tất cả các nút và kiểm tra từng nút một. Chúng tôi nhận thấy có một số trường hợp mà chúng tôi biết trước rằng kết quả kiểm tra sẽ là sai, bất kể các nút mà chúng tôi có tại thời điểm đó. Trong những trường hợp này, chúng ta có thể bỏ qua hoàn toàn việc lặp lại, giảm từ 1,023% xuống còn ~0,3% và cải thiện thời gian chạy của GVN thêm ~15%.
Triển khai các biện pháp tối ưu hoá hữu ích
Chúng ta đã tìm hiểu cách đo lường và cách phát hiện thời gian được sử dụng ở đâu, nhưng đây mới chỉ là bước khởi đầu. Bước tiếp theo là cách tối ưu hoá thời gian biên dịch.
Thông thường, trong trường hợp như `Kill` ở trên, chúng ta sẽ xem xét cách lặp lại qua các nút và thực hiện nhanh hơn, chẳng hạn như thực hiện song song hoặc cải thiện chính thuật toán. Trên thực tế, đó là điều chúng tôi đã thử làm lúc đầu và chỉ khi không tìm thấy gì để làm, chúng tôi mới nhận ra rằng giải pháp (trong một số trường hợp) là không lặp lại gì cả! Khi thực hiện những kiểu tối ưu hoá này, bạn rất dễ bỏ qua những điều quan trọng.
Trong các trường hợp khác, chúng tôi sử dụng một số kỹ thuật khác nhau, bao gồm:
- sử dụng phương pháp phỏng đoán để quyết định xem một hoạt động tối ưu hoá có không mang lại kết quả đáng giá hay không, từ đó có thể bỏ qua hoạt động đó
- sử dụng cấu trúc dữ liệu bổ sung để lưu dữ liệu đã tính toán vào bộ nhớ đệm
- thay đổi cấu trúc dữ liệu hiện tại để tăng tốc độ
- tính toán kết quả một cách lười biếng để tránh chu kỳ trong một số trường hợp
- sử dụng đúng mức độ trừu tượng – các tính năng không cần thiết có thể làm chậm mã
- tránh việc theo dõi một con trỏ thường dùng thông qua nhiều lượt tải
Làm cách nào để biết liệu các biện pháp tối ưu hoá có đáng để thực hiện hay không?
Điều thú vị là bạn không cần làm gì cả. Sau khi phát hiện một khu vực đang tiêu tốn nhiều thời gian biên dịch và sau khi dành thời gian phát triển để cố gắng cải thiện khu vực đó, đôi khi bạn không thể tìm ra giải pháp. Có thể bạn không cần làm gì cả, hoặc có thể việc triển khai sẽ mất quá nhiều thời gian, hoặc có thể việc này sẽ làm giảm đáng kể một chỉ số khác, làm tăng độ phức tạp của toàn bộ mã nguồn, v.v. Đối với mỗi hoạt động tối ưu hoá thành công mà bạn có thể thấy trong bài đăng trên blog này, hãy biết rằng có vô số hoạt động khác không mang lại kết quả.
Nếu bạn đang ở trong tình huống tương tự, hãy cố gắng ước tính mức độ cải thiện chỉ số bằng cách làm ít việc nhất có thể. Điều này có nghĩa là:
- Ước tính bằng chỉ số bạn đã thu thập hoặc chỉ dựa vào cảm tính
- Ước tính bằng một nguyên mẫu nhanh và đơn giản
- Triển khai giải pháp.
Đừng quên cân nhắc việc ước tính những hạn chế của giải pháp. Ví dụ: nếu bạn định dựa vào các cấu trúc dữ liệu bổ sung, bạn sẵn sàng sử dụng bao nhiêu bộ nhớ?
Tìm hiểu sâu hơn
Không dông dài thêm nữa, hãy cùng tìm hiểu một số thay đổi mà chúng tôi đã triển khai.
Chúng tôi đã triển khai một thay đổi để tối ưu hoá một phương thức có tên là FindReferenceInfoOf. Phương thức này thực hiện tìm kiếm tuyến tính một vectơ để tìm một mục. Chúng tôi đã cập nhật cấu trúc dữ liệu đó để được lập chỉ mục theo mã nhận dạng của lệnh, nhờ đó FindReferenceInfoOf sẽ là O(1) thay vì O(n). Ngoài ra, chúng tôi đã phân bổ trước vectơ để tránh việc đổi kích thước. Chúng tôi đã tăng nhẹ bộ nhớ vì phải thêm một trường bổ sung để đếm số lượng mục nhập mà chúng tôi đã chèn vào vectơ, nhưng đây là một sự đánh đổi nhỏ vì bộ nhớ tối đa không tăng. Điều này giúp tăng tốc giai đoạn LoadStoreAnalysis thêm 34-66%, từ đó cải thiện thời gian biên dịch thêm khoảng 0,5-1,8%.
Chúng tôi có một cách triển khai tuỳ chỉnh của HashSet mà chúng tôi sử dụng ở một số nơi. Việc tạo cấu trúc dữ liệu này mất khá nhiều thời gian và chúng tôi đã tìm ra lý do. Nhiều năm trước, cấu trúc dữ liệu này chỉ được dùng ở một vài nơi sử dụng HashSet rất lớn và được điều chỉnh để tối ưu hoá cho việc đó. Tuy nhiên, ngày nay, nó được sử dụng theo hướng ngược lại với chỉ một vài mục và có tuổi thọ ngắn. Điều này có nghĩa là chúng tôi đã lãng phí chu kỳ khi tạo HashSet khổng lồ này nhưng chỉ sử dụng cho một vài mục trước khi loại bỏ. Với thay đổi này, chúng tôi đã cải thiện khoảng 1,3-2% thời gian biên dịch. Ngoài ra, mức sử dụng bộ nhớ cũng giảm khoảng 0,5-1% vì chúng tôi không sử dụng cấu trúc dữ liệu lớn như trước.
Chúng tôi đã cải thiện khoảng 0,5-1% thời gian biên dịch bằng cách truyền cấu trúc dữ liệu theo tham chiếu đến lambda để tránh sao chép chúng. Đây là một vấn đề bị bỏ sót trong lần xem xét ban đầu và đã tồn tại trong toàn bộ mã nguồn của chúng tôi trong nhiều năm. Nhờ xem xét các hồ sơ trong pprof, chúng tôi nhận thấy rằng các phương thức này đang tạo và huỷ nhiều cấu trúc dữ liệu, từ đó chúng tôi điều tra và tối ưu hoá các cấu trúc dữ liệu đó.
Chúng tôi đã tăng tốc giai đoạn ghi kết quả biên dịch bằng cách lưu các giá trị đã tính vào bộ nhớ đệm, giúp cải thiện tổng thời gian biên dịch khoảng 1,3% đến 2,8%. Đáng buồn là việc ghi sổ bổ sung quá nhiều và quy trình kiểm thử tự động đã cảnh báo chúng tôi về sự hồi quy bộ nhớ. Sau đó, chúng tôi xem xét lại mã đó và triển khai một phiên bản mới. Phiên bản này không chỉ xử lý được vấn đề hồi quy bộ nhớ mà còn cải thiện thời gian biên dịch thêm khoảng 0,5-1,8%! Trong thay đổi thứ hai này, chúng ta phải tái cấu trúc và hình dung lại cách hoạt động của giai đoạn này để loại bỏ một trong hai cấu trúc dữ liệu.
Chúng tôi có một giai đoạn trong trình biên dịch tối ưu hoá, trong đó các lệnh gọi hàm cùng dòng để có hiệu suất tốt hơn. Để chọn phương thức nào cần cùng dòng, chúng tôi sử dụng cả phương pháp phỏng đoán trước khi thực hiện bất kỳ phép tính nào và kiểm tra lần cuối sau khi thực hiện công việc nhưng ngay trước khi hoàn tất việc cùng dòng. Nếu bất kỳ trình tối ưu hoá nào trong số đó phát hiện thấy việc cùng dòng không đáng thực hiện (ví dụ: có quá nhiều lệnh mới được thêm vào), thì chúng tôi sẽ không cùng dòng lệnh gọi phương thức.
Chúng tôi đã chuyển hai bước kiểm tra từ danh mục "kiểm tra cuối cùng" sang danh mục "phỏng đoán" để ước tính xem việc chèn cùng dòng có thành công hay không trước khi thực hiện bất kỳ phép tính tốn thời gian nào. Vì đây là thông tin ước tính nên không hoàn toàn chính xác, nhưng chúng tôi đã xác minh rằng phương pháp phỏng đoán mới của chúng tôi bao gồm 99,9% những gì đã được chèn trước đó mà không ảnh hưởng đến hiệu suất. Một trong những phương pháp phỏng đoán mới này là về các thanh ghi DEX cần thiết (cải thiện ~0,2-1,3%) và phương pháp còn lại là về số lượng lệnh (cải thiện ~2%).
Chúng tôi có một cách triển khai tuỳ chỉnh của BitVector mà chúng tôi sử dụng ở một số nơi. Chúng tôi đã thay thế lớp BitVector có thể đổi kích thước bằng một BitVectorView đơn giản hơn cho một số vectơ bit có kích thước cố định. Điều này giúp loại bỏ một số lệnh gián tiếp và kiểm tra phạm vi thời gian chạy, đồng thời tăng tốc quá trình tạo đối tượng vectơ bit.
Hơn nữa, lớp BitVectorView được tạo mẫu trên loại bộ nhớ cơ bản (thay vì luôn sử dụng uint32_t làm BitVector cũ). Điều này cho phép một số thao tác (ví dụ: Union()) xử lý số bit nhiều gấp đôi trên các nền tảng 64 bit. Tổng số mẫu của các hàm bị ảnh hưởng đã giảm hơn 1% khi biên dịch Hệ điều hành Android. Điều này được thực hiện thông qua một số thay đổi [1, 2, 3, 4, 5, 6]
Nếu chúng ta nói chi tiết về tất cả các hoạt động tối ưu hoá, chúng ta sẽ mất cả ngày! Nếu bạn quan tâm đến một số cách tối ưu hoá khác, hãy xem một số thay đổi khác mà chúng tôi đã triển khai:
- Thêm tính năng ghi sổ để cải thiện thời gian biên dịch khoảng 0,6-1,6%.
- Tính toán dữ liệu từng phần để tránh chu kỳ (nếu có thể).
- Tái cấu trúc mã để bỏ qua công việc tính toán trước khi không sử dụng.
- Tránh một số chuỗi tải phụ thuộc khi có thể dễ dàng lấy được trình phân bổ từ những nơi khác.
- Một trường hợp khác của việc thêm một bước kiểm tra để tránh công việc không cần thiết.
- Tránh phân nhánh thường xuyên trên loại thanh ghi (core/FP) trong bộ cấp phát thanh ghi.
- Đảm bảo một số mảng được khởi chạy tại thời điểm biên dịch. Đừng dựa vào clang để làm việc đó.
- Dọn dẹp một số vòng lặp. Sử dụng vòng lặp phạm vi mà clang có thể tối ưu hoá tốt hơn vì không cần tải lại con trỏ nội bộ của vùng chứa do tác dụng phụ của vòng lặp. Tránh gọi hàm ảo `HInstruction::GetInputRecords()` trong vòng lặp thông qua `InputAt(.)` cùng dòng cho mỗi dữ liệu đầu vào.
- Tránh các hàm Accept() cho mẫu khách truy cập bằng cách khai thác tính năng tối ưu hoá trình biên dịch.
Kết luận
Chúng tôi đã nỗ lực cải thiện tốc độ biên dịch của ART và đạt được những kết quả đáng kể, giúp Android hoạt động mượt mà và hiệu quả hơn, đồng thời góp phần cải thiện thời lượng pin và nhiệt độ của thiết bị. Bằng cách siêng năng xác định và triển khai các biện pháp tối ưu hoá, chúng tôi đã chứng minh rằng có thể đạt được mức tăng đáng kể về thời gian biên dịch mà không ảnh hưởng đến mức sử dụng bộ nhớ hoặc chất lượng mã.
Hành trình của chúng tôi bao gồm việc lập hồ sơ bằng các công cụ như pprof, sẵn sàng lặp lại và đôi khi thậm chí là từ bỏ những hướng đi ít hiệu quả hơn. Nỗ lực chung của nhóm ART không chỉ giúp giảm thời gian biên dịch theo tỷ lệ đáng kể mà còn đặt nền móng cho những cải tiến trong tương lai.
Tất cả những điểm cải tiến này đều có trong bản cập nhật Android cuối năm 2025 và cho Android 12 trở lên thông qua các bản cập nhật chính. Chúng tôi hy vọng bài viết chuyên sâu về quy trình tối ưu hoá này sẽ cung cấp thông tin chi tiết có giá trị về những điểm phức tạp và lợi ích của kỹ thuật trình biên dịch!
-
Tin tức về sản phẩmKhi quá trình phát triển tác nhân AI tăng tốc, kỹ thuật Android đang đạt đến một ranh giới mới thú vị: chuyển từ tính năng hoàn thành mã hỗ trợ sang các tác nhân thực thi từ đầu đến cuối theo hướng dẫn của bạn. Bạn có toàn quyền lựa chọn và linh hoạt sử dụng các mô hình và tác nhân.
Jason Buchel • Đọc trong 3 phút -
Tin tức về sản phẩmHôm nay, chúng tôi đã chia sẻ về cách chúng tôi đưa tầm nhìn "không chỉ là một cửa hàng" sang một chương mới. Từ thẻ Bạn (hiện là trang chủ cá nhân của hơn 250 triệu số người dùng hoạt động hằng tháng) cho đến nền tảng trò chơi hợp nhất kết nối hơn 700 triệu hồ sơ người chơi, Google Play luôn đáp ứng nhu cầu của người dùng mọi lúc mọi nơi.
Paul Feng • Đọc trong 3 phút -
Tin tức về sản phẩmChúng tôi rất vui được giúp người dùng thể hiện bản thân và sử dụng những công cụ này thông qua Googlebook.
Cyrus Stoller • Thời gian đọc: 2 phút
Nhận thông tin chi tiết mới nhất về hoạt động phát triển Android trong hộp thư đến của bạn hằng tuần.