Tại sao nên sử dụng một cách tiếp cận OO thay vì một tuyên bố chuyển đổi khổng lồ của Viking?


59

Tôi đang làm việc trong một cửa hàng .Net, C # và tôi có một đồng nghiệp luôn khăng khăng rằng chúng ta nên sử dụng các câu lệnh Switch khổng lồ trong mã của mình với nhiều "Trường hợp" hơn là các cách tiếp cận hướng đối tượng hơn. Lập luận của anh ta liên tục quay trở lại thực tế là một câu lệnh Switch biên dịch thành "bảng nhảy cpu" và do đó là lựa chọn nhanh nhất (mặc dù trong những điều khác, nhóm của chúng tôi được thông báo rằng chúng tôi không quan tâm đến tốc độ).

Tôi thực sự không tranh cãi về điều này ... bởi vì tôi không biết anh ta đang nói cái quái gì nữa.
Anh ấy có đúng không?
Có phải anh ta chỉ nói ra mông của mình?
Chỉ cần cố gắng học ở đây.


7
Bạn có thể xác minh xem anh ấy có đúng không bằng cách sử dụng một cái gì đó như .NET Reflector để xem mã lắp ráp và tìm "bảng nhảy cpu".
Thất vọngWithFormsDesigner

5
"Chuyển câu lệnh biên dịch thành" bảng nhảy cpu "Vậy phương thức trong trường hợp xấu nhất có thể gửi đi với tất cả các hàm thuần ảo. Không có hàm ảo nào được liên kết trực tiếp. Bạn đã bỏ bất kỳ mã nào để so sánh chưa?
S.Lott

64
Mã phải được viết cho NHÂN DÂN không dành cho máy móc, nếu không chúng tôi sẽ chỉ làm mọi thứ trong lắp ráp.
maple_shaft

8
Nếu anh ta là một người khó tính, hãy trích dẫn Knuth với anh ta: "Chúng ta nên quên đi những hiệu quả nhỏ, hãy nói khoảng 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi."
DaveE

12
Bảo trì. Bất kỳ câu hỏi nào khác với một câu trả lời tôi có thể giúp bạn với?
Matt Ellen

Câu trả lời:


48

Anh ta có lẽ là một hacker C cũ và vâng, anh ta nói ra khỏi mông của mình. .Net không phải là C ++; trình biên dịch .Net tiếp tục nhận được các bản hack tốt hơn và thông minh nhất là phản tác dụng, nếu không phải hôm nay thì trong phiên bản .Net tiếp theo. Các hàm nhỏ được ưu tiên hơn vì .Net JIT-s mỗi hàm một lần trước khi nó được sử dụng. Vì vậy, nếu một số trường hợp không bao giờ bị ảnh hưởng trong Vòng đời của chương trình, do đó, không có chi phí nào phát sinh trong quá trình biên dịch JIT. Dù sao đi nữa, nếu tốc độ không phải là vấn đề, thì không nên tối ưu hóa. Viết cho lập trình viên trước, cho trình biên dịch thứ hai. Đồng nghiệp của bạn sẽ không dễ bị thuyết phục, vì vậy tôi sẽ chứng minh bằng thực nghiệm rằng mã được tổ chức tốt hơn thực sự nhanh hơn. Tôi sẽ chọn một trong những ví dụ tồi tệ nhất của anh ấy, viết lại chúng theo cách tốt hơn, và sau đó đảm bảo rằng mã của bạn nhanh hơn. Cherry-pick nếu bạn phải. Sau đó chạy nó vài triệu lần, hồ sơ và cho anh ta thấy.

BIÊN TẬP

Bill Wagner đã viết:

