C ++ 'da istisnalar nasıl çalışır (perde arkasında)


109

İnsanların istisnaların yavaş olduğunu söylediklerini görüyorum, ama hiçbir kanıt göremiyorum. Öyleyse, öyle olup olmadıklarını sormak yerine, istisnaların perde arkasında nasıl çalıştığını soracağım, böylece onları ne zaman kullanacağıma ve yavaş olup olmadığına karar verebilirim.

Bildiğim kadarıyla, istisnalar bir çok kez geri dönüş yapmakla aynıdır, ancak her dönüşten sonra başka bir tane yapması mı yoksa durması mı gerektiğini kontrol etmesi dışında. Dönmeyi ne zaman durduracağını nasıl kontrol ediyor? Sanırım istisnanın türünü ve bir yığın konumunu tutan ikinci bir yığın var, oraya gelene kadar geri dönüyor. Ayrıca, bu ikinci desteye dokunulduğu tek zamanın bir atışta ve her deneme / yakalamada olduğunu tahmin ediyorum. AFAICT dönüş kodları ile benzer bir davranış uygulamak aynı süreyi alacaktır. Ama bunların hepsi sadece bir tahmin, bu yüzden gerçekten ne olduğunu bilmek istiyorum.

İstisnalar gerçekten nasıl çalışır?



Yanıtlar:


105

Tahmin etmek yerine, üretilen koda küçük bir C ++ kodu ve biraz eski bir Linux kurulumu ile bakmaya karar verdim.

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);
}

Onu ile derledim g++ -m32 -W -Wall -O3 -save-temps -cve oluşturulan montaj dosyasına baktım.

    .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

_ZN11MyExceptionD1Evolduğu MyException::~MyException()derleyici o yıkıcı olmayan bir satır içi kopyasını ihtiyacı olduğuna karar verdi, böylece.

.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ürpriz! Normal kod yolunda hiçbir ekstra talimat yoktur. Derleyici bunun yerine, işlevin sonundaki bir tablo aracılığıyla başvurulan (aslında yürütülebilir dosyanın ayrı bir bölümüne yerleştirilen) fazladan hat dışı düzeltme kodu blokları oluşturdu. Tüm çalışmalar (Bu tablolarda dayalı standart kitaplığı tarafından perde arkasında yapılır _ZTI11MyExceptionedilirtypeinfo for MyException ) .

Tamam, bu aslında benim için sürpriz değildi, bu derleyicinin bunu nasıl yaptığını zaten biliyordum. Montaj çıktısı ile devam ederek:

    .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

Burada bir istisna atmanın kodunu görüyoruz. Sırf bir istisna atılabileceği için fazladan bir yük olmamasına rağmen, bir istisnayı gerçekten fırlatıp yakalamada açıkça çok fazla ek yük vardır. Çoğu, içinde gizlidir __cxa_throw, bu da:

  • Bu istisna için bir işleyici bulana kadar yığını istisna tablolarının yardımıyla yürütün.
  • Yığını o işleyiciye ulaşana kadar çözün.
  • Aslında işleyiciyi ara.

Bunu basitçe bir değeri döndürmenin maliyetiyle karşılaştırın ve istisnaların neden yalnızca istisnai getiriler için kullanılması gerektiğini anlayın.

Bitirmek için montaj dosyasının geri kalanı:

    .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"

Typeinfo verileri.

    .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

Daha da fazla istisna işleme tablosu ve çeşitli ekstra bilgiler.

Dolayısıyla, sonuç, en azından Linux'taki GCC için: maliyet, istisnaların atılıp atılmadığına bakılmaksızın fazladan alan (işleyiciler ve tablolar için) artı bir istisna atıldığında tabloları ayrıştırmanın ve işleyicileri yürütmenin ekstra maliyetidir. Hata kodları yerine istisnalar kullanırsanız ve bir hata nadir görülürse, artık hatalar için test ek yükünüz olmadığından daha hızlı olabilir .

Daha fazla bilgi istiyorsanız, özellikle tüm __cxa_işlevlerin ne işe yaradığını , geldikleri orijinal teknik özelliklere bakın:


23
Yani özet. Hiçbir istisna yapılmazsa maliyeti yoktur. Bir istisna atıldığında bir miktar maliyet olur, ancak soru 'Bu maliyet, hata işleme koduna kadar hata kodlarını kullanmaktan ve test etmekten daha mı büyüktür'.
Martin York

