Massima potenza computazionale di un'implementazione C.


28

Se seguiamo il libro (o qualsiasi altra versione della specifica del linguaggio, se preferisci), quanta potenza computazionale può avere un'implementazione C?

Si noti che "implementazione C" ha un significato tecnico: è un'istanza particolare della specifica del linguaggio di programmazione C in cui è documentato il comportamento definito dall'implementazione. L'implementazione in CA non deve poter essere eseguita su un computer reale. Deve implementare l'intero linguaggio, inclusi tutti gli oggetti con una rappresentazione in stringa di bit e tipi con dimensioni definite dall'implementazione.

Ai fini di questa domanda, non esiste memoria esterna. L'unico input / output che è possibile eseguire è getchar(per leggere l'input del programma) e putchar(per scrivere l'output del programma). Inoltre, qualsiasi programma che invoca comportamenti indefiniti non è valido: un programma valido deve avere il suo comportamento definito dalla specifica C più la descrizione dell'implementazione dei comportamenti definiti dall'implementazione elencati nell'appendice J (per C99). Si noti che chiamare le funzioni di libreria che non sono menzionate nello standard è un comportamento indefinito.

La mia reazione iniziale è stata che un'implementazione C non è altro che un automa finito, perché ha un limite alla quantità di memoria indirizzabile (non è possibile indirizzare più di sizeof(char*) * CHAR_BITbit di memoria, poiché indirizzi di memoria distinti devono avere schemi di bit distinti quando memorizzati in un puntatore di byte).

Tuttavia, penso che un'implementazione possa fare di più. Per quanto ne so, lo standard non impone alcun limite alla profondità della ricorsione. Quindi puoi effettuare tutte le chiamate ricorsive alle funzioni che desideri, solo tutte le chiamate, tranne un numero finito, devono usare registerargomenti non indirizzabili ( ). Pertanto un'implementazione C che consente la ricorsione arbitraria e non ha limiti al numero di registeroggetti può codificare automi pushdown deterministici.

È corretto? Riesci a trovare un'implementazione C più potente? Esiste un'implementazione C completa di Turing?


4
@Dave: come ha spiegato Gilles, sembra che tu possa avere memoria illimitata, ma non c'è modo di affrontarla direttamente.
Jukka Suomela,

2
Dalla tua spiegazione sembra che qualsiasi implementazione C possa essere programmata solo per accettare le lingue accettate dagli automi pushdown deterministici , che sono più deboli anche delle lingue senza contesto. Questa osservazione, tuttavia, è di scarso interesse per la mia opinione, in quanto la domanda è un'errata applicazione degli asintotici.
Warren Schudy,

3
Un punto da tenere a mente è che ci sono molti modi per innescare "comportamenti definiti dall'implementazione" (o "comportamenti indefiniti"). E in generale, un'implementazione può fornire, ad esempio, funzioni di libreria che forniscono funzionalità che non sono definite nello standard C. Tutti questi forniscono "scappatoie" attraverso le quali è possibile accedere, per esempio, a una macchina completa di Turing. O anche qualcosa di molto più forte, come un oracolo che risolve il problema dell'arresto. Un esempio stupido: il comportamento definito dall'implementazione di overflow di numeri interi con segno o conversioni di numero intero-puntatore potrebbe consentire di accedere a tale oracolo.
Jukka Suomela,

7
A proposito, potrebbe essere una buona idea aggiungere il tag "ricreativo" (o qualunque cosa stiamo usando per puzzle divertenti) in modo che le persone non lo prendano troppo sul serio. È ovviamente la "domanda sbagliata" da porre, ma tuttavia l'ho trovata divertente e intrigante. :)
Jukka Suomela,

2
@Jukka: bella idea. Ad esempio, l'overflow di X = scrive X / 3 sul nastro e si sposta nella direzione X% 3, underflow = attiva il segnale corrispondente al simbolo sul nastro. Sembra un po 'un abuso, ma è sicuramente nello spirito della mia domanda. Potresti scriverlo come risposta? (@others: Non che io voglia scoraggiare altri suggerimenti così intelligenti!)
Gilles 'SO- smetti di essere malvagio'

Risposte:


8

Come notato nella domanda, lo standard C richiede che esista un valore UCHAR_MAX tale che ogni variabile di tipo unsigned charconterrà sempre un valore compreso tra 0 e UCHAR_MAX, inclusi. Richiede inoltre che ogni oggetto allocato dinamicamente sia rappresentato da una sequenza di byte che sia identificabile tramite puntatore di tipo unsigned char*e che ci sia una costante sizeof(unsigned char*)tale che ogni puntatore di quel tipo sia identificabile da una sequenza di sizeof(unsigned char *)valori di tipo unsigned char. Il numero di oggetti che possono essere assegnati contemporaneamente in modo dinamico è quindi rigidamente limitato a . Nulla impedirebbe a un compilatore teorico di assegnare i valori di quelle costanti in modo da supportare più di 10 10 10 oggetti, ma da una prospettiva teorica l'esistenza di qualsiasi limite, non importa quanto grande, significhi che qualcosa non è infinito.UCHAR_MAXsizeof(unsigned char)101010

Un programma potrebbe archiviare una quantità illimitata di informazioni sullo stack se non viene mai preso il suo indirizzo su nulla di allocato nello stack ; si potrebbe quindi avere un programma C in grado di fare alcune cose che non possono essere fatte da nessun automa finito di qualsiasi dimensione. Pertanto, anche se (o forse perché) l'accesso alle variabili dello stack è molto più limitato dell'accesso alle variabili allocate dinamicamente, trasforma C da un automa finito in un automa push-down.

Vi è, tuttavia, un'altra potenziale ruga: è necessario che se un programma esamina le sequenze a lunghezza fissa sottostanti dei valori dei caratteri associati a due puntatori a oggetti diversi, tali sequenze devono essere univoche. Perché ci sono solo UCHAR_MAXsizeof(unsigned char)possibili sequenze di valori di carattere, qualsiasi programma che ha creato un numero di puntatori a oggetti distinti in eccesso rispetto a quello non potrebbe essere conforme allo standard C se il codice esaminasse mai la sequenza di caratteri associati a tali puntatori . In alcuni casi, tuttavia, sarebbe possibile per un compilatore determinare che nessun codice avrebbe mai esaminato la sequenza di caratteri associati a un puntatore. Se ogni "carattere" fosse effettivamente in grado di contenere qualsiasi numero intero finito, e la memoria della macchina fosse una sequenza infinita numerabile di numeri interi [data una macchina di Turing a nastro illimitato, si potrebbe emulare una macchina del genere anche se sarebbe molto lenta], quindi sarebbe davvero possibile rendere C un linguaggio completo di Turing.


Con una macchina del genere, cosa restituirebbe sizeof (char)?
TLW,

1
@TLW: Come qualsiasi altra macchina: 1. Le macro CHAR_BITS e CHAR_MAX sarebbero un po 'più problematiche; lo standard non consentirebbe il concetto di tipi che non hanno limiti.
supercat,

Spiacenti, intendevo CHAR_BITS, come hai detto, scusa.
TLW,

7

Con la libreria di threading C11 (opzionale), è possibile effettuare un'implementazione completa di Turing data una profondità di ricorsione illimitata.

La creazione di un nuovo thread produce un secondo stack; due pile sono sufficienti per completezza di Turing. Uno stack rappresenta ciò che è a sinistra della testa, l'altro stack ciò che è a destra.


Ma le macchine di Turing con un nastro che avanzano all'infinito in una sola direzione sono potenti quanto le macchine di Turing con un nastro che avanzano all'infinito in due direzioni. Oltre a ciò, è possibile simulare più thread da uno scheduler. Comunque, non abbiamo nemmeno bisogno di una libreria di threading.
Xamid,

3

Penso che Turing sia completo : possiamo scrivere un programma che simula un UTM usando questo trucco (ho scritto rapidamente il codice a mano, quindi probabilmente ci sono alcuni errori di sintassi ... ma spero che non ci siano errori (importanti) nella logica :-)

  • definire una struttura che può essere utilizzata come doppio elenco collegato per la rappresentazione su nastro
    typdef struct {
      cell_t * pred; // cella a sinistra
      cell_t * succ; // cella a destra
      int val; // valore della cella
    } cell_t 

Il headsarà un puntatore a una cell_tstruttura

  • definire una struttura che può essere utilizzata per memorizzare lo stato corrente e un flag
    typedef struct {
      int state;
      int flag;
    } info_t 
  • quindi definire una funzione a loop singolo che simula una Universal TM quando la testa si trova tra i confini della doppia lista collegata; quando la testa colpisce un limite imposta la bandiera della struttura info_t (HIT_LEFT, HIT_RIGHT) e ritorna:
void simulate_UTM (cell_t * head, info_t * info) {
  while (true) {
    head-> val = UTM_nextsymbol [info-> state, head-> val]; // scrivi il simbolo
    info-> state = UTM_nextstate [info-> state, head-> val]; // prossimo stato
    if (info-> state == HALT_STATE) {// stampa se accetta ed esce dal programma
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       exit (0);
    }
    int move = UTM_nextmove [info-> state, head-> val];
    if (move == MOVE_LEFT) {
      head = head-> pred; // muovere a sinistra
      if (head == NULL) {info-> flag = HIT_LEFT; ritorno; }
    } altro {
      head = head-> succ; // vai a destra
      if (head == NULL) {info-> flag = HIT_RIGHT; ritorno; }
    }
  } // ancora al limite ... continua
}
  • quindi definire una funzione ricorsiva che prima chiama la routine UTM di simulazione e poi si chiama ricorsivamente quando il nastro deve essere espanso; quando il nastro deve essere espanso in alto (HIT_RIGHT) nessun problema, quando deve essere spostato in basso (HIT_LEFT) basta spostare i valori delle celle usando l'elenco doppio linkato:
stacker vuoto (cell_t * top, cell_t * bottom, cell_t * head, info_t * info) {
  simulate_UTM (head, informazioni);
  cell_t newcell; // la nuova cella
  newcell.pred = top; // aggiorna il doppio elenco collegato con la nuova cella
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  switch (info-> hit) {
    caso HIT_RIGHT:
      stacker (& newcell, bottom, newcell, informazioni);
      rompere;
    case HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// sposta in alto i valori
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      stacker (& newcell, bottom, bottom, informazioni);
      rompere;
  }
}
  • il nastro iniziale può essere riempito con una semplice funzione ricorsiva che crea il doppio elenco collegato e quindi chiama la stackerfunzione quando legge l'ultimo simbolo del nastro di input (usando readchar)
void init_tape (cell_t * top, cell_t * bottom, info_t * info) {
  cell_t newcell;
  int c = readchar ();
  if (c == END_OF_INPUT) impilatore (& top, bottom, bottom, info); // niente più simboli, inizia
  newcell.pred = top;
  if (top! = NULL) top.succ = & newcell; else bottom = & newcell;
  init_tape (& newcell, bottom, info);
}

EDIT: dopo averci pensato un po ', c'è un problema con i puntatori ...

se ogni chiamata della funzione ricorsiva stackerpuò mantenere un puntatore valido a una variabile definita localmente nel chiamante, allora tutto va bene ; altrimenti il ​​mio algoritmo non è in grado di mantenere un elenco doppio-link valido sulla ricorsione illimitata (e in questo caso non vedo un modo per usare la ricorsione per simulare una memoria ad accesso casuale illimitata).


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@Gilles: hai ragione (vedi la mia modifica); se si limita la profondità di ricorsione si ottiene un automa finito
Marzio De Biasi

@MarzioDeBiasi No, ha torto poiché si riferisce a un'implementazione concreta che lo standard non presuppone. Infatti, non v'è alcun limite teorico di profondità di ricorsione in C . La scelta di utilizzare un'implementazione basata su stack limitato non dice nulla sui limiti teorici del linguaggio. Ma la completezza di Turing è un limite teorico.
Xamid,

0

Finché si dispone di una dimensione dello stack di chiamate illimitata, è possibile codificare il nastro sullo stack di chiamate e accedervi in ​​modo casuale riavvolgendo il puntatore dello stack senza tornare dalle chiamate di funzione.

Modifica : se puoi usare solo il ram, che è finito, questa costruzione non funziona più, quindi vedi sotto.

Tuttavia è altamente discutibile il motivo per cui il tuo stack può essere infinito, ma il ram intrinseco non lo è. Quindi in realtà direi che non riesci nemmeno a riconoscere tutte le lingue regolari, poiché il numero di stati è limitato (se non conti il ​​trucco dello stack-rewind per sfruttare lo stack infinito).

Vorrei anche ipotizzare che il numero di lingue che puoi riconoscere sia finito (anche se le lingue stesse possono essere infinite, ad esempio a*va bene, ma b^kfunziona solo per un numero finito di ks).

EDIT : Questo non è vero, in quanto puoi codificare lo stato corrente in funzioni extra, in modo da poter veramente riconoscere TUTTE le lingue regolari.

Molto probabilmente puoi ottenere tutte le lingue di Tipo 2 per lo stesso motivo, ma non sono sicuro che riesci a mettere entrambi, lo stato e la costante dello stack nello stack di chiamate. Ma su una nota generale, puoi effettivamente dimenticare il ram, poiché puoi sempre ridimensionare le dimensioni dell'automa in modo che il tuo alfabeto superi la capacità del ram. Quindi, se potessi simulare una TM con solo uno stack, il Tipo 2 equivarrebbe al Tipo 0, no?


5
Che cos'è un "stack-pointer"? (Si noti che la parola "stack" non appare nello standard C.) La mia domanda riguarda C come una classe di linguaggi formali, non sulle implementazioni di C su un computer (che sono ovviamente macchine a stati finiti). Se si desidera accedere allo stack di chiamate, è necessario farlo in un modo fornito dalla lingua. Ad esempio prendendo l'indirizzo degli argomenti della funzione - ma ogni data implementazione ha solo un numero finito di indirizzi, che quindi limita la profondità della ricorsione.
Gilles 'SO- smetti di essere malvagio' il

Ho modificato la mia risposta per escludere l'uso di uno stack-pointer.
maschera di bit

1
Non capisco dove stai andando con la tua risposta rivista (oltre a cambiare la formulazione da funzioni calcolabili a lingue riconosciute). Poiché anche le funzioni hanno un indirizzo, è necessaria un'implementazione sufficientemente ampia per implementare una data macchina a stati finiti. La domanda è se e come un'implementazione C potrebbe fare di più (diciamo, implementare una macchina di Turing universale) senza fare affidamento su comportamenti non definiti.
Gilles 'SO- smetti di essere malvagio' il

0

Ci ho pensato una volta e ho deciso di provare a implementare un linguaggio senza contesto usando la semantica prevista; la parte fondamentale dell'implementazione è la seguente funzione:

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}

