Máxima potencia computacional de una implementación en C


28

Si seguimos el libro (o cualquier otra versión de la especificación del lenguaje si lo prefiere), ¿cuánta potencia computacional puede tener una implementación de C?

Tenga en cuenta que "implementación C" tiene un significado técnico: es una instanciación particular de la especificación del lenguaje de programación C donde se documenta el comportamiento definido por la implementación. La implementación de CA no tiene que poder ejecutarse en una computadora real. Tiene que implementar todo el lenguaje, incluidos todos los objetos que tienen una representación de cadena de bits y los tipos que tienen un tamaño definido por la implementación.

A los efectos de esta pregunta, no hay almacenamiento externo. La única entrada / salida que puede realizar es getchar(para leer la entrada del programa) y putchar(para escribir la salida del programa). Además, cualquier programa que invoque un comportamiento indefinido no es válido: un programa válido debe tener su comportamiento definido por la especificación C más la descripción de la implementación de los comportamientos definidos por la implementación enumerados en el apéndice J (para C99). Tenga en cuenta que llamar a funciones de biblioteca que no se mencionan en el estándar es un comportamiento indefinido.

Mi reacción inicial fue que una implementación en C no es más que un autómata finito, ya que tiene un límite en la cantidad de memoria direccionable (no se puede direccionar más que sizeof(char*) * CHAR_BITbits de almacenamiento, ya que distintas direcciones de memoria deben tener patrones de bits distintos cuando se almacenan en un puntero de byte).

Sin embargo, creo que una implementación puede hacer más que esto. Por lo que puedo decir, el estándar no impone ningún límite en la profundidad de la recursión. Por lo tanto, puede realizar tantas llamadas a funciones recursivas como desee, solo todas menos un número finito de llamadas deben usar registerargumentos no direccionables ( ). Por lo tanto, una implementación en C que permite la recursión arbitraria y no tiene límite en el número de registerobjetos puede codificar autómatas de pushdown determinista.

¿Es esto correcto? ¿Puedes encontrar una implementación de C más potente? ¿Existe una implementación de Turing-complete C?


44
@Dave: Como explicó Gilles, parece que puedes tener memoria ilimitada, pero no hay forma de abordarla directamente.
Jukka Suomela

2
Desde su explicación suena como cualquier aplicación C sólo se puede programar para aceptar idiomas aceptados por determinista autómatas de pila, que son más débiles que incluso lenguajes libres de contexto. Sin embargo, esta observación es de poco interés en mi opinión, ya que la pregunta es una aplicación errónea de los asintóticos.
Warren Schudy

3
Un punto a tener en cuenta es que hay muchas formas de activar el "comportamiento definido por la implementación" (o "comportamiento indefinido"). Y, en general, una implementación puede proporcionar, por ejemplo, funciones de biblioteca que proporcionan funcionalidad que no está definida en el estándar C. Todos estos proporcionan "lagunas" a través de las cuales puede acceder, por ejemplo, a una máquina completa de Turing. O incluso algo mucho más fuerte, como un oráculo que resuelve el problema de detención. Un ejemplo estúpido: el comportamiento definido por la implementación de desbordamientos de enteros con signo o conversiones de enteros y punteros podría permitirle acceder a dicho oráculo.
Jukka Suomela

77
Por cierto, podría ser una buena idea agregar la etiqueta "recreativo" (o lo que sea que estemos usando para acertijos divertidos) para que las personas no lo tomen demasiado en serio. Obviamente, es la "pregunta equivocada", pero, sin embargo, me pareció divertida e intrigante. :)
Jukka Suomela

2
@ Jukka: Buena idea. Por ejemplo, el desbordamiento de X = escribir X / 3 en la cinta y moverse en la dirección X% 3, underflow = disparar la señal correspondiente al símbolo en la cinta. Se siente un poco como un abuso, pero definitivamente está en el espíritu de mi pregunta. ¿Podrías escribirlo como respuesta? (@other: ¡No es que quiera desalentar otras sugerencias tan inteligentes!)
Gilles 'SO- deja de ser malvado'

Respuestas:


8

Como se señaló en la pregunta, el estándar C requiere que exista un valor UCHAR_MAX de modo que cada variable de tipo unsigned charsiempre tenga un valor entre 0 y UCHAR_MAX, inclusive. Además, requiere que cada objeto asignado dinámicamente esté representado por una secuencia de bytes que sea identificable mediante un puntero de tipo unsigned char*, y que exista una constante sizeof(unsigned char*)tal que cada puntero de ese tipo sea identificable por una secuencia de sizeof(unsigned char *)valores de tipo unsigned char. El número de objetos que pueden asignarse dinámicamente simultáneamente está, por lo tanto, rígidamente limitado a . Nada impediría que un compilador teórico asigne los valores de esas constantes para soportar más de 10 10 10 objetos, pero desde una perspectiva teórica la existencia de cualquier límite, no importa cuán grande sea, significa que algo no es infinito.UCHAR_MAXsizeof(unsigned char)101010

