Các ngoại lệ hoạt động như thế nào (đằng sau hậu trường) trong c ++


109

Tôi tiếp tục thấy mọi người nói rằng các trường hợp ngoại lệ là chậm, nhưng tôi không bao giờ thấy bất kỳ bằng chứng nào. Vì vậy, thay vì hỏi nếu chúng có, tôi sẽ hỏi các ngoại lệ hoạt động như thế nào đằng sau hậu trường, để tôi có thể đưa ra quyết định khi nào sử dụng chúng và liệu chúng có chậm hay không.

Theo những gì tôi biết, các ngoại lệ cũng giống như thực hiện một loạt các lần quay lại, ngoại trừ việc nó cũng kiểm tra sau mỗi lần quay lại xem nó có cần thực hiện một lần nữa hay dừng lại. Làm thế nào để nó kiểm tra khi nào ngừng quay trở lại? Tôi đoán có một ngăn xếp thứ hai chứa loại ngoại lệ và vị trí ngăn xếp, sau đó nó sẽ quay trở lại cho đến khi nó đến đó. Tôi cũng đoán rằng lần duy nhất chạm vào ngăn xếp thứ hai này là khi ném và mỗi lần thử / bắt. AFAICT thực hiện một hành vi tương tự với mã trả lại sẽ mất cùng một khoảng thời gian. Nhưng tất cả chỉ là phỏng đoán, vì vậy tôi muốn biết điều gì thực sự xảy ra.

Làm thế nào để các ngoại lệ thực sự hoạt động?



Câu trả lời:


105

Thay vì phỏng đoán, tôi quyết định thực sự xem mã được tạo bằng một đoạn mã C ++ nhỏ và bản cài đặt Linux hơi cũ.

class MyException
{
public:
    MyException() { }
    ~MyException() { }
};

void my_throwing_function(bool throwit)
{
    if (throwit)
        throw MyException();
}

void another_function();
void log(unsigned count);

void my_catching_function()
{
    log(0);
    try
    {
        log(1);
        another_function();
        log(2);
    }
    catch (const MyException& e)
    {
        log(3);
    }
    log(4);
}

Tôi đã biên dịch nó với g++ -m32 -W -Wall -O3 -save-temps -cvà xem tệp lắp ráp được tạo.

    .file   "foo.cpp"
    .section    .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
    .align 2
    .p2align 4,,15
    .weak   _ZN11MyExceptionD1Ev
    .type   _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
    pushl   %ebp
.LCFI0:
    movl    %esp, %ebp
.LCFI1:
    popl    %ebp
    ret
.LFE7:
    .size   _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev

_ZN11MyExceptionD1EvMyException::~MyException(), vì vậy trình biên dịch quyết định nó cần một bản sao không nội dòng của trình hủy.

.globl __gxx_personality_v0
.globl _Unwind_Resume
    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_catching_functionv
    .type   _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ebx
.LCFI4:
    subl    $20, %esp
.LCFI5:
    movl    $0, (%esp)
.LEHB0:
    call    _Z3logj
.LEHE0:
    movl    $1, (%esp)
.LEHB1:
    call    _Z3logj
    call    _Z16another_functionv
    movl    $2, (%esp)
    call    _Z3logj
.LEHE1:
.L5:
    movl    $4, (%esp)
.LEHB2:
    call    _Z3logj
    addl    $20, %esp
    popl    %ebx
    popl    %ebp
    ret
.L12:
    subl    $1, %edx
    movl    %eax, %ebx
    je  .L16
.L14:
    movl    %ebx, (%esp)
    call    _Unwind_Resume
.LEHE2:
.L16:
.L6:
    movl    %eax, (%esp)
    call    __cxa_begin_catch
    movl    $3, (%esp)
.LEHB3:
    call    _Z3logj
.LEHE3:
    call    __cxa_end_catch
    .p2align 4,,3
    jmp .L5