Almeno, penso che funzioni. Tuttavia, è possibile che stia commettendo un errore fondamentale.

Una versione fissa:

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

Bene, non è un errore fondamentale, ma it = *itdovrebbe essere sostituito da it = * (void **) it, come altrimenti *itè di tipo void.
Ben Standeven,

Sarei molto sorpreso se viaggiare nello stack di chiamate in quel modo sarebbe definito comportamento in C.
Radu GRIGore

Oh, questo non funzionerà, perché la prima 'b' fa fallire read_a () e quindi innesca un rifiuto.
Ben Standeven,

Ma è legittimo viaggiare nello stack di chiamate in questo modo, poiché lo standard C dice: "Per un tale oggetto [cioè uno con memorizzazione automatica] che non ha un tipo di array a lunghezza variabile, la sua durata si estende dall'entrata nel blocco con a cui è associato fino a quando l'esecuzione di quel blocco non termina in alcun modo. (L'inserimento di un blocco racchiuso o la chiamata di una funzione sospende, ma non termina, l'esecuzione del blocco corrente.) Se il blocco viene inserito in modo ricorsivo, una nuova istanza dell'oggetto viene creato ogni volta. " Quindi ogni chiamata di read_triple creerebbe un nuovo puntatore che può essere utilizzato nella ricorsione.
Ben Standeven,

