C实现的最大计算能力


28

如果我们按照本书(或您愿意的话,可以选择其他任何语言规范版本)进行阅读,那么C实现可以具有多少计算能力?

请注意,“ C实现”具有技术含义:它是C编程语言规范的特定实例,其中记录了实现定义的行为。AC实施不必一定可以在实际计算机上运行。它确实必须实现整个语言,包括每个具有位字符串表示形式的对象和具有实现定义的大小的类型。

出于此问题的目的,没有外部存储。您可能执行的唯一输入/输出是getchar(读取程序输入)和putchar(写入程序输出)。同样,任何调用未定义行为的程序都是无效的:有效程序的行为必须由C规范以及附录J中列出的实现定义的行为的实现描述(对于C99)进行定义。请注意,标准中未提及的调用库函数是未定义的行为。

我最初的反应是,C实现只不过是有限的自动机,因为它对可寻址内存的数量有限制(您不能寻址的存储sizeof(char*) * CHAR_BIT位数更多,因为存储时不同的内存地址必须具有不同的位模式)在字节指针中)。

但是,我认为实现不仅仅可以做到这一点。据我所知,该标准对递归的深度没有限制。因此,您可以根据需要进行任意数量的递归函数调用,只有有限数量的调用中的所有调用都必须使用不可寻址(register)参数。因此,允许任意递归并且对register对象数量没有限制的C实现可以编码确定性下推自动机。

它是否正确?您能找到更强大的C实现吗?是否存在图灵完备的C实现?


4
@Dave:正如Gilles解释的那样,似乎您可以拥有无​​限的内存,但是无法直接解决它。
Jukka Suomela 2010年

2
从您的解释看来,任何C实现都只能编程为接受确定性下推自动机接受的语言,甚至比上下文无关的语言还要弱。但是,在我看来,这种观察没有什么意义,因为这个问题是渐进性疗法的错误应用。
沃伦·舒迪

3
要记住的一点是,有许多方法可以触发“实现定义的行为”(或“未定义的行为”)。并且通常,实现可以提供例如库函数,该库函数提供C标准中未定义的功能。所有这些都提供了“漏洞”,您可以通过这些漏洞访问图灵完整的计算机。甚至还有更强大的功能,例如解决停止问题的甲骨文。一个愚蠢的例子:带符号的整数溢出或整数指针转换的实现定义的行为可能使您可以访问此类oracle。
Jukka Suomela 2010年

7
顺便说一句,添加标签“ recreational”(或我们用于搞笑拼图的任何东西)可能是个好主意,这样人们就不会太在意这个。显然,这是一个“错误的问题”,但是尽管如此,我还是发现它很有趣且令人着迷。:)
Jukka Suomela 2010年

2
@Jukka:好主意。例如,X溢出=将X / 3写在磁带上,并沿X%3方向移动,下溢=触发与磁带上符号相对应的信号。感觉有点像虐待,但这绝对是我的问题的实质。你能写出来作为答案吗?(@others:不是我不想劝阻其他这样的聪明建议!)
Gilles'SO-别再邪恶了',2010年

Answers:


8

正如问题中指出的那样,标准C要求存在一个值UCHAR_MAX,以便每个类型的变量unsigned char将始终保持一个介于0到UCHAR_MAX(含)之间的值。它还要求每个动态分配的对象都由一个字节序列表示,该字节序列可以通过type的指针标识unsigned char*,并且必须有一个常量sizeof(unsigned char*),使得该类型的每个指针都可以由sizeof(unsigned char *)type 的值序列标识unsigned char。因此,可以同时动态分配的对象数严格限制为。没有什么可以阻止理论上的编译器分配这些常量的值以支持超过UCHAR_MAXsizeof(unsigned char)101010 对象,但从理论上讲,无论有多大,任何边界的存在都意味着事物不是无限的。

如果没有分配任何在堆栈上分配的地址,那么程序可以在堆栈上存储无限量的信息。因此,可能会有一个C程序能够执行某些事情,而这是任何大小的有限自动机都无法完成的。因此,尽管(或者也许是因为)对堆栈变量的访问比对动态分配的变量的访问受到更多限制,但它使C从有限的自动机变成了下推式自动机。

但是,还有另一个潜在的问题:如果程序检查与两个指向不同对象的指针关联的字符值的基础固定长度序列,则这些序列必须是唯一的。由于只有可能的字符值序列,因此,如果代码曾经检查过以下对象,则任何创建了多个指向不同对象的指针的程序均不符合C标准。与那些指针关联的字符序列UCHAR_MAXsizeof(unsigned char)。但是,在某些情况下,编译器可能会确定没有代码会检查与指针关联的字符序列。如果每个“字符”实际上都能够容纳任何有限的整数,并且该机器的内存是一个无穷无尽的整数序列(给定无限磁带的图灵机,尽管它确实很慢,但可以模拟这样的机器),然后确实有可能使C成为图灵完备的语言。


在这样的机器上,sizeof(char)返回什么?
TLW

1
@TLW:与任何其他机器相同:1. CHAR_BITS和CHAR_MAX宏会有些问题;标准将不允许无界限的类型的概念。
超级猫

