Kekuatan komputasi maksimum dari implementasi C

28

Jika kita membaca buku (atau versi lain dari spesifikasi bahasa jika Anda mau), berapa banyak daya komputasi yang dapat dimiliki oleh implementasi C?

Perhatikan bahwa "implementasi C" memiliki arti teknis: itu adalah contoh khusus dari spesifikasi bahasa pemrograman C di mana perilaku yang ditentukan implementasi didokumentasikan. Implementasi AC tidak harus dapat berjalan di komputer yang sebenarnya. Itu memang harus mengimplementasikan seluruh bahasa, termasuk setiap objek yang memiliki representasi bit-string dan tipe yang memiliki ukuran implementasi yang ditentukan.

Untuk keperluan pertanyaan ini, tidak ada penyimpanan eksternal. Satu-satunya input / output yang dapat Anda lakukan adalah getchar(untuk membaca input program) dan putchar(untuk menulis output program). Juga setiap program yang memanggil perilaku tidak terdefinisi tidak valid: program yang valid harus memiliki perilakunya yang didefinisikan oleh spesifikasi C ditambah deskripsi implementasi dari perilaku yang didefinisikan implementasi yang tercantum dalam lampiran J (untuk C99). Perhatikan bahwa memanggil fungsi perpustakaan yang tidak disebutkan dalam standar adalah perilaku yang tidak ditentukan.

Reaksi awal saya adalah bahwa implementasi C tidak lebih dari otomat terbatas, karena memiliki batasan pada jumlah memori yang dapat dialamatkan (Anda tidak dapat menangani lebih dari sizeof(char*) * CHAR_BITbit penyimpanan, karena alamat memori yang berbeda harus memiliki pola bit yang berbeda ketika disimpan dalam byte pointer).

Namun saya pikir implementasi dapat melakukan lebih dari ini. Sejauh yang saya tahu, standar tidak menetapkan batasan pada kedalaman rekursi. Jadi, Anda dapat membuat panggilan fungsi rekursif sebanyak yang Anda suka, hanya semua kecuali sejumlah panggilan yang terbatas harus menggunakan registerargumen yang tidak dapat dialamatkan ( ). Dengan demikian implementasi C yang memungkinkan rekursi sewenang-wenang dan tidak memiliki batasan jumlah registerobjek dapat mengkodekan automata pushdown deterministik.

Apakah ini benar? Bisakah Anda menemukan implementasi C yang lebih kuat? Apakah implementasi C lengkap-Turing ada?

Gilles 'SANGAT berhenti menjadi jahat'
sumber
4
@Dave: Seperti yang dijelaskan Gilles, tampaknya Anda dapat memiliki memori tidak terbatas, tetapi tidak ada cara untuk mengatasinya secara langsung.
Jukka Suomela
2
Dari penjelasan Anda, sepertinya implementasi C hanya dapat diprogram untuk menerima bahasa yang diterima oleh automata pushdown deterministik , yang lebih lemah daripada bahasa bebas konteks. Namun pengamatan ini kurang menarik bagi saya, karena pertanyaannya adalah penerapan asimptotik yang salah.
Warren Schudy
3
Satu hal yang perlu diingat adalah bahwa ada banyak cara untuk memicu "perilaku yang ditentukan implementasi" (atau "perilaku tidak terdefinisi"). Dan secara umum, implementasi dapat menyediakan, misalnya, fungsi perpustakaan yang menyediakan fungsionalitas yang tidak didefinisikan dalam standar C. Semua ini menyediakan "celah" di mana Anda dapat mengakses, katakanlah, mesin lengkap Turing. Atau bahkan sesuatu yang jauh lebih kuat, seperti oracle yang memecahkan masalah penghentian. Contoh bodoh: perilaku yang ditentukan oleh implementasi dari integer overflow atau konversi integer-pointer dapat memungkinkan Anda mengakses oracle tersebut.
Jukka Suomela
7
Ngomong-ngomong, mungkin ide yang baik untuk menambahkan tag "rekreasi" (atau apa pun yang kita gunakan untuk teka-teki lucu) sehingga orang tidak menganggap ini terlalu serius. Jelas itu adalah "pertanyaan yang salah" untuk diajukan, tetapi saya merasa itu lucu dan membangkitkan minat. :)
Jukka Suomela
2
@Jukka: Ide bagus. Sebagai contoh, overflow oleh X = tulis X / 3 pada kaset dan bergerak ke arah X% 3, underflow = memicu sinyal yang sesuai dengan simbol pada rekaman. Rasanya agak seperti pelecehan, tapi itu jelas dalam semangat pertanyaan saya. Bisakah Anda menuliskannya sebagai jawaban? (@others: Bukannya aku ingin mengecilkan anjuran cerdik lainnya!)
Gilles 'SO- stop being evil'

