Bir C uygulamasının maksimum hesaplama gücü


28

Kitabın önüne geçersek (veya tercih ederseniz dil spesifikasyonunun herhangi bir versiyonunu kullanırsak), bir C uygulamasının ne kadar hesaplama gücü olabilir?

“C uygulaması” nın teknik bir anlamı olduğunu unutmayın: Uygulama-tanımlanmış davranışın belgelendiği C programlama dili spesifikasyonunun özel bir örneğidir. AC uygulamasının gerçek bir bilgisayarda çalışabilmesi gerekmez. Bit-dizili bir temsili olan her nesne ve uygulama-tanımlı boyutta olan türler dahil olmak üzere, tüm dili uygulamak zorundadır.

Bu sorunun amacı için harici depolama yoktur. Yapabileceğiniz tek giriş / çıkış getchar(program girişini okumak için) ve putchar(program çıkışını yazmak için) 'dir. Ayrıca tanımsız davranışı çağıran herhangi bir program geçersizdir: geçerli bir program, C belirtimi ile tanımlanmış davranışı artı uygulamanın Ek J'de listelenen uygulama tanımlı davranışları tanımlamasıyla tanımlamalıdır (C99 için). Standartta belirtilmeyen çağıran kütüphane işlevlerinin tanımsız davranış olduğunu unutmayın.

İlk tepkim, bir C uygulamasının sonlu bir otomattan başka bir şey olmamasıydı, çünkü adreslenebilir bellek miktarını sınırlandırıyordu sizeof(char*) * CHAR_BIT(depolanan bitlerden daha fazlasını ele alamazsınız , çünkü farklı bellek adreslerinin depolandığında farklı bit desenleri olması gerekir) bir bayt işaretçisinde).

Ancak bir uygulamanın bundan daha fazlasını yapabileceğini düşünüyorum. Söyleyebileceğim kadarıyla, standart özyinelemenin derinliğine sınır koymuyor. Böylece istediğiniz kadar özyinelemeli işlev çağrısı yapabilirsiniz, ancak sınırlı sayıda çağrı yalnızca adreslenebilir olmayan ( register) argümanları kullanmalıdır. Böylece, keyfi tekrarlama sağlayan ve registernesne sayısını sınırlamayan bir C uygulaması deterministik aşağı itme otomatlarını kodlayabilir.

Bu doğru mu? Daha güçlü bir C uygulaması bulabilir misiniz? Bir Turing tamamlandı C uygulaması var mı?


4
@Dave: Gilles'un açıkladığı gibi, sınırsız hafızanız olabilir, ancak doğrudan adreslemenin bir yolu yok gibi görünüyor.
Jukka Suomela

2
Açıklamanıza göre, herhangi bir C uygulaması, yalnızca bağlamsal olmayan dillerden bile daha zayıf olan deterministik aşağı itme otomatları tarafından kabul edilen dilleri kabul edecek şekilde programlanabilir . Bununla birlikte, bu gözlem asimptotiklerin yanlış bir uygulaması olduğundan benim görüşüme ilgi duymuyor.
Warren Schudy

3
Akılda tutulması gereken bir nokta "uygulama tanımlı davranış" (veya "tanımsız davranış") tetiklemek için birçok yol vardır. Genel olarak, bir uygulama, örneğin, C standardında tanımlanmayan işlevsellik sağlayan kütüphane işlevlerini sağlayabilir. Bunların tümü, Turing-complete bir makineye erişebileceğiniz “boşluklar” sağlar. Ya da çok daha güçlü bir şey, durma problemini çözen bir kehanet gibi. Aptal bir örnek: imzalı tamsayı taşmalarının veya tamsayı işaretçi dönüşümlerinin uygulama tanımlı davranışı böyle bir oracle erişmenize izin verebilir.
Jukka Suomela

7
Bu arada, "rekreasyonel" etiketini (veya komik bulmacalar için ne kullanıyorsak) etiketini eklemek iyi bir fikir olabilir, böylece insanlar bunu çok ciddiye almazlar. Açıkça sorulması gereken "yanlış soru", ama yine de eğlenceli ve ilgi çekici buldum. :)
Jukka Suomela

2
@Jukka: Güzel fikir. Örneğin, X ile taşma = kasette X / 3 yazın ve X% 3 yönünde hareket edin, underflow = kasetteki sembole karşılık gelen sinyali tetikleyin. Biraz suistimal gibi geliyor, ama kesinlikle sorumun ruhu içinde. Cevap olarak yazabilir misin? (@ anne: Diğer akıllıca önerileri reddetmek istemem!)
Gilles 'SO' kötü olmaktan

