C 구현의 최대 계산 능력


28

우리가 책을 읽 거나 원하는 경우 언어 버전의 다른 버전으로 가면 C 구현이 얼마나 많은 계산 능력을 가질 수 있습니까?

"C 구현"은 기술적 의미를 갖습니다. 구현 정의 동작이 문서화 된 C 프로그래밍 언어 사양의 특정 인스턴스입니다. 실제 컴퓨터에서 AC 구현을 실행할 필요는 없습니다. 비트 문자열 표현을 가진 모든 객체와 구현 정의 크기를 가진 유형을 포함하여 전체 언어를 구현해야합니다.

이 질문의 목적 상 외부 저장소는 없습니다. 수행 할 수있는 유일한 입력 / 출력은 getchar(프로그램 입력 읽기) 및 putchar(프로그램 출력 쓰기)입니다. 또한 정의되지 않은 동작 을 호출하는 모든 프로그램 이 유효하지 않습니다. 유효한 프로그램에는 C 스펙에 정의 된 동작과 부록 J (C99의 경우)에 나열된 구현 정의 동작에 대한 구현 설명이 있어야합니다. 표준에서 언급되지 않은 라이브러리 함수 호출은 정의되지 않은 동작입니다.

필자의 초기 반응은 C 구현이 유한 오토 마톤에 지나지 않는다는 것이 었습니다. 주소 지정 가능한 메모리의 양에 제한이 있기 때문 sizeof(char*) * CHAR_BIT입니다 (저장 할 때 별개의 메모리 주소는 별개의 비트 패턴을 가져야하기 때문에 저장 비트 수 이상을 처리 할 수 ​​없습니다) 바이트 포인터로).

그러나 구현이 이것보다 더 많은 것을 할 수 있다고 생각합니다. 내가 알 수있는 한, 표준은 재귀 깊이에 제한을 두지 않습니다. 따라서 원하는만큼 재귀 함수 호출을 수행 할 수 있으며, 한정된 수의 호출을 제외한 모든 호출 만 주소를 지정할 수없는 ( register) 인수를 사용해야합니다 . 따라서 임의의 재귀를 허용하고 register객체 수 에 제한이없는 C 구현은 결정 론적 푸시 다운 오토마타를 인코딩 할 수 있습니다.

이 올바른지? 보다 강력한 C 구현을 찾을 수 있습니까? Turing-complete C 구현이 존재합니까?


4
@Dave : Gilles가 설명했듯이 무한한 메모리를 가질 수는 있지만 직접 해결할 수는 없습니다.
— Jukka Suomela

2
귀하의 설명에 따르면 C 구현은 컨텍스트가없는 언어보다 약한 결정적 푸시 다운 오토 마타가 허용하는 언어를 허용하도록 프로그래밍 할 수있는 것처럼 들립니다 . 그러나이 관찰은 무증상의 오용이므로 질문에 거의 관심이 없습니다.
— Warren Schudy

3
명심해야 할 점은 "구현 정의 동작"(또는 "정의되지 않은 동작")을 유발할 수있는 방법이 많이 있다는 것입니다. 그리고 일반적으로 구현은 예를 들어 C 표준에 정의되지 않은 기능을 제공하는 라이브러리 함수를 제공 할 수 있습니다. 이 모든 장치는 튜링 완료 기계에 액세스 할 수있는 "루프 홀"을 제공합니다. 또는 중단 문제를 해결하는 오라클과 같은 훨씬 강력한 것. 어리석은 예 : 부호있는 정수 오버플로 또는 정수 포인터 변환의 구현 정의 동작으로 이러한 오라클에 액세스 할 수 있습니다.
— Jukka Suomela

7
그건 그렇고, 사람들이 이것을 너무 심각하게 받아들이지 않도록 태그 "레크리에이션"(또는 재미있는 퍼즐에 사용하는 모든 것)을 추가하는 것이 좋습니다. 분명히 물어 보는 것은 "잘못 된 질문"이지만 그럼에도 불구하고 재미 있고 흥미로 웠습니다. :)
— Jukka Suomela