.L11:
.L8:
    movl    %eax, %ebx
    .p2align 4,,6
    call    __cxa_end_catch
    .p2align 4,,6
    jmp .L14
.LFE9:
    .size   _Z20my_catching_functionv, .-_Z20my_catching_functionv
    .section    .gcc_except_table,"a",@progbits
    .align 4
.LLSDA9:
    .byte   0xff
    .byte   0x0
    .uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
    .byte   0x1
    .uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
    .uleb128 .LEHB0-.LFB9
    .uleb128 .LEHE0-.LEHB0
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB1-.LFB9
    .uleb128 .LEHE1-.LEHB1
    .uleb128 .L12-.LFB9
    .uleb128 0x1
    .uleb128 .LEHB2-.LFB9
    .uleb128 .LEHE2-.LEHB2
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB3-.LFB9
    .uleb128 .LEHE3-.LEHB3
    .uleb128 .L11-.LFB9
    .uleb128 0x0
.LLSDACSE9:
    .byte   0x1
    .byte   0x0
    .align 4
    .long   _ZTI11MyException
.LLSDATT9:

Sự ngạc nhiên! Không có hướng dẫn bổ sung nào trên đường dẫn mã bình thường. Thay vào đó, trình biên dịch đã tạo thêm các khối mã sửa lỗi ngoài dòng, được tham chiếu qua một bảng ở cuối hàm (thực sự được đặt trên một phần riêng biệt của tệp thực thi). Tất cả công việc được thực hiện ở hậu trường bởi thư viện tiêu chuẩn, dựa trên các bảng này ( _ZTI11MyExceptiontypeinfo for MyException).

OK, đó thực sự không phải là điều ngạc nhiên đối với tôi, tôi đã biết cách trình biên dịch này thực hiện điều đó. Tiếp tục với đầu ra lắp ráp:

    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_throwing_functionb
    .type   _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
    pushl   %ebp
.LCFI6:
    movl    %esp, %ebp
.LCFI7:
    subl    $24, %esp
.LCFI8:
    cmpb    $0, 8(%ebp)
    jne .L21
    leave
    ret
.L21:
    movl    $1, (%esp)
    call    __cxa_allocate_exception
    movl    $_ZN11MyExceptionD1Ev, 8(%esp)
    movl    $_ZTI11MyException, 4(%esp)
    movl    %eax, (%esp)
    call    __cxa_throw
.LFE8:
    .size   _Z20my_throwing_functionb, .-_Z20my_throwing_functionb

Ở đây chúng ta thấy mã để ném một ngoại lệ. Mặc dù không có thêm chi phí đơn giản vì một ngoại lệ có thể được ném ra, rõ ràng là có rất nhiều chi phí trong việc thực sự ném và bắt một ngoại lệ. Hầu hết nó được ẩn bên trong __cxa_throw, phải:

  • Đi bộ ngăn xếp với sự trợ giúp của các bảng ngoại lệ cho đến khi nó tìm thấy một trình xử lý cho ngoại lệ đó.
  • Mở ngăn xếp cho đến khi nó đến tay trình xử lý đó.
  • Trên thực tế, hãy gọi người xử lý.

So sánh điều đó với chi phí chỉ trả về một giá trị và bạn thấy lý do tại sao các ngoại lệ chỉ nên được sử dụng cho các lợi nhuận đặc biệt.

Để kết thúc, phần còn lại của tệp lắp ráp:

    .weak   _ZTI11MyException
    .section    .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
    .align 4
    .type   _ZTI11MyException, @object
    .size   _ZTI11MyException, 8
_ZTI11MyException:
    .long   _ZTVN10__cxxabiv117__class_type_infoE+8
    .long   _ZTS11MyException
    .weak   _ZTS11MyException
    .section    .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
    .type   _ZTS11MyException, @object
    .size   _ZTS11MyException, 14
_ZTS11MyException:
    .string "11MyException"

