Bài viết không thành công về việc tăng tốc phản xạ

Tôi sẽ giải thích ngay tiêu đề của bài viết. Kế hoạch ban đầu là đưa ra lời khuyên tốt, đáng tin cậy về cách tăng tốc độ sử dụng phản chiếu bằng một ví dụ đơn giản nhưng thực tế, nhưng trong quá trình đo điểm chuẩn, hóa ra phản xạ không chậm như tôi nghĩ, LINQ chậm hơn trong cơn ác mộng của tôi. Nhưng cuối cùng hóa ra tôi cũng mắc sai lầm trong phép đo... Chi tiết về câu chuyện cuộc đời này nằm dưới phần cắt và trong phần bình luận. Vì ví dụ này khá phổ biến và được thực hiện về nguyên tắc như thường được thực hiện trong một doanh nghiệp, nên đối với tôi, nó hóa ra là một minh chứng khá thú vị về cuộc sống: tác động đến tốc độ của chủ đề chính của bài viết là không đáng chú ý do logic bên ngoài: Moq, Autofac, EF Core và các "dây đeo" khác.

Tôi bắt đầu làm việc theo ấn tượng của bài viết này: Tại sao Phản xạ chậm

Như bạn có thể thấy, tác giả đề xuất sử dụng các đại biểu được biên dịch thay vì gọi trực tiếp các phương thức kiểu phản chiếu như một cách tuyệt vời để tăng tốc đáng kể ứng dụng. Tất nhiên, có phát xạ IL, nhưng tôi muốn tránh nó, vì đây là cách tốn nhiều công sức nhất để thực hiện nhiệm vụ và có nhiều sai sót.

Xét rằng tôi luôn có quan điểm tương tự về tốc độ phản ánh, tôi không đặc biệt có ý định đặt câu hỏi về kết luận của tác giả.

Tôi thường gặp phải việc sử dụng sự phản ánh một cách ngây thơ trong doanh nghiệp. Loại được thực hiện. Thông tin về tài sản được lấy. Phương thức SetValue được gọi và mọi người đều vui mừng. Giá trị đã đến trường mục tiêu, mọi người đều vui vẻ. Những người rất thông minh - cấp cao và trưởng nhóm - viết các phần mở rộng của họ cho đối tượng, dựa trên cách triển khai ngây thơ như vậy đối với các trình ánh xạ “phổ quát” từ loại này sang loại khác. Bản chất thường là thế này: chúng tôi lấy tất cả các trường, lấy tất cả các thuộc tính, lặp lại chúng: nếu tên của các thành viên loại khớp nhau, chúng tôi sẽ thực thi SetValue. Đôi khi, chúng tôi gặp phải các trường hợp ngoại lệ do nhầm lẫn trong đó chúng tôi không tìm thấy thuộc tính nào đó thuộc một trong các loại, nhưng ngay cả ở đây cũng có một lối thoát giúp cải thiện hiệu suất. Cố gắng bắt.

Tôi đã thấy mọi người phát minh lại các trình phân tích cú pháp và trình ánh xạ mà không được trang bị đầy đủ thông tin về cách thức hoạt động của các máy trước đó. Tôi đã thấy mọi người che giấu cách triển khai ngây thơ của họ đằng sau các chiến lược, sau các giao diện, đằng sau các lần tiêm, như thể điều này sẽ bào chữa cho các lần tiêm trực tiếp tiếp theo. Tôi hếch mũi trước những nhận thức như vậy. Trên thực tế, tôi đã không đo lường mức độ rò rỉ hiệu suất thực sự và nếu có thể, tôi chỉ đơn giản thay đổi cách triển khai thành một cách “tối ưu” hơn nếu tôi có thể bắt tay vào thực hiện. Vì vậy, các phép đo đầu tiên được thảo luận dưới đây khiến tôi thực sự bối rối.

Tôi nghĩ nhiều người trong số các bạn, khi đọc Richter hoặc các nhà tư tưởng khác, đã đưa ra một tuyên bố hoàn toàn công bằng rằng sự phản chiếu trong mã là một hiện tượng có tác động cực kỳ tiêu cực đến hiệu suất của ứng dụng.