2
2CHAR_BITsizeof(char*)

0

Sulla falsariga della risposta di @ supercat:

Le affermazioni di incompletezza di C sembrano centrate sul fatto che oggetti distinti dovrebbero avere indirizzi distinti e si presume che l'insieme di indirizzi sia finito. Come scrive @supercat

Come notato nella domanda, lo standard C richiede che esista un valore UCHAR_MAXtale che ogni variabile di tipo unsigned char conterrà sempre un valore compreso tra 0 e UCHAR_MAX, incluso. Richiede inoltre che ogni oggetto allocato dinamicamente sia rappresentato da una sequenza di byte che sia identificabile tramite puntatore di tipo unsigned char *, e che ci sia una costante sizeof(unsigned char*)tale che ogni puntatore di quel tipo sia identificabile da una sequenza di sizeof(unsigned char *)valori di tipo unsigned char.

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

A questo punto, si dovrebbe verificare che lo standard C lo permetterebbe davvero.

sizeofZ


1
Molte operazioni sui tipi integrali sono definite per avere un risultato "modulo ridotto uno in più rispetto al valore massimo rappresentabile nel tipo di risultato". Come funzionerebbe se quel massimo è un ordinale non finito?
Gilles 'SO- smetti di essere malvagio' il

@Gilles Questo è un punto interessante. Non è infatti chiaro quale sarebbe la semantica di uintptr_t p = (uintptr_t)sizeof(void*)(mettere \ omega in qualcosa che contiene numeri interi senza segno). Non lo so. Potremmo evitare di definire il risultato come 0 (o qualsiasi altro numero).
Alexey B.

