Các lớp niêm phong có thực sự mang lại lợi ích hiệu suất?


140

Tôi đã bắt gặp rất nhiều mẹo tối ưu hóa nói rằng bạn nên đánh dấu các lớp của mình là niêm phong để có thêm lợi ích hiệu suất.

Tôi đã chạy một số thử nghiệm để kiểm tra sự khác biệt hiệu suất và không tìm thấy. Tôi có làm điều gì sai? Tôi có thiếu trường hợp các lớp niêm phong sẽ cho kết quả tốt hơn không?

Có ai chạy thử nghiệm và thấy một sự khác biệt?

Giúp tôi học :)


8
Tôi không nghĩ rằng các lớp kín được dự định để tăng một cách hoàn hảo. Thực tế là họ có thể là ngẫu nhiên. Ngoài ra, hãy lập hồ sơ cho ứng dụng của bạn sau khi bạn tái cấu trúc nó để sử dụng các lớp niêm phong và xác định xem nó có đáng để bỏ công sức hay không. Việc khóa khả năng mở rộng của bạn để thực hiện tối ưu hóa vi mô không cần thiết sẽ khiến bạn mất nhiều thời gian. Tất nhiên, nếu bạn lập hồ sơ, và nó cho phép bạn đạt điểm chuẩn hoàn hảo của bạn (chứ không phải hoàn hảo vì lợi ích của sự hoàn hảo), thì bạn có thể đưa ra quyết định với tư cách là một đội nếu nó xứng đáng với số tiền bỏ ra. Nếu bạn có các lớp niêm phong vì những lý do không hoàn hảo, thì hãy giữ em :)
Merlyn Morgan-Graham

1
Bạn đã thử với sự phản ánh? Tôi đã đọc ở đâu đó rằng việc khởi tạo bằng phản xạ nhanh hơn với các lớp được niêm phong
vào

Theo hiểu biết của tôi thì không có. Bịt kín là có một lý do khác - để chặn khả năng mở rộng, có thể hữu ích / cần thiết trong rất nhiều trường hợp. Tối ưu hóa hiệu suất không phải là một mục tiêu ở đây.
TomTom

... nhưng nếu bạn nghĩ về trình biên dịch: nếu lớp của bạn được niêm phong, bạn sẽ biết địa chỉ của một phương thức bạn gọi trên lớp của mình tại thời gian biên dịch. Nếu lớp của bạn không được niêm phong, bạn phải giải quyết phương thức trong thời gian chạy vì bạn có thể cần phải gọi một ghi đè. Nó chắc chắn sẽ không đáng kể, nhưng tôi có thể thấy rằng sẽ có một số khác biệt.
Dan Puzey

Có, nhưng loại đó không chuyển thành lợi ích thực sự, như OP đã chỉ ra. Sự khác biệt về kiến ​​trúc là / có thể phù hợp hơn rất nhiều.
TomTom

Câu trả lời:


60

JITter đôi khi sẽ sử dụng các cuộc gọi không ảo đến các phương thức trong các lớp kín vì không có cách nào chúng có thể được mở rộng hơn nữa.

Có các quy tắc phức tạp liên quan đến kiểu gọi, ảo / không ảo và tôi không biết tất cả chúng vì vậy tôi thực sự không thể phác thảo chúng cho bạn, nhưng nếu bạn google cho các lớp niêm phong và phương thức ảo, bạn có thể tìm thấy một số bài viết về chủ đề này.

Lưu ý rằng bất kỳ loại lợi ích hiệu suất nào bạn sẽ có được từ mức tối ưu hóa này phải được coi là biện pháp cuối cùng, luôn luôn tối ưu hóa ở cấp thuật toán trước khi bạn tối ưu hóa ở cấp độ mã.

Đây là một liên kết đề cập đến điều này: Chuyển đổi từ khóa kín


2
liên kết 'lan man' thú vị ở chỗ nó nghe có vẻ tốt về mặt kỹ thuật nhưng thực tế lại vô nghĩa. Đọc các bình luận về bài viết để biết thêm. Tóm tắt: 3 lý do được đưa ra là Phiên bản, Hiệu suất và Bảo mật / Dự đoán - [xem bình luận tiếp theo]
Steven A. Lowe