Việc gọi phản ánh buộc CLR phải đi qua các tập hợp để tìm ra thứ họ cần, lấy siêu dữ liệu của họ, phân tích chúng, v.v. Ngoài ra, sự phản chiếu trong khi duyệt qua các chuỗi dẫn đến việc phân bổ một lượng lớn bộ nhớ. Chúng tôi đang sử dụng hết bộ nhớ, CLR phát hiện ra GC và các đường viền bắt đầu. Nó sẽ chậm đáng kể, tin tôi đi. Lượng bộ nhớ khổng lồ trên các máy chủ sản xuất hiện đại hoặc máy đám mây không ngăn cản được độ trễ xử lý cao. Trên thực tế, càng có nhiều bộ nhớ, bạn càng có nhiều khả năng THÔNG BÁO cách thức hoạt động của GC. Về lý thuyết, sự phản chiếu là một miếng giẻ đỏ bổ sung đối với anh ta.

Tuy nhiên, tất cả chúng ta đều sử dụng bộ chứa IoC và trình ánh xạ ngày, nguyên tắc hoạt động của chúng cũng dựa trên sự phản ánh nhưng thường không có thắc mắc nào về hiệu suất của chúng. Không, không phải vì việc đưa ra các phụ thuộc và trừu tượng hóa từ các mô hình bối cảnh giới hạn bên ngoài là cần thiết đến mức chúng ta phải hy sinh hiệu suất trong mọi trường hợp. Mọi thứ đơn giản hơn - nó thực sự không ảnh hưởng nhiều đến hiệu suất.

Thực tế là các khung phổ biến nhất dựa trên công nghệ phản chiếu sử dụng tất cả các loại thủ thuật để làm việc với nó một cách tối ưu hơn. Thông thường đây là một bộ đệm. Thông thường đây là các Biểu thức và đại biểu được biên dịch từ cây biểu thức. Trình ánh xạ tự động tương tự duy trì một từ điển cạnh tranh khớp các loại với các hàm có thể chuyển đổi loại này sang loại khác mà không cần gọi phản ánh.

Làm thế nào điều này đạt được? Về cơ bản, điều này không khác gì logic mà chính nền tảng này sử dụng để tạo mã JIT. Khi một phương thức được gọi lần đầu tiên, nó sẽ được biên dịch (và, vâng, quá trình này không nhanh); trong các cuộc gọi tiếp theo, quyền điều khiển sẽ được chuyển sang phương thức đã được biên dịch và sẽ không có sự suy giảm hiệu suất đáng kể nào.

Trong trường hợp của chúng tôi, bạn cũng có thể sử dụng trình biên dịch JIT và sau đó sử dụng hành vi được biên dịch với hiệu suất tương tự như các đối tác AOT của nó. Biểu thức sẽ hỗ trợ chúng ta trong trường hợp này.

Nguyên tắc được đề cập có thể được trình bày ngắn gọn như sau:
Bạn nên lưu trữ kết quả phản ánh cuối cùng với tư cách là đại biểu chứa hàm đã biên dịch. Việc lưu vào bộ đệm tất cả các đối tượng cần thiết với thông tin loại trong các trường thuộc loại của bạn, nhân viên, được lưu trữ bên ngoài đối tượng cũng rất hợp lý.

Có logic trong việc này. Cảm giác thông thường cho chúng ta biết rằng nếu một cái gì đó có thể được biên dịch và lưu vào bộ nhớ đệm thì nó nên được thực hiện.

Nhìn về phía trước, cần phải nói rằng bộ đệm khi làm việc với sự phản chiếu có những ưu điểm của nó, ngay cả khi bạn không sử dụng phương pháp biên dịch biểu thức được đề xuất. Thực ra ở đây tôi chỉ đơn giản nhắc lại luận điểm của tác giả bài viết mà tôi đề cập ở trên.