5
Hata maliyetleri gerçekten de muhtemelen daha yüksektir. İstisna kodu büyük olasılıkla hala diskte! Hata işleme kodu normal koddan kaldırıldığı için, hata olmayan durumlarda önbellek davranışı iyileşir.
MSalters

ARM gibi bazı işlemcilerde, bir "bl" [dal-ve-bağlantı, "çağrı" olarak da bilinir] talimatını geçen sekiz "ekstra" bayta adrese geri dönmenin maliyeti, "bl". Basitçe her "bl" ye sahip olmanın verimliliğinin, bir "gelen istisna" işleyicisinin adresini takip etmesinin, tablo tabanlı bir yaklaşımla nasıl karşılaştırılacağını ve herhangi bir derleyicinin böyle bir şey yapıp yapmadığını merak ediyorum. Görebildiğim en büyük tehlike, uyumsuz çağrı kurallarının tuhaf davranışlara neden olabilmesidir.
supercat

2
@supercat: Bu şekilde istisna işleme kodu ile I-önbelleğinizi kirletiyorsunuz. Sonuçta istisna işleme kodunun ve tabloların normal koddan uzak olma eğiliminde olmasının bir nedeni vardır.
CesarB

1
@CesarB: Her aramayı takip eden bir talimat kelimesi. Özellikle sadece "dış" kodu kullanan istisna işleme tekniklerinin genellikle kodun her zaman geçerli bir çerçeve işaretçisi tutmasını gerektirdiği düşünüldüğünde (bazı durumlarda 0 ekstra talimat gerektirebilir, ancak diğerlerinde daha fazlasını gerektirebilir bir).
supercat

13

İstisnalar yavaş ediliyor oldu eski günlerde gerçek.
Çoğu modern derleyicide bu artık geçerli değildir.

Not: İstisnalarımızın olması, hata kodlarını da kullanmadığımız anlamına gelmez. Hata yerel olarak ele alınabildiğinde hata kodlarını kullanın. Hatalar, düzeltme kullanım istisnaları için daha fazla bağlam gerektirdiğinde: Bunu burada çok daha anlamlı bir şekilde yazdım: İstisna işleme politikanıza rehberlik eden ilkeler nelerdir?

Hiçbir istisna kullanılmadığında istisna işleme kodunun maliyeti pratik olarak sıfırdır.

Bir istisna atıldığında bazı işler yapılır.
Ancak bunu, hata kodlarını döndürmenin maliyetiyle karşılaştırmanız ve bunları hatanın ele alınabileceği noktaya kadar kontrol etmeniz gerekir. Her ikisi de yazmak ve sürdürmek için daha fazla zaman alır.

Ayrıca acemiler için bir şey var:
İstisna nesnelerinin küçük olması gerekse de, bazı insanlar içlerine çok şey koyarlar. O zaman istisna nesnesini kopyalamanın maliyetine sahip olursunuz. Çözüm iki katlıdır:

  • İstisnanıza fazladan şeyler koymayın.
  • Const referansına göre yakala.

Benim düşünceme göre, istisnalarla aynı kodun ya daha verimli ya da en azından istisnalar olmaksızın kodla karşılaştırılabilir olduğuna bahse girerim (ancak fonksiyon hatası sonuçlarını kontrol etmek için tüm ekstra koda sahiptir). Ücretsiz olarak hiçbir şey almadığınızı unutmayın derleyici, hata kodlarını kontrol etmek için ilk başta yazmanız gereken kodu üretir (ve genellikle derleyici bir insandan çok daha etkilidir).


1
Bahse girerim ki insanlar, algılanan herhangi bir yavaşlık nedeniyle değil, nasıl uygulandıklarını ve kodunuza ne yaptıklarını bilmedikleri için istisnaları kullanmaktan çekiniyorlar. Sihir gibi görünmeleri, metale yakın pek çok türü rahatsız ediyor.
sürat uçağı

@speedplane: Sanırım. Ancak derleyicilerin asıl amacı, donanımı anlamamıza gerek kalmamasıdır (bir soyutlama katmanı sağlar). Modern derleyicilerle, modern bir C ++ derleyicisinin her yönünü anlayan tek bir kişi bulabileceğinizden şüpheliyim. Öyleyse neden istisnaları anlamak, karmaşık özellik X'i anlamaktan farklıdır.
Martin York