1
[tiếp tục] Phiên bản chỉ áp dụng khi không có lớp con, duh, nhưng mở rộng đối số này cho mọi lớp và đột nhiên bạn không có sự kế thừa và đoán xem, ngôn ngữ không còn hướng đối tượng (mà chỉ dựa trên đối tượng)! [xem tiếp]
Steven A. Lowe

3
[tiếp tục] ví dụ về hiệu suất là một trò đùa: tối ưu hóa một cuộc gọi phương thức ảo; Tại sao một lớp kín có một phương thức ảo ngay từ đầu vì nó không thể được phân lớp? Cuối cùng, đối số Bảo mật / Dự đoán rõ ràng rất nguy hiểm: 'bạn không thể sử dụng nó để nó an toàn / có thể dự đoán được'. CƯỜI LỚN!
Steven A. Lowe

16
@Steven A. Lowe - Tôi nghĩ điều mà Jeffrey Richter đang cố nói theo cách hơi vòng vo là nếu bạn rời khỏi lớp học của mình, bạn cần suy nghĩ về cách các lớp dẫn xuất có thể / sẽ sử dụng nó và nếu bạn không có thời gian hoặc thiên hướng để làm điều này đúng cách, sau đó niêm phong nó vì nó ít có khả năng gây ra thay đổi phá vỡ mã của người khác trong tương lai. Điều đó không vô nghĩa chút nào, đó là lẽ thường.
Greg Beech

6
Một lớp niêm phong có thể có một phương thức ảo vì nó có thể xuất phát từ một lớp khai báo nó. Khi bạn, sau đó, khai báo một biến của lớp con cháu bị niêm phong và gọi phương thức đó, trình biên dịch có thể phát ra một cuộc gọi trực tiếp đến việc thực hiện đã biết, vì nó không có cách nào khác với điều này có thể khác với những gì đã biết - at-compile-time vtable cho lớp đó. Đối với niêm phong / không niêm phong, đó là một cuộc thảo luận khác và tôi đồng ý với lý do khiến các lớp được niêm phong theo mặc định.
Lasse V. Karlsen

143

Câu trả lời là không, các lớp niêm phong không hoạt động tốt hơn không niêm phong.

Vấn đề thuộc về các mã op callvs callvirtIL. Callnhanh hơn callvirtcallvirtchủ yếu được sử dụng khi bạn không biết liệu đối tượng đã được phân lớp hay chưa. Vì vậy, mọi người cho rằng nếu bạn niêm phong một lớp, tất cả các mã op sẽ thay đổi từ calvirtssang callsvà sẽ nhanh hơn.

Thật không may callvirt, những thứ khác cũng làm cho nó hữu ích, như kiểm tra các tài liệu tham khảo null. Điều này có nghĩa là ngay cả khi một lớp được niêm phong, tham chiếu vẫn có thể là null và do đó callvirtcần có một tham chiếu . Bạn có thể giải quyết vấn đề này (không cần phải niêm phong lớp học), nhưng nó trở nên hơi vô nghĩa.

Cấu trúc sử dụng callvì chúng không thể được phân lớp và không bao giờ là null.

Xem câu hỏi này để biết thêm thông tin:

Gọi và gọi


5
AFAIK, các trường hợp callđược sử dụng là: trong tình huống new T().Method(), cho structcác phương thức, cho các cuộc gọi không ảo đến virtualcác phương thức (như base.Virtual()) hoặc cho staticcác phương thức. Ở mọi nơi khác sử dụng callvirt.
porges

1
Uhh ... Tôi nhận ra điều này đã cũ, nhưng điều này không hoàn toàn đúng ... chiến thắng lớn với các lớp niêm phong là khi Trình tối ưu hóa JIT có thể thực hiện cuộc gọi ... trong trường hợp đó, lớp niêm phong có thể là một chiến thắng lớn .
Brian Kennedy

5
Tại sao câu trả lời này là sai. Từ thay đổi Mono: "Tối ưu hóa ảo hóa cho các lớp và phương thức niêm phong, cải thiện hiệu suất pystone IronPython 2.0 lên 4%. Các chương trình khác có thể mong đợi cải thiện tương tự [Rodrigo]." Các lớp kín có thể cải thiện hiệu suất, nhưng như mọi khi, nó phụ thuộc vào tình huống.
Smilediver