Jawaban:

8

Seperti disebutkan dalam pertanyaan, standar C mensyaratkan bahwa ada nilai UCHAR_MAX sehingga setiap variabel tipe unsigned charakan selalu memiliki nilai antara 0 dan UCHAR_MAX, inklusif. Lebih jauh lagi mengharuskan setiap objek yang dialokasikan secara dinamis diwakili oleh urutan byte yang dapat diidentifikasi melalui pointer tipe unsigned char*, dan bahwa ada konstan sizeof(unsigned char*)sehingga setiap pointer dari tipe tersebut dapat diidentifikasi oleh urutan sizeof(unsigned char *)nilai tipe unsigned char. Jumlah objek yang dapat dialokasikan secara simultan secara dinamis dengan demikian terbatas pada . Tidak ada yang akan mencegah kompiler teoretis dari menetapkan nilai-nilai konstanta tersebut untuk mendukung lebih dari 10UCHAR_MAXsizeof(unsigned char) objek, tetapi dari perspektif teoritis keberadaan ikatan apa pun, tidak peduli seberapa besar, berarti ada sesuatu yang tidak terbatas.101010

Suatu program dapat menyimpan jumlah informasi yang tidak terbatas pada stack jika tidak ada yang dialokasikan pada stack yang pernah diambil alamatnya ; dengan demikian seseorang dapat memiliki program C yang mampu melakukan beberapa hal yang tidak dapat dilakukan oleh robot berapapun dengan ukuran berapa pun. Jadi, meskipun (atau mungkin karena) akses ke variabel stack jauh lebih terbatas daripada akses ke variabel yang dialokasikan secara dinamis, C berubah dari otomat terbatas menjadi otomat push-down.

Namun, ada potensi kerutan lain: diperlukan bahwa jika suatu program memeriksa urutan nilai-nilai karakter tetap terkait panjang yang terkait dengan dua pointer ke objek yang berbeda, urutan tersebut harus unik. Karena hanya ada UCHAR_MAXsizeof(unsigned char)kemungkinan urutan nilai karakter, program apa pun yang membuat sejumlah pointer ke objek berbeda melebihi yang tidak dapat memenuhi standar C jika kode pernah memeriksa urutan karakter yang terkait dengan pointer tersebut . Akan tetapi, dalam beberapa kasus mungkin bagi kompiler untuk menentukan bahwa tidak ada kode yang akan memeriksa urutan karakter yang terkait dengan pointer. Jika setiap "char" benar-benar mampu menahan bilangan bulat terbatas, dan memori mesin adalah urutan bilangan bulat tak terhingga [diberikan mesin Turing tanpa pita, orang dapat meniru mesin seperti itu meskipun akan sangat lambat], kemudian memang mungkin untuk membuat bahasa C-Turing lengkap.

supercat
sumber
Dengan mesin seperti itu, apa yang akan ukuran (char) kembali?
TLW
1
@ TTW: Sama seperti mesin lain: 1. Macro CHAR_BITS dan CHAR_MAX akan sedikit lebih bermasalah, meskipun; Standar tidak akan mengizinkan konsep tipe yang tidak memiliki batasan.
supercat
Ups, maksud saya CHAR_BITS, seperti yang Anda katakan, maaf.
TLW
7

Dengan pustaka threading C11 (opsional), dimungkinkan untuk membuat implementasi Turing yang lengkap mengingat kedalaman rekursi yang tidak terbatas.

Membuat utas baru menghasilkan tumpukan kedua; dua tumpukan sudah cukup untuk kelengkapan Turing. Satu tumpukan mewakili apa yang ada di sebelah kiri kepala, tumpukan lainnya apa yang ada di sebelah kanan.

Jared
sumber
Tetapi mesin-mesin Turing dengan pita yang maju tanpa batas hanya dalam satu arah sama kuatnya dengan mesin-mesin Turing dengan pita yang maju tanpa batas dalam dua arah. Selain itu, banyak utas dapat disimulasikan oleh penjadwal. Bagaimanapun, kita bahkan tidak memerlukan pustaka threading.
xamid
3

