Bạn có cảm thấy có sự đánh đổi giữa việc viết mã định hướng đối tượng "đẹp" và viết mã độ trễ thấp rất nhanh không? Ví dụ, tránh các chức năng ảo trong C ++ / chi phí hoạt động của đa hình, v.v., việc viết lại mã có vẻ khó chịu, nhưng rất nhanh, v.v.?
Tôi làm việc trong một lĩnh vực tập trung nhiều hơn vào thông lượng hơn độ trễ, nhưng nó rất quan trọng về hiệu suất và tôi sẽ nói "sorta" .
Tuy nhiên, một vấn đề là rất nhiều người hiểu sai về hiệu suất của họ. Người mới thường hiểu sai về mọi thứ và toàn bộ mô hình khái niệm "chi phí tính toán" của họ cần phải làm lại, chỉ có sự phức tạp về thuật toán là điều duy nhất họ có thể làm đúng. Trung gian nhận được rất nhiều điều sai. Các chuyên gia nhận được một số điều sai.
Đo lường bằng các công cụ chính xác có thể cung cấp các số liệu như lỗi bộ nhớ cache và dự đoán sai chi nhánh là điều giúp mọi người ở bất kỳ cấp độ chuyên môn nào trong lĩnh vực này kiểm tra.
Đo lường cũng là những gì chỉ ra những gì không tối ưu hóa . Các chuyên gia thường dành ít thời gian để tối ưu hóa hơn người mới, vì họ tối ưu hóa các điểm nóng đo được thực sự và không cố gắng tối ưu hóa các cú đâm hoang dã trong bóng tối dựa trên linh cảm về những gì có thể chậm (ở dạng cực đoan, có thể cám dỗ một người để tối ưu hóa vi mô về mọi dòng khác trong cơ sở mã).
Thiết kế cho hiệu suất
Bên cạnh đó, chìa khóa để thiết kế cho hiệu suất đến từ phần thiết kế , như trong thiết kế giao diện. Một trong những vấn đề với thiếu kinh nghiệm là có xu hướng thay đổi sớm các số liệu thực hiện tuyệt đối, như chi phí của một cuộc gọi hàm gián tiếp trong một số bối cảnh tổng quát, như thể chi phí (được hiểu rõ hơn theo nghĩa tức thời từ điểm tối ưu hóa của quan điểm chứ không phải là một quan điểm phân nhánh) là một lý do để tránh nó trong toàn bộ cơ sở mã.
Chi phí tương đối . Mặc dù có một chi phí cho một cuộc gọi chức năng gián tiếp, ví dụ, tất cả các chi phí là tương đối. Nếu bạn trả một lần chi phí đó để gọi một chức năng lặp qua hàng triệu yếu tố, thì việc lo lắng về chi phí này cũng giống như việc bạn mất hàng giờ đồng hồ để mua một sản phẩm tỷ đô, chỉ để kết luận không mua sản phẩm đó vì nó là một xu quá đắt
Thiết kế giao diện Coarser
Khía cạnh thiết kế giao diện của hiệu suất thường tìm kiếm sớm hơn để đẩy các chi phí này đến mức thô hơn. Ví dụ, thay vì trả chi phí trừu tượng thời gian chạy cho một hạt, chúng ta có thể đẩy chi phí đó đến mức của hệ thống / bộ phát hạt, biến hiệu quả của hạt thành một chi tiết thực hiện và / hoặc đơn giản là dữ liệu thô của bộ sưu tập hạt này.
Vì vậy, thiết kế hướng đối tượng không phải không tương thích với thiết kế cho hiệu suất (dù là độ trễ hay thông lượng), nhưng có thể có những cám dỗ trong ngôn ngữ tập trung vào nó để mô hình hóa các đối tượng dạng hạt ngày càng trẻ hơn và ở đó trình tối ưu hóa mới nhất không thể Cứu giúp. Nó không thể thực hiện những việc như kết hợp một lớp đại diện cho một điểm duy nhất theo cách mang lại một biểu diễn SoA hiệu quả cho các mẫu truy cập bộ nhớ của phần mềm. Một tập hợp các điểm với thiết kế giao diện được mô hình hóa ở mức độ thô mang lại cơ hội đó và cho phép lặp lại theo hướng ngày càng nhiều giải pháp tối ưu hơn khi cần. Thiết kế như vậy được thiết kế cho bộ nhớ số lượng lớn *.
* Lưu ý tập trung vào bộ nhớ ở đây và không phải dữ liệu , vì làm việc trong các khu vực quan trọng về hiệu năng trong một thời gian dài sẽ có xu hướng thay đổi cách nhìn của bạn về các loại dữ liệu và cấu trúc dữ liệu và xem cách chúng kết nối với bộ nhớ. Cây tìm kiếm nhị phân không còn trở nên đơn thuần về độ phức tạp logarit trong các trường hợp như các khối bộ nhớ không thân thiện và bộ nhớ cache không thân thiện cho các nút cây trừ khi được hỗ trợ bởi bộ cấp phát cố định. Khung nhìn không loại bỏ độ phức tạp thuật toán, nhưng nó thấy nó không còn độc lập với bố cục bộ nhớ. Người ta cũng bắt đầu thấy các công việc lặp đi lặp lại nhiều hơn về các lần lặp truy cập bộ nhớ. *
Rất nhiều thiết kế quan trọng về hiệu năng thực sự có thể rất tương thích với khái niệm thiết kế giao diện cấp cao, dễ hiểu cho con người và sử dụng. Sự khác biệt là "mức cao" trong ngữ cảnh này sẽ là tập hợp số lượng lớn bộ nhớ, một giao diện được mô hình hóa cho các bộ sưu tập dữ liệu có khả năng lớn và với việc triển khai dưới mui xe có thể ở mức khá thấp. Một sự tương tự trực quan có thể là một chiếc xe thực sự thoải mái, dễ lái và dễ điều khiển và rất an toàn khi đi ở tốc độ âm thanh, nhưng nếu bạn bật mui xe, có rất ít con quỷ thở lửa bên trong.
Với thiết kế thô hơn cũng có xu hướng trở thành một cách dễ dàng hơn để cung cấp các kiểu khóa hiệu quả hơn và khai thác tính song song trong mã (đa luồng là một chủ đề toàn diện mà tôi sẽ bỏ qua ở đây).
Bộ nhớ
Một khía cạnh quan trọng của lập trình độ trễ thấp có lẽ sẽ là một sự kiểm soát rất rõ ràng đối với bộ nhớ để cải thiện tính cục bộ của tham chiếu cũng như tốc độ chung của việc phân bổ và giải phóng bộ nhớ. Một bộ nhớ gộp cấp phát tùy chỉnh thực sự lặp lại cùng một kiểu tư duy thiết kế mà chúng tôi đã mô tả. Nó được thiết kế cho số lượng lớn ; nó được thiết kế ở mức độ thô. Nó phân bổ bộ nhớ trong các khối lớn và gộp bộ nhớ đã được phân bổ thành các khối nhỏ.
Ý tưởng này hoàn toàn giống với việc đẩy những thứ tốn kém (phân bổ một đoạn bộ nhớ theo phân bổ mục đích chung, ví dụ) đến mức thô hơn và thô hơn. Nhóm bộ nhớ được thiết kế để xử lý hàng loạt bộ nhớ .
Loại hệ thống Bộ nhớ tách riêng
Một trong những khó khăn với thiết kế hướng đối tượng chi tiết trong bất kỳ ngôn ngữ nào là nó thường muốn giới thiệu rất nhiều kiểu và cấu trúc dữ liệu do người dùng định nghĩa. Những loại đó sau đó có thể muốn được phân bổ theo từng phần nhỏ nếu chúng được phân bổ động.
Một ví dụ phổ biến trong C ++ sẽ là cho các trường hợp bắt buộc phải có tính đa hình, trong đó sự cám dỗ tự nhiên là phân bổ từng thể hiện của một lớp con so với cấp phát bộ nhớ cho mục đích chung.
Điều này cuối cùng phá vỡ các bố cục bộ nhớ liền kề có thể thành các bit và mảnh nhỏ của nó nằm rải rác trong phạm vi địa chỉ, điều này dẫn đến nhiều lỗi trang hơn và lỗi bộ nhớ cache.
Các lĩnh vực đòi hỏi độ trễ thấp nhất, không nói lắp, đáp ứng xác định có lẽ là một nơi mà các điểm nóng không phải lúc nào cũng sôi sục đến một nút cổ chai, nơi mà sự thiếu hiệu quả thực sự có thể thực sự là "tích lũy" (điều mà nhiều người tưởng tượng xảy ra không chính xác với một trình lược tả để giữ chúng trong tầm kiểm soát, nhưng trong các trường điều khiển độ trễ, thực sự có thể có một số trường hợp hiếm hoi khi thiếu hiệu quả tích lũy nhỏ). Và rất nhiều lý do phổ biến nhất cho sự tích lũy như vậy có thể là đây: sự phân bổ quá mức của các khối bộ nhớ tuổi teen ở khắp mọi nơi.
Trong các ngôn ngữ như Java, có thể hữu ích khi sử dụng nhiều mảng dữ liệu cũ đơn giản hơn khi có thể cho các khu vực tắc nghẽn (các khu vực được xử lý trong các vòng lặp chặt chẽ) như một mảng int
(nhưng vẫn nằm sau giao diện cấp cao cồng kềnh) thay vì nói , một đối tượng do ArrayList
người dùng định nghĩa Integer
. Điều này tránh sự phân biệt bộ nhớ thường đi kèm với cái sau. Trong C ++, chúng ta không phải làm suy giảm cấu trúc nhiều như vậy nếu các mẫu phân bổ bộ nhớ của chúng ta hiệu quả, vì các kiểu do người dùng xác định có thể được phân bổ liền kề ở đó và ngay cả trong bối cảnh của một thùng chứa chung.
Bộ nhớ hợp nhất trở lại với nhau
Một giải pháp ở đây là tiếp cận một công cụ cấp phát tùy chỉnh cho các loại dữ liệu đồng nhất và thậm chí có thể trên các loại dữ liệu đồng nhất. Khi các kiểu dữ liệu nhỏ và cấu trúc dữ liệu được làm phẳng thành bit và byte trong bộ nhớ, chúng có tính chất đồng nhất (mặc dù có một số yêu cầu căn chỉnh khác nhau). Khi chúng ta không nhìn chúng từ một tư duy tập trung vào bộ nhớ, hệ thống các ngôn ngữ lập trình "muốn" phân tách / tách biệt các vùng bộ nhớ có khả năng tiếp giáp nhau thành các khối nhỏ rải rác.
Ngăn xếp sử dụng tiêu điểm tập trung vào bộ nhớ này để tránh điều này và có khả năng lưu trữ bất kỳ kết hợp hỗn hợp nào có thể có của các thể hiện loại do người dùng xác định bên trong nó. Sử dụng ngăn xếp nhiều hơn là một ý tưởng tuyệt vời khi có thể vì phần trên của nó hầu như luôn luôn nằm trong một dòng bộ đệm, nhưng chúng ta cũng có thể thiết kế các bộ cấp phát bộ nhớ bắt chước một số đặc điểm này mà không có mẫu LIFO, kết hợp bộ nhớ giữa các loại dữ liệu khác nhau chunk thậm chí cho các mô hình phân bổ bộ nhớ và phân bổ bộ nhớ phức tạp hơn.
Phần cứng hiện đại được thiết kế để đạt đến đỉnh cao khi xử lý các khối bộ nhớ liền kề (liên tục truy cập vào cùng một dòng bộ đệm, cùng một trang, ví dụ). Từ khóa có sự liên tục, vì điều này chỉ có lợi nếu có dữ liệu quan tâm xung quanh. Vì vậy, rất nhiều chìa khóa (nhưng cũng khó khăn) để thực hiện là kết hợp các khối bộ nhớ tách biệt lại với nhau thành các khối liền kề được truy cập toàn bộ (tất cả dữ liệu xung quanh có liên quan) trước khi bị trục xuất. Hệ thống loại phong phú gồm các loại đặc biệt do người dùng xác định trong ngôn ngữ lập trình có thể là trở ngại lớn nhất ở đây, nhưng chúng ta luôn có thể tiếp cận và giải quyết vấn đề thông qua bộ cấp phát tùy chỉnh và / hoặc thiết kế cồng kềnh khi thích hợp.
Xấu xí
"Xấu xí" thật khó nói. Đó là một số liệu chủ quan và ai đó làm việc trong một lĩnh vực rất quan trọng về hiệu suất sẽ bắt đầu thay đổi ý tưởng về "vẻ đẹp" của họ thành một định hướng dữ liệu nhiều hơn và tập trung vào các giao diện xử lý hàng loạt.
Nguy hiểm
"Nguy hiểm" có thể dễ dàng hơn. Nói chung, hiệu suất có xu hướng muốn đạt tới mã cấp thấp hơn. Việc thực hiện cấp phát bộ nhớ, chẳng hạn, là không thể nếu không tiếp cận bên dưới các kiểu dữ liệu và làm việc ở mức nguy hiểm của các bit và byte thô. Kết quả là, nó có thể giúp tăng sự tập trung vào quy trình thử nghiệm cẩn thận trong các hệ thống con quan trọng về hiệu năng này, mở rộng quy mô của thử nghiệm với mức độ tối ưu hóa được áp dụng.
sắc đẹp, vẻ đẹp
Tuy nhiên, tất cả điều này sẽ ở cấp độ chi tiết thực hiện. Trong cả một tư duy quan trọng về hiệu suất và quy mô lớn kỳ cựu, "vẻ đẹp" có xu hướng chuyển sang các thiết kế giao diện hơn là chi tiết triển khai. Nó trở thành một ưu tiên cao hơn theo cấp số nhân để tìm kiếm các giao diện "đẹp", có thể sử dụng, an toàn, hiệu quả thay vì triển khai do sự phá vỡ khớp nối và xếp tầng có thể xảy ra khi thay đổi thiết kế giao diện. Việc thực hiện có thể được hoán đổi bất cứ lúc nào. Chúng tôi thường lặp lại theo hướng hiệu suất khi cần thiết, và như được chỉ ra bởi các phép đo. Chìa khóa với thiết kế giao diện là mô hình hóa ở mức đủ thô để chừa chỗ cho các lần lặp như vậy mà không phá vỡ toàn bộ hệ thống.
Trên thực tế, tôi sẽ đề nghị rằng một cựu chiến binh tập trung vào phát triển quan trọng về hiệu suất thường sẽ có xu hướng tập trung chủ yếu vào an toàn, kiểm tra, bảo trì, chỉ là môn đệ của SE nói chung, vì một cơ sở mã hóa quy mô lớn có số lượng hiệu suất hệ thống con -critical (hệ thống hạt, thuật toán xử lý hình ảnh, xử lý video, phản hồi âm thanh, raytracers, động cơ lưới, v.v.) sẽ cần hết sức chú ý đến công nghệ phần mềm để tránh chìm trong cơn ác mộng bảo trì. Không phải ngẫu nhiên mà thường các sản phẩm hiệu quả đáng kinh ngạc nhất ngoài kia cũng có thể có số lượng lỗi ít nhất.
TL; DR
Dù sao, đó là việc tôi tham gia vào chủ đề này, từ các ưu tiên trong các lĩnh vực quan trọng về hiệu suất thực sự, điều gì có thể làm giảm độ trễ và gây ra sự thiếu hiệu quả nhỏ và điều thực sự tạo nên "vẻ đẹp" (khi nhìn vào mọi thứ một cách hiệu quả nhất).