1
@Smilediver Nó có thể cải thiện hiệu suất, nhưng chỉ khi bạn có JIT xấu (không biết ngày nay JITs tốt như thế nào - trước đây thường khá tệ trong vấn đề đó). Ví dụ, Hotspot sẽ thực hiện các cuộc gọi ảo nội tuyến và mở rộng lại sau này nếu cần - do đó bạn chỉ phải trả chi phí bổ sung nếu bạn thực sự phân lớp lớp (và thậm chí sau đó không nhất thiết).
Voo

1
-1 JIT không nhất thiết phải tạo cùng một mã máy cho cùng các mã op IL. Kiểm tra Null và cuộc gọi ảo là các bước trực tiếp và riêng biệt của cuộc gọi. Trong trường hợp các loại niêm phong, trình biên dịch JIT vẫn có thể tối ưu hóa một phần của callvirt. Điều tương tự cũng xảy ra khi trình biên dịch JIT có thể đảm bảo rằng một tham chiếu sẽ không có giá trị.
đúng vào

29

Cập nhật: Kể từ .NET Core 2.0 và .NET Desktop 4.7.1, CLR hiện hỗ trợ ảo hóa. Nó có thể thực hiện các phương thức trong các lớp kín và thay thế các cuộc gọi ảo bằng các cuộc gọi trực tiếp - và nó cũng có thể thực hiện điều này cho các lớp không được niêm phong nếu nó có thể tìm ra nó an toàn để làm như vậy.

Trong trường hợp như vậy (một lớp niêm phong mà CLR không thể phát hiện là an toàn để phát hiện), một lớp kín thực sự sẽ cung cấp một số loại lợi ích hiệu suất.

Điều đó nói rằng, tôi sẽ không nghĩ rằng đáng để lo lắng trừ khi bạn đã lập hồ sơ mã và xác định rằng bạn đang ở trong một con đường đặc biệt nóng bỏng được gọi là hàng triệu lần, hoặc đại loại như thế:

https://bloss.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


Câu trả lời gốc:

Tôi đã thực hiện chương trình thử nghiệm sau, và sau đó dịch ngược nó bằng Reflector để xem mã MSIL được phát ra.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

Trong mọi trường hợp, trình biên dịch C # (Visual studio 2010 trong cấu hình phát hành bản phát hành) phát ra MSIL giống hệt nhau, như sau:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

Lý do được trích dẫn mà mọi người nói rằng niêm phong cung cấp lợi ích hiệu năng là trình biên dịch biết rằng lớp không bị ghi đè, và do đó có thể sử dụng callthay vìcallvirt không phải kiểm tra ảo, v.v. Như đã chứng minh ở trên, đây không phải là thật.

Suy nghĩ tiếp theo của tôi là mặc dù MSIL giống hệt nhau, nhưng có lẽ trình biên dịch JIT xử lý các lớp niêm phong khác nhau?

Tôi đã chạy một bản phát hành dưới trình gỡ lỗi của studio trực quan và xem đầu ra x86 được dịch ngược. Trong cả hai trường hợp, mã x86 đều giống hệt nhau, ngoại trừ tên lớp và địa chỉ bộ nhớ hàm (tất nhiên phải khác nhau). Nó đây rồi

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

Sau đó tôi nghĩ có lẽ chạy theo trình gỡ lỗi khiến nó thực hiện tối ưu hóa ít tích cực hơn?

Sau đó, tôi đã chạy một bản phát hành độc lập có thể thực thi được bên ngoài bất kỳ môi trường gỡ lỗi nào và đã sử dụng WinDBG + SOS để đột nhập sau khi chương trình hoàn thành và xem sự phân tách mã x86 do JIT biên dịch.

Như bạn có thể thấy từ đoạn mã bên dưới, khi chạy bên ngoài trình gỡ lỗi, trình biên dịch JIT mạnh hơn và nó đã đưa WriteItphương thức vào thẳng trình gọi. Tuy nhiên, điều quan trọng là nó giống hệt nhau khi gọi một lớp kín và không niêm phong. Không có sự khác biệt nào giữa một lớp kín hoặc không kín.