Saya pikir itu Turing lengkap : kita dapat menulis sebuah program yang mensimulasikan UTM menggunakan trik ini (saya dengan cepat menulis kode dengan tangan sehingga mungkin ada beberapa kesalahan sintaksis ... tapi saya harap tidak ada kesalahan (utama) dalam logika :-)

  • mendefinisikan struktur yang dapat digunakan sebagai daftar tautan ganda untuk representasi rekaman
    typdef struct {
      cell_t * pred; // sel di sebelah kiri
      cell_t * succ; // sel di sebelah kanan
      int val; // nilai sel
    } cell_t 

The headakan menjadi pointer ke cell_tstruktur

  • mendefinisikan struktur yang dapat digunakan untuk menyimpan keadaan saat ini dan sebuah bendera
    typedef struct {
      keadaan int;
      bendera int;
    } info_t 
  • kemudian tentukan fungsi loop tunggal yang mensimulasikan Universal TM saat head berada di antara batas-batas daftar yang ditautkan ganda; ketika kepala mencapai batas set bendera struktur info_t (HIT_LEFT, HIT_RIGHT) dan kembali:
membatalkan simulate_UTM (cell_t * head, info_t * info) {
  while (true) {
    head-> val = UTM_nextsymbol [info-> state, head-> val]; // tulis simbol
    info-> state = UTM_nextstate [info-> state, head-> val]; // keadaan selanjutnya
    if (info-> state == HALT_STATE) {// cetak jika menerima dan keluar dari program
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       keluar (0);
    }
    int move = UTM_nextmove [info-> state, head-> val];
    if (move == MOVE_LEFT) {
      head = head-> pred; // bergerak ke kiri
      if (head == NULL) {info-> flag = HIT_LEFT; kembali; }
    } lain {
      head = head-> succ; // bergerak ke kanan
      if (head == NULL) {info-> flag = HIT_RIGHT; kembali; }
    }
  } // masih dalam batas ... terus
}
  • kemudian tentukan fungsi rekursif yang pertama memanggil simulasi UTM rutin dan kemudian secara rekursif memanggil dirinya sendiri ketika pita perlu diperluas; ketika rekaman itu perlu diperluas di atas (HIT_RIGHT) tidak ada masalah, ketika rekaman itu perlu digeser di bagian bawah (HIT_LEFT) hanya menggeser nilai sel menggunakan daftar tautan ganda:
stacker kosong (cell_t * atas, cell_t * bawah, cell_t * head, info_t * info) {
  simulate_UTM (head, info);
  cell_t newcell; // sel baru
  newcell.pred = atas; // perbarui daftar ditautkan ganda dengan sel baru
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  switch (info-> hit) {
    huruf besar HIT_RIGHT:
      stacker (& newcell, bottom, newcell, info);
      istirahat;
    case HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// menggeser nilai
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      stacker (& newcell, bottom, bottom, info);
      istirahat;
  }
}
  • rekaman awal dapat diisi dengan fungsi rekursif sederhana yang membangun daftar ditautkan ganda dan kemudian memanggil stackerfungsi ketika membaca simbol terakhir dari pita input (menggunakan readchar)
membatalkan init_tape (cell_t * atas, cell_t * bawah, info_t * info) {
  cell_t newcell;
  int c = readchar ();
  if (c == END_OF_INPUT) stacker (& atas, bawah, bawah, info); // tidak ada lagi simbol, mulai
  newcell.pred = atas;
  if (top! = NULL) top.succ = & newcell; lain bawah = & newcell;
  init_tape (& newcell, bottom, info);
}

EDIT: setelah berpikir sedikit tentang itu, ada masalah dengan petunjuk ...

jika setiap panggilan fungsi rekursif stackerdapat mempertahankan pointer yang valid ke variabel yang ditentukan secara lokal di pemanggil maka semuanya baik-baik saja ; jika tidak, algoritme saya tidak dapat mempertahankan daftar tautan ganda yang valid pada rekursi tak terbatas (dan dalam hal ini a tidak melihat cara untuk menggunakan rekursi untuk mensimulasikan penyimpanan akses acak tak terbatas).

Marzio De Biasi
sumber
3
stackernewcellstacker2n/sns=sizeof(cell_t)
@Gilles: Anda benar (lihat hasil edit saya); jika Anda membatasi kedalaman rekursi, Anda akan mendapatkan robot yang terbatas
Marzio De Biasi
@MarzioDeBiasi Tidak, dia salah karena dia merujuk pada implementasi konkret yang standarnya tidak mengandaikan. Bahkan, tidak ada batasan teoritis untuk kedalaman rekursi di C . Pilihan untuk menggunakan implementasi berbasis stack terbatas tidak mengatakan apa-apa tentang batas teoritis bahasa. Tapi Turing-kelengkapan adalah batas teoretis.
xamid
0

