it-swarm-tr.com

Kesin takma kural nedir?

C 'de ortak tanımsız davranış hakkında soru sorulduğunda, insanlar bazen katı takma kuralına başvururlar.
Onlar ne hakkında konuşuyor?

759
Benoit

Sıkı örtüşme sorunları ile karşılaştığınız tipik bir durum, bir yapının (bir aygıt/ağ msg'si gibi) sisteminizin Word boyutunun bir tamponuna (uint32_ts veya uint16_ts işaretçisi gibi) bindirilmesidir. İşaretçi dökümü yoluyla böyle bir arabellek üzerine bir yapıyı ya da bu tür bir yapı üzerine bir arabellek yerleştirdiğinizde, sıkı takma kurallarını kolayca ihlal edebilirsiniz.

Yani bu tür bir kurulumda, bir şeye mesaj göndermek istersem, aynı bellek yığınına işaret eden iki uyumsuz işaretçi bulundurmam gerekir. Daha sonra saf bir şekilde şöyle bir şey kodlayabilirim:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Kesin aliasing kuralı bu kurulumu geçersiz kılar: C --- 6.5 paragraf 7'de izin verilen bir yumlu tür veya diğer türlerden biri olmayan bir nesneyi taklit eden işaretçiyi reddetmek1 tanımsız davranış. Ne yazık ki, yine de bu şekilde kod yazabilirsiniz, belki bazı uyarılar alabilirsiniz, sadece kodu çalıştırdığınızda beklenmedik bir davranışa sahip olmanız yeterlidir.

(GCC, takma uyarılar verme kabiliyetinde bir miktar tutarsız görünmektedir, bazen bize dostane bir uyarı verir, bazen de değil).

Bu davranışın neden tanımlanmadığını görmek için, katı takma kuralının derleyiciyi ne aldığını düşünmemiz gerekir. Temel olarak, bu kuralla, her döngünün buff içeriğini yenilemek için talimatlar eklemeyi düşünmek zorunda değildir. Bunun yerine, optimizasyon yaparken, takma hakkında sinir bozucu bazı güçsüz varsayımlarla, bu komutları çıkartabilir, döngü çalıştırılmadan önce buff[0] ve buff[1] CPU kayıtlarına yükleyebilir ve döngünün gövdesini hızlandırabilir . Kesin takma ad tanıtılmadan önce, derleyici, buff içeriğinin istediği zaman herhangi bir yerden herhangi bir kişi tarafından değiştirilebileceği bir paranoya durumunda yaşamak zorunda kaldı. Böylece, ekstra bir performans elde etmek için Edge ve çoğu insanın işaretçilere zarar vermediğini varsayarsak, katı takma kuralı uygulanmıştır.

Örneğin kabul edildiğini düşünüyorsanız, bunun yerine göndermeyi yapan başka bir işleve bir arabellek iletiyorsanız bile olabilir.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Ve bu kullanışlı fonksiyondan yararlanmak için önceki döngümüzü yeniden yazdık.

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Derleyici, SendMessage'ı satır içi olarak denemek için yeterince yetenekli veya akıllı olabilir veya buff'ı yeniden yüklemeye veya yüklememeye karar verebilir. SendMessage ayrı olarak derlenen başka bir API'nin parçasıysa, buff'ın içeriğini yüklemek için muhtemelen yönergeleri vardır. Sonra tekrar, belki C++ 'tasınız ve bu sadece derleyicinin satır içi yapabileceğini düşündüğü bazı uygulamalardır. Ya da belki de sadece kendi rahatınız için .c dosyasına yazdığınız bir şey. Yine de tanımsız davranış hala ortaya çıkabilir. Kaputun altında olanların bir kısmını bilsek bile, bu hala kuralın ihlalidir, bu nedenle iyi tanımlanmış bir davranış garanti edilmez. Bu yüzden sadece Word ile ayrılmış arabelleğimizi alan bir fonksiyonun içine kaydırmak mutlaka yardımcı olmaz.

Peki bunu nasıl çözebilirim?

  • Bir birlik kullan. Çoğu derleyici bunu sıkı takma işleminden şikayet etmeden destekliyor. Buna C99'da izin verilir ve açıkça C11'de izin verilir.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Derleyicinizde katı takma adını devre dışı bırakabilirsiniz ( f [no-] strict-aliasing gcc))

  • Sisteminizin Word'ü yerine takma isimlendirmek için char* kullanabilirsiniz. Kurallar, char* için bir istisna sağlar (signed char ve unsigned char dahil). Her zaman, char* 'in diğer türleri takma olduğu varsayılır. Ancak bu diğer şekilde çalışmaz: yapınızın bir tampon karakteri taklit ettiği varsayımı yoktur.