Yanıtlar:


8

Soruda belirtildiği gibi, standart C, her tür değişkenin unsigned chardaima 0 ile UCHAR_MAX arasında bir değer tutacağı şekilde bir UCHAR_MAX değeri olmasını gerektirir . Ayrıca, dinamik olarak tahsis edilen her nesnenin, bir tip gösterici vasıtasıyla tanımlanabilen bir bayt dizisi ile temsil edilmesini unsigned char*ve bu tipteki sizeof(unsigned char*)her göstericinin tip sizeof(unsigned char *)değerleri dizisi ile tanımlanabileceği bir sabit olmasını gerektirir unsigned char. Eş zamanlı olarak dinamik, böylece katı sınırlıdır tahsis edilebilir nesne sayısı . Hiçbir şey bu kadar fazla destek olarak bu sabitlerin değerleri atamak gelen teorik derleyici önleyecek 10 10 10 nesneler, ancak teorik bir perspektiften herhangi varlığı büyük, araçlar şey sonsuz değildir, nasıl olursa olsun bağlanmış.UC'HbirR,_MbirXsbenzeOf(unsbengned chbirr*)101010

Bir program, yığında tahsis edilen hiçbir şey adresi alınmamışsa, yığın üzerinde sınırsız miktarda bilgi depolayabilir ; Böylece, herhangi bir boyuttaki sonlu otomatlar tarafından yapılamayan bazı şeyleri yapabilen bir C programı olabilir. Bu nedenle, yığın değişkenlerine erişim (veya belki de nedeniyle), dinamik olarak tahsis edilmiş değişkenlere erişimden çok daha sınırlı olsa da, C'yi sonlu bir otomat olmaktan, bir basma otomatına çevirir.

Bununla birlikte, başka bir potansiyel kırışıklık vardır: eğer bir program iki işaretçi ile ilişkili sabit uzunluktaki karakter dizilerini farklı nesnelere incelerse, bu dizilerin benzersiz olması gerekir. Sadece olduğundan UC'HbirR,_MbirXsbenzeOf(unsbengned chbirr*)karakter değerlerinin muhtemel dizileri, herhangi bir nesneyi aşmak için çok sayıda işaretçi oluşturan herhangi bir program, kodun bu işaretçilerle ilişkili karakterlerin sırasını incelemesi halinde C standardına uymayacaktı . Bununla birlikte, bazı durumlarda bir derleyicinin hiçbir kodun bir imleçle ilişkili karakter sırasını inceleyemeyeceğini belirlemesi mümkün olabilir. Her "karakter" aslında herhangi bir sonlu tamsayıyı tutabilmişse ve makinenin hafızası sayısız tam sayıdaki bir dizilim [sınırsız bant Turing makinesi verildiğinde, gerçekten yavaş olmasına rağmen böyle bir makineyi taklit edebilir ], o zaman C'yi Turing-eksiksiz bir dil yapmak gerçekten mümkün olabilirdi.


Böyle bir makineyle sizeof (char) ne döndürürdü?
TLW

1
@TLW: Diğer tüm makinelerle aynı: 1. CHAR_BITS ve CHAR_MAX makroları biraz daha problemli olurdu; Standart, sınırı olmayan türler kavramına izin vermez.
supercat,

Hata! Dediğiniz gibi CHAR_BITS demek istedim, üzgünüm.
TLW

7

C11'in (isteğe bağlı) iş parçacığı kitaplığıyla, sınırsız özyineleme derinliği verilen bir Turing uygulamasının tamamlanması mümkündür.

Yeni bir iş parçacığı oluşturmak, ikinci bir yığın oluşturur; Turing'in eksiksiz olması için iki istif yeterlidir. Bir yığın başın solunda, diğer yığın sağda olanı temsil eder.


Ancak, yalnızca bir yönde sonsuz şekilde ilerleyen bir bantla Turing makineleri, iki yönde sonsuz şekilde ilerleyen bir bantla Turing makineleri kadar güçlüdür. Bunun dışında, bir zamanlayıcı tarafından birden fazla iş parçacığı simüle edilebilir. Neyse, bir iş parçacığı kütüphanesine bile ihtiyacımız yok.
xamid

3

Bence Turing tamamlandı : Bu numarayı kullanarak UTM'yi simüle eden bir program yazabiliriz (Kodu el ile hızlı bir şekilde yazdım, bu yüzden muhtemelen bazı sözdizimi hataları olabilir ... ama umarım mantıkta hiçbir büyük hata yoktur. :-)

  • Bant gösterimi için çift bağlantılı bir liste olarak kullanılabilecek bir yapı tanımlayabilir.
    typdef struct {
      cell_t * pred; // soldaki hücre
      cell_t * succ; // sağdaki hücre
      int val; // hücre değeri
    } cell_t 