Đây là khi gọi một lớp bình thường:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Vs một lớp kín:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

Đối với tôi, điều này cung cấp bằng chứng chắc chắn rằng không thể có bất kỳ cải thiện hiệu suất nào giữa các phương thức gọi trên các lớp kín và không kín ... Tôi nghĩ rằng tôi đang hạnh phúc :-)


Bất kỳ lý do tại sao bạn đăng câu trả lời này hai lần thay vì đóng cái kia như một bản sao? Chúng trông giống như bản sao hợp lệ với tôi mặc dù đây không phải là khu vực của tôi.
Flexo

1
Điều gì xảy ra nếu các phương thức dài hơn (hơn 32 byte mã IL) để tránh nội tuyến và xem thao tác cuộc gọi nào sẽ được sử dụng? Nếu nó được đặt nội tuyến, bạn không thấy cuộc gọi nên không thể đánh giá hiệu ứng.
ygoe

Tôi bối rối: tại sao có callvirtnhững phương thức này không ảo để bắt đầu?
quái dị

@freakish Tôi không thể nhớ mình đã thấy cái này ở đâu, nhưng tôi đọc rằng CLR được sử dụng callvirtcho các phương thức được niêm phong bởi vì nó vẫn phải kiểm tra null đối tượng trước khi gọi lệnh phương thức, và một khi bạn tính đến nó, bạn cũng có thể sử dụng callvirt. Để loại bỏ callvirtvà chỉ cần nhảy trực tiếp, họ cần phải sửa đổi C # để cho phép ((string)null).methodCall()giống như C ++, hoặc họ cần phải chứng minh một cách tĩnh rằng đối tượng không có giá trị (điều họ có thể làm, nhưng không bận tâm)
Orion Edwards

1
Đạo cụ cho đi đến nỗ lực của đào xuống mức mã máy, nhưng phải rất phát biểu làm cẩn thận như 'này cung cấp bằng chứng vững chắc rằng có không thể có bất kỳ cải thiện hiệu suất. Những gì bạn đã chỉ ra là đối với một kịch bản cụ thể, không có sự khác biệt trong đầu ra gốc. Đó là một điểm dữ liệu và người ta không thể cho rằng nó khái quát cho tất cả các kịch bản. Đối với người mới bắt đầu, các lớp của bạn không định nghĩa bất kỳ phương thức ảo nào, vì vậy các cuộc gọi ảo hoàn toàn không bắt buộc.
Matt Craig

24

Theo tôi biết, không có gì đảm bảo lợi ích hiệu suất. Nhưng có một cơ hội để giảm hình phạt hiệu suất trong một số điều kiện cụ thể với phương pháp niêm phong. (lớp niêm phong làm cho tất cả các phương thức được niêm phong.)

Nhưng nó phụ thuộc vào môi trường thực thi và thực hiện trình biên dịch.


Chi tiết

Nhiều CPU hiện đại sử dụng cấu trúc đường ống dài để tăng hiệu suất. Vì CPU nhanh hơn đáng kể so với bộ nhớ, CPU phải tìm nạp trước mã từ bộ nhớ để tăng tốc đường ống. Nếu mã không sẵn sàng vào thời điểm thích hợp, các đường ống sẽ không hoạt động.

Có một trở ngại lớn gọi là công văn động phá vỡ sự tối ưu hóa 'tìm nạp trước' này. Bạn có thể hiểu điều này chỉ là một nhánh có điều kiện.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

CPU không thể tìm nạp mã tiếp theo để thực thi trong trường hợp này vì vị trí mã tiếp theo không xác định cho đến khi điều kiện được giải quyết. Vì vậy, điều này làm cho nguy hiểm gây ra nhàn rỗi đường ống. Và hiệu suất phạt bởi nhàn rỗi là rất lớn thường xuyên.

Điều tương tự xảy ra trong trường hợp ghi đè phương thức. Trình biên dịch có thể xác định phương thức thích hợp ghi đè cho lệnh gọi phương thức hiện tại, nhưng đôi khi không thể. Trong trường hợp này, phương pháp thích hợp chỉ có thể được xác định khi chạy. Đây cũng là một trường hợp của công văn động và, một lý do chính của các ngôn ngữ được gõ động thường chậm hơn so với các ngôn ngữ được nhập tĩnh.

