Puissance de calcul maximale d'une implémentation C

28

Si nous nous en remettons au livre (ou à toute autre version de la spécification du langage si vous préférez), quelle puissance de calcul une implémentation C peut-elle avoir?

Notez que «implémentation C» a une signification technique: il s'agit d'une instanciation particulière de la spécification du langage de programmation C où le comportement défini par l'implémentation est documenté. L'implémentation AC ne doit pas pouvoir fonctionner sur un ordinateur réel. Il doit implémenter le langage entier, y compris chaque objet ayant une représentation de chaîne de bits et les types ayant une taille définie par l'implémentation.

Aux fins de cette question, il n'y a pas de stockage externe. La seule entrée / sortie que vous pouvez effectuer est getchar(pour lire l'entrée de programme) et putchar(pour écrire la sortie de programme). De même, tout programme qui invoque un comportement non défini n'est pas valide: un programme valide doit avoir son comportement défini par la spécification C plus la description de l'implémentation des comportements définis par l'implémentation répertoriés dans l'annexe J (pour C99). Notez que l'appel de fonctions de bibliothèque non mentionnées dans la norme est un comportement non défini.

Ma première réaction a été qu'une implémentation C n'est rien de plus qu'un automate fini, car elle a une limite sur la quantité de mémoire adressable (vous ne pouvez pas adresser plus de sizeof(char*) * CHAR_BITbits de stockage, car les adresses de mémoire distinctes doivent avoir des modèles de bits distincts lorsqu'elles sont stockées dans un pointeur d'octets).

Cependant, je pense qu'une mise en œuvre peut faire plus que cela. Autant que je sache, la norme n'impose aucune limite à la profondeur de récursivité. Vous pouvez donc effectuer autant d'appels de fonction récursifs que vous le souhaitez, mais tous, sauf un nombre fini d'appels, doivent utiliser des registerarguments non adressables ( ). Ainsi, une implémentation C qui permet une récursion arbitraire et n'a pas de limite sur le nombre d' registerobjets peut coder des automates déterministes de refoulement.

Est-ce correct? Pouvez-vous trouver une implémentation C plus puissante? Existe-t-il une implémentation C de Turing complète?

Gilles 'SO- arrête d'être méchant'
la source
4
@Dave: Comme Gilles l'a expliqué, il semble que vous puissiez avoir une mémoire illimitée, mais aucun moyen de l'adresser directement.
Jukka Suomela
2
D'après votre explication, il semble que toute implémentation C ne peut être programmée que pour accepter les langues acceptées par les automates déterministes de refoulement, qui sont plus faibles que même les langues sans contexte. Cette observation est cependant de peu d'intérêt à mon avis, car la question est une mauvaise application des asymptotiques.
Warren Schudy
3
Un point à garder à l'esprit est qu'il existe de nombreuses façons de déclencher un "comportement défini par l'implémentation" (ou "comportement indéfini"). Et en général, une implémentation peut fournir, par exemple, des fonctions de bibliothèque qui fournissent des fonctionnalités qui ne sont pas définies dans la norme C. Tous ces éléments fournissent des "failles" à travers lesquelles vous pouvez accéder, par exemple, à une machine complète de Turing. Ou même quelque chose de beaucoup plus fort, comme un oracle qui résout le problème de l'arrêt. Un exemple stupide: le comportement défini par l'implémentation de débordements d'entiers signés ou de conversions de pointeurs entiers pourrait vous permettre d'accéder à un tel oracle.
Jukka Suomela du
7
Soit dit en passant, il pourrait être judicieux d'ajouter la balise «récréative» (ou tout ce que nous utilisons pour des puzzles amusants) afin que les gens ne prennent pas cela trop au sérieux. C'est évidemment la "mauvaise question" à poser, mais néanmoins je l'ai trouvée amusante et intrigante. :)
Jukka Suomela
2
@Jukka: Bonne idée. Par exemple, un débordement de X = écrit X / 3 sur la bande et se déplace dans la direction X% 3, un débordement = déclenche le signal correspondant au symbole sur la bande. Cela ressemble un peu à un abus, mais c'est certainement dans l'esprit de ma question. Pourriez-vous l'écrire comme réponse? (@others: Pas que je veuille décourager d'autres suggestions intelligentes!)
Gilles 'SO- arrête d'être méchant'

Réponses:

8

unsigned charunsigned char*sizeof(unsigned char*)sizeof(unsigned char *)unsigned charUCHAR_MAXsizeof(unsigned char)101010