headBir için bir işaretçi olacak cell_tyapı

  • Mevcut durumu ve bayrağı depolamak için kullanılabilecek bir yapı tanımlayabilir.
    typedef yapı {
      int devlet;
      int flag;
    } info_t 
  • daha sonra, kafa çift bağlantılı listenin sınırları arasındayken bir Universal TM'yi simüle eden tek bir döngü fonksiyonu tanımlayın; baş bir sınıra çarptığında info_t yapısının (HIT_LEFT, HIT_RIGHT) bayrağını ayarlayın ve geri dönün:
geçersiz simulate_UTM (cell_t * head, info_t * info) {
  while (true) {
    head-> val = UTM_nextsymbol [info-> durum, head-> val]; // sembol yaz
    info-> state = UTM_nextstate [bilgi-> durum, baş-> val]; // sonraki durum
    if (info-> state == HALT_STATE) {// kabul ederse yazdır ve programdan çık
       putchar ((bilgi-> durum == ACCEPT_STATE)? '1': '0');
       çıkış (0);
    }
    int move = UTM_nextmove [info-> durum, head-> val];
    eğer (hareket == MOVE_LEFT) {
      head = head-> pred; // Sola hareket et
      if (head == NULL) {info-> flag = HIT_LEFT; dönüş; }
    } Başka {
      head = head-> succ; // sağa hareket et
      if (head == NULL) {info-> flag = HIT_RIGHT; dönüş; }
    }
  } // hala sınırda ... devam et
}
  • daha sonra önce simülasyon UTM rutini çağıran ve daha sonra teyp genişletilmesi gerektiğinde kendisini tekrar eden çağıran özyinelemeli bir işlev tanımlayın; bandın üst kısımda genişletilmesi gerektiğinde (HIT_RIGHT) hiç sorun çıkarmaz, alttan kaydırılması gerektiğinde (HIT_LEFT) sadece çift bağlantılı listeyi kullanarak hücrelerin değerlerini yükseltirsiniz:
boşluk istifleyici (cell_t * top, cell_t * alt, cell_t * kafa, info_t * info) {
  simulate_UTM (kafa, bilgi);
  cell_t newcell; // yeni hücre
  newcell.pred = üst; // çift bağlantılı listeyi yeni hücreyle güncelle
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  anahtarı (info-> hit) {
    vaka HIT_RIGHT:
      istifleyici (& newcell, alt, newcell, bilgi);
      break;
    vaka HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// değerleri yükseltir
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      istifleyici (& newcell, alt, alt, bilgi);
      break;
  }
}
  • İlk bant çift bağlantılı listeyi oluşturan basit bir özyinelemeli fonksiyonla doldurulabilir ve daha sonra stackergiriş bandının son sembolünü okuduğunda fonksiyonu çağırır (readchar kullanarak).
void init_tape (hücre_t * üst, hücre_t * alt, info_t * bilgi) {
  cell_t newcell;
  int c = readchar ();
  eğer (c == END_OF_INPUT) istifleyici (& üst, alt, alt, bilgi); // daha fazla sembol yok, başla
  newcell.pred = üst;
  if (top! = NULL) top.succ = & newcell; başka alt = & newcell;
  init_tape (& newcell, alt, bilgi);
}

EDIT: biraz düşündükten sonra, işaretçilerle ilgili bir sorun var ...

özyinelemeli işlevin her çağrısı, stackerarayanda yerel olarak tanımlanmış bir değişkene geçerli bir gösterici tutabilirse, her şey yolundadır ; Aksi halde algoritmam sınırsız özyinelemede geçerli çift bağlantılı bir liste tutamaz (ve bu durumda özyinelemeyi sınırsız rastgele erişimli depolamayı simüle etmek için kullanmanın bir yolunu göremezsiniz).


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@Gilles: haklısın (düzenlememe bakın); özyineleme derinliğini sınırlandırırsanız sonlu bir otomat elde edersiniz
Marzio De Biasi

@MarzioDeBiasi Hayır, standardın öngörmediği somut bir uygulamaya atıfta bulunduğundan yanlış. Aslında, C'de özyineleme derinliği için teorik bir sınır yoktur . Sınırlı yığın tabanlı bir uygulamayı kullanma seçimi, dilin teorik sınırları hakkında bir şey söylemez. Ancak Turing'in bütünlüğü teorik bir sınırdır.
xamid