Bây giờ về mã. Hãy xem một ví dụ dựa trên nỗi đau gần đây của tôi mà tôi phải đối mặt trong quá trình sản xuất nghiêm túc của một tổ chức tín dụng nghiêm túc. Tất cả các thực thể đều là hư cấu nên không ai có thể đoán được.

Có một số bản chất. Hãy để có Liên hệ. Có những chữ cái có phần thân được tiêu chuẩn hóa, từ đó trình phân tích cú pháp và bộ cung cấp nước tạo ra các liên hệ giống nhau. Một lá thư được gửi đến, chúng tôi đọc nó, phân tích nó thành các cặp khóa-giá trị, tạo một liên hệ và lưu nó vào cơ sở dữ liệu.

Đó là điều cơ bản. Giả sử một liên hệ có các thuộc tính Tên đầy đủ, Tuổi và Số điện thoại liên hệ. Dữ liệu này được truyền đi trong thư. Doanh nghiệp cũng muốn được hỗ trợ để có thể nhanh chóng thêm các khóa mới để ánh xạ các thuộc tính thực thể thành từng cặp trong nội dung thư. Trong trường hợp ai đó mắc lỗi đánh máy trong mẫu hoặc nếu trước khi phát hành, cần khẩn trương khởi chạy bản đồ từ đối tác mới, thích ứng với định dạng mới. Sau đó, chúng ta có thể thêm một mối tương quan ánh xạ mới dưới dạng một bản sửa lỗi dữ liệu giá rẻ. Đó là một ví dụ cuộc sống.

Chúng tôi thực hiện, tạo ra các thử nghiệm. Làm.

Tôi sẽ không cung cấp mã: có rất nhiều nguồn và chúng có sẵn trên GitHub thông qua liên kết ở cuối bài viết. Bạn có thể tải chúng, tra tấn chúng đến mức không thể nhận ra và đo lường chúng, vì nó sẽ ảnh hưởng đến trường hợp của bạn. Tôi sẽ chỉ đưa ra mã của hai phương pháp mẫu để phân biệt máy cấp nước được cho là nhanh và máy cấp nước được cho là chậm.

Logic như sau: phương thức mẫu nhận các cặp được tạo bởi logic phân tích cú pháp cơ bản. Lớp LINQ là trình phân tích cú pháp và logic cơ bản của trình phân tích cú pháp, đưa ra yêu cầu tới bối cảnh cơ sở dữ liệu và so sánh các khóa với các cặp từ trình phân tích cú pháp (đối với các hàm này, có mã không có LINQ để so sánh). Tiếp theo, các cặp được chuyển sang phương thức hydrat hóa chính và giá trị của các cặp được đặt thành thuộc tính tương ứng của thực thể.

“Nhanh” (Tiền tố nhanh trong điểm chuẩn):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

Như chúng ta có thể thấy, một bộ sưu tập tĩnh với các thuộc tính setter được sử dụng - các lambda được biên dịch gọi thực thể setter. Được tạo bởi đoạn mã sau:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Nói chung là rõ ràng. Chúng tôi duyệt qua các thuộc tính, tạo đại biểu cho chúng để gọi setters và lưu chúng. Sau đó chúng tôi gọi khi cần thiết.

“Chậm” (Tiền tố Chậm trong điểm chuẩn):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

Ở đây chúng tôi ngay lập tức bỏ qua các thuộc tính và gọi trực tiếp SetValue.

Để rõ ràng và mang tính tham khảo, tôi đã triển khai một phương pháp đơn giản ghi trực tiếp các giá trị của các cặp tương quan của chúng vào các trường thực thể. Tiền tố - Hướng dẫn sử dụng.

Bây giờ hãy lấy BenchmarkDotNet và kiểm tra hiệu suất. Và đột nhiên... (spoiler - đây không phải là kết quả chính xác, chi tiết ở bên dưới)

Bài viết không thành công về việc tăng tốc phản xạ