Yeni başlayanlar için dikkat

Bu, iki tipin birbirinin üzerine bindirilmesi sırasında ortaya çıkan tek potansiyel mayın tarlasıdır. Ayrıca endianness , Kelime hizalama ve nasıl hizalama sorunları ile paketleme yapıları doğru şekilde başa çıkacağınızı da öğrenmelisiniz.

Dipnot

1 C 2011 6.5 7 'nin bir değere erişmesine izin verdiği türler:

  • nesnenin etkin türüyle uyumlu bir tür,
  • nesnenin etkin türüyle uyumlu bir türün nitelikli bir versiyonunu,
  • nesnenin etkin türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin etkin türünün nitelikli bir sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplama veya sendika türü (yinelemeyle, bir alt sendika üyesi veya sendika üyesi dahil), veya
  • bir karakter türü.
545
Doug T.

Bulduğum en iyi açıklama Mike Acton, Sıkı Aliasing'i Anlamak . PS3 gelişimine biraz odaklandı, ancak bu temelde sadece GCC.

Makaleden:

"Kesin takma adlandırma, C (veya C++) derleyicisi tarafından yapılan ve işaretçileri farklı türdeki nesnelere yönlendiren bir varsayımdır, asla aynı bellek konumuna (yani, diğerlerine diğer adı vermeyecektir)."

Bu nedenle, temel olarak, bir int içeren bir belleğe işaret eden bir int* varsa ve sonra o belleğe bir float* işaretleyin ve onu float olarak kullanın, kuralı çiğneyin. Kodunuz buna uymuyorsa, derleyicinin iyileştiricisi kodunuzu büyük olasılıkla bozacaktır.

Kuralın istisnası, herhangi bir türe işaret etmesine izin verilen bir char*.

228
Niall

Bu, C++ standardının 3.10 bölümünde bulunan kesin takma kuraldır (diğer yanıtlar iyi açıklama sağlar, ancak hiçbiri kuralın kendisini vermez):

Bir program, bir nesnenin saklanan değerine, aşağıdaki türlerden farklı bir değer aracılığıyla erişmeye çalışırsa, davranış tanımsızdır:

  • nesnenin dinamik türü,
  • nesnenin dinamik tipinin cv kalitesinde bir versiyonunu,
  • nesnenin dinamik türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin dinamik türünün cv onaylı sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya birleşik tip (özyinelemeyle, bir alt agrega veya bir birliğin üyesi dahil),
  • nesnenin dinamik türünün (muhtemelen cv kalitesinde) temel sınıf tipi olan bir tür,
  • a char veya unsigned char türünde.

C++ 11 ve C++ 14 ifadeler (değişikliklerin üzerinde duruldu):

Bir program, bir nesnenin saklanan değerine, aşağıdaki türlerden başka bir glvalue aracılığıyla erişmeye çalışırsa, davranış tanımlanmaz:

  • nesnenin dinamik türü,
  • nesnenin dinamik tipinin cv kalitesinde bir versiyonunu,
  • Nesnenin dinamik türüne (4.4'te tanımlandığı gibi) benzer bir tür,
  • nesnenin dinamik türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin dinamik türünün cv onaylı sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • elemanları veya statik olmayan veri üyeleri (yukarıda belirtilenler dahil, yinelemeli olarak Bir alt kümenin veya içerdiği birliğin eleman veya statik olmayan veri üyesi ,
  • nesnenin dinamik türünün (muhtemelen cv kalitesinde) temel sınıf tipi olan bir tür,
  • a char veya unsigned char türünde.

İki değişiklik küçüktü: glvalue yerine lvalue ve toplu/sendika davası.

Üçüncü değişiklik daha güçlü bir garanti verir (güçlü takma kuralını gevşetir): Artık, takma ad için güvenli olan yeni ( benzer tür kavramı .


Ayrıca C ifadeler (C99; ISO/IEC 9899: 1999 6.5/7; ISO/IEC 9899: 2011 §6.5 ¶7’de de aynı ifadeler kullanılmıştır):

Bir nesnenin kayıtlı değerine, yalnızca aşağıdaki türlerden birine sahip olan bir değer ifadesiyle erişilebilmelidir 73) veya 88):

  • nesnenin etkin türüyle uyumlu bir tür,
  • nesnenin etkin türüyle uyumlu bir türün kalifiye bir versiyonunu,
  • nesnenin etkin türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin etkin türünün kalifiye versiyonuna karşılık gelen imzalı veya imzasız tipte bir tip,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplama veya sendika türü (yinelemeyle, bir alt sendika üyesi veya sendika üyesi dahil), veya
  • bir karakter türü.

73) veya 88) Bu listenin amacı, bir nesnenin takma olabileceği veya eklenemeyeceği koşulları belirlemektir.