0

Sınırlandırılmamış bir çağrı yığını boyutunuz olduğu sürece, bantınızı çağrı yığınında kodlayabilir ve işlev çağrılarından geri dönmeden yığın işaretçisini geri sararak rastgele erişebilirsiniz.

EDIT : Sadece sonlu olan tokmağı kullanabiliyorsanız, bu yapı artık çalışmaz, bu nedenle aşağıya bakın.

Ancak yığınınızın neden sonsuz olabileceğine dair son derece tartışmalı ama içsel ram değil. Bu yüzden aslında devletlerin sayısı sınırlı olduğu için bütün düzenli dilleri bile tanıyamadığınızı söyleyebilirim (sonsuz yığını kullanmak için yığın-geri sarma numarasını saymazsanız).

Hatta tanıyabileceğiniz dil sayısının sınırlı olduğunu bile söyleyebilirim (dillerin kendisi sınırsız olsa bile, örneğin a*tamamdır, ancak b^kyalnızca sınırlı sayıda ks için çalışır ).

EDIT : Mevcut durumu ekstra fonksiyonlarda kodlayabildiğiniz için bu doğru değildir, böylece TÜM normal dilleri gerçekten tanıyabilirsiniz .

Büyük olasılıkla tüm Tip-2 dillerini aynı nedenden ötürü alabilirsiniz, ancak hem durumu hem de yığın tutarlılığını çağrı yığınına koyabildiğinizden emin değilim . Ancak genel bir notta, her zaman otomat boyutunu ölçekleyebildiğiniz için, koçu etkin bir şekilde unutabilirsiniz; Öyleyse bir TM'yi sadece bir yığınla simüle edebilseydiniz, Tip-2, Tip-0'a eşit olurdu, değil mi?


5
“Yığın imleci” nedir? (“Yığın” kelimesinin C standardında görünmediğine dikkat edin.) Benim sorum, resmi bir sınıf sınıfı olarak C ile ilgili (bilgisayardaki C uygulamaları ile ilgili değil). Çağrı yığına erişmek istiyorsanız, dili tarafından sağlanan şekilde yapmalısınız. Örneğin, işlev argümanlarının adresini alarak - ancak herhangi bir uygulamanın sadece sınırlı sayıda adresi vardır, bu da özyineleme derinliğini sınırlar.
Gilles 'SO- kötülük olmayı'

Bir yığın işaretçisi kullanımını dışlamak için cevabımı değiştirdim.
bitmask

1
Revize edilmiş cevabınızla nereye gittiğinizi anlamıyorum (formülasyonu hesaplanabilir fonksiyonlardan tanınmış dillere değiştirmek dışında). İşlevlerin de bir adresi olduğundan, belirli bir sonlu durum makinesini uygulamak için yeterince büyük bir uygulamaya ihtiyacınız vardır. Sorusu C uygulaması dayanmadan (diyelim ki, evrensel Turing makinesi uygulamak) daha fazlasını yapabileceğine ve nasıl olduğunu un tanımlanan davranışa.
Gilles 'SO- kötülük yapmayı kes'

0

Bunu bir kez düşündüm ve beklenen anlambilim kullanarak bağlamsız bir dili uygulamaya çalışmaya karar verdim; uygulamanın kilit kısmı aşağıdaki fonksiyondur:

void *it;

void read_triple(void *back)
{
  if(read_a()) read_triple(&back);
  else reject();
  for(it = back; it != NULL; it = *it)
     if(!read_b()) reject();
  if(read_c()) return;
  else reject();
}

{birnbncn}

En azından, bunun işe yaradığını düşünüyorum. Yine de bazı temel hatalar yapıyorum olabilir.

Sabit bir versiyon:

void *it;

void read_triple(void *back)
{
  if(read_a()) read_triple(&back);
  else for(it = back; it != NULL; it = * (void **) it)
     if(!read_b()) reject();
  if(read_c()) return;
  else reject();
}

Eh, temel bir hata değil , aksi takdirde tipte olduğu gibi it = *itdeğiştirilmelidir . it = * (void **) it*itvoid
Ben Standeven

Çağrı yığınını bu şekilde tanımlamak C'de tanımlanmış davranış olarak tanımlanırsa çok şaşırdım.
Radu GRIGore

Oh, bu işe yaramaz, çünkü ilk 'b' read_a () 'nın başarısız olmasına neden olur ve bu nedenle bir reddetme tetikler.
Ben Standeven