Một số CPU (bao gồm cả chip x86 của Intel gần đây) sử dụng kỹ thuật được gọi là thực thi đầu cơ để sử dụng đường ống ngay cả trong tình huống. Chỉ cần tìm nạp trước một trong các đường dẫn thực thi. Nhưng tỷ lệ trúng của kỹ thuật này không quá cao. Và đầu cơ thất bại gây ra gian hàng đường ống cũng làm cho hiệu suất rất lớn. (điều này hoàn toàn do thực hiện CPU. Một số CPU di động được gọi là không phải loại tối ưu hóa này để tiết kiệm năng lượng)

Về cơ bản, C # là một ngôn ngữ được biên dịch tĩnh. Nhưng không phải lúc nào cũng vậy. Tôi không biết điều kiện chính xác và điều này hoàn toàn phụ thuộc vào việc thực hiện trình biên dịch. Một số trình biên dịch có thể loại bỏ khả năng gửi động bằng cách ngăn chặn phương thức ghi đè nếu phương thức được đánh dấu là sealed. Trình biên dịch ngu ngốc có thể không. Đây là lợi ích hiệu suất của sealed.


Câu trả lời này ( Tại sao xử lý một mảng được sắp xếp nhanh hơn một mảng chưa sắp xếp? ) Đang mô tả dự đoán nhánh tốt hơn rất nhiều.


1
CPU lớp Pentium tìm nạp trực tiếp công văn. Đôi khi chuyển hướng con trỏ hàm nhanh hơn nếu (không thể đoán được) vì lý do này.
Joshua

2
Một lợi thế của chức năng không ảo hoặc kín là chúng có thể được nội tuyến trong nhiều tình huống hơn.
CodeInChaos

4

<off-topic-rant>

Tôi ghê tởm các lớp niêm phong. Ngay cả khi lợi ích hiệu suất là đáng kinh ngạc (mà tôi nghi ngờ), chúng phá hủy mô hình hướng đối tượng bằng cách ngăn chặn tái sử dụng thông qua kế thừa. Ví dụ, lớp Thread được niêm phong. Mặc dù tôi có thể thấy rằng người ta có thể muốn các chủ đề hiệu quả nhất có thể, tôi cũng có thể tưởng tượng các tình huống trong đó việc có thể phân lớp Chủ đề sẽ có lợi ích lớn. Các tác giả của lớp, nếu bạn phải niêm phong các lớp của mình vì lý do "hiệu suất", vui lòng cung cấp ít nhất một giao diện để chúng tôi không phải bọc và thay thế ở mọi nơi mà chúng tôi cần một tính năng mà bạn quên.

Ví dụ: SafeThread phải bọc lớp Thread vì Thread được niêm phong và không có giao diện IThread; SafeThread tự động bẫy các ngoại lệ chưa được xử lý trên các luồng, một thứ hoàn toàn bị thiếu trong lớp Thread. [và không, các sự kiện ngoại lệ chưa được xử lý không nhận các ngoại lệ chưa được xử lý trong các luồng phụ].

</ off-topic-rant>


35
Tôi không niêm phong các lớp học của tôi vì lý do hiệu suất. Tôi niêm phong chúng vì lý do thiết kế. Thiết kế để kế thừa là khó khăn, và nỗ lực đó sẽ bị lãng phí phần lớn thời gian. Tôi hoàn toàn đồng ý về việc cung cấp các giao diện mặc dù - đó là một xa giải pháp vượt trội so với các lớp học mở hộp.
Jon Skeet

6
Đóng gói nói chung là một giải pháp tốt hơn so với thừa kế. Để lấy ví dụ về luồng cụ thể của bạn, các ngoại lệ của bẫy bẫy phá vỡ Nguyên tắc thay thế Liskov vì bạn đã thay đổi hành vi được ghi lại của lớp Thread, vì vậy ngay cả khi bạn có thể rút ra từ nó, sẽ không hợp lý khi nói rằng bạn có thể sử dụng SafeThread ở mọi nơi bạn có thể sử dụng Thread. Trong trường hợp này, bạn nên đóng gói Thread vào một lớp khác có hành vi được ghi thành tài liệu khác mà bạn có thể thực hiện. Đôi khi mọi thứ được niêm phong vì lợi ích của riêng bạn.
Greg Beech