Dữ liệu typeinfo.

    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zPL"
    .uleb128 0x1
    .sleb128 -4
    .byte   0x8
    .uleb128 0x6
    .byte   0x0
    .long   __gxx_personality_v0
    .byte   0x0
    .byte   0xc
    .uleb128 0x4
    .uleb128 0x4
    .byte   0x88
    .uleb128 0x1
    .align 4
.LECIE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB9
    .long   .LFE9-.LFB9
    .uleb128 0x4
    .long   .LLSDA9
    .byte   0x4
    .long   .LCFI2-.LFB9
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x5
    .byte   0x4
    .long   .LCFI5-.LCFI3
    .byte   0x83
    .uleb128 0x3
    .align 4
.LEFDE3:
.LSFDE5:
    .long   .LEFDE5-.LASFDE5
.LASFDE5:
    .long   .LASFDE5-.Lframe1
    .long   .LFB8
    .long   .LFE8-.LFB8
    .uleb128 0x4
    .long   0x0
    .byte   0x4
    .long   .LCFI6-.LFB8
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI7-.LCFI6
    .byte   0xd
    .uleb128 0x5
    .align 4
.LEFDE5:
    .ident  "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
    .section    .note.GNU-stack,"",@progbits

Thậm chí nhiều bảng xử lý ngoại lệ và các loại thông tin bổ sung.

Vì vậy, kết luận, ít nhất là đối với GCC trên Linux: chi phí là thêm không gian (cho trình xử lý và bảng) cho dù ngoại lệ có được ném ra hay không, cộng với chi phí bổ sung để phân tích các bảng và thực thi trình xử lý khi có ngoại lệ. Nếu bạn sử dụng ngoại lệ thay vì mã lỗi và lỗi hiếm khi xảy ra, thì có thể nhanh hơn , vì bạn không phải kiểm tra lỗi nữa.

Trong trường hợp bạn muốn biết thêm thông tin, cụ thể là tất cả các __cxa_chức năng làm gì, hãy xem thông số kỹ thuật ban đầu của chúng:


23
Vì vậy, tóm tắt. Không phải trả phí nếu không có trường hợp ngoại lệ nào được ném ra. Một số chi phí khi một ngoại lệ được đưa ra, nhưng câu hỏi đặt ra là 'Chi phí này có lớn hơn việc sử dụng và kiểm tra mã lỗi trở lại mã xử lý lỗi không'.
Martin York

5
Chi phí lỗi thực sự có khả năng lớn hơn. Mã ngoại lệ có thể vẫn còn trên đĩa! Vì mã xử lý lỗi bị xóa khỏi mã bình thường, hành vi của bộ đệm trong các trường hợp không lỗi được cải thiện.
MSalters

Trên một số bộ xử lý, chẳng hạn như ARM, việc quay lại địa chỉ 8 byte "bổ sung" trước lệnh "bl" [branch-and-link, còn được gọi là "call"] sẽ có giá tương đương với việc quay lại địa chỉ ngay sau lệnh "bl". Tôi tự hỏi làm thế nào hiệu quả của việc chỉ cần có mỗi "bl" theo sau là địa chỉ của một trình xử lý "ngoại lệ đến" sẽ so sánh với cách tiếp cận dựa trên bảng và liệu có trình biên dịch nào làm điều đó không. Mối nguy hiểm lớn nhất mà tôi có thể thấy là các quy ước gọi điện không khớp có thể gây ra hành vi lập dị.
supercat

2
@supercat: bạn đang làm ô nhiễm I-cache của mình với mã xử lý ngoại lệ theo cách đó. Rốt cuộc, có một lý do khiến mã xử lý ngoại lệ và bảng có xu hướng khác xa với mã bình thường.
CesarB