Chúng ta thấy gì ở đây? Các phương thức mang tiền tố Nhanh một cách thành công hóa ra lại chậm hơn trong hầu hết các lần vượt qua so với các phương thức có tiền tố Chậm. Điều này đúng cho cả việc phân bổ và tốc độ làm việc. Mặt khác, việc triển khai ánh xạ đẹp mắt và tinh tế bằng các phương pháp LINQ dành cho việc này bất cứ khi nào có thể, ngược lại, sẽ làm giảm đáng kể năng suất. Sự khác biệt là về thứ tự. Xu hướng không thay đổi với số lần vượt qua khác nhau. Sự khác biệt duy nhất là ở quy mô. Với LINQ, tốc độ chậm hơn 4 - 200 lần, có nhiều rác hơn trên cùng một quy mô.

CẬP NHẬT

Tôi không tin vào mắt mình, nhưng quan trọng hơn, đồng nghiệp của chúng tôi cũng không tin vào mắt mình hay mã của tôi - Dmitry Tikhonov 0x1000000. Sau khi kiểm tra kỹ giải pháp của tôi, anh ấy đã phát hiện một cách xuất sắc và chỉ ra một lỗi mà tôi đã bỏ sót do một số thay đổi trong quá trình triển khai, từ đầu đến cuối. Sau khi sửa lỗi tìm thấy trong thiết lập Moq, tất cả các kết quả đều ổn. Theo kết quả kiểm tra lại, xu hướng chính không thay đổi - LINQ vẫn ảnh hưởng đến hiệu suất nhiều hơn là phản ánh. Tuy nhiên, thật tuyệt khi công việc biên dịch Biểu thức không được thực hiện một cách vô ích và kết quả có thể nhìn thấy được cả về thời gian phân bổ và thời gian thực hiện. Lần khởi chạy đầu tiên, khi các trường tĩnh được khởi tạo, phương thức “nhanh” đương nhiên sẽ chậm hơn, nhưng sau đó tình hình sẽ thay đổi.

Đây là kết quả của cuộc kiểm tra lại:

Bài viết không thành công về việc tăng tốc phản xạ

Kết luận: khi sử dụng sự phản ánh trong doanh nghiệp, không cần thiết phải dùng đến thủ thuật - LINQ sẽ ngốn năng suất hơn. Tuy nhiên, trong các phương thức tải cao yêu cầu tối ưu hóa, bạn có thể lưu phản ánh dưới dạng trình khởi tạo và trình biên dịch ủy quyền, sau đó sẽ cung cấp logic “nhanh”. Bằng cách này, bạn có thể duy trì cả tính linh hoạt của phản ánh và tốc độ của ứng dụng.

Mã điểm chuẩn có sẵn ở đây. Bất cứ ai cũng có thể kiểm tra lại lời nói của tôi:
HabraPhản ÁnhKiểm Tra

Tái bút: mã trong các thử nghiệm sử dụng IoC và trong các điểm chuẩn, mã này sử dụng cấu trúc rõ ràng. Thực tế là trong lần triển khai cuối cùng, tôi đã loại bỏ tất cả các yếu tố có thể ảnh hưởng đến hiệu suất và khiến kết quả bị nhiễu.

PPS: Cảm ơn người dùng Dmitry Tikhonov @0x1000000 vì đã phát hiện ra lỗi của tôi khi thiết lập Moq, điều này đã ảnh hưởng đến các phép đo đầu tiên. Nếu độc giả nào có đủ nghiệp lực thì hãy like. Người đàn ông dừng lại, người đàn ông đọc, người đàn ông kiểm tra lại và chỉ ra chỗ sai. Tôi nghĩ điều này đáng được tôn trọng và cảm thông.

PPPS: cảm ơn người đọc tỉ mỉ đã tìm hiểu kỹ về kiểu dáng và thiết kế. Tôi ủng hộ sự đồng nhất và thuận tiện. Tính ngoại giao của bài thuyết trình còn nhiều điều đáng mong đợi, nhưng tôi đã tính đến những lời chỉ trích. Tôi yêu cầu đạn.

Nguồn: www.habr.com

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