1
@ [Greg Beech]: ý kiến, không thực tế - có thể kế thừa từ Thread để khắc phục sự giám sát ghê gớm trong thiết kế của nó KHÔNG phải là điều xấu ;-) Và tôi nghĩ rằng bạn đang nói quá LSP - tài sản có thể chứng minh được q (x) trong trường hợp này là 'một ngoại lệ chưa được xử lý sẽ phá hủy chương trình' không phải là "tài sản mong muốn" :-)
Steven A. Lowe

1
Không, nhưng tôi đã chia sẻ mã crappy của mình khi tôi cho phép các nhà phát triển khác lạm dụng nội dung của mình bằng cách không niêm phong hoặc cho phép các trường hợp góc. Hầu hết các mã của tôi ngày nay là các xác nhận và các công cụ liên quan đến hợp đồng khác. Và tôi khá cởi mở về việc tôi làm điều này chỉ là một nỗi đau trong ***.
Turing Hoàn thành

2
Kể từ khi chúng tôi đang làm off-topic rants đây, giống như bạn không ưa các lớp niêm phong, tôi không ưa ngoại lệ nuốt. Không có gì tồi tệ hơn khi một cái gì đó đi lên, nhưng chương trình tiếp tục. JavaScript là yêu thích của tôi. Bạn thực hiện thay đổi một số mã và đột nhiên nhấp vào nút hoàn toàn không có gì. Tuyệt quá! ASP.NET và UpdatePanel là một cái khác; Nghiêm túc mà nói, nếu trình xử lý nút của tôi ném nó là một Thỏa thuận lớn và nó cần CRASH, để tôi biết có gì đó cần sửa! Một nút mà không có gì là nhiều vô dụng hơn một nút đó sẽ trả về một màn hình tai nạn!
Roman Starkov

4

Đánh dấu một lớp sealed nên không có tác động hiệu suất.

Có những trường hợp csccó thể phải phát ra callvirtopcode thay vì callopcode. Tuy nhiên, dường như những trường hợp đó rất hiếm.

Và dường như đối với tôi, JIT sẽ có thể phát ra lệnh gọi hàm không ảo tương tự cho callvirtcall, nếu nó biết rằng lớp không có bất kỳ lớp con nào (chưa). Nếu chỉ có một triển khai phương thức tồn tại, sẽ không có điểm nào tải địa chỉ của nó từ một vtable chỉ cần gọi trực tiếp một triển khai. Đối với vấn đề đó, JIT thậm chí có thể nội tuyến hàm.

Đó là một chút đánh cược về phần của JIT, bởi vì nếu một lớp con tải sau đó, JIT sẽ phải vứt bỏ mã máy đó và biên dịch lại mã, phát ra một cuộc gọi ảo thực sự. Tôi đoán là điều này không xảy ra thường xuyên trong thực tế.

(Và vâng, các nhà thiết kế VM thực sự tích cực theo đuổi những chiến thắng hiệu suất nhỏ bé này.)


3

Các lớp kín nên cung cấp một cải tiến hiệu suất. Vì một lớp kín không thể có nguồn gốc, bất kỳ thành viên ảo nào cũng có thể được biến thành thành viên không ảo.

Tất nhiên, chúng ta đang nói về lợi nhuận thực sự nhỏ. Tôi sẽ không đánh dấu một lớp là niêm phong chỉ để cải thiện hiệu suất trừ khi hồ sơ tiết lộ nó là một vấn đề.


Họ nên , nhưng có vẻ như họ không. Nếu CLR có khái niệm về các loại tham chiếu không null, thì một lớp niêm phong thực sự sẽ tốt hơn, vì trình biên dịch có thể phát ra callthay vì callvirt... Tôi cũng thích các loại tham chiếu không null vì nhiều lý do khác ... thở dài :-(
Orion Edwards

3

Tôi coi các lớp "niêm phong" là trường hợp bình thường và tôi LUÔN có lý do để bỏ qua từ khóa "niêm phong".

Những lý do quan trọng nhất đối với tôi là:

a) Kiểm tra thời gian biên dịch tốt hơn (truyền tới các giao diện không được triển khai sẽ được phát hiện tại thời gian biên dịch, không chỉ trong thời gian chạy)