Un programa podría almacenar una cantidad ilimitada de información en la pila si nada de lo que se asigna en la pila tiene su dirección tomada ; por lo tanto, se podría tener un programa en C que fuera capaz de hacer algunas cosas que ningún autómata finito de ningún tamaño podría hacer. Por lo tanto, aunque (o tal vez porque) el acceso a las variables de la pila es mucho más limitado que el acceso a las variables asignadas dinámicamente, convierte a C de ser un autómata finito en un autómata pushdown.

Sin embargo, existe otra posible arruga: se requiere que si un programa examina las secuencias subyacentes de longitud fija de valores de caracteres asociados con dos punteros a diferentes objetos, esas secuencias deben ser únicas. Porque solo hay UCHAR_MAXsizeof(unsigned char)posibles secuencias de valores de caracteres, cualquier programa que creara una cantidad de punteros a objetos distintos que no pudieran cumplir con el estándar C si el código alguna vez examinara la secuencia de caracteres asociados con esos punteros . Sin embargo, en algunos casos sería posible que un compilador determinara que ningún código examinaría la secuencia de caracteres asociada a un puntero. Si cada "char" era realmente capaz de contener cualquier número entero finito, y la memoria de la máquina era una secuencia infinita de números enteros [dada una máquina Turing de cinta ilimitada, uno podría emular una máquina de este tipo aunque sería realmente lenta], entonces de hecho, sería posible hacer de C un lenguaje completo de Turing.


Con tal máquina, ¿qué devolvería sizeof (char)?
TLW

1
@TLW: Igual que cualquier otra máquina: 1. Sin embargo, las macros CHAR_BITS y CHAR_MAX serían un poco más problemáticas; el Estándar no permitiría el concepto de tipos que no tienen límites.
supercat

Vaya, me refería a CHAR_BITS, como dijiste, lo siento.
TLW

7

Con la biblioteca de subprocesos de C11 (opcional), es posible realizar una implementación completa de Turing con una profundidad de recursión ilimitada.

Crear un nuevo hilo produce una segunda pila; dos pilas son suficientes para completar Turing. Una pila representa lo que está a la izquierda de la cabeza, la otra pila lo que está a la derecha.


Pero las máquinas de Turing con una cinta que progresa infinitamente en una sola dirección son tan poderosas como las máquinas de Turing con una cinta que progresa infinitamente en dos direcciones. Además de eso, un programador puede simular múltiples hilos. De todos modos, ni siquiera necesitamos una biblioteca de subprocesos.
xamid

3

Creo que está completando Turing : podemos escribir un programa que simule un UTM usando este truco (escribí rápidamente el código a mano, por lo que probablemente haya algunos errores de sintaxis ... pero espero que no haya errores (mayores) en la lógica :-)

  • Definir una estructura que se pueda utilizar como una lista de doble enlace para la representación en cinta
    typdef struct {
      cell_t * pred; // celda a la izquierda
      cell_t * succ; // celda a la derecha
      int val; // valor de celda
    } cell_t 

El headserá un puntero a una cell_testructura

  • definir una estructura que se pueda usar para almacenar el estado actual y una bandera
    typedef struct {
      int estado;
      bandera int;
    } info_t 
  • luego defina una función de bucle único que simule un TM universal cuando el encabezado esté entre los límites de la lista de doble enlace; cuando la cabeza alcanza un límite, establece la bandera de la estructura info_t (HIT_LEFT, HIT_RIGHT) y regresa:
void simulate_UTM (cell_t * head, info_t * info) {
  while (verdadero) {
    head-> val = UTM_nextsymbol [info-> state, head-> val]; // escribir símbolo
    info-> state = UTM_nextstate [info-> state, head-> val]; // siguiente estado
    if (info-> state == HALT_STATE) {// print if acepta y sale del programa
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       salida (0);
    }
    int move = UTM_nextmove [info-> estado, head-> val];
    if (mover == MOVE_LEFT) {
      cabeza = cabeza-> pred; // mover hacia la izquierda
      if (head == NULL) {info-> flag = HIT_LEFT; regreso; }
    } más {
      cabeza = cabeza-> succ; // moverse a la derecha
      if (head == NULL) {info-> flag = HIT_RIGHT; regreso; }
    }
  } // todavía en el límite ... continúa
}
  • luego defina una función recursiva que primero llama a la rutina UTM de simulación y luego se llama recursivamente a sí misma cuando la cinta necesita expandirse; cuando la cinta necesita expandirse en la parte superior (HIT_RIGHT) sin problemas, cuando necesita desplazarse en la parte inferior (HIT_LEFT) simplemente cambie los valores de las celdas usando la lista de doble enlace:
apilador vacío (cell_t * top, cell_t * bottom, cell_t * head, info_t * info) {
  simulate_UTM (head, info);
  cell_t newcell; // la nueva celda
  newcell.pred = top; // actualiza la lista de doble enlace con la nueva celda
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  interruptor (información-> golpear) {
    caso HIT_RIGHT:
      apilador (& newcell, bottom, newcell, info);
      descanso;
    caso HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// valores ascendentes
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      apilador (& newcell, fondo, fondo, información);
      descanso;
  }
}
  • la cinta inicial se puede llenar con una función recursiva simple que construye la lista de doble enlace y luego llama a la stackerfunción cuando lee el último símbolo de la cinta de entrada (usando readchar)
vacío init_tape (cell_t * top, cell_t * bottom, info_t * info) {
  cell_t newcell;
  int c = readchar ();
  if (c == END_OF_INPUT) apilador (& arriba, abajo, abajo, información); // no más símbolos, comenzar
  newcell.pred = top;
  if (top! = NULL) top.succ = & newcell; de lo contrario bottom = & newcell;
  init_tape (& newcell, bottom, info);
}

EDITAR: después de pensar un poco al respecto, hay un problema con los punteros ...

si cada llamada de la función recursiva stackerpuede mantener un puntero válido a una variable definida localmente en la persona que llama, entonces todo está bien ; de lo contrario, mi algoritmo no puede mantener una lista válida de doble enlace en la recursión ilimitada (y en este caso no veo una manera de usar la recursión para simular un almacenamiento ilimitado de acceso aleatorio).


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@Gilles: tienes razón (mira mi edición); si limitas la profundidad de recursión obtienes un autómata finito
Marzio De Biasi

@MarzioDeBiasi No, está equivocado ya que se refiere a una implementación concreta que el estándar no presupone. De hecho, no hay límite teórico a nivel de recursividad en C . La elección de usar una implementación basada en una pila limitada no dice nada sobre los límites teóricos del lenguaje. Pero la integridad de Turing es un límite teórico.
xamid

0

Siempre que tenga un tamaño ilimitado de la pila de llamadas, puede codificar su cinta en la pila de llamadas y acceder aleatoriamente rebobinando el puntero de la pila sin regresar de las llamadas a funciones.

Edición : si solo puede usar el carnero, que es finito, esta construcción ya no funciona, así que vea a continuación.

Sin embargo, es muy cuestionable por qué su pila puede ser infinita pero el ariete intrínseco no. Entonces, en realidad, diría que ni siquiera puede reconocer todos los idiomas regulares, ya que el número de estados está limitado (si no cuenta el truco de pila-rebobinado para explotar la pila infinita).

Incluso podría especular que el número de idiomas que puede reconocer es finito (incluso si los idiomas mismos pueden ser infinitos, por ejemplo, a*está bien, pero b^ksolo funciona para un número finito de ks).

EDITAR : Esto no es cierto, ya que puede codificar el estado actual en funciones adicionales, por lo que realmente puede reconocer TODOS los idiomas regulares.

Lo más probable es que pueda obtener todos los idiomas de Tipo 2 por la misma razón, pero no estoy seguro de si puede colocar ambos, el estado y el contenido de la pila en la pila de llamadas. Pero en términos generales, puede olvidarse efectivamente del carnero, ya que siempre puede escalar el tamaño del autómata para que su alfabeto exceda la capacidad del carnero. Entonces, si pudieras simular una TM con solo una pila, Tipo-2 sería igual a Tipo-0, ¿no?


55
¿Qué es un "puntero de pila"? (Tenga en cuenta que la palabra "apilar" no aparece en el estándar C). Mi pregunta es sobre C como una clase de lenguajes formales, no sobre implementaciones de C en una computadora (que obviamente son máquinas de estados finitos). Si desea acceder a la pila de llamadas, debe hacerlo de la manera proporcionada por el idioma. Por ejemplo, tomando la dirección de los argumentos de la función, pero cualquier implementación dada tiene solo un número finito de direcciones, lo que limita la profundidad de la recursividad.
Gilles 'SO- deja de ser malvado'

He modificado mi respuesta para excluir el uso de un puntero de pila.
bitmask