抱歉,我是说CHAR_BITS,对不起。
TLW

7

使用C11的(可选)线程库,可以在无限递归深度的情况下完成Turing的完整实现。

创建一个新线程将产生第二个堆栈;图灵完整性的两个堆栈就足够了。一个堆栈表示头部的左侧,另一个堆栈表示右侧。


但是,具有仅在一个方向上无限延伸的磁带的图灵机与具有在两个方向上无限地延伸的磁带的图灵机一样强大。除此之外,调度程序可以模拟多个线程。无论如何,我们甚至不需要线程库。
xamid

3

我认为这已经完成了图灵:我们可以使用此技巧来编写一个模拟UTM的程序(我很快就手工编写了代码,因此可能存在一些语法错误...但是我希望逻辑中没有(主要)错误。 :-)

  • 定义一个可用作磁带表示的双链表的结构
    typdef struct {
      cell_t * pred; //左侧的单元格
      cell_t * succ; //右边的单元格
      int val; //储存格值
    } cell_t 

head将是一个指向cell_t结构

  • 定义可用于存储当前状态和标志的结构
    typedef struct {
      int状态
      int标志;
    } info_t 
  • 然后定义一个单循环函数,当头部位于双链表的边界之间时,该函数将模拟Universal TM;当头部碰到边界时,设置info_t结构的标志(HIT_LEFT,HIT_RIGHT)并返回:
void Simulation_UTM(cell_t * head,info_t * info){
  而(true){
    head-> val = UTM_nextsymbol [info-> state,head-> val]; //写符号
    info-> state = UTM_nextstate [info-> state,head-> val]; //下一个状态
    if(info-> state == HALT_STATE){//打印是否接受并退出程序
       putchar((info-> state == ACCEPT_STATE)?'1':'0');
       退出(0);
    }
    int move = UTM_nextmove [info-> state,head-> val];
    如果(移动== MOVE_LEFT){
      head = head-> pred; // 向左移动
      如果(head == NULL){info-> flag = HIT_LEFT; 返回; }
    }其他{
      头=头->成功; // 向右移
      如果(head == NULL){info-> flag = HIT_RIGHT; 返回; }
    }
  } //仍在边界中……继续
}
  • 然后定义一个递归函数,该函数首先调用模拟UTM例程,然后在需要扩展磁带时递归调用自身。当磁带需要在顶部扩展(HIT_RIGHT)没问题时,当磁带需要在底部扩展(HIT_LEFT)时,只需使用双链表向上移动单元格的值即可:
void stacker(cell_t * top,cell_t * bottom,cell_t * head,info_t * info){
  Simulation_UTM(head,info);
  cell_t newcell; //新单元格
  newcell.pred =顶部; //用新单元格更新双链表
  newcell.succ = NULL;
  top-> succ =&newcell;
  newcell.val = EMPTY_SYMBOL;

  开关(信息->点击){
    案例HIT_RIGHT:
      堆栈器(&newcell,bottom,newcell,info);
      打破;
    案例HIT_BOTTOM:
      cell_t * tmp = newcell;
      while(tmp-> pred!= NULL){//上移值
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      堆栈器(&newcell,bottom,bottom,info);
      打破;
  }
}
  • 初始磁带可以用一个简单的递归函数填充,该函数构建双链表,然后stacker在读取输入磁带的最后一个符号时调用该函数(使用readchar)
void init_tape(cell_t * top,cell_t * bottom,info_t * info){
  cell_t newcell;
  int c = readchar();
  如果(c == END_OF_INPUT)堆栈器(&top,bottom,bottom,info); //没有更多的符号,开始
  newcell.pred =顶部;
  如果(top!= NULL)top.succ =&newcell; else bottom =&newcell;
  init_tape(&newcell,bottom,info);
}

编辑:经过一番思考之后,指针出现了问题...

如果递归函数的每个调用都stacker可以维护一个有效的指针指向调用者本地定义的变量,那么一切都很好 ; 否则,我的算法将无法在无限递归上保留有效的双向链接列表(在这种情况下,看不到使用递归模拟无限随机访问存储的方法)。


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@吉尔斯:你是对的(见我的编辑);如果您限制递归深度,您将获得有限的自动机
Marzio De Biasi 2012年

@MarzioDeBiasi不,他错了,因为他指的是该标准不以为前提的具体实现。实际上,C中的递归深度没有理论限制。选择使用基于有限堆栈的实现方式并没有说明该语言的理论限制。但是图灵完备性是一个理论极限。
xamid

0

只要您拥有无限的调用堆栈大小,就可以在调用堆栈上对磁带进行编码,并通过回绕堆栈指针来随机访问它,而无需从函数调用中返回。

EdIT如果只能使用有限的ram,则此构造将不再起作用,因此请参见下文。

但是,为什么您的堆栈可以是无限的,而固有内存不是,这是一个非常可疑的问题。因此,实际上我想说的是,由于状态的数量是有限的,因此您甚至无法识别所有常规语言(如果您不计算利用无限堆栈的堆栈倒数技巧)。

我什至推测您可以识别的语言数量是有限的(即使语言本身可以是无限的,例如a*也可以,但b^k仅适用于ks的数量有限)。

编辑这是不正确的,因为您可以在其他功能中对当前状态进行编码,因此您可以真正识别所有常规语言。

出于相同的原因,您很可能会获得所有Type-2语言,但是我不确定是否可以将状态堆栈常量都放在调用堆栈上。但总的来说,您可以有效地忽略ram,因为您始终可以缩放自动机的大小,因此您的字母超过了ram的容量。因此,如果您可以仅模拟一个具有堆栈的TM,则Type-2等于Type-0,不是吗?


5
什么是“堆栈指针”?(请注意,“堆栈”一词在C标准中未出现。)我的问题是关于C作为一种形式语言的,而不是关于计算机(显然是有限状态机)上的C实现。如果要访问调用堆栈,则必须以该语言提供的方式进行。例如,通过获取函数参数的地址,但是任何给定的实现都只有有限数量的地址,这限制了递归的深度。
吉尔(Gilles)'所以

我已经修改了答案,以排除使用堆栈指针。
bitmask

1
我不明白修改后的答案在哪里(除了将表述从可计算函数更改为公认的语言之外)。由于函数也具有地址,因此您确实需要足够大的实现来实现任何给定的有限状态机。现在的问题是一个C语言实现是否以及如何做更多的(比如,实现一个通用图灵机),而不依赖于联合国定义的行为。
吉尔斯(Gilles)“所以,别再邪恶了”,2010年

0

我曾经考虑过这一点,因此决定尝试使用预期的语义实现非上下文无关的语言。实现的关键部分是以下功能:

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}