Ancak, çağrı standardını bu şekilde hareket ettirmek yasaldır, çünkü C standardı şöyledir: “Değişken uzunluklu bir dizi tipine sahip olmayan bir nesne [yani otomatik depolama alanına sahip bir kişi için], kullanım ömrü bloğa girişten itibaren uzanır. bu bloğun yürütülmesi herhangi bir şekilde sona erene kadar ilişkilidir. (Kapalı bir bloğa girme veya bir işlev çağırma, geçerli bloğun yürütülmesini askıya alır, ancak sona ermez.) Blok yinelemeli olarak girilirse, nesnenin yeni bir örneği her seferinde yaratılır. " Böylece her read_triple çağrısı özyinelemede kullanılabilecek yeni bir işaretçi oluşturur.
Ben Standeven

2
2CHAR_BITsizeof (char *)

0

@ Supercat'ın cevabının satırları boyunca:

C'nin eksik olduğu iddiası, farklı nesnelerin farklı adreslere sahip olması gerektiği ve merkezlerin adreslerinin sınırlı olduğu varsayımı etrafında toplanmış gibi görünmektedir. @Supercat'in yazdığı gibi

Soruda belirtildiği gibi, standart C, UCHAR_MAXimzasız char türündeki her değişkenin her zaman 0 ile UCHAR_MAXkapsayıcı arasında bir değer tutacağı bir değer olmasını gerektirir . Ayrıca, dinamik olarak tahsis edilen her nesnenin, imzasız char * tipinde bir işaretçi ile tanımlanabilen bir bayt dizisi ile temsil edilmesini sizeof(unsigned char*)ve bu tipteki her ibrenin sizeof(unsigned char *)işaretsiz tipteki bir değerler dizisi ile tanımlanabileceği bir sabit olmasını gerektirir . Char.

unsigned char*N-{0,1}sizeof(unsigned char*){0,1}boyutu(unsbengned chbirr*)N-sizeof(unsigned char*)N-ω isterseniz).

Bu noktada, bir kişi C standardının gerçekten buna izin verdiğini kontrol etmelidir.

sizeofZ


1
İntegral tipleri üzerindeki birçok işlem, “sonuç türünde gösterilebilen maksimum değerden bir adet daha azaltılmış modülo” olan bir sonuca sahip olmak için tanımlanmıştır. Eğer bu maksimum, sonlu olmayan bir sıra sayılırsa, bu nasıl işe yarar?
Gilles 'SO- kötülükten

@Gilles Bu ilginç bir nokta. Gerçekten de uintptr_t p = (uintptr_t)sizeof(void*)( anlamsız tamsayıları tutan bir şeye omega koymak) anlamının ne olacağı açık değildir . Bilmiyorum. Sonucu 0 olarak tanımlamaktan kaçınabiliriz (veya başka herhangi bir sayı).
Alexey B.

1
uintptr_tÇok sonsuz olması gerekirdi. Unutmayın, bu tür isteğe bağlıdır - ancak sonsuz sayıda belirgin işaretçi değeriniz varsa sizeof(void*), o size_tzaman da sonsuz olmalıdır , bu yüzden sonsuz olmalıdır. Bununla birlikte, redüksiyon modülojuna itirazım çok açık değildir - ancak yalnızca bir taşma varsa devreye girer, ancak sonsuz türlere izin verirseniz o zaman asla taşmazlar. Ancak sürükleyici eldeki her türün minimum ve maksimum değerlere sahip olduğu söylenebilir UINT_MAX+1.
Gilles 'SO- kötülükten

Aynı zamanda iyi bir nokta. Gerçekten, ℕ, ℤ olması gereken bir sürü tür (işaretçiler ve size_t) veya bunlara dayanan bazı yapılar elde ederiz (_ ∪ {ω} gibi bir şey olursa size_t için). Şimdi, eğer bu tiplerden bazıları için, standart maksimum değeri tanımlayan bir makroya ihtiyaç duyarsa (PTR_MAX veya bunun gibi bir şey), şeyler tüylenir. Ancak şimdiye kadar sadece işaretçi olmayan tipler için MIN / MAX makrolarının ihtiyacını karşılayabiliyordum.
Alexey B.

Araştırılacak bir başka olasılık size_tda pointer ∪ {ω} olacak şekilde hem işaretçi hem de tanımlamaktır . Bu min / max probleminden kurtulur. Taşma anlambilimi sorunu hala devam etmektedir. Anlamının ne olması uint x = (uint)ωbenim için belli değil. Yine, gelişigüzel 0 alabiliriz, ama biraz çirkin görünüyor.
Alexey B.
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.