2
@ Juka : 좋은 생각입니다. 예를 들어, X에 의한 오버 플로우 = 테이프에 X / 3 쓰기 및 X % 3 방향으로 이동, 언더 플로우 = 테이프의 기호에 해당하는 신호를 트리거합니다. 그것은 학대와 같은 느낌이 들지만 확실히 내 질문의 정신에 있습니다. 답으로 쓸 수 있습니까? (@others : 다른 영리한 제안을하지 말고 싶지는 않습니다!)
— Gilles 'SO-Stop

답변:


8

질문에서 언급했듯이 표준 C는 모든 유형의 변수 unsigned char가 항상 0에서 UCHAR_MAX 사이의 값을 보유하도록 UCHAR_MAX 값이 있어야합니다. 또한 동적으로 할당 된 모든 객체는 type의 포인터를 통해 식별 할 수있는 바이트 시퀀스로 표시 unsigned char*되어야하며 sizeof(unsigned char*)해당 유형의 모든 포인터가 type의 sizeof(unsigned char *)값 시퀀스로 식별 할 수있는 상수가 있어야합니다 unsigned char. 동시에 동적으로 할당 될 수있는 객체의 수는 . 이론적 컴파일러가 10 10 10 개 이상의객체를 지원하기 위해 이러한 상수의 값을 할당하는 것을 막을 수는없지만 이론적 관점에서 볼 때 크기에 상관없이 어떤 한계도 존재한다는 것은 어떤 것이 무한하지 않다는 것을 의미합니다.UCHAR_MAXsizeof(unsigned char∗)101010

스택 에 할당 된 주소에 주소가없는 경우 프로그램은 무한한 양의 정보를 스택에 저장할 수 있습니다 . 따라서 어떤 크기의 유한 한 자동 기계로는 수행 할 수없는 몇 가지 작업을 수행 할 수있는 C 프로그램을 가질 수 있습니다. 따라서 스택 변수에 대한 액세스가 동적으로 할당 된 변수에 대한 액세스보다 훨씬 더 제한적이지만 C가 유한 오토 마톤에서 푸시 다운 오토 마톤으로 바뀝니다.

그러나 다른 잠재적 인 주름이 있습니다. 프로그램이 다른 객체에 대한 두 개의 포인터와 관련된 기본 고정 길이 문자 값 시퀀스를 검사하는 경우 해당 시퀀스는 고유해야합니다. U C H A R _ M A X s i 만 있기 때문에UCHAR_MAXsizeof(unsigned char∗)문자 값의 가능한 시퀀스, C 표준을 준수하지 수있는 초과 별개의 객체에 대한 포인터의 숫자를 생성하는 프로그램 코드는 지금까지 그 포인터와 관련된 문자의 순서를 조사하는 경우 . 그러나 어떤 경우에는 컴파일러가 포인터와 관련된 문자 시퀀스를 검사 할 코드가 없다고 판단 할 수 있습니다. 각 "char"가 실제로 임의의 유한 정수를 보유 할 수 있고 기계의 메모리가 셀 수없이 무한한 정수 시퀀스 인 경우 (무제한 튜링 기계를 제공 한 경우, 실제로는 느리지 만 그러한 기계를 에뮬레이트 할 수 있음 ), 실제로 C를 튜링 완성 언어로 만드는 것이 가능할 것입니다.


그러한 기계에서 sizeof (char)는 무엇을 반환합니까?
— TLW

1
@TLW : 다른 머신과 동일 : 1. CHAR_BITS 및 CHAR_MAX 매크로는 조금 더 문제가 될 수 있습니다. 표준은 범위가없는 유형의 개념을 허용하지 않습니다.
— supercat

아시다시피, CHAR_BITS을 의미했습니다. 죄송합니다.
— TLW

7

C11 (옵션) 스레딩 라이브러리를 사용하면 무제한 재귀 깊이를 고려하여 Turing을 완벽하게 구현할 수 있습니다.

새 스레드를 작성하면 두 번째 스택이 생성됩니다. 튜링 완성도를 위해 두 개의 스택으로 충분합니다. 한 스택은 머리 왼쪽에있는 것을 나타내고 다른 스택은 오른쪽에있는 것을 나타냅니다.


그러나 테이프가 한 방향으로 무한대로 진행되는 Turing 기계는 테이프가 양방향으로 무한대로 진행되는 Turing 기계와 마찬가지로 강력합니다. 그 외에도 스케줄러로 여러 스레드를 시뮬레이션 할 수 있습니다. 어쨌든 스레딩 라이브러리가 필요하지 않습니다.
— xamid