Mục 11: Hiểu được sự hấp dẫn của các chức năng nhỏ (Phiên bản thứ hai C # hiệu quả) Hãy nhớ rằng việc dịch mã C # của bạn thành mã thực thi bằng máy là một quá trình gồm hai bước. Trình biên dịch C # tạo ra IL được phân phối trong các cụm. Trình biên dịch JIT tạo mã máy cho từng phương thức (hoặc nhóm phương thức, khi có liên quan đến nội tuyến), khi cần. Các hàm nhỏ giúp trình biên dịch JIT dễ dàng hơn trong việc khấu hao chi phí đó. Các chức năng nhỏ cũng có nhiều khả năng là ứng cử viên cho nội tuyến. Đó không chỉ là sự nhỏ bé: Dòng điều khiển đơn giản cũng quan trọng không kém. Ít nhánh điều khiển bên trong các hàm giúp trình biên dịch JIT dễ dàng hơn trong việc liệt kê các biến. Nó không chỉ là thực hành tốt để viết mã rõ ràng hơn; đó là cách bạn tạo mã hiệu quả hơn trong thời gian chạy.

EDIT2:

Vì vậy, ... rõ ràng một câu lệnh chuyển đổi nhanh hơn và tốt hơn một loạt các câu lệnh if / other, bởi vì một so sánh là logarit và một câu lệnh khác là tuyến tính. http:// resultence-point.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Chà, cách tiếp cận yêu thích của tôi để thay thế một câu lệnh chuyển đổi khổng lồ là bằng một từ điển (hoặc đôi khi là một mảng nếu tôi đang bật enum hoặc ints nhỏ) đang ánh xạ các giá trị thành các hàm được gọi để đáp ứng với chúng. Làm như vậy buộc người ta phải loại bỏ rất nhiều trạng thái spaghetti chia sẻ khó chịu, nhưng đó là một điều tốt. Một tuyên bố chuyển đổi lớn thường là một cơn ác mộng bảo trì. Vì vậy, ... với các mảng và từ điển, việc tra cứu sẽ mất một thời gian liên tục và sẽ có ít bộ nhớ bị lãng phí.

Tôi vẫn không tin rằng tuyên bố chuyển đổi là tốt hơn.


47
Đừng lo lắng về việc chứng minh nó nhanh hơn. Đây là tối ưu hóa sớm. Một phần nghìn giây bạn có thể lưu không là gì so với chỉ số mà bạn quên thêm vào cơ sở dữ liệu khiến bạn mất 200ms. Bạn đang chiến đấu sai trận.
Rein Henrichs

27
@Job nếu anh ấy thực sự đúng? Vấn đề không phải là anh ấy sai, vấn đề là anh ấy đúng và không thành vấn đề .
Rein Henrichs

2
Ngay cả khi anh ta đúng khoảng 100% các trường hợp, anh ta vẫn lãng phí thời gian của chúng tôi.
Jeremy

6
Tôi muốn thu hút sự chú ý của mình khi cố gắng đọc trang mà bạn đã liên kết.
Tấn côngHobo

3
C ++ ghét cái gì? Các trình biên dịch C ++ cũng đang trở nên tốt hơn và các chuyển đổi lớn cũng tệ như trong C ++ như trong C #, và vì lý do chính xác như vậy. Nếu bạn bị bao vây bởi các lập trình viên C ++ trước đây khiến bạn đau buồn thì không phải vì họ là lập trình viên C ++, mà là vì họ là những lập trình viên tồi.
Sebastian Redl

39

Trừ khi đồng nghiệp của bạn có thể cung cấp bằng chứng, rằng sự thay đổi này mang lại lợi ích có thể đo lường thực tế trên quy mô của toàn bộ ứng dụng, nó kém hơn so với phương pháp của bạn (tức là đa hình), thực sự mang lại lợi ích như vậy: khả năng duy trì.

Microoptimisation chỉ nên được thực hiện, sau khi tắc nghẽn được ghim xuống. Tối ưu hóa sớm là gốc rễ của mọi tội lỗi .

Tốc độ là định lượng. Có rất ít thông tin hữu ích trong "cách tiếp cận A nhanh hơn cách tiếp cận B". Câu hỏi là " Nhanh hơn bao nhiêu? ".


2
Hoàn toàn đúng. Không bao giờ tuyên bố rằng một cái gì đó là nhanh hơn, luôn luôn đo lường. Và chỉ đo khi phần đó của ứng dụng là nút cổ chai hiệu năng.
Kilian Foth

6
-1 cho "Tối ưu hóa sớm là gốc rễ của mọi tội lỗi." Vui lòng hiển thị toàn bộ trích dẫn, không chỉ một phần làm sai lệch ý kiến ​​của Knuth.
thay thế

2
@mathepic: Tôi cố tình không trình bày đây là một trích dẫn. Câu này, như là, là ý kiến ​​cá nhân của tôi, mặc dù tất nhiên không phải là sáng tạo của tôi. Mặc dù có thể lưu ý rằng những kẻ từ c2 dường như chỉ coi đó là một phần của trí tuệ cốt lõi.
back2dos

8
@alternative Câu trích dẫn đầy đủ của Knuth "Không có nghi ngờ gì về khả năng hiệu quả dẫn đến lạm dụng. Các lập trình viên lãng phí rất nhiều thời gian để suy nghĩ, hoặc lo lắng về tốc độ của các phần không chính xác trong chương trình của họ và những nỗ lực này có hiệu quả thực sự có một Tác động tiêu cực mạnh mẽ khi gỡ lỗi và bảo trì được xem xét. Chúng ta nên quên đi những hiệu quả nhỏ, nói khoảng 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi. " Mô tả hoàn hảo đồng nghiệp của OP. IMHO back2dos đã tóm tắt câu trích dẫn rất hay với "tối ưu hóa sớm là gốc rễ của mọi tội lỗi"
MarkJ

2
@MarkJ 97% thời gian
thay thế vào

27

Ai quan tâm nếu nó nhanh hơn?

Trừ khi bạn đang viết phần mềm thời gian thực, không chắc rằng tốc độ tăng tốc rất nhỏ mà bạn có thể có được từ việc làm một việc hoàn toàn điên rồ sẽ tạo ra nhiều khác biệt cho khách hàng của bạn. Tôi thậm chí sẽ không chiến đấu với người này trên mặt trận tốc độ, anh chàng này rõ ràng sẽ không lắng nghe bất kỳ tranh luận về chủ đề này.

Tuy nhiên, khả năng bảo trì là mục tiêu của trò chơi và một câu lệnh chuyển đổi khổng lồ thậm chí không thể duy trì được một chút, làm thế nào để bạn giải thích các đường dẫn khác nhau thông qua mã cho một kẻ mới? Các tài liệu sẽ phải được miễn là mã chính nó!

Thêm vào đó, sau đó bạn đã hoàn toàn không có khả năng kiểm tra đơn vị một cách hiệu quả (quá nhiều đường dẫn có thể, chưa kể đến việc thiếu giao diện có thể xảy ra, v.v.), khiến mã của bạn thậm chí ít được bảo trì hơn.

[Về mặt quan tâm: JITter hoạt động tốt hơn trên các phương thức nhỏ hơn, do đó, các câu lệnh chuyển đổi khổng lồ (và các phương thức vốn có của chúng) sẽ làm hại tốc độ của bạn trong các cụm lớn, IIRC.]


1
+ một ví dụ rất lớn về tối ưu hóa sớm.
ShaneC

Chắc chắn điều này.
DeadMG

+1 cho 'một tuyên bố chuyển đổi khổng lồ thậm chí không thể duy trì một chút'
Korey Hinton

2
Một tuyên bố chuyển đổi khổng lồ dễ dàng hơn rất nhiều cho anh chàng mới hiểu: tất cả các hành vi có thể được thu thập ngay trong một danh sách gọn gàng đẹp. Các cuộc gọi gián tiếp rất khó theo dõi, trong trường hợp xấu nhất (con trỏ hàm), bạn cần tìm kiếm toàn bộ cơ sở mã cho các chức năng của chữ ký bên phải và các cuộc gọi ảo chỉ tốt hơn một chút (tìm kiếm các chức năng của tên và chữ ký đúng và liên quan bằng thừa kế). Nhưng khả năng bảo trì không phải là chỉ đọc.
Ben Voigt

14

Bước ra khỏi câu lệnh chuyển đổi ...

Loại câu lệnh chuyển đổi này nên được tránh xa như một bệnh dịch vì nó vi phạm Nguyên tắc Đóng mở . Nó buộc nhóm phải thay đổi mã hiện có khi cần thêm chức năng mới, trái ngược với việc chỉ thêm mã mới.


11
Điều đó đi kèm với một cảnh báo. Có các hoạt động (chức năng / phương pháp) và các loại. Khi bạn thêm một thao tác mới, bạn chỉ phải thay đổi mã ở một nơi cho các câu lệnh chuyển đổi (thêm một hàm mới bằng câu lệnh chuyển đổi), nhưng bạn phải thêm phương thức đó vào tất cả các lớp trong trường hợp OO (vi phạm mở / nguyên tắc khép kín). Nếu bạn thêm các loại mới, bạn phải chạm vào mọi câu lệnh chuyển đổi, nhưng trong trường hợp OO, bạn chỉ cần thêm một lớp nữa. Do đó, để đưa ra quyết định sáng suốt, bạn phải biết liệu bạn sẽ thêm nhiều hoạt động hơn vào các loại hiện có hay thêm nhiều loại.
Scott Whitlock

3
Nếu bạn cần thêm nhiều hoạt động hơn cho các loại hiện có trong mô hình OO mà không vi phạm OCP, thì tôi tin rằng đó là mô hình khách truy cập dành cho.
Scott Whitlock

3
@Martin - gọi tên nếu bạn muốn, nhưng đây là một sự đánh đổi nổi tiếng. Tôi giới thiệu bạn đến Clean Code của RC Martin. Ông xem lại bài viết của mình về OCP, giải thích những gì tôi đã nêu ở trên. Bạn không thể đồng thời thiết kế cho tất cả các yêu cầu trong tương lai. Bạn phải đưa ra lựa chọn giữa việc có nhiều khả năng thêm nhiều hoạt động hơn hay nhiều loại hơn không. OO ủng hộ việc bổ sung các loại. Bạn có thể sử dụng OO để thêm nhiều hoạt động hơn nếu bạn mô hình hóa các hoạt động như các lớp, nhưng điều đó dường như đi vào mô hình khách truy cập, có vấn đề riêng của nó (đáng chú ý là chi phí hoạt động).
Scott Whitlock

8
@Martin: Bạn đã bao giờ viết một trình phân tích cú pháp chưa? Điều khá phổ biến là có các trường hợp chuyển đổi lớn bật mã thông báo tiếp theo trong bộ đệm lookahead. Bạn có thể thay thế các công tắc đó bằng các cuộc gọi chức năng ảo sang mã thông báo tiếp theo, nhưng đó sẽ là một cơn ác mộng duy trì. Điều này rất hiếm, nhưng đôi khi trường hợp chuyển đổi thực sự là lựa chọn tốt hơn, bởi vì nó giữ mã nên được đọc / sửa đổi gần nhau.
nikie

1
@Martin: Bạn đã sử dụng các từ như "không bao giờ", "bao giờ" và "Poppycock", vì vậy tôi cho rằng bạn đang nói về tất cả các trường hợp mà không có ngoại lệ, không chỉ về trường hợp phổ biến nhất. (Và BTW: Mọi người vẫn viết trình phân tích cú pháp bằng tay. Ví dụ: trình phân tích cú pháp CPython vẫn được viết bằng tay, IIRC.)
nikie

8

Tôi đã sống sót qua cơn ác mộng được gọi là cỗ máy trạng thái hữu hạn khổng lồ bị thao túng bởi các câu lệnh chuyển đổi lớn. Tệ hơn nữa, trong trường hợp của tôi, FSM đã kéo dài ba DLL C ++ và mã này khá đơn giản được viết bởi một người thông thạo C.

Các số liệu bạn cần quan tâm là:

  • Tốc độ thay đổi
  • Tốc độ tìm kiếm vấn đề khi nó xảy ra

Tôi được giao nhiệm vụ thêm một tính năng mới vào tập hợp các DLL đó và có thể thuyết phục ban quản lý rằng tôi sẽ mất nhiều thời gian để viết lại 3 DLL như một DLL định hướng đối tượng đúng như tôi sẽ vá khỉ và bồi thẩm đoàn giải pháp cho những gì đã có. Việc viết lại là một thành công lớn, vì nó không chỉ hỗ trợ chức năng mới mà còn dễ dàng mở rộng hơn nhiều. Trên thực tế, một nhiệm vụ thường sẽ mất một tuần để đảm bảo bạn không phá vỡ bất cứ điều gì cuối cùng sẽ mất vài giờ.

Vậy làm thế nào về thời gian thực hiện? Không có tăng hoặc giảm tốc độ. Để công bằng, hiệu suất của chúng tôi đã được điều khiển bởi các trình điều khiển hệ thống, vì vậy nếu giải pháp hướng đối tượng trên thực tế chậm hơn, chúng tôi sẽ không biết điều đó.

Có gì sai với các câu lệnh chuyển đổi lớn cho một ngôn ngữ OO?

  • Luồng điều khiển chương trình được lấy đi từ đối tượng nơi nó thuộc và đặt bên ngoài đối tượng
  • Nhiều điểm kiểm soát bên ngoài chuyển thành nhiều nơi bạn cần xem lại
  • Không rõ trạng thái được lưu trữ ở đâu, đặc biệt nếu công tắc nằm trong vòng lặp
  • So sánh nhanh nhất là không so sánh chút nào (bạn có thể tránh sự cần thiết phải so sánh nhiều với một thiết kế hướng đối tượng tốt)
  • Sẽ hiệu quả hơn khi lặp qua các đối tượng của bạn và luôn gọi cùng một phương thức trên tất cả các đối tượng hơn là thay đổi mã của bạn dựa trên loại đối tượng hoặc enum mã hóa loại.

8

Tôi không mua đối số hiệu suất; tất cả là về khả năng duy trì mã.

NHƯNG: đôi khi , một câu lệnh chuyển đổi khổng lồ dễ duy trì (ít mã hơn) so với một nhóm các lớp nhỏ ghi đè (các) hàm ảo của một lớp cơ sở trừu tượng. Ví dụ: nếu bạn triển khai trình giả lập CPU, bạn sẽ không thực hiện chức năng của từng lệnh trong một lớp riêng biệt - bạn sẽ nhét nó vào một swtich khổng lồ trên opcode, có thể gọi các hàm trợ giúp cho các lệnh phức tạp hơn.

Nguyên tắc chung: nếu công tắc được thực hiện bằng cách nào đó trên TYPE, có lẽ bạn nên sử dụng chức năng kế thừa và chức năng ảo. Nếu công tắc được thực hiện trên GIÁ TRỊ của một loại cố định (ví dụ: mã opcode hướng dẫn, như trên), bạn có thể để nguyên như vậy.


5

Bạn không thể thuyết phục tôi rằng:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

Nhanh hơn đáng kể so với:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

Ngoài ra, phiên bản OO dễ bảo trì hơn.


8
Đối với một số thứ và đối với số lượng hành động nhỏ hơn, phiên bản OO sẽ mạnh hơn nhiều. Nó phải có một số loại nhà máy để chuyển đổi một số giá trị thành việc tạo ra IAction. Trong nhiều trường hợp, thay vào đó chỉ cần bật giá trị đó là dễ đọc hơn nhiều.
Zan Lynx

@Zan Lynx: Đối số của bạn quá chung chung. Việc tạo đối tượng IAction cũng khó như lấy ra số nguyên hành động không khó hơn không dễ hơn. Vì vậy, chúng ta có thể có một cuộc trò chuyện thực sự mà không có cách để chung chung. Hãy xem xét một máy tính. Có gì khác biệt về sự phức tạp ở đây? Câu trả lời là không. Như tất cả các hành động được tạo trước. Bạn nhận được đầu vào từ người dùng và nó đã là một hành động.
Martin York

3
@Martin: Bạn đang giả sử một ứng dụng máy tính GUI. Thay vào đó, hãy dùng ứng dụng máy tính bàn phím được viết cho C ++ trên hệ thống nhúng. Bây giờ bạn có một số nguyên mã quét từ một thanh ghi phần cứng. Bây giờ cái gì ít phức tạp hơn?
Zan Lynx

2
@Martin: Bạn không thấy cách số nguyên -> bảng tra cứu -> tạo đối tượng mới -> gọi hàm ảo phức tạp hơn số nguyên -> chuyển đổi -> hàm? Làm thế nào để bạn không nhìn thấy điều đó?
Zan Lynx

2
@Martin: Có lẽ tôi sẽ làm. Trong lúc này, hãy giải thích cách bạn lấy đối tượng IAction để gọi hành động () từ một số nguyên mà không cần bảng tra cứu.
Zan Lynx

4

Ông đúng là mã máy kết quả có thể sẽ hiệu quả hơn. Trình biên dịch cần thiết biến đổi một câu lệnh chuyển đổi thành một tập hợp các bài kiểm tra và các nhánh, sẽ tương đối ít hướng dẫn. Có khả năng cao là mã kết quả từ các cách tiếp cận trừu tượng hơn sẽ yêu cầu nhiều hướng dẫn hơn.

TUY NHIÊN : Gần như chắc chắn trường hợp ứng dụng cụ thể của bạn không cần phải lo lắng về loại tối ưu hóa vi mô này hoặc bạn sẽ không sử dụng .net ở nơi đầu tiên. Đối với bất kỳ thứ gì thiếu các ứng dụng nhúng rất hạn chế hoặc công việc nặng về CPU, bạn phải luôn để trình biên dịch xử lý tối ưu hóa. Tập trung vào viết sạch, mã duy trì. Điều này hầu như luôn có giá trị lớn hơn nhiều so với một phần mười của nano giây trong thời gian thực hiện.


3

Một lý do chính để sử dụng các lớp thay vì các câu lệnh chuyển đổi là các câu lệnh chuyển đổi có xu hướng dẫn đến một tệp khổng lồ có nhiều logic. Đây vừa là cơn ác mộng bảo trì vừa là vấn đề với quản lý nguồn vì bạn phải kiểm tra và chỉnh sửa tệp khổng lồ đó thay vì các tệp lớp nhỏ hơn khác


3

một câu lệnh chuyển đổi trong mã OOP là một dấu hiệu mạnh mẽ của các lớp bị thiếu

thử cả hai cách và chạy một số bài kiểm tra tốc độ đơn giản; cơ hội là sự khác biệt không đáng kể. Nếu chúng là và mã là thời gian quan trọng thì hãy giữ câu lệnh chuyển đổi


3

Thông thường tôi ghét từ "tối ưu hóa sớm", nhưng đây là sự xuất hiện của nó. Điều đáng chú ý là Knuth đã sử dụng câu nói nổi tiếng này trong bối cảnh thúc đẩy sử dụng các gotocâu lệnh để tăng tốc mã trong các khu vực quan trọng . Đó là chìa khóa: những con đường quan trọng .

Ông đã đề nghị sử dụng gotođể tăng tốc mã nhưng cảnh báo chống lại những lập trình viên muốn thực hiện những loại việc này dựa trên linh cảm và mê tín đối với mã thậm chí không quan trọng.

Để ủng hộ switchtuyên bố càng nhiều càng tốt thống nhất trong suốt một codebase (có hay không có tải nặng được xử lý) là ví dụ điển hình về những gì Knuth gọi là "xu-khôn ngoan và lb-ngu ngốc" lập trình viên người dành cả ngày phải vật lộn để duy trì họ "tối ưu hóa "Mã biến thành cơn ác mộng gỡ lỗi là kết quả của việc cố gắng tiết kiệm đồng xu hơn bảng Anh. Mã như vậy hiếm khi có thể được duy trì, ngay cả khi hiệu quả ngay từ đầu.

Anh ấy có đúng không?

Ông là chính xác từ quan điểm hiệu quả rất cơ bản. Không có trình biên dịch theo kiến ​​thức của tôi có thể tối ưu hóa mã đa hình liên quan đến các đối tượng và công văn động tốt hơn một câu lệnh chuyển đổi. Bạn sẽ không bao giờ kết thúc với bảng LUT hoặc nhảy sang mã được in từ mã đa hình, vì mã đó có xu hướng đóng vai trò là rào cản tối ưu hóa cho trình biên dịch (nó sẽ không biết nên gọi hàm nào cho đến thời điểm mà công văn động xảy ra).

Sẽ hữu ích hơn khi không nghĩ về chi phí này về mặt bảng nhảy mà nhiều hơn về hàng rào tối ưu hóa. Đối với đa hình, việc gọi Base.method()không cho phép trình biên dịch biết chức năng nào thực sự sẽ được gọi nếu methodlà ảo, không bị niêm phong và có thể bị ghi đè. Vì nó không biết chức năng nào thực sự sẽ được gọi trước, nên nó không thể tối ưu hóa cuộc gọi chức năng và sử dụng nhiều thông tin hơn trong việc đưa ra quyết định tối ưu hóa, vì thực tế nó không biết chức năng nào sẽ được gọi tại thời gian mã đang được biên dịch.

Tối ưu hóa là tốt nhất khi họ có thể tham gia vào một cuộc gọi chức năng và thực hiện tối ưu hóa hoàn toàn làm phẳng người gọi và callee, hoặc ít nhất là tối ưu hóa người gọi để làm việc hiệu quả nhất với callee. Họ không thể làm điều đó nếu họ không biết chức năng nào thực sự sẽ được gọi trước.

Có phải anh ta chỉ nói ra mông của mình?

Sử dụng chi phí này, thường là đồng xu, để biện minh cho việc biến điều này thành một tiêu chuẩn mã hóa được áp dụng thống nhất nói chung là rất ngu ngốc, đặc biệt là đối với những nơi có nhu cầu mở rộng. Đó là điều chính bạn muốn đề phòng với các trình tối ưu hóa sớm chính hãng: họ muốn biến các mối quan tâm hiệu suất nhỏ thành các tiêu chuẩn mã hóa được áp dụng thống nhất trong toàn bộ một cơ sở mã hóa mà không liên quan đến khả năng bảo trì.

Mặc dù vậy, tôi có một chút xúc phạm đến trích dẫn của "hacker C cũ" được sử dụng trong câu trả lời được chấp nhận, vì tôi là một trong số đó. Không phải tất cả mọi người đã mã hóa trong nhiều thập kỷ bắt đầu từ phần cứng rất hạn chế đã biến thành một trình tối ưu hóa sớm. Nhưng tôi cũng đã gặp và làm việc với những người đó. Nhưng những kiểu đó không bao giờ đo lường được những thứ như hiểu sai về chi nhánh hoặc lỗi bộ nhớ cache, họ nghĩ rằng họ biết rõ hơn và dựa trên những quan niệm không hiệu quả của họ trong một cơ sở sản xuất phức tạp dựa trên những mê tín ngày nay không đúng và đôi khi không bao giờ đúng. Những người đã thực sự làm việc trong các lĩnh vực quan trọng về hiệu suất thường hiểu rằng tối ưu hóa hiệu quả là ưu tiên hiệu quả và cố gắng khái quát hóa một tiêu chuẩn mã hóa làm giảm khả năng duy trì để tiết kiệm xu là rất ưu tiên.

Các nhãn hiệu rất quan trọng khi bạn có một chức năng giá rẻ mà không thực hiện quá nhiều công việc được gọi là một tỷ lần trong một vòng lặp rất quan trọng, hiệu suất. Trong trường hợp đó, chúng tôi cuối cùng đã tiết kiệm được 10 triệu đô la. Nó không đáng để cạo râu khi bạn có một chức năng được gọi là hai lần mà một mình cơ thể có giá hàng ngàn đô la. Sẽ không khôn ngoan khi bạn dành thời gian mặc cả những đồng xu trong quá trình mua xe. Thật đáng để mặc cả đồng xu nếu bạn mua một triệu lon soda từ nhà sản xuất. Chìa khóa để tối ưu hóa hiệu quả là hiểu những chi phí này trong bối cảnh thích hợp của chúng. Một người nào đó cố gắng tiết kiệm từng đồng xu cho mỗi lần mua và đề nghị mọi người khác cố gắng mặc cả đồng xu cho dù họ mua gì không phải là một trình tối ưu hóa có kỹ năng.


2

Có vẻ như đồng nghiệp của bạn rất quan tâm đến hiệu suất. Có thể trong một số trường hợp, cấu trúc case / switch lớn sẽ hoạt động nhanh hơn, nhưng hy vọng các bạn sẽ thực hiện một thử nghiệm bằng cách thực hiện các thử nghiệm thời gian trên phiên bản OO và phiên bản switch / case. Tôi đoán phiên bản OO có ít mã hơn và dễ theo dõi, hiểu và bảo trì hơn. Tôi sẽ tranh luận về phiên bản OO trước tiên (vì bảo trì / khả năng đọc ban đầu nên quan trọng hơn) và chỉ xem xét phiên bản chuyển đổi / trường hợp chỉ khi phiên bản OO có vấn đề nghiêm trọng về hiệu suất và có thể chứng minh rằng công tắc / vỏ máy sẽ tạo ra cải thiện đáng kể.


1
Cùng với các bài kiểm tra thời gian, kết xuất mã có thể giúp hiển thị cách thức gửi phương thức C ++ (và C #).
S.Lott

2

Một lợi thế duy trì của tính đa hình mà không ai đã đề cập là bạn sẽ có thể cấu trúc mã của mình một cách độc đáo hơn bằng cách sử dụng tính kế thừa nếu bạn luôn chuyển đổi trong cùng một danh sách các trường hợp, nhưng đôi khi một số trường hợp được xử lý theo cùng một cách không

Ví dụ. nếu bạn đang chuyển đổi giữa Dog, CatElephant, và đôi khi DogCatcó cùng một trường hợp, bạn có thể làm cho cả hai kế thừa từ một lớp trừu tượng DomesticAnimalvà đặt các hàm đó trong lớp trừu tượng.

Ngoài ra, tôi đã ngạc nhiên khi một số người sử dụng trình phân tích cú pháp làm ví dụ về việc bạn sẽ không sử dụng đa hình. Đối với một trình phân tích cú pháp giống như cây, đây chắc chắn là cách tiếp cận sai, nhưng nếu bạn có một cái gì đó giống như lắp ráp, trong đó mỗi dòng là hơi độc lập và bắt đầu với một opcode cho biết phần còn lại của dòng nên được giải thích như thế nào, tôi hoàn toàn sẽ sử dụng đa hình và một Nhà máy. Mỗi lớp có thể thực hiện các chức năng như ExtractConstantshoặc ExtractSymbols. Tôi đã sử dụng phương pháp này cho một thông dịch viên BASIC đồ chơi.


Một công tắc cũng có thể kế thừa các hành vi, thông qua trường hợp mặc định của nó. "... Mở rộng BaseOperationVisitor" trở thành "mặc định: BaseOperation (nút)"
Samuel Danielson

0

"Chúng ta nên quên đi những hiệu quả nhỏ, nói về 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi"

Donald Knuth


0

Ngay cả khi điều này không tệ cho khả năng bảo trì, tôi không tin rằng nó sẽ tốt hơn cho hiệu suất. Một cuộc gọi chức năng ảo chỉ đơn giản là một lần bổ sung thêm (giống như trường hợp tốt nhất cho câu lệnh chuyển đổi), do đó, ngay cả trong C ++, hiệu suất phải gần bằng nhau. Trong C #, trong đó tất cả các lệnh gọi hàm là ảo, câu lệnh chuyển đổi sẽ tệ hơn, vì bạn có cùng chi phí gọi hàm ảo trong cả hai phiên bản.


1
Thiếu "không"? Trong C #, không phải tất cả các cuộc gọi chức năng là ảo. C # không phải là Java.
Ben Voigt

0

Đồng nghiệp của bạn không nói ra từ phía sau của anh ấy, theo như nhận xét về bàn nhảy. Tuy nhiên, sử dụng điều đó để biện minh cho việc viết mã xấu là nơi anh ta sai.

Trình biên dịch C # chuyển đổi các câu lệnh chuyển đổi chỉ với một vài trường hợp thành một chuỗi if / other, vì vậy không nhanh hơn sử dụng if / other. Trình biên dịch chuyển đổi các câu lệnh chuyển đổi lớn hơn thành một Từ điển (bảng nhảy mà đồng nghiệp của bạn đang đề cập đến). Vui lòng xem câu trả lời này cho câu hỏi Stack Overflow về chủ đề này để biết thêm chi tiết .

Một tuyên bố chuyển đổi lớn là khó đọc và duy trì. Một từ điển "trường hợp" và chức năng dễ đọc hơn rất nhiều. Vì đó là những gì công tắc được chuyển thành, bạn và đồng nghiệp của bạn sẽ được khuyên nên sử dụng từ điển trực tiếp.


0

Anh ấy không nhất thiết phải nói ra khỏi mông của mình. Ít nhất trong các switchcâu lệnh C và C ++ có thể được tối ưu hóa để nhảy các bảng trong khi tôi chưa bao giờ thấy điều đó xảy ra với một công văn động trong một hàm chỉ có quyền truy cập vào một con trỏ cơ sở. Ít nhất thì cái sau đòi hỏi một trình tối ưu hóa thông minh hơn nhiều, nhìn vào mã xung quanh nhiều hơn để tìm ra chính xác kiểu con nào đang được sử dụng từ một lệnh gọi hàm ảo thông qua một con trỏ / tham chiếu cơ sở.

Trên hết, công văn động thường đóng vai trò là "hàng rào tối ưu hóa", có nghĩa là trình biên dịch thường không thể mã hóa nội tuyến và phân bổ tối ưu các thanh ghi để giảm thiểu sự cố tràn ngăn xếp và tất cả những thứ lạ mắt đó, vì nó không thể tìm ra cái gì chức năng ảo sẽ được gọi thông qua con trỏ cơ sở để nội tuyến nó và thực hiện tất cả các phép thuật tối ưu hóa của nó. Tôi không chắc rằng bạn thậm chí muốn trình tối ưu hóa trở nên thông minh và cố gắng tối ưu hóa các cuộc gọi hàm gián tiếp, vì điều đó có khả năng dẫn đến nhiều nhánh mã phải được tạo riêng biệt trong một ngăn xếp cuộc gọi nhất định (một chức năng mà các cuộc gọi foo->f()sẽ có để tạo mã máy hoàn toàn khác với mã gọibar->f() thông qua một con trỏ cơ sở và hàm gọi hàm đó sau đó sẽ phải tạo hai hoặc nhiều phiên bản mã, v.v. - số lượng mã máy được tạo sẽ bùng nổ - có thể không quá tệ với một JIT theo dõi tạo mã khi đang di chuyển khi nó truy tìm các đường dẫn thực thi nóng).

Tuy nhiên, như nhiều câu trả lời đã lặp lại, đó là một lý do tồi để ủng hộ một lượng lớn các switchtuyên bố ngay cả khi nó được xử lý nhanh hơn bởi một số tiền biên. Bên cạnh đó, khi nói đến hiệu quả vi mô, những thứ như phân nhánh và nội tuyến thường có mức độ ưu tiên khá thấp so với những thứ như mô hình truy cập bộ nhớ.

Điều đó nói rằng, tôi đã nhảy vào đây với một câu trả lời khác thường. Tôi muốn tạo ra một trường hợp cho khả năng duy trì của các switchcâu lệnh đối với một giải pháp đa hình khi và chỉ khi nào, bạn biết chắc chắn rằng sẽ chỉ có một nơi cần thực hiện switch.

Một ví dụ điển hình là một xử lý sự kiện trung tâm. Trong trường hợp đó, bạn thường không có nhiều nơi xử lý các sự kiện, chỉ một (tại sao nó là "trung tâm"). Đối với những trường hợp đó, bạn không được hưởng lợi từ khả năng mở rộng mà một giải pháp đa hình cung cấp. Một giải pháp đa hình có lợi khi có nhiều nơi sẽ thực hiện switchtuyên bố tương tự . Nếu bạn biết chắc chắn sẽ chỉ có một, một switchcâu lệnh với 15 trường hợp có thể đơn giản hơn nhiều so với việc thiết kế một lớp cơ sở được thừa hưởng bởi 15 kiểu con với các hàm bị ghi đè và một nhà máy để khởi tạo chúng, chỉ sau đó được sử dụng trong một hàm trong toàn bộ hệ thống. Trong những trường hợp đó, việc thêm một kiểu con mới sẽ tẻ nhạt hơn nhiều so với việc thêm một casecâu lệnh vào một hàm. Nếu có bất cứ điều gì, tôi sẽ tranh luận về khả năng duy trì, không phải hiệu suất,switch báo cáo trong trường hợp đặc biệt này khi bạn không được hưởng lợi gì từ khả năng mở rộng.

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.