Un programme pourrait stocker une quantité illimitée d'informations sur la pile si rien de ce qui est alloué sur la pile n'a jamais pris son adresse ; on pourrait donc avoir un programme C capable de faire certaines choses qui ne pourraient être faites par aucun automate fini de n'importe quelle taille. Ainsi, même si (ou peut-être parce que) l'accès aux variables de pile est beaucoup plus limité que l'accès aux variables allouées dynamiquement, il transforme C d'un automate fini en automate déroulant.

UCHAR_MAXsizeof(unsigned char)séquences possibles de valeurs de caractères, tout programme qui a créé un certain nombre de pointeurs vers des objets distincts en plus de cela ne pourrait pas se conformer à la norme C si le code examinait jamais la séquence de caractères associée à ces pointeurs . Il serait cependant possible dans certains cas pour un compilateur de déterminer qu'aucun code n'allait jamais examiner la séquence de caractères associée à un pointeur. Si chaque "char" était capable de contenir n'importe quel entier fini, et que la mémoire de la machine était une séquence infinie d'innombrables [étant donné une machine de Turing à bande illimitée, on pourrait émuler une telle machine bien qu'elle soit vraiment lente], alors il serait en effet possible de faire de C un langage complet de Turing.

supercat
la source
Avec une telle machine, que retournerait sizeof (char)?
TLW
1
@TLW: Identique à toute autre machine: 1. Les macros CHAR_BITS et CHAR_MAX seraient cependant un peu plus problématiques; la norme ne permettrait pas le concept de types qui n'ont pas de limites.
supercat
Oups, je voulais dire CHAR_BITS, comme vous l'avez dit, désolé.
TLW
7

Avec la bibliothèque de threads C11 (en option), il est possible de réaliser une implémentation complète de Turing avec une profondeur de récursivité illimitée.

La création d'un nouveau thread produit une seconde pile; deux piles suffisent pour que Turing soit complet. Une pile représente ce qui est à gauche de la tête, l'autre pile ce qui est à droite.

Jared
la source
Mais les machines Turing avec un ruban progressant à l'infini dans une seule direction sont tout aussi puissantes que les machines Turing avec un ruban progressant à l'infini dans deux directions. En dehors de cela, plusieurs threads peuvent être simulés par un planificateur. Quoi qu'il en soit, nous n'avons même pas besoin d'une bibliothèque de threads.
xamid
3

Je pense que c'est Turing complet : nous pouvons écrire un programme qui simule un UTM en utilisant cette astuce (j'ai rapidement écrit le code à la main donc il y a probablement des erreurs de syntaxe ... mais j'espère qu'il n'y a pas d'erreurs (majeures) dans la logique :-)

  • définir une structure qui peut être utilisée comme une double liste chaînée pour la représentation sur bande
    typdef struct {
      cell_t * pred; // cellule à gauche
      cell_t * succ; // cellule à droite
      int val; // valeur de cellule
    } cell_t 

Le headsera un pointeur vers une cell_tstructure

  • définir une structure qui peut être utilisée pour stocker l'état actuel et un indicateur
    typedef struct {
      état int;
      drapeau int;
    } info_t 
  • définissez ensuite une fonction de boucle unique qui simule un TM universel lorsque la tête se trouve entre les limites de la liste à double lien; lorsque la tête atteint une limite, placez le drapeau de la structure info_t (HIT_LEFT, HIT_RIGHT) et retournez:
void simulate_UTM (cell_t * head, info_t * info) {
  tandis que (vrai) {
    head-> val = UTM_nextsymbol [info-> state, head-> val]; // écrit le symbole
    info-> state = UTM_nextstate [info-> state, head-> val]; // état suivant
    if (info-> state == HALT_STATE) {// affiche if accepte et quitte le programme
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       sortie (0);
    }
    int move = UTM_nextmove [info-> état, tête-> val];
    if (move == MOVE_LEFT) {
      head = head-> pred; // se déplacer à gauche
      if (head == NULL) {info-> flag = HIT_LEFT; revenir; }
    } autre {
      tête = tête-> succ; // déplacer vers la droite
      if (head == NULL) {info-> flag = HIT_RIGHT; revenir; }
    }
  } // toujours dans la limite ... continue
}
  • définissez ensuite une fonction récursive qui appelle d'abord la routine UTM de simulation, puis s'appelle récursivement quand la bande doit être étendue; lorsque la bande doit être étendue en haut (HIT_RIGHT) aucun problème, quand elle doit être décalée en bas (HIT_LEFT) il suffit de remonter les valeurs des cellules en utilisant la liste à double lien:
void stacker (cell_t * top, cell_t * bottom, cell_t * head, info_t * info) {
  simulate_UTM (head, info);
  cell_t newcell; // la nouvelle cellule
  newcell.pred = top; // met à jour la liste double liée avec la nouvelle cellule
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  commutateur (info-> hit) {
    cas HIT_RIGHT:
      empileur (& newcell, bottom, newcell, info);
      Pause;
    cas HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// décaler vers le haut les valeurs
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      stacker (& newcell, bottom, bottom, info);
      Pause;
  }
}
  • la bande initiale peut être remplie avec une simple fonction récursive qui construit la double liste chaînée, puis appelle la stackerfonction lorsqu'elle lit le dernier symbole de la bande d'entrée (en utilisant readchar)
void init_tape (cell_t * top, cell_t * bottom, info_t * info) {
  cell_t newcell;
  int c = readchar ();
  if (c == END_OF_INPUT) stacker (& top, bottom, bottom, info); // plus de symboles, commence
  newcell.pred = top;
  if (top! = NULL) top.succ = & newcell; else bottom = & newcell;
  init_tape (& newcell, bottom, info);
}

EDIT: après y avoir réfléchi un peu, il y a un problème avec les pointeurs ...

si chaque appel de la fonction récursive stackerpeut conserver un pointeur valide vers une variable définie localement dans l'appelant, alors tout va bien ; sinon mon algorithme ne peut pas maintenir une liste valide à double lien sur la récursion illimitée (et dans ce cas, je ne vois pas un moyen d'utiliser la récursivité pour simuler un stockage à accès aléatoire illimité).

Marzio De Biasi
la source
3
stackernewcellstacker2n/sns=sizeof(cell_t)
@ Gilles: vous avez raison (voir ma modification); si vous limitez la profondeur de récursivité, vous obtenez un automate fini
Marzio De Biasi
@MarzioDeBiasi Non, il a tort puisqu'il fait référence à une implémentation concrète que la norme ne présuppose pas. En fait, il n'y a pas de limite théorique à la profondeur de récursivité en C . Le choix d'utiliser une implémentation basée sur une pile limitée ne dit rien sur les limites théoriques du langage. Mais l'exhaustivité de Turing est une limite théorique.
xamid
0

Tant que vous avez une taille de pile d'appels illimitée, vous pouvez encoder votre bande sur la pile d'appels et y accéder de manière aléatoire en rembobinant le pointeur de pile sans revenir des appels de fonction.

EDIT : Si vous ne pouvez utiliser que le bélier, qui est fini, cette construction ne fonctionne plus, alors voyez ci-dessous.

Cependant, il est très discutable pourquoi votre pile peut être infinie, mais le bélier intrinsèque ne l'est pas. Donc, en fait, je dirais que vous ne pouvez même pas reconnaître tous les langages réguliers, car le nombre d'états est limité (si vous ne comptez pas l'astuce de rembobinage pour exploiter la pile infinie).

Je suppose même que le nombre de langues que vous pouvez reconnaître est fini (même si les langues elles-mêmes peuvent être infinies, par exemple a*, ça va, mais b^kne fonctionne que pour un nombre fini de ks).

EDIT : Ce n'est pas vrai, car vous pouvez coder l'état actuel dans des fonctions supplémentaires, de sorte que vous pouvez vraiment reconnaître TOUTES les langues régulières.

Vous pouvez très probablement obtenir toutes les langues de type 2 pour la même raison, mais je ne sais pas si vous pouvez réussir à mettre les deux, l'état et la constante de pile sur la pile d'appels. Mais d'une manière générale, vous pouvez effectivement oublier le bélier, car vous pouvez toujours mettre à l'échelle la taille de l'automate afin que votre alphabet dépasse la capacité du bélier. Donc, si vous pouviez simuler une MT avec seulement une pile, Type-2 serait égal à Type-0, n'est-ce pas?

bitmask
la source
5
Qu'est-ce qu'un «pointeur de pile»? (Notez que le mot «pile» n'apparaît pas dans la norme C.) Ma question concerne C en tant que classe de langages formels, pas les implémentations C sur un ordinateur (qui sont évidemment des machines à états finis). Si vous souhaitez accéder à la pile d'appels, vous devez le faire d'une manière fournie par la langue. Par exemple, en prenant l'adresse des arguments de la fonction - mais toute implémentation donnée n'a qu'un nombre fini d'adresses, ce qui limite alors la profondeur de récursivité.
Gilles 'SO- arrête d'être méchant'
J'ai modifié ma réponse pour exclure l'utilisation d'un pointeur de pile.
bitmask
1
Je ne comprends pas où vous allez avec votre réponse révisée (à part changer la formulation des fonctions calculables en langages reconnus). Étant donné que les fonctions ont également une adresse, vous avez besoin d'une implémentation suffisamment grande pour implémenter une machine à états finis donnée. La question est de savoir si et comment une implémentation C pourrait faire plus (par exemple, implémenter une machine Turing universelle) sans s'appuyer sur un comportement non défini.
Gilles 'SO- arrête d'être méchant'
0

J'y ai pensé une fois et j'ai décidé d'essayer d'implémenter un langage non contextuel en utilisant la sémantique attendue; l'élément clé de la mise en œuvre est la fonction suivante:

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}

Au moins, je pense que cela fonctionne. Il se peut cependant que je commette une erreur fondamentale.

Une version fixe:

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
la source
Eh bien, pas une erreur fondamentale, mais it = *itdevrait être remplacé par it = * (void **) it, sinon *itest de type void.
Ben Standeven
Je serais très surpris si voyager dans la pile d'appels comme ça serait un comportement défini en C.
Radu GRIGore
Oh, cela ne fonctionnera pas, car le premier «b» provoque l'échec de read_a () et déclenche donc un rejet.
Ben Standeven
Mais il est légitime de parcourir la pile d'appels de cette façon, puisque la norme C dit: "Pour un tel objet [c'est-à-dire un avec stockage automatique] qui n'a pas un type de tableau de longueur variable, sa durée de vie s'étend de l'entrée dans le bloc avec auquel il est associé jusqu'à ce que l'exécution de ce bloc se termine de quelque manière que ce soit. (La saisie d'un bloc fermé ou l'appel d'une fonction suspend, mais ne termine pas, l'exécution du bloc en cours.) Si le bloc est entré récursivement, une nouvelle instance de l'objet est créé à chaque fois. " Ainsi, chaque appel de read_triple créerait un nouveau pointeur qui peut être utilisé dans la récursivité.
Ben Standeven
2
2CHAR_BITsizeof(char*)
0