Donanımın ne yaptığına dair her zaman bir fikrinizin olması gerekir, bu bir derece meselesidir. C ++ (Java veya betik dili üzerinden) kullananların çoğu bunu genellikle performans için yapıyor. Onlar için soyutlama katmanı nispeten şeffaf olmalıdır, böylece metalde neler olup bittiğine dair bir fikriniz olur.
sürat uçağı

@speedplane: Öyleyse, soyutlama katmanının tasarım gereği çok daha ince olduğu C kullanıyor olmalılar.
Martin York

12

İstisnaları uygulayabileceğiniz birkaç yol vardır, ancak bunlar tipik olarak işletim sisteminin bazı temel desteğine güvenirler. Windows'ta bu, yapılandırılmış istisna işleme mekanizmasıdır.

Kod Projesi ile ilgili ayrıntılar hakkında iyi bir tartışma var: Bir C ++ derleyicisi istisna işlemeyi nasıl uygular?

İstisnaların ek yükü, bir istisna kapsamın dışına yayılırsa, derleyicinin her yığın çerçevesinde (veya daha kesin olarak kapsamda) hangi nesnelerin yok edilmesi gerektiğini izlemek için kod üretmesi gerektiğinden oluşur. Bir işlevin yığın üzerinde yıkıcıların çağrılmasını gerektiren yerel değişkenleri yoksa, bu durumda bir performans cezası wrt istisna işlemesi olmamalıdır.

Bir dönüş kodu kullanmak, bir seferde yığının yalnızca tek bir seviyesini çözebilir, oysa bir istisna işleme mekanizması, ara yığın çerçevelerinde yapacak bir şey yoksa, bir işlemde yığının çok daha altına atlayabilir.


"İstisnaların ek yükü, derleyicinin her yığın çerçevesinde (veya daha kesin olarak kapsamda) hangi nesnelerin yok edilmesi gerektiğini izlemek için kod üretmesi gerektiğinden oluşur" Derleyicinin, nesneleri bir dönüşten yok etmek için zaten bunu yapması gerekmiyor mu?

Hayır. Dönüş adresleri ve bir tablo içeren bir yığın verildiğinde, derleyici hangi işlevlerin yığın üzerinde olduğunu belirleyebilir. Bundan, hangi nesnelerin yığında olması gerekir. Bu, istisna atıldıktan sonra yapılabilir. Biraz pahalı, ancak yalnızca gerçekten bir istisna atıldığında gerekli.
MSalters

komik, sadece kendi kendime merak ediyordum "eğer her yığın çerçevesi içindeki nesnelerin sayısını, türlerini, adlarını takip etseydi harika olmaz mıydı, böylece işlevim yığını kazabilir ve hata ayıklama sırasında hangi kapsamları miras aldığını görebilirdi" ve bir bakıma, bu böyle bir şey yapar, ancak her zaman bir tabloyu her kapsamın ilk değişkeni olarak manuel olarak bildirmeden.
Dmitry


5

Bu makale sorunu inceler ve temel olarak, istisna atılmazsa maliyet oldukça düşük olmasına rağmen, uygulamada istisnaların çalıştırma zamanı maliyeti olduğunu bulur. İyi makale tavsiye edilir.



0

Hepsi güzel cevaplar.

Ayrıca, kodun istisnalar atmasına izin vermek yerine, yöntemlerin tepesindeki kapı olarak 'eğer kontrol eder' kodda hata ayıklamanın ne kadar kolay olduğunu düşünün.

Sloganım, işe yarayan kod yazmanın kolay olmasıdır. En önemli şey, ona bakan bir sonraki kişi için kodu yazmaktır. Bazı durumlarda, 9 ay içinde sizsiniz ve isminize küfretmek istemezsiniz!


Ortak olarak katılıyorum, ancak bazı durumlarda istisnalar kodu basitleştirebilir. Kuruculardaki hata işlemeyi düşünün ... - diğer yollar a) hata kodlarını referans parametrelerine göre döndürmek veya b) genel değişkenleri ayarlamak olacaktır
Uhli
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.