C\C++ belirtimleri derleyicilerin kendi yöntemleriyle gerçekleştirebileceği çok sayıda davranışı açık bırakır. Burada her zaman aynı soru sorulmaya devam eden bir dizi soru var ve bu konuda bazı mükemmel yayınlarımız var:
Benim sorum tanımsız davranışların ne olduğu ile ilgili değil, ya da gerçekten kötü mü? Standartların tehlikelerini ve ilgili tanımlanmamış davranış tekliflerinin çoğunu biliyorum, bu yüzden lütfen ne kadar kötü olduğu hakkında cevaplar göndermekten kaçının. Bu soru, derleyici uygulaması için çok fazla davranışı açık bırakmanın arkasındaki felsefe ile ilgilidir.
Performansın ana nedeni olduğunu belirten bir mükemmel blog yazısı okudum. Performansın buna izin veren tek kriter olup olmadığını merak ediyordum ya da derleyici uygulaması için işleri açık bırakma kararını etkileyen başka faktörler var mı?
Belirli bir tanımlanmamış davranışın derleyicinin optimize etmesi için nasıl yeterli alan sağladığına dair herhangi bir örneğiniz varsa, lütfen bunları listeleyin. Performans dışında başka faktörler biliyorsanız, lütfen cevabınızı yeterli ayrıntıyla geri verin.
Soruyu anlamadıysanız veya yanıtınızı destekleyecek yeterli kanıt/kaynağınız yoksa, lütfen geniş kapsamlı spekülasyon cevapları göndermeyin.
İlk olarak, burada sadece "C" denilse de, aynı şey C++ için de aynı derecede geçerlidir.
Godel'den bahseden yorum kısmen (ama sadece kısmen) önemliydi.
Aşağı indiğinizde, C standartlarında tanımlanmamış davranış büyük ölçüde , sadece standardın tanımlamaya çalıştığı ile ne yapmadığı arasındaki sınırı gösterir.
Godel'in teoremleri (iki tane vardır) temel olarak hem kendi kurallarına göre hem tam hem de tutarlı olduğu kanıtlanabilen bir matematik sistemi tanımlamanın imkansız olduğunu söyler. Kurallarınızı tamamlanabilmesi için yapabilirsiniz (ele aldığı durum doğal sayılar için "normal" kurallardı) ya da tutarlılığını kanıtlamayı mümkün kılabilirsiniz, ancak her ikisine de sahip olamazsınız.
C gibi doğrudan olmayan bir şey söz konusu olduğunda - çoğunlukla, sistemin bütünlüğünün veya tutarlılığının “geçerliliği” çoğu dil tasarımcısı için yüksek bir öncelik değildir. Aynı zamanda, evet, muhtemelen (en azından bir dereceye kadar), "mükemmel" bir sistem tanımlamanın imkansız olduğunu bilerek etkilendiler - kanıtlanabilir şekilde eksiksiz ve tutarlı bir sistem. Böyle bir şeyin imkansız olduğunu bilmek, geri adım atmayı, biraz nefes almayı ve tanımlamaya çalışacaklarının sınırlarına karar vermeyi biraz daha kolay hale getirmiş olabilir.
(Yine) kibirle suçlanma riski altında, C standardını (kısmen) iki temel fikirle yönetiliyor olarak nitelendiririm:
Birincisi, eğer birisi yeni bir CPU tanımlarsa, tasarımın en azından birkaç basit yönerge makul bir şekilde yakın olduğu sürece, bunun için iyi, sağlam, kullanışlı bir C uygulaması sağlamak mümkün olmalıdır. Von Neumann modelinin genel sırasına göre bir şey izler ve C uygulamasına izin vermek için yeterli olması gereken en azından makul miktarda minimum bellek sağlar. "Barındırılan" bir uygulama için (bir işletim sisteminde çalışan bir uygulama), dosyalara makul şekilde karşılık gelen bir kavramı desteklemeniz ve belirli bir minimum karakter kümesine sahip bir karakter kümesine sahip olmanız gerekir (91 gereklidir).
İkincisi, donanımı doğrudan manipüle eden kod yazmanın mümkün olması anlamına gelir, böylece önyükleme yükleyicileri, işletim sistemleri, herhangi bir işletim sistemi olmadan çalışan gömülü yazılım vb. Yazabilirsiniz. Sonuçta some bu açıdan sınırlar, bu nedenle neredeyse tüm pratik işletim sistemleri, önyükleme yükleyicisi vb. en azından bir little Meclis dilinde yazılmış kod biti içeriyor olabilir. Benzer şekilde, küçük bir gömülü sistemin bile Host sistemindeki cihazlara erişim vermek için en azından bir tür önceden yazılmış kütüphane rutini içermesi muhtemeldir. Kesin bir sınırın tanımlanması zor olsa da, niyet, bu koda bağımlılığın asgari düzeyde tutulmasıdır.
Dildeki tanımlanmamış davranış, büyük ölçüde dilin bu yetenekleri destekleme niyetinden kaynaklanmaktadır. Örneğin, dil rasgele bir tamsayıyı bir işaretçiye dönüştürmenize ve o adreste ne olursa olsun erişmenize olanak tanır. Standart, ne zaman yapacağınızı söyleme girişiminde bulunmaz (örneğin, bazı adreslerden okumak bile harici olarak görünür etkiler gösterebilir). Aynı zamanda, bu tür şeyleri yapmanızı engellemeye kalkışmaz, çünkü C = yazmanız gereken bazı yazılımlar için need .
Diğer tasarım öğeleri tarafından yönlendirilen bazı tanımlanmamış davranışlar da vardır. Örneğin, C'nin diğer bir amacı ayrı bir derlemeyi desteklemektir. Bu (örneğin), çoğumuzun bir bağlayıcının olağan modeli olarak gördüklerini kabaca takip eden bir bağlayıcı kullanarak parçaları birbirine "bağlayabileceğiniz" anlamına gelir. Özellikle, dilin semantiği hakkında bilgi sahibi olmadan ayrı olarak derlenmiş modülleri eksiksiz bir programda birleştirmek mümkün olmalıdır.
Temel olarak bildiğimiz şeyler hatalardır ve muhtemelen derleyicinin hata olarak teşhis etmesini isteyecek şeyler olan tanımlanmamış başka bir tür (C++ 'da C'den çok daha yaygındır) vardır. ancak derleyici teknolojisindeki mevcut sınırlar göz önüne alındığında, bunların her koşulda teşhis edilebileceği şüphelidir. Bunların birçoğu ayrı derleme gibi diğer gereksinimler tarafından yönlendirilir, bu nedenle büyük ölçüde çelişen gereksinimleri dengeleme meselesidir, bu durumda komite genellikle bazı olası sorunları teşhis etmemek anlamına gelse bile, daha büyük kapasiteleri desteklemeyi seçmiştir, olası tüm sorunların teşhis edilmesini sağlamak için yetenekleri sınırlamak yerine.
intent içindeki bu farklılıklar, C ile Java veya Microsoft'un CLI tabanlı sistemleri gibi) arasındaki farkların çoğunu yönlendirir. çok daha sınırlı bir donanım seti ile çalışmak ya da hedefledikleri daha spesifik donanımı taklit etmek için yazılım gerektirmek ve ayrıca bunun yerine doğrudan donanımın herhangi bir doğrudan manipülasyonu için : prevent böyle bir girişimde bulunmak için JNI veya P/Invoke (ve C gibi bir şeyle yazılmış kod) gibi bir şey kullanırsınız.
Bir an için Godel'in teoremlerine geri dönersek, buna paralel bir şey çizebiliriz: Java ve CLI "dahili olarak tutarlı" alternatifi seçti, C ise "tam" alternatifini seçti. Tabii ki, bu çok kaba bir benzetmedir - herhangi birinin ya da iç tutarlılık veya ya da Bununla birlikte, genel kavram oldukça aldıkları seçimlerle yakından örtüşmektedir.
Belirtilmemiş davranış, tanımlanmamış davranış ve uygulama tanımlı davranış terimleri, Standardı özellikleri tam olarak tanımlamayan veya tanımlayamayan programların yazılmasının sonucunu kategorize etmek için kullanılır. Bu kategorileştirmeyi benimsemenin amacı, uygulama kalitesinin pazarda aktif bir güç olmasına izin veren uygulamalar arasında belirli bir çeşitliliğe izin vermenin yanı sıra belirli popüler uzantılara izin vermektir, Standarda uygunluk. Standart F'ye Ek F, bu üç kategoriden birine giren davranışları kataloglar.
Belirtilmemiş davranış, çevirmene programları çevirmede bir miktar enlem verir. Bu enlem, programın tercüme edilemediği kadar uzanmaz.
Tanımsız davranış, uygulayıcıya, teşhis edilmesi zor bazı program hatalarını yakalamaması için lisans verir. Ayrıca, uygun dil uzantısının mümkün olduğu alanları da tanımlar: uygulayıcı, resmi olarak tanımlanmamış davranışın bir tanımını sağlayarak dili artırabilir.
Uygulama tanımlı davranış, bir uygulayıcıya uygun yaklaşımı seçme özgürlüğü verir, ancak bu seçimin kullanıcıya açıklanmasını gerektirir. Uygulama tanımlı olarak belirlenen davranışlar genellikle kullanıcının uygulama tanımına dayalı olarak anlamlı kodlama kararları verebildiği davranışlardır. Bir uygulama tanımının ne kadar kapsamlı olması gerektiğine karar verirken uygulayıcılar bu kriteri dikkate almalıdır. Belirtilmemiş davranışlarda olduğu gibi, yalnızca uygulama tanımlı davranışı içeren kaynağı çevirememek yeterli bir yanıt değildir.
Önemli olan programların yararıdır, sadece uygulamaların yararı değildir. Tanımsız davranışa bağlı bir program, uygun bir uygulama tarafından kabul edilirse yine de uygun olabilir. Tanımlanmamış davranışın varlığı, bir programın uygun olmayan şekilde açıkça işaretlenmiş ("tanımlanmamış davranış") olarak taşınabilir olmayan özellikleri kullanmasına olanak tanır. Gerekçe notları:
C kodu taşınabilir olmayabilir. Programcılara gerçekten taşınabilir programlar yazma fırsatı vermesine rağmen, Komite programcıları yazmaya zorlamak istemedi taşınabilir bir şekilde, C'nin bir `` yüksek seviye birleştirici '' olarak kullanılmasını engellemek için: makineye özgü kod yazma yeteneği C'nin güçlü yönlerinden biridir. arasındaki farkı çizmeyi büyük ölçüde motive eden bu prensiptir. kesinlikle uygun program ve uygun program (§1.7).
Ve 1.7'de notları
Üç katlı uyum tanımı, uygun programların popülasyonunu genişletmek ve tek bir uygulama ve taşınabilir uygun programlar kullanarak uygun programlar arasında ayrım yapmak için kullanılır.
Kesinlikle uyumlu bir program, maksimum taşınabilir bir program için başka bir terimdir. Amaç, programcıya, taşınabilir olmamaları için mükemmel derecede faydalı C programlarını demonte etmeden, son derece portatif olan güçlü C programları yapma şansı vermektir. Böylece zarf kesinlikle.
Böylece, GCC üzerinde mükemmel çalışan bu küçük kirli program hala uygun!
Hız şey özellikle C ile karşılaştırıldığında bir sorundur. C++, ilkel tiplerin büyük dizilerini başlatmak gibi mantıklı olabilecek bazı şeyler yaptıysa, C koduna bir ton kriter kaybeder. Böylece C++ kendi veri türlerini başlatır, ancak C türlerini olduğu gibi bırakır.
Diğer tanımlanmamış davranışlar sadece gerçeği yansıtır. Bir örnek, türden daha büyük bir sayı ile bit kaydırmadır. Bu aslında aynı ailenin donanım nesilleri arasında farklılık gösterir. 16 bitlik bir uygulamanız varsa, aynı ikili dosya 80286 ve 80386'da farklı sonuçlar verecektir. Yani dil standardı bilmediğimizi söylüyor!
Bazı şeyler sadece olduğu gibi tutulur, örneğin alt ifadelerin değerlendirme sırası belirtilmemiş. Başlangıçta bunun derleyici yazarlarının daha iyi optimizasyon yapmasına yardımcı olduğuna inanılıyordu. Günümüzde derleyiciler bunu anlamaya yetecek kadar iyidir, ancak mevcut derleyicilerin özgürlüğünden faydalanan tüm yerleri bulma maliyeti çok yüksektir.
Örnek olarak, işaretçi erişimlerinin sadece performans nedenleriyle değil, tanımsız olması gerekir. Örneğin, bazı sistemlerde, belirli kayıtların işaretçi ile yüklenmesi bir donanım istisnası oluşturur. Açık SPARC yanlış hizalanmış bir bellek nesnesine erişmek bir veri yolu hatasına neden olur, ancak x86'da "sadece" yavaş olur. Bu durumda, temel donanım, olur ve C++ birçok donanım türüne taşınabilir.
Tabii ki derleyiciye mimariye özgü bilgileri kullanma özgürlüğü verir. Belirtilmemiş bir davranış örneği için, işaretli değerlerin sağa kaydırılması, hangi donanımın kullanılabileceğini ve yazılım öykünmesini zorlamamak için altta bulunan donanıma bağlı olarak mantıksal veya aritmetik olabilir.
Derleyici yazarın işini daha kolay hale getirdiğine inanıyorum, ancak örneği şimdi hatırlayamıyorum. Durumu hatırlarsam ekleyeceğim.
Basit: Hız ve taşınabilirlik. C++, geçersiz bir işaretçiyi referanstan kaldırdığınızda bir istisnanız olduğunu garanti ederse, gömülü donanıma taşınabilir olmaz. C++ her zaman başlatılmış ilkel öğeler gibi başka şeyleri garanti ettiyse, daha yavaş olurdu ve C++ Kökeni zamanında, yavaş, gerçekten çok kötü bir şeydi.
C, 9 bit baytlı ve kayan nokta birimi olmayan bir makinede icat edildi - varsayalım ki baytlar 9 bit, kelimeler 18 bit olmalı ve şamandıralar IEEE754 öncesi aritmatik kullanılarak uygulanmalı mı?
UB'nin ilk mantığının derleyiciye optimizasyon için yer bırakması olduğunu düşünmüyorum, ancak sadece mimarilerin şimdiye kadar daha fazla çeşitliliğe sahip olduğu bir zamanda hedefler için bariz uygulamayı kullanma olasılığı (C'nin Biraz tanıdık bir mimariye sahip olan PDP-11, ilk bağlantı noktası Honeywell 635 çok daha az tanıdıktı - 36 bit sözcük, 6 veya 9 bit bayt, 18 bit adres kullanarak sözcük adreslenebilir. en azından 2'nin tamamlayıcısını kullandı). Ancak, ağır optimizasyon bir hedef değilse, açık uygulama, taşma için çalışma zamanı kontrolleri, kayıt boyutu üzerindeki vardiya sayısı, birden çok değeri değiştiren ifadelerde takma adlar eklemeyi içermez.
Dikkate alınan bir diğer şey, uygulama kolaylığıydı. O zamanlar bir C derleyicisi birden çok işlem kullanan birden fazla geçişti çünkü bir işlemin ele alınması her şeyin mümkün olmayacağını (program çok büyük olurdu). Ağır tutarlılık kontrolü istemek - özellikle de birkaç CU içerdiğinde. (Bunun için C derleyicilerinden başka bir program, tiftik kullanıldı).
Erken klasik vakalardan biri tamsayı ilavesi imzalandı. Kullanılan işlemcilerin bazılarında, bu bir hataya neden olurken, diğerlerinde sadece bir değerle devam eder (muhtemelen uygun modüler değer). Her iki durumun da belirtilmesi, lezzetsiz aritmetik stile sahip makineler için programların, tamsayı toplamaya benzer bir şey için koşullu bir dal da dahil olmak üzere ekstra kodlara sahip olması gerektiği anlamına gelir.
Felsefe ile ilgili olarak gerçeklikten daha az olduğunu söyleyebilirim - C her zaman bir çapraz platform dili olmuştur ve standart bunu yansıtmak zorundadır ve herhangi bir standardın serbest bırakıldığı sırada, birçok farklı donanımda çok sayıda uygulama. Gerekli davranışı yasaklayan bir standart ya dikkate alınmayacak ya da rakip bir standartlar organı oluşturacaktır.
Bazı davranışlar makul bir yolla tanımlanamaz. Silinmiş bir işaretçiye erişmek demek. Bunu tespit etmenin tek yolu, silindikten sonra işaretçi değerini yasaklamak olacaktır (değerini bir yerde ezberlemek ve artık herhangi bir ayırma işlevinin geri döndürmesine izin vermemek). Sadece böyle bir ezberleme aşırıya kaçmakla kalmaz, aynı zamanda uzun süren bir program için izin verilen işaretçi değerlerinin tükenmesine neden olur.
Tarihsel olarak, Tanımsız Davranışın iki temel amacı vardı:
Derleyici yazarlarından, asla gerçekleşmemesi gereken koşulları işlemek için kod üretmelerini istemekten kaçınmak için.
Kodun yokluğunda bu koşulları açıkça ele alma olasılığını sağlamak için, uygulamaların bazı durumlarda yararlı olabilecek çeşitli "doğal" davranışları olabilir.
Basit bir örnek olarak, bazı donanım platformlarında, toplamı işaretli bir tam sayıya sığmayacak kadar büyük olan iki pozitif işaretli tam sayıyı toplamaya çalışmak, belirli bir negatif işaretli tamsayı verecektir. Diğer uygulamalarda bir işlemci tuzağını tetikleyecektir. C standardının her iki davranışı da zorunlu kılması için, doğal davranışı standarttan farklı olan platformlar için derleyicilerin, gerçek davranışı eklemek için koddan daha pahalı olabilecek doğru davranışı sağlamak için ekstra kod üretmesi gerekecektir. Daha da kötüsü, "doğal" davranışı isteyen programcıların bunu başarmak için daha fazla kod eklemek zorunda kalacakları anlamına gelir (ve bu ekstra kod tekrar eklemeden daha pahalı olurdu).
Ne yazık ki, bazı derleyici yazarları, derleyicilerin Tanımsız Davranışı uyandıracak koşulları bulmak için kendi yollarından çekilmesi gerektiği felsefesini benimsemiş ve bu tür durumların asla ortaya çıkmayabileceğini varsayarak, bundan geniş çıkarımlar yapmıştır. Bu nedenle, 32 bit
int içeren bir sistemde, aşağıdaki gibi bir kod verilir:
uint32_t foo(uint16_t q, int *p) { if (q > 46340) *p++; return q*q; }
c standardı, derleyiciye q'nun 46341 veya daha büyük olması durumunda q * q ifadesinin
int içine sığmayacak kadar büyük bir sonuç vereceğini ve sonuç olarak Tanımsız Davranışa neden olacağını söyler. bunun gerçekleşemeyeceğini ve bu nedenle
*p Eğer yaparsa. Çağrı kodu
*p, hesaplamanın sonuçlarını atması gerektiğinin bir göstergesi olarak, optimizasyonun etkisi, tamsayı taşması ile hemen hemen her türlü akla uygun bir şekilde performans gösteren sistemler üzerinde mantıklı sonuçlar verebilecek kodu almak olabilir (yakalama çirkin olabilir, ancak en azından mantıklı olur) ve bunu saçma sapan davranışlar gösterebilecek bir koda dönüştürdü.
Tanımsız davranıştan başka mantıklı bir seçimin olmadığı bir örnek vereceğim. Prensip olarak, herhangi bir işaretçi herhangi bir değişkeni içeren belleğe işaret edebilir, derleyicinin bilebileceği yerel değişkenlerin küçük istisnaları dışında adresleri hiç alınmamıştır. Ancak, modern bir CPU'da kabul edilebilir performans elde etmek için, bir derleyici değişken değerlerini kayıtlara kopyalamalıdır. Tamamen bellek dışında çalışmak bir marş değildir.
Bu temel olarak size iki seçenek sunar:
1) İşaretçinin söz konusu değişkenin belleğini işaret etmesi durumunda, bir işaretçi aracılığıyla herhangi bir erişimden önce her şeyi kayıtlardan temizleyin. Ardından, değerlerin işaretçiden değiştirilmesi durumunda, gereken her şeyi tekrar kayıt defterine yükleyin.
2) İşaretçinin bir değişkeni diğer adıyla doldurmasına izin verildiğinde ve derleyicinin bir işaretçinin bir değişkeni diğer adı takmadığını varsaymasına izin verildiğinde bir dizi kurala sahip olun.
C, seçenek 2'yi tercih eder, çünkü 1 performans için korkunç olur. Ancak, bir işaretçi bir değişkeni C kurallarının yasakladığı şekilde taklit ederse ne olur? Etki, derleyicinin aslında değişkeni bir kayıt defterinde saklayıp saklamadığına bağlı olduğundan, C standardının belirli sonuçları kesin olarak garanti etmesinin bir yolu yoktur.