3

튜링 완료 라고 생각합니다 .이 트릭을 사용하여 UTM을 시뮬레이트하는 프로그램을 작성할 수 있습니다 (빠른 코드를 직접 작성하여 구문 오류가있을 수 있습니다 ...하지만 논리에 (주) 오류가 없기를 바랍니다. :-)

  • 테이프 표현을위한 이중 연결 목록으로 사용할 수있는 구조 정의
    typdef 구조체 {
      cell_t * pred; // 왼쪽의 셀
      cell_t * succ; // 오른쪽에있는 셀
      int val; // 셀 값
    } cell_t 

는 headA와 포인터 될 것입니다 cell_t구조

  • 현재 상태 및 플래그를 저장하는 데 사용할 수있는 구조 정의
    typedef 구조체 {
      int 상태;
      int 플래그;
    } info_t 
  • 그런 다음 헤드가 이중 연결 목록의 경계 사이에있을 때 Universal TM을 시뮬레이트하는 단일 루프 함수를 정의하십시오. 헤드가 경계에 도달하면 info_t 구조체의 플래그 (HIT_LEFT, HIT_RIGHT)를 설정하고 다음을 반환합니다.
void simulation_UTM (cell_t * head, info_t * info) {
  while (true) {
    head-> val = UTM_nextsymbol [정보-> 상태, 헤드-> val]; // 기호 쓰기
    info-> state = UTM_nextstate [정보-> 상태, 헤드-> val]; // 다음 상태
    if (info-> state == HALT_STATE) {// 프로그램을 수락하고 종료하면 인쇄
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       이탈 (0);
    }
    int move = UTM_nextmove [정보-> 상태, 헤드-> val];
    if (move == MOVE_LEFT) {
      머리 = 머리-> 먹이; // 왼쪽으로 이동
      if (head == NULL) {info-> flag = HIT_LEFT; 반환; }
    } else {
      머리 = 머리-> succ; // 오른쪽으로 이동해라
      if (head == NULL) {info-> flag = HIT_RIGHT; 반환; }
    }
  } // 여전히 경계에 있습니다 ... 계속
}
  • 그런 다음 먼저 시뮬레이션 UTM 루틴을 호출하고 테이프를 확장해야 할 때 재귀 적으로 호출하는 재귀 함수를 정의하십시오. 테이프를 맨 위에 확장해야 할 때 (HIT_RIGHT) 문제가없고 맨 아래에서 이동해야 할 때 (HIT_LEFT) 이중 링크 된 목록을 사용하여 셀 값을 위로 이동하면됩니다.