1
No entiendo a dónde va con su respuesta revisada (aparte de cambiar la formulación de funciones computables a lenguajes reconocidos). Dado que las funciones también tienen una dirección, necesita una implementación lo suficientemente grande como para implementar cualquier máquina de estados finitos. La pregunta es si y cómo una implementación de C podría hacer más (por ejemplo, implementar una máquina Turing universal) sin depender de un comportamiento no definido.
Gilles 'SO- deja de ser malvado'

0

Pensé en esto una vez y decidí intentar implementar un lenguaje sin contexto utilizando la semántica esperada; La parte clave de la implementación es la siguiente función:

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}

Al menos, creo que esto funciona. Sin embargo, puede ser que esté cometiendo un error fundamental.

Una versión fija:

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

Bueno, no es un error fundamental, pero it = *itdebe ser reemplazado por it = * (void **) it, ya que de lo contrario *ites de tipo void.
Ben Standeven

Me sorprendería mucho si viajar en la pila de llamadas de esa manera fuera un comportamiento definido en C.
Radu GRIGore

Oh, esto no funcionará, porque la primera 'b' hace que read_a () falle y, por lo tanto, desencadena un rechazo.
Ben Standeven

Pero es legítimo viajar por la pila de llamadas de esta manera, ya que el estándar C dice: "Para tal objeto [es decir, uno con almacenamiento automático] que no tiene un tipo de matriz de longitud variable, su vida útil se extiende desde la entrada en el bloque con que está asociado hasta que la ejecución de ese bloque termine de alguna manera (ingresar un bloque cerrado o llamar a una función suspende, pero no finaliza, la ejecución del bloque actual). Si el bloque se ingresa recursivamente, una nueva instancia del objeto se crea cada vez ". Por lo tanto, cada llamada de read_triple crearía un nuevo puntero que se puede usar en la recursión.
Ben Standeven

2
2CHAR_BITsizeof(char*)

0

En la línea de la respuesta de @ supercat:

Las afirmaciones de incompletitud de C parecen centrarse en que los objetos distintos deben tener direcciones distintas, y se supone que el conjunto de direcciones es finito. Como escribe @supercat

Como se señaló en la pregunta, el estándar C requiere que exista un valor UCHAR_MAXtal que cada variable de tipo unsigned char siempre tendrá un valor entre 0 e UCHAR_MAX, inclusive. Además, requiere que cada objeto asignado dinámicamente esté representado por una secuencia de bytes que sea identificable mediante un puntero de tipo unsigned char *, y que exista una constante sizeof(unsigned char*)tal que cada puntero de ese tipo sea identificable por una secuencia de sizeof(unsigned char *)valores de tipo unsigned carbonizarse.

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

En este punto, uno debería verificar que el estándar C lo permitiría.

sizeofZ


1
Muchas operaciones en tipos integrales se definen para tener un resultado que es "módulo reducido uno más que el valor máximo representable en el tipo de resultado". ¿Cómo funcionaría eso si ese máximo es un ordinal no finito?
Gilles 'SO- deja de ser malvado'

@Gilles Este es un punto interesante. De hecho, no está claro cuál sería la semántica de uintptr_t p = (uintptr_t)sizeof(void*)(poner \ omega en algo que contiene enteros sin signo). Yo no sé. Podemos evitar definir el resultado como 0 (o cualquier otro número).
Alexey B.

1
uintptr_tTendría que ser infinito también. Eso sí, este tipo es opcional, pero si tiene un número infinito de valores de puntero distintos, sizeof(void*)también debe ser infinito, por lo que size_tdebe ser infinito. Sin embargo, mi objeción sobre el módulo de reducción no es tan obvia: solo entra en juego si hay un desbordamiento, pero si permite tipos infinitos, es posible que nunca se desborden. Pero, por otro lado, cada tipo tiene valores mínimos y máximos, que por lo que puedo decir implica que UINT_MAX+1debe desbordarse.
Gilles 'SO- deja de ser malvado'

También un buen punto. De hecho, obtenemos un montón de tipos (punteros y size_t) que deberían ser ℕ, ℤ o alguna construcción basada en ellos (para size_t si sería algo así como ℕ ∪ {ω}). Ahora, si para algunos de estos tipos, el estándar requiere una macro que defina el valor máximo (PTR_MAX o algo así), las cosas se pondrán difíciles. Pero hasta ahora solo pude financiar el requisito de macros MIN / MAX para tipos sin puntero.
Alexey B.

Otra posibilidad de investigar es definir ambos size_ttipos de puntero para que sean ℕ ∪ {ω}. Esto elimina el problema min / max. El problema con la semántica de desbordamiento aún persiste. Lo que debería ser la semántica uint x = (uint)ωno me resulta claro. De nuevo, podríamos tomar 0 al azar, pero se ve un poco feo.
Alexey B.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.