Selama Anda memiliki ukuran tumpukan panggilan yang tidak terbatas, Anda dapat menyandikan rekaman Anda di tumpukan panggilan, dan mengaksesnya secara acak dengan memutar penumpukan tumpukan tanpa kembali dari panggilan fungsi.

Suntingan : Jika Anda hanya dapat menggunakan ram, yang terbatas, konstruksi ini tidak berfungsi lagi, jadi lihat di bawah.

Namun sangat dipertanyakan mengapa stack Anda bisa tidak terbatas tetapi ram intrinsiknya tidak. Jadi sebenarnya saya akan mengatakan Anda bahkan tidak dapat mengenali semua bahasa biasa, karena jumlah negara dibatasi (jika Anda tidak menghitung trik tumpukan-mundur untuk mengeksploitasi tumpukan tak terbatas).

Saya bahkan akan berspekulasi bahwa jumlah bahasa yang dapat Anda kenali adalah terbatas (bahkan jika bahasa itu sendiri bisa tak terbatas, misalnya a*tidak apa-apa, tetapi b^khanya berfungsi untuk sejumlah terbatas ks).

EDIT : Ini tidak benar, karena Anda dapat menyandikan status saat ini dalam fungsi tambahan, sehingga Anda benar-benar dapat mengenali SEMUA bahasa biasa.

Anda kemungkinan besar bisa mendapatkan semua bahasa Tipe-2 untuk alasan yang sama, tetapi saya tidak yakin apakah Anda bisa mengatur keduanya, state dan stack-constent pada call-stack. Tetapi pada catatan umum, Anda dapat secara efektif melupakan ram, karena Anda selalu dapat skala ukuran robot sehingga alfabet Anda melebihi kapasitas ram. Jadi jika Anda bisa mensimulasikan TM dengan hanya tumpukan, Tipe-2 akan sama dengan Tipe-0, bukan?

bitmask
sumber
5
Apa itu "stack-pointer"? (Perhatikan bahwa kata "tumpukan" tidak muncul dalam standar C.) Pertanyaan saya adalah tentang C sebagai kelas bahasa formal, bukan tentang implementasi C pada komputer (yang jelas-jelas mesin negara terbatas). Jika Anda ingin mengakses tumpukan panggilan, Anda harus melakukannya dengan cara yang disediakan oleh bahasa. Misalnya dengan mengambil alamat argumen fungsi - tetapi setiap implementasi yang diberikan hanya memiliki jumlah alamat yang terbatas, yang kemudian membatasi kedalaman rekursi.
Gilles 'SANGAT berhenti menjadi jahat'
Saya telah memodifikasi jawaban saya untuk mengecualikan penggunaan stack-pointer.
bitmask
1
Saya tidak mengerti ke mana Anda akan pergi dengan jawaban yang direvisi (selain dari mengubah formulasi dari fungsi yang dapat dikomputasi ke bahasa yang dikenali). Karena fungsi memiliki alamat juga, Anda perlu implementasi yang cukup besar untuk mengimplementasikan mesin keadaan terbatas apa pun yang diberikan. Pertanyaannya adalah apakah dan bagaimana implementasi C dapat berbuat lebih banyak (katakanlah, mengimplementasikan mesin Turing universal) tanpa mengandalkan perilaku yang tidak didefinisikan.
Gilles 'SO- stop being evil'
0

Saya memikirkan hal ini sekali, dan memutuskan untuk mencoba menerapkan bahasa bebas-konteks dengan menggunakan semantik yang diharapkan; bagian penting dari implementasi adalah fungsi berikut:

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

{anbncn}

Setidaknya, saya pikir ini berhasil. Mungkin saya membuat beberapa kesalahan mendasar.