Dans le sens de la réponse de @ supercat:

Les affirmations d'incomplétude de C semblent être centrées sur le fait que des objets distincts devraient avoir des adresses distinctes, et l'ensemble des adresses est supposé être fini. Comme l'écrit @supercat

Comme indiqué dans la question, la norme C requiert qu'il existe une valeur UCHAR_MAXtelle que chaque variable de type char non signé contiendra toujours une valeur comprise entre 0 et UCHAR_MAX, inclus. Il requiert en outre que chaque objet alloué dynamiquement soit représenté par une séquence d'octets identifiable via un pointeur de type unsigned char *, et qu'il y ait une constante sizeof(unsigned char*)telle que chaque pointeur de ce type soit identifiable par une séquence de sizeof(unsigned char *)valeurs de type unsigned carboniser.

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

À ce stade, il faut vérifier que la norme C permettrait effectivement cela.

sizeofZ

Alexey B.
la source
1
De nombreuses opérations sur les types intégraux sont définies pour avoir un résultat qui est «réduit modulo de plus que la valeur maximale représentable dans le type de résultat». Comment cela fonctionnerait-il si ce maximum était un ordinal non fini?
Gilles 'SO- arrête d'être méchant'
@Gilles C'est un point intéressant. Il n'est en effet pas clair quelle serait la sémantique de uintptr_t p = (uintptr_t)sizeof(void*)(mettre \ oméga dans quelque chose qui contient des entiers non signés). Je ne sais pas. Nous pouvons nous en sortir en définissant le résultat comme étant 0 (ou tout autre nombre).
Alexey B.
1
uintptr_tdevrait être infini aussi. Attention, ce type est facultatif - mais si vous avez un nombre infini de valeurs de pointeur distinctes, il sizeof(void*)doit également être infini, donc size_tdoit être infini. Mon objection à propos de la réduction modulo n'est pas si évidente cependant - elle n'entre en jeu que s'il y a un débordement, mais si vous autorisez des types infinis, ils pourraient ne jamais déborder. Mais sur la main de préhension, chaque type a une valeur minimale et maximale, ce qui, pour autant que je sache, implique qu'il UINT_MAX+1doit déborder.
Gilles 'SO- arrête d'être méchant'
Un bon point également. En effet, nous obtenons un tas de types (pointeurs et size_t) qui devraient être ℕ, ℤ, ou une construction basée sur eux (pour size_t si ce serait quelque chose comme ℕ ∪ {ω}). Maintenant, si pour certains de ces types, la norme requiert une macro définissant la valeur maximale (PTR_MAX ou quelque chose comme ça), les choses deviendront velues. Mais jusqu'à présent, je n'ai pu financer l'exigence de macros MIN / MAX que pour les types sans pointeur.
Alexey B.
Une autre possibilité d'investigation consiste à définir les deux size_ttypes de pointeurs comme étant ℕ ∪ {ω}. Cela supprime le problème min / max. Le problème avec la sémantique de débordement demeure. Ce qui devrait être la sémantique uint x = (uint)ωn'est pas clair pour moi. Encore une fois, nous pourrions prendre au hasard 0, mais cela semble un peu moche.
Alexey B.