131
Ben Voigt

Kesin takma ad sadece işaretçilere atıfta bulunmaz, referansları da etkiler, destekleyici wiki için bu konuda bir yazı yazdım ve o kadar iyi karşılandım ki danışmanlık web sitemde bir sayfaya döndüm. Ne olduğunu, insanları neden bu kadar karıştırdığını ve bu konuda ne yapılacağını tamamen açıklıyor. Sıkı Örtüşme Beyaz Kağıdı . Özellikle, sendikaların neden C++ için riskli davranış olduğunu ve memcpy kullanımının neden hem C hem de C++ 'ta taşınabilir tek çözüm olduğunu açıklamaktadır. Umarım bu yardımcı olur.

43
Patrick

Doug T.'nin zaten yazdıklarının eki olarak, işte muhtemelen gcc ile tetikleyen basit bir test durumu:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

gcc -O2 -o check check.c ile derleyin. Genellikle (denediğim çoğu gcc sürümünde) bu, "katı örtüşme sorunu" çıkarır, çünkü derleyici "h" nin "check" işlevindeki "k" ile aynı adres olamayacağını varsayar. Bundan dolayı derleyici if (*h == 5) away'i optimize eder ve her zaman printf'yi çağırır.

Burada ilgilenenler için x64 için ubuntu 12.04.2'de çalışan, gcc 4.6.3 tarafından üretilen x64 assembler kodu:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Yani if ​​koşulu assembler kodundan tamamen çıkmış.

34
Ingo Blackman

punning yazın imleç göstergeleri (bir sendika kullanmanın aksine) aracılığıyla katı takma adın kesilmesinin önemli bir örneğidir.

16

C89 gerekçesine göre, Standardın yazarları aşağıdaki gibi kodlar verilmesini istemediler:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

x'ın p ve atama *p olarak gösterilebilmesi ihtimaline izin vermek için atama ve return ifadesi arasında x değerini yeniden yüklemeliyiz sonuç olarak x değerini değiştirebilir. Bir derleyicinin takma olmayacağını varsayma hakkı olduğu iddiası yukarıdaki gibi durumlarda tartışmalı değildi.

Maalesef, C89’un yazarları kurallarını kelimenin tam anlamıyla okunursa aşağıdaki işlevi Tanımsız Davranışı çağıracak şekilde yazdı:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

struct S türündeki bir nesneye erişmek için int türünde bir değer kullanır, ve int, struct S öğesine erişmek için kullanılabilecek türler arasında değildir. Karakter tipi olmayan yapı elemanlarının ve sendikaların tüm kullanımlarının Tanımsız Davranış olarak ele alınması saçma olacağından, neredeyse herkes bir tür değerinin başka bir türdeki bir nesneye erişmek için kullanılabileceği en azından bazı koşulların olduğunu kabul eder. . Ne yazık ki, C Standartları Komitesi bu koşulların ne olduğunu tanımlayamadı.

Sorunun çoğu, gibi bir programın davranışı hakkında sorulan 028 Hata Raporu'nun bir sonucudur.

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Hata Raporu # 28, programın Tanımsız Davranışı, çünkü "double" türünde bir sendika üyesi yazma ve "int" türünden birini okuma eylemi Uygulama-Tanımlı davranışını çağırdığını belirtir. Bu akıl yürütme saçmadır, ancak asıl sorunu ele almak için hiçbir şey yapmadan dili gereksiz yere karmaşıklaştıran Etkili Tip kurallarının temelini oluşturur.

Asıl sorunu çözmenin en iyi yolu muhtemelen kuralın amacı ile ilgili dipnotu normatifmiş gibi ele almak ve kuralın takma adları kullanarak çakışan erişimleri içeren durumlar dışında uygulanamaz hale getirilmesidir. Gibi bir şey verildi:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

inc_int içinde herhangi bir çelişki yoktur, çünkü *p aracılığıyla erişilen depolamaya tüm erişimler, int türündeki bir değerle yapılır ve test türünde bir çelişki yoktur çünkü p, gözle görülür bir şekilde struct S türevinden türetilir ve bir dahaki sefer s kullanıldığında, p aracılığıyla yapılacak tüm bu depolamaya erişilirdi.

Eğer kod biraz değiştirildiyse ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Burada, işaretlenmiş satırda p ile s.x arasındaki erişim arasında bir takma uyuşmazlığı var çünkü uygulamada o noktada başka bir referans var aynı depoya erişmek için kullanılacak =.