至少,我认为这可行。不过,可能是我犯了一些根本性的错误。

固定版本:

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

嗯,这不是一个根本性的错误,但是it = *it应该由代替it = * (void **) it,否则*it就是type void
Ben Standeven

我会感到非常惊讶,如果行驶调用堆栈那样是用C语言来定义的行为
拉杜·格里戈里

哦,这行不通,因为第一个“ b”会导致read_a()失败并因此引发拒绝。
Ben Standeven

但是以这种方式行进调用堆栈是合法的,因为C标准说:“对于这样的对象(即具有自动存储的对象),它不具有可变长度的数组类型,其生存期从输入到使用它与之关联,直到该块的执行以任何方式结束(输入封闭的块或调用函数将暂停,但不会结束当前块的执行。)如果递归地输入该块,则该对象的新实例每次都创建。” 因此,每次read_triple调用都会创建一个新的指针,该指针可以在递归中使用。
Ben Standeven

2
2CHAR_BITsizeof(char*)

0

按照@supercat的答案:

C的不完全性主张似乎围绕着不同的对象应该具有不同的地址,并且假定地址集是有限的。正如@supercat所写

正如问题中指出的那样,标准C要求存在一个值UCHAR_MAX,以使每个unsigned char类型的变量始终保持介于0到0 UCHAR_MAX(含)之间的值。它还要求每个动态分配的对象都由一个字节序列表示,该字节序列可以通过unsigned char *类型的指针来标识,并且必须有一个常量sizeof(unsigned char*),使得该类型的每个指针都可以由一系列sizeof(unsigned char *)unsigned类型的值来标识。字符

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

在这一点上,应该检查一下C标准是否确实允许这样做。

sizeofZ


1
整数类型上的许多运算被定义为具有“比结果类型中可表示的最大值多模减一的结果”。如果最大值是非有限序数,那将如何工作?
吉尔斯(Gilles)'所以

@Gilles这是一个有趣的观点。确实不清楚uintptr_t p = (uintptr_t)sizeof(void*)(将\ omega放入保存无符号整数的内容)的语义是什么。我不知道。我们可能无法将结果定义为0(或任何其他数字)。
Alexey B.

1
uintptr_t也必须是无限的。请注意,此类型是可选的-但是,如果您有无限数量的不同指针值,则sizeof(void*)也必须是无限的,因此size_t必须是无限的。但是,我对归约模的异议并不那么明显-仅在发生溢出时才起作用,但是如果您允许无限类型,则它们可能永远不会溢出。但是,在令人抓紧的手上,每种类型都有一个最小值和最大值,据我所知,这意味着UINT_MAX+1必须溢出。
吉尔(Gilles)'所以

也是一个好点。确实,我们得到了一堆类型(指针和size_t),它们应该是ℕ,ℤ或基于它们的某种构造(对于size_t,如果它类似于ℕ} {ω})。现在,如果对于这些类型中的某些类型,该标准需要一个定义最大值的宏(PTR_MAX或类似的东西),事情就会变得繁琐。但是到目前为止,我只能为非指针类型的MIN / MAX宏提供资金。
Alexey B.

研究的另一种可能性是将size_t和指针类型都定义为∪{ω}。这摆脱了最小/最大问题。溢出语义问题仍然存在。我的意思uint x = (uint)ω不清楚。同样,我们可以随意取0,但看起来确实有点难看。
Alexey B.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.