1
@CesarB: Một từ hướng dẫn sau mỗi cuộc gọi. Có vẻ không quá lố, đặc biệt là khi các kỹ thuật xử lý ngoại lệ chỉ sử dụng mã "bên ngoài" thường yêu cầu mã luôn duy trì một con trỏ khung hợp lệ (trong một số trường hợp có thể yêu cầu 0 hướng dẫn bổ sung, nhưng trong một số trường hợp khác, có thể yêu cầu nhiều hơn một).
supercat

13

Các trường hợp ngoại lệ là chậm đúng trong ngày xưa.
Trong hầu hết các trình biên dịch hiện đại, điều này không còn đúng nữa.

Lưu ý: Chỉ vì chúng tôi có các ngoại lệ không có nghĩa là chúng tôi cũng không sử dụng mã lỗi. Khi lỗi có thể được xử lý cục bộ, hãy sử dụng mã lỗi. Khi các lỗi yêu cầu thêm ngữ cảnh để sửa, hãy sử dụng các ngoại lệ: Tôi đã viết nó một cách hùng hồn hơn nhiều ở đây: Các nguyên tắc hướng dẫn chính sách xử lý ngoại lệ của bạn là gì?

Chi phí của mã xử lý ngoại lệ khi không có ngoại lệ nào được sử dụng trên thực tế bằng không.

Khi một ngoại lệ được ném ra thì sẽ có một số công việc được thực hiện.
Nhưng bạn phải so sánh điều này với chi phí trả lại mã lỗi và kiểm tra lại chúng để biết lỗi có thể được xử lý. Cả hai tốn nhiều thời gian hơn để viết và duy trì.

Ngoài ra, có một điểm cần lưu ý cho người mới:
Mặc dù các đối tượng Exception được cho là nhỏ nhưng một số người lại đặt rất nhiều thứ bên trong chúng. Sau đó, bạn có chi phí sao chép đối tượng ngoại lệ. Giải pháp có hai lần:

  • Đừng đặt thêm những thứ khác vào ngoại lệ của bạn.
  • Bắt bằng tham chiếu const.

Theo ý kiến ​​của tôi, tôi sẽ đặt cược rằng cùng một mã có ngoại lệ hoặc hiệu quả hơn hoặc ít nhất là có thể so sánh với mã không có ngoại lệ (nhưng có tất cả mã bổ sung để kiểm tra kết quả lỗi chức năng). Hãy nhớ rằng bạn không nhận được bất cứ thứ gì miễn phí trình biên dịch đang tạo mã mà bạn đáng lẽ phải viết ngay từ đầu để kiểm tra mã lỗi (và thông thường trình biên dịch hiệu quả hơn nhiều so với con người).


1
Tôi dám cá rằng mọi người ngần ngại sử dụng các ngoại lệ, không phải vì bất kỳ sự chậm chạp nào mà họ không biết cách chúng được triển khai và những gì chúng đang làm với mã của bạn. Thực tế là chúng có vẻ như ma thuật gây khó chịu cho rất nhiều loại gần kim loại.
bay cao tốc

@speedplane: Tôi cho là vậy. Nhưng điểm chung của các trình biên dịch là chúng ta không cần hiểu phần cứng (nó cung cấp một lớp trừu tượng). Với các trình biên dịch hiện đại, tôi nghi ngờ liệu bạn có thể tìm thấy một người duy nhất hiểu mọi khía cạnh của trình biên dịch C ++ hiện đại hay không. Vậy tại sao việc hiểu các ngoại lệ khác với việc hiểu tính năng phức tạp X.
Martin York

Bạn luôn cần có một số ý tưởng về những gì phần cứng đang làm, đó là vấn đề về mức độ. Nhiều người đang sử dụng C ++ (trên Java hoặc một ngôn ngữ có tập lệnh) thường làm như vậy để đạt hiệu suất. Đối với họ, lớp trừu tượng phải tương đối trong suốt, để bạn có một số ý tưởng về những gì đang xảy ra trong kim loại.
bay cao tốc

