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_BITsizeof (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.