무효 스태커 (cell_t * top, cell_t * bottom, cell_t * head, info_t * info) {
  시뮬레이션 _UTM (헤드, 정보);
  cell_t 뉴셀; // 새로운 셀
  newcell.pred = 상단; // 새로운 셀로 이중 연결리스트 업데이트
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  스위치 (정보-> 적중) {
    case HIT_RIGHT :
      스태커 (& newcell, bottom, newcell, info);
      단절;
    case HIT_BOTTOM :
      cell_t * tmp = 뉴셀;
      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 뉴셀;
  int c = readchar ();
  if (c == END_OF_INPUT) 스태커 (& top, bottom, bottom, info); // 더 이상 기호가 없습니다. 시작
  newcell.pred = 상단;
  if (top! = NULL) top.succ = & newcell; else bottom = & newcell;
  init_tape (& newcell, 하단, 정보);
}

편집 : 그것에 대해 조금 생각한 후 포인터에 문제가 있습니다 ...

재귀 함수의 모든 호출이 stacker호출자에 로컬로 정의 된 변수에 대한 유효한 포인터를 가질 수 있다면 모든 것이 정상입니다 . 그렇지 않으면 내 알고리즘이 무제한 재귀에서 유효한 이중 연결 목록을 유지할 수 없습니다 (이 경우 재귀를 사용하여 무제한 무작위 액세스 저장소를 시뮬레이션하는 방법을 보지 못함).


3
stackernewcellstacker2엔/에스엔에스=sizeof(cell_t)

@Gilles : 네 말이 맞아 (내 편집 참조); 당신은 재귀 수준을 제한 할 경우 당신은 유한 기계적 얻을
— MARZIO 드 BIASI

@MarzioDeBiasi 아니요, 그는 표준이 전제하지 않는 구체적인 구현을 언급하기 때문에 잘못되었습니다. 실제로, C에서의 재귀 깊이에 대한 이론적 한계는 없다 . 제한된 스택 기반 구현을 사용한다고해서 언어의 이론적 한계에 대해서는 아무 말도하지 않습니다. 그러나 Turing-completeness는 이론적 인 한계입니다.
— xamid

0

무제한 호출 스택 크기를 갖는 한, 호출 스택에서 테이프를 인코딩하고 함수 호출에서 리턴하지 않고 스택 포인터를 되 감아 서 임의 액세스 할 수 있습니다.

EdIT : 유한 한 램만 사용할 수 있다면이 구조는 더 이상 작동하지 않으므로 아래를 참조하십시오.

그러나 왜 스택이 무한 할 수는 있지만 내재적 램은 그렇지 않은지는 매우 의문입니다. 따라서 실제로는 상태 수가 제한되어 있기 때문에 모든 일반 언어를 인식 할 수 없다고 말하고 싶습니다 (무한 스택을 악용하기 위해 스택 되감기 트릭을 계산하지 않으면).

나는 심지어 당신이 인식 할 수있는 언어의 수가 유한하다는 것을 추측 할 것이다 (언어 자체가 무한 할 수 a*있지만 예를 들어 괜찮지 b^k만 유한 한 수의에서만 작동한다 k).

편집 : 현재 상태를 추가 함수로 인코딩 할 수 있으므로 모든 일반 언어를 진정으로 인식 할 수 있으므로 이것은 사실이 아닙니다 .

같은 이유로 모든 유형 -2 언어를 얻을 수 있지만 상태 와 스택 일관성을 모두 콜 스택 에 둘 수 있는지 잘 모르겠습니다 . 그러나 일반적으로 램을 잊어 버릴 수 있습니다. 알파벳의 크기가 램의 용량을 초과하도록 항상 오토 마톤의 크기를 조정할 수 있기 때문입니다. 따라서 스택만으로 TM을 시뮬레이션 할 수 있다면 Type-2는 Type-0과 같지 않습니까?


5
"스택 포인터"란 무엇입니까? ( "스택"이라는 단어는 C 표준에 나타나지 않습니다.) 제 질문은 C를 컴퓨터의 C 구현 (확실한 유한 상태 머신)이 아니라 공식 언어 클래스로서 C에 관한 것입니다. 호출 스택에 액세스하려면 언어에서 제공하는 방식으로 호출 스택을 수행해야합니다. 예를 들어 함수 인수의 주소를 사용하면 주어진 구현에는 유한 한 수의 주소 만 있으므로 재귀 수준이 제한됩니다.
— Gilles 'SO- 악마 중지

스택 포인터 사용을 제외하도록 답변을 수정했습니다.
— 비트 마스크

1
수식을 계산 가능한 기능에서 인식되는 언어로 변경하는 것 외에는 수정 된 답변으로 어디에서 가고 있는지 이해할 수 없습니다. 함수에도 주소가 있기 때문에 주어진 유한 상태 머신을 구현하려면 충분히 큰 구현이 필요합니다. 문제는 C 구현이 정의 되지 않은 동작 에 의존하지 않고 더 많은 작업을 수행 할 수 있는지 여부 (예 : 범용 Turing 머신 구현) 입니다.
— Gilles 'SO- 악마 그만

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

{에이엔비엔기음엔}

적어도 이것이 효과가 있다고 생각합니다. 그래도 근본적인 실수를 저지른 것일 수도 있습니다.

고정 버전 :

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*itvoid
— Ben Standeven

C에서 동작이 정의 된 것과 같은 호출 스택을 여행하는 경우 매우 놀랍습니다.
— Radu GRIGore

첫 번째 'b'는 read_a ()가 실패하여 거부를 트리거하기 때문에 작동하지 않습니다.
— Ben Standeven

그러나 C 표준은 다음과 같이 말합니다. "가변 길이 배열 유형이없는 객체 (예 : 자동 스토리지가있는 객체)의 경우 수명은 블록에서 시작하여 블록으로 확장됩니다. 블록의 실행이 어떤 식 으로든 끝날 때까지 연결됩니다. (동봉 된 블록을 입력하거나 함수를 호출하면 현재 블록의 실행이 일시 중단되지만 종료되지는 않습니다.) 블록이 재귀 적으로 입력되면 객체의 새 인스턴스 "매번 만들어집니다." 따라서 read_triple을 호출 할 때마다 재귀에 사용할 수있는 새 포인터가 만들어집니다.
— 벤 스탠 데븐

2
2CHAR_BIT⋅sizeof (char *)

0

@supercat의 답변을 따라 :

C의 불완전성에 대한 주장은 별개의 객체가 별개의 주소를 가져야한다는 중심에있는 것으로 보이며 주소 세트는 유한 한 것으로 가정합니다. @supercat이 쓴 것처럼

질문에서 언급했듯이 표준 C는 UCHAR_MAXunsigned char 유형의 모든 변수가 항상 0과 사이의 값을 갖도록 값이 있어야 UCHAR_MAX합니다. 또한 모든 동적으로 할당 된 객체는 unsigned char * 유형의 포인터를 통해 식별 할 수있는 바이트 시퀀스로 표시되어야하며 sizeof(unsigned char*)해당 유형의 모든 포인터가 sizeof(unsigned char *)unsigned 유형 의 값 시퀀스로 식별 될 수있는 상수가 있어야합니다. 숯.

unsigned char*엔{0,1}sizeof(unsigned char*){0,1}sizeof⁡(유엔에스나는지엔이자형디 기음h에이아르 자형※)엔 합니다. 이런 식으로 모든 포인터에 적절한 길이의 부호없는 문자 시퀀스를 할당 할 수 있습니다. 정의 sizeof(unsigned char*)하면 작동합니다.엔 (ω

이 시점에서 C 표준이 실제로 허용하는지 확인해야합니다.

sizeof∈지


1
정수 유형에 대한 많은 연산은 "결과 유형에서 표현할 수있는 최대 값보다 하나 더 모듈로 감소 된"결과를 갖도록 정의됩니다. 최대 값이 무한 수가 아닌 경우 어떻게 작동합니까?
— 질 'SO-정지 존재 악마'

@Gilles 이것은 흥미로운 포인트입니다. 실제로 의미가 무엇인지 명확하지 않습니다 uintptr_t p = (uintptr_t)sizeof(void*)(서명되지 않은 정수를 보유한 무언가에 \ omega 입력). 나도 몰라. 결과를 0 (또는 다른 숫자)으로 정의하면 벗어날 수 있습니다.
— Alexey B.

1
uintptr_t무한해야합니다. 이 유형은 선택 사항입니다. 그러나 무한한 개수의 고유 한 포인터 값이 sizeof(void*)있으면 무한 size_t해야하므로 무한해야합니다. 감소 모듈로에 대한 나의 반대 의견은 분명하지 않습니다. 오버플로가있는 경우에만 작동하지만 무한 유형을 허용하면 절대 오버플로하지 않을 수 있습니다. 그러나 그립 유형에서 각 유형에는 최소값과 최대 값이 있으며, 내가 알 수있는 한 UINT_MAX+1오버플로해야 함을 나타 냅니다.
— Gilles 'SO- 악마 그만

또한 좋은 지적입니다. 실제로, 우리는 ℕ, ℤ, 또는 그것들을 기반으로하는 어떤 구조 (포인터 및 size_t)를 얻습니다 (size _의 경우 ℕ ∪ {ω}와 같을 경우). 이제 이러한 유형 중 일부의 경우 표준에 최대 값 (PTR_MAX 또는 이와 유사한 값)을 정의하는 매크로가 필요하면 문제가 생길 수 있습니다. 그러나 지금까지 나는 포인터가 아닌 유형에 대한 MIN / MAX 매크로의 요구 사항에만 자금을 지원할 수있었습니다.
— Alexey B.

조사 할 다른 가능성은 size_t포인터 유형과 포인터 유형을 ℕ ∪ {ω} 로 정의 하는 것입니다. 이것은 최소 / 최대 문제를 제거합니다. 오버 플로우 시맨틱 문제는 여전히 남아 있습니다. 의미론이 무엇인지 uint x = (uint)ω분명하지 않습니다. 다시, 우리는 아마도 0을 가져갈 수는 있지만 조금 추악하게 보입니다.
— Alexey B.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.