Versi tetap:

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();
}
Ben Standeven
sumber
Yah, bukan kesalahan mendasar, tapi it = *it harus diganti dengan it = * (void **) it, karena selain itu *itadalah tipe void.
Ben Standeven
Saya akan sangat terkejut jika bepergian dengan tumpukan panggilan seperti itu akan didefinisikan perilaku dalam C.
Radu GRIGore
Oh, ini tidak akan berhasil, karena 'b' pertama menyebabkan read_a () gagal dan karenanya memicu penolakan.
Ben Standeven
Tapi itu sah untuk melakukan perjalanan tumpukan panggilan dengan cara ini, karena standar C mengatakan: "Untuk objek seperti itu [yaitu satu dengan penyimpanan otomatis] yang tidak memiliki tipe array panjang variabel, masa pakainya memanjang dari masuk ke blok dengan yang dikaitkan sampai eksekusi blok itu berakhir dengan cara apa pun (Memasuki blok tertutup atau memanggil fungsi menunda, tetapi tidak berakhir, eksekusi blok saat ini.) Jika blok dimasukkan secara rekursif, instance objek baru dibuat setiap waktu. " Jadi setiap panggilan read_triple akan membuat pointer baru yang dapat digunakan dalam rekursi.
Ben Standeven
2
2CHAR_BITsizeof(char*)
Gilles 'SO-stop being evil'
0

Sejalan dengan jawaban @ supercat:

Klaim ketidaklengkapan C tampaknya berpusat di sekitar bahwa objek yang berbeda harus memiliki alamat yang berbeda, dan set alamat diasumsikan terbatas. Seperti @supercat menulis

Seperti disebutkan dalam pertanyaan, standar C mensyaratkan bahwa ada nilai UCHAR_MAXsedemikian rupa sehingga setiap variabel dari tipe unsigned char akan selalu memiliki nilai antara 0 dan UCHAR_MAX, termasuk. Lebih jauh lagi mengharuskan setiap objek yang dialokasikan secara dinamis diwakili oleh urutan byte yang dapat diidentifikasi melalui pointer dari tipe unsigned char *, dan bahwa ada konstanta sizeof(unsigned char*)sehingga setiap pointer dari tipe tersebut dapat diidentifikasi oleh urutan sizeof(unsigned char *)nilai dari tipe unsigned arang.

unsigned char*N{0,1}sizeof(unsigned char*){0,1}sizeof(unsigned char)Nsizeof(unsigned char*)Nω

Pada titik ini, orang harus memeriksa bahwa standar C memang memungkinkan.

sizeofZ

Alexey B.
sumber
1
Banyak operasi pada tipe integral didefinisikan memiliki hasil yang "mengurangi modulo satu lebih dari nilai maksimum yang diwakili dalam tipe hasil". Bagaimana itu bekerja jika maksimum itu adalah ordinal yang tidak terbatas?
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles Ini adalah poin yang menarik. Memang tidak jelas apa yang akan menjadi semantik uintptr_t p = (uintptr_t)sizeof(void*)(menempatkan \ omega ke dalam sesuatu yang memegang bilangan bulat tak bertanda). Saya tidak tahu. Kami mungkin lolos dengan mendefinisikan hasil menjadi 0 (atau nomor lainnya).
Alexey B.
1
uintptr_tharus menjadi tak terbatas juga. Pikiran Anda, tipe ini opsional - tetapi jika Anda memiliki jumlah nilai pointer berbeda yang tak terbatas maka sizeof(void*)harus juga tak terbatas, jadi size_tharus tak terbatas. Keberatan saya tentang reduksi modulo tidak begitu jelas - itu hanya berlaku jika ada overflow, tetapi jika Anda mengizinkan tipe tak terbatas maka mereka mungkin tidak pernah overflow. Tetapi di sisi lain, masing-masing jenis memiliki nilai minimum dan maksimum, yang sejauh yang saya tahu menyiratkan bahwa UINT_MAX+1harus meluap.
Gilles 'SANGAT berhenti menjadi jahat'
Poin yang bagus juga. Memang, kita mendapatkan banyak jenis (pointer dan size_t) yang seharusnya ℕ, ℤ, atau konstruksi berdasarkan mereka (untuk size_t jika akan menjadi sesuatu seperti ℕ ∪ {ω}). Sekarang, jika untuk beberapa jenis ini, standar membutuhkan makro yang mendefinisikan nilai maksimum (PTR_MAX atau sesuatu seperti itu), semuanya akan menjadi berbulu. Namun sejauh ini saya hanya dapat mendanai persyaratan makro MIN / MAX untuk jenis non-pointer.
Alexey B.
Kemungkinan lain untuk menyelidiki adalah mendefinisikan kedua size_tdan tipe pointer menjadi to ∪ {ω}. Ini menghilangkan masalah min / max. Masalah dengan semantik melimpah masih tetap. Apa yang seharusnya menjadi semantik uint x = (uint)ωtidak jelas bagi saya. Sekali lagi, kita dapat secara acak mengambil 0, tetapi itu terlihat agak jelek.
Alexey B.