1
uintptr_tdovrebbe essere anche infinito. Intendiamoci, questo tipo è facoltativo, ma se si dispone di un numero infinito di valori puntatore distinti, sizeof(void*)anche questo deve essere infinito, quindi size_tdeve essere infinito. La mia obiezione sulla riduzione del modulo non è così ovvia: entra in gioco solo se c'è un overflow, ma se si consentono tipi infiniti, potrebbero non traboccare mai. Ma per quanto riguarda la presa, ogni tipo ha un valore minimo e massimo, il che, per quanto ne so, implica che UINT_MAX+1deve traboccare.
Gilles 'SO- smetti di essere malvagio' il

Anche un buon punto. In effetti, otteniamo un sacco di tipi (puntatori e size_t) che dovrebbero essere ℕ, ℤ o una costruzione basata su di essi (per size_t se sarebbe qualcosa come ℕ ∪ {ω}). Ora, se per alcuni di questi tipi, lo standard richiede una macro che definisce il valore massimo (PTR_MAX o qualcosa del genere), le cose diventeranno pelose. Ma finora sono stato in grado di finanziare il requisito di macro MIN / MAX solo per i tipi senza puntatore.
Alexey B.

Un'altra possibilità da investigare è definire entrambi i size_ttipi di puntatore come ℕ ∪ {ω}. Questo elimina il problema min / max. Il problema con la semantica di overflow rimane ancora. Quale dovrebbe essere la semantica di uint x = (uint)ωnon è chiaro per me. Ancora una volta, potremmo prendere a casaccio 0, ma sembra un po 'brutto.
Alexey B.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.