@speedplane: Sau đó, họ nên sử dụng C trong đó lớp trừu tượng mỏng hơn nhiều theo thiết kế.
Martin York

12

Có một số cách bạn có thể thực hiện các ngoại lệ, nhưng thông thường chúng sẽ dựa vào một số hỗ trợ cơ bản từ Hệ điều hành. Trên Windows, đây là cơ chế xử lý ngoại lệ có cấu trúc.

Có một cuộc thảo luận khá chi tiết về Dự án Mã: Cách trình biên dịch C ++ thực hiện xử lý ngoại lệ

Chi phí của các ngoại lệ xảy ra bởi vì trình biên dịch phải tạo mã để theo dõi đối tượng nào phải bị hủy trong mỗi khung ngăn xếp (hoặc chính xác hơn là phạm vi) nếu một ngoại lệ lan truyền ra khỏi phạm vi đó. Nếu một hàm không có biến cục bộ trên ngăn xếp yêu cầu hàm hủy được gọi thì nó sẽ không có xử lý ngoại lệ wrt phạt hiệu suất.

Việc sử dụng mã trả về chỉ có thể giải phóng một cấp độ duy nhất của ngăn xếp tại một thời điểm, trong khi cơ chế xử lý ngoại lệ có thể nhảy xuống ngăn xếp hơn nữa trong một thao tác nếu nó không có gì để làm trong các khung ngăn xếp trung gian.


"Chi phí của các ngoại lệ xảy ra bởi vì trình biên dịch phải tạo mã để theo dõi các đối tượng nào phải được hủy trong mỗi khung ngăn xếp (hoặc chính xác hơn là phạm vi)" Trình biên dịch có phải làm điều đó để hủy các đối tượng khỏi một lượt trả về không?

Không. Cho một ngăn xếp với các địa chỉ trả về và một bảng, trình biên dịch có thể xác định các chức năng nào có trong ngăn xếp. Từ đó, những đối tượng nào phải có trong ngăn xếp. Điều này có thể được thực hiện sau khi ngoại lệ được ném. Một chút tốn kém, nhưng chỉ cần thiết khi một ngoại lệ thực sự được ném ra.
MSalters

vui nhộn, tôi chỉ tự hỏi bản thân "sẽ không tuyệt nếu mỗi khung ngăn xếp theo dõi số lượng đối tượng trong đó, loại, tên của chúng, để hàm của tôi có thể đào ngăn xếp và xem phạm vi mà nó thừa hưởng trong quá trình gỡ lỗi" và theo một cách nào đó, điều này thực hiện một cái gì đó giống như vậy, nhưng không phải luôn khai báo một bảng theo cách thủ công là biến đầu tiên của mọi phạm vi.
Dmitry


5

Bài viết này xem xét vấn đề và về cơ bản thấy rằng trong thực tế, có chi phí thời gian chạy cho các ngoại lệ, mặc dù chi phí khá thấp nếu ngoại lệ không được ném ra. Bài báo tốt, được khuyến khích.



0

Tất cả các câu trả lời tốt.

Ngoài ra, hãy nghĩ về việc gỡ lỗi mã dễ dàng hơn bao nhiêu khi có mã 'nếu kiểm tra' dưới dạng cổng ở đầu các phương thức thay vì cho phép mã ném các ngoại lệ.

Phương châm của tôi là thật dễ dàng để viết mã hiệu quả. Điều quan trọng nhất là viết mã cho người tiếp theo nhìn vào nó. Trong một số trường hợp, đó là bạn trong 9 tháng, và bạn không muốn bị nguyền rủa tên của mình!


Tôi đồng ý ở điểm chung, nhưng trong một số trường hợp, ngoại lệ có thể đơn giản hóa mã. Hãy suy nghĩ về xử lý trong constructors lỗi ... - những cách khác sẽ là một) mã lỗi trả lại bởi các thông số tham chiếu hoặc b) thiết lập các biến toàn cục
Uhli
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.