Hata Raporu 028 olsaydı, orijinal örnekte, iki işaretleyicinin yaratılması ve kullanılması arasındaki çakışma nedeniyle UB'yi çağırdı; bu, "Etkili Tipler" veya başka bir karmaşıklık eklemek zorunda kalmadan işleri daha açık hale getirecekti.

15
supercat

Cevapların birçoğunu okuduktan sonra, bir şeyler eklemek zorunda olduğumu hissediyorum:

Kesin takma adlandırma (biraz tanımlayacağım) önemlidir çünkü :

  1. Hafıza erişimi pahalı olabilir (performans açısından), bu nedenle CPU kayıtlarında veri manipüle edilir fiziksel belleğe geri yazılmadan önce.

  2. İki farklı CPU kaydındaki veriler aynı hafıza alanına yazılacaksa, C olarak kodladığımızda hangi verilerin "hayatta kalacağını" tahmin edemeyiz .

    CPU sicillerinin yükleme ve boşaltma işlemlerini manuel olarak kodladığımız Assembly'de hangi verilerin sağlam kalacağını bileceğiz. Fakat C (çok şükür) bu detayı uzaklaştırıyor.

İki işaretçi bellekte aynı yere işaret edebileceğinden, bu, olası çarpışmaları işleyen karmaşık kodla sonuçlanabilir .

Bu ekstra kod yavaştır ve performansı düşürür hem yavaş hem de (muhtemelen) gereksiz ekstra bellek okuma/yazma işlemlerini gerçekleştirdiğinden.

Sıkı diğer adlandırma kuralı, fazladan makine kodundan kaçınmamıza izin verir , iki işaretçilerin olmadığını varsaymak için güvenli olması gerektiği durumlarda t aynı bellek bloğunu göster (ayrıca restrict anahtar sözcüğüne de bakın).

Sıkı diğer adlandırma, farklı türlere işaretçilerin bellekteki farklı konumlara işaret ettiğini varsaymanın güvenli olduğunu belirtir.

Bir derleyici, iki işaretçinin farklı türlere işaret ettiğini fark ederse (örneğin, bir int * ve bir float *), bellek adresinin farklı olduğunu varsayar ve yapmaz hafıza adresi çarpışmalar, daha hızlı makine kodu ile sonuçlanır.

Örneğin :

Aşağıdaki işlevi varsayalım:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

a == b (her iki işaretçi de aynı belleğe işaret ediyor) durumunu ele almak için, verileri hafızadan CPU kaydedicilere yükleme yöntemini sipariş etmeli ve test etmeliyiz, böylece kod bu şekilde bitebilir :

  1. a ve b 'i bellekten yükleyin.

  2. a, b 'a ekleyin.

  3. save b ve yeniden yükle a.

    (CPU kaydından hafızaya kaydedin ve hafızadan CPU kaydına yükleyin).

  4. b, a 'a ekleyin.

  5. a (CPU register'ından) hafızaya kaydedin.

Adım 3 çok yavaş çünkü fiziksel belleğe erişmesi gerekiyor. Bununla birlikte, a ve b öğelerinin aynı bellek adresini gösterdiği örneklere karşı koruma gerekir.

Sıkı diğer adlandırma, derleyiciye bu bellek adreslerinin belirgin bir şekilde farklı olduğunu söyleyerek bunu önlememize izin verir (bu durumda, işaretçiler bir bellek adresini paylaşırsa gerçekleştirilemeyecek olan daha da iyileştirmeye izin verir).

  1. Bu, derleyiciye, işaret etmek için farklı türler kullanılarak iki yolla söylenebilir. yani .:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. restrict anahtar sözcüğünü kullanarak. yani .:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Şimdi, Sıkı Örtüşme kuralını yerine getirerek, 3. adımdan kaçınılabilir ve kod önemli ölçüde daha hızlı çalışacaktır.

Aslında, restrict anahtar sözcüğünü ekleyerek, tüm işlev aşağıdakiler için optimize edilebilir:

  1. a ve b 'i bellekten yükleyin.

  2. a, b 'a ekleyin.

  3. sonucu hem a hem de b olarak kaydedin.

Bu optimizasyon olası çarpışma nedeniyle daha önce yapılamazdı (burada a ve b iki katına yerine üçe katlanırdı).

10
Myst

Kesin takma adlandırma, aynı işaretçiye farklı işaretçi türlerine izin vermiyor.

Bu makale konuyu tam olarak anlamanıza yardımcı olacaktır.

6
Jason Dagit