và, lý do hàng đầu:

b) Lạm dụng các lớp học của tôi là không thể theo cách đó

Tôi ước Microsoft sẽ thực hiện "niêm phong" tiêu chuẩn chứ không phải "chưa niêm phong".


Tôi tin rằng "bỏ qua" (bỏ đi) nên là "phát ra" (để sản xuất)?
dùng2864740

2

@Vaibhav, bạn đã thực hiện loại thử nghiệm nào để đo lường hiệu suất?

Tôi đoán người ta sẽ phải sử dụng Rotor và để đi sâu vào CLI và hiểu làm thế nào một lớp kín sẽ cải thiện hiệu suất.

SSCLI (Cánh quạt)
SSCLI: Cơ sở hạ tầng ngôn ngữ chung

Cơ sở hạ tầng ngôn ngữ chung (CLI) là tiêu chuẩn ECMA mô tả cốt lõi của .NET Framework. CLI nguồn chung (SSCLI), còn được gọi là Rotor, là một kho lưu trữ nén mã nguồn để triển khai hoạt động của ECMA CLI và đặc tả ngôn ngữ ECMA C #, công nghệ trung tâm của kiến ​​trúc .NET của Microsoft.


Thử nghiệm bao gồm việc tạo một hệ thống phân cấp lớp, với một số phương thức đang thực hiện công việc giả (phần lớn thao tác chuỗi). Một số phương pháp này là ảo. Họ đã gọi nhau ở đây và ở đó. Sau đó gọi các phương thức này 100, 10000 và 100000 lần ... và đo thời gian đã trôi qua. Sau đó chạy chúng sau khi đánh dấu các lớp là niêm phong. và đo lại. Không có sự khác biệt trong họ.
Vaibhav

2

các lớp được niêm phong sẽ nhanh hơn ít nhất một chút, nhưng đôi khi có thể nhanh hơn ... nếu Trình tối ưu hóa JIT có thể gọi các cuộc gọi nội tuyến có thể là các cuộc gọi ảo. Vì vậy, trong trường hợp có các phương thức được gọi là đủ nhỏ để được nội tuyến, chắc chắn xem xét việc niêm phong lớp.

Tuy nhiên, lý do tốt nhất để niêm phong một lớp là nói rằng "Tôi đã không thiết kế cái này để được thừa kế từ đó, vì vậy tôi sẽ không để bạn bị đốt cháy khi cho rằng nó được thiết kế như vậy, và tôi sẽ không để tự thiêu mình bằng cách bị khóa trong một triển khai vì tôi cho phép bạn xuất phát từ nó. "

Tôi biết một số người ở đây đã nói rằng họ ghét các lớp niêm phong vì họ muốn có cơ hội rút ra từ bất cứ điều gì ... nhưng đó không phải là sự lựa chọn bền vững nhất ... bởi vì việc tiết lộ một lớp để phái sinh khóa bạn nhiều hơn là không phơi bày tất cả cái đó. Nó tương tự như câu nói "Tôi ghét các lớp có thành viên riêng ... Tôi thường không thể khiến lớp đó làm những gì tôi muốn vì tôi không có quyền truy cập." Đóng gói là quan trọng ... niêm phong là một hình thức đóng gói.


Trình biên dịch C # vẫn sẽ sử dụng callvirt(gọi phương thức ảo) cho các phương thức ví dụ trên các lớp được niêm phong, bởi vì nó vẫn phải thực hiện kiểm tra đối tượng null trên chúng. Về nội tuyến, CLR JIT có thể (và không) gọi phương thức ảo nội tuyến cho cả các lớp được niêm phong và không niêm phong ... vì vậy, vâng. Điều hiệu suất là một huyền thoại.
Orion Edwards

1

Để thực sự nhìn thấy chúng, bạn cần phân tích cod e -Compiled (cái cuối cùng).

Mã C #

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

Mã mẹ

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT- Mã tổng hợp

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

Mặc dù việc tạo các đối tượng là như nhau, nhưng lệnh được thực thi để gọi các phương thức của lớp niêm phong và lớp dẫn xuất / cơ sở hơi khác nhau. Sau khi di chuyển dữ liệu vào các thanh ghi hoặc RAM (lệnh Mov), việc gọi phương thức được niêm phong, thực hiện so sánh giữa dword ptr [ecx], ecx (lệnh cmp) và sau đó gọi phương thức trong khi lớp dẫn xuất / cơ sở thực hiện trực tiếp phương thức. .

Theo báo cáo được viết bởi Torbj¨orn Granlund, Độ trễ và thông lượng hướng dẫn cho bộ xử lý AMD và Intel x86 , tốc độ của hướng dẫn sau trong Intel Pentium 4 là:

  • Mov : có 1 chu kỳ là độ trễ và bộ xử lý có thể duy trì 2,5 lệnh trên mỗi chu kỳ của loại này
  • cmp : có 1 chu kỳ là độ trễ và bộ xử lý có thể duy trì 2 lệnh cho mỗi chu kỳ thuộc loại này

Liên kết : https://gmplib.org/~tege/x86-timing.pdf

Điều này có nghĩa là, lý tưởng nhất là thời gian cần thiết để gọi một phương thức kín là 2 chu kỳ trong khi thời gian cần thiết để gọi một phương thức lớp dẫn xuất hoặc lớp cơ sở là 3 chu kỳ.

Việc tối ưu hóa các trình biên dịch đã tạo ra sự khác biệt giữa hiệu suất của một lớp được niêm phong và không được niêm phong thấp đến mức chúng ta đang nói về vòng tròn của bộ xử lý và vì lý do này không liên quan đến phần lớn các ứng dụng.


-10

Chạy mã này và bạn sẽ thấy rằng các lớp niêm phong nhanh hơn 2 lần:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

đầu ra: Lớp kín: 00: 00: 00.1897568 Lớp không niêm phong: 00: 00: 00.3826678


9
Có một vài vấn đề với điều này. Trước hết, bạn không đặt lại đồng hồ bấm giờ giữa thử nghiệm đầu tiên và thử nghiệm thứ hai. Thứ hai, cách bạn gọi phương thức có nghĩa là tất cả các mã op sẽ được gọi không phải là cuộc gọi nên loại không quan trọng.
Cameron MacFarland

1
RuslanG, Bạn đã quên gọi watch.Reset () sau khi chạy thử nghiệm đầu tiên. o_O:]

Có, mã ở trên có lỗi. Sau đó, một lần nữa, ai có thể khoe khoang về bản thân để không bao giờ giới thiệu sai lầm trong mã? Nhưng câu trả lời này có một điều quan trọng tốt hơn tất cả những điều khác: Nó cố gắng đo lường hiệu quả trong câu hỏi. Và câu trả lời này cũng chia sẻ mã cho tất cả các bạn kiểm tra và [mass-] downvote (tôi tự hỏi tại sao việc giảm tải hàng loạt lại cần thiết?). Ngược lại với các câu trả lời khác. Điều đó đáng được tôn trọng theo ý kiến ​​của tôi ... Cũng xin lưu ý rằng người dùng là người mới, vì vậy cũng không phải là cách tiếp cận rất đáng hoan nghênh. Một số sửa chữa đơn giản và mã này trở nên hữu ích cho tất cả chúng ta. Khắc phục + chia sẻ phiên bản cố định, nếu bạn dám
Roland Pihlakas

Tôi không đồng ý với @RolandPihlakas. Như bạn đã nói, "ai có thể khoe khoang về bản thân mình để không bao giờ đưa ra những sai lầm trong mã". Trả lời -> Không, không có. Nhưng đó không phải là vấn đề. Vấn đề là, đó là một thông tin sai có thể đánh lừa một lập trình viên mới khác. Nhiều người có thể dễ dàng bỏ lỡ rằng đồng hồ bấm giờ không được đặt lại. Họ có thể tin rằng thông tin điểm chuẩn là đúng. Điều đó có hại hơn không? Một người đang nhanh chóng tìm kiếm một câu trả lời thậm chí có thể không đọc những bình luận này, thay vào đó anh ấy / cô ấy sẽ thấy một câu trả lời và anh ấy / cô ấy có thể tin điều đó.
Emran Hussain
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.