C実装の最大計算能力


28

この(または、必要に応じて言語仕様のその他のバージョン)を調べてみると、C実装の計算能力はどれくらいですか?

「C実装」には技術的な意味があることに注意してください。実装定義の動作が文書化されているCプログラミング言語仕様の特定のインスタンス化です。AC実装は、実際のコンピューターで実行できる必要はありません。ビット文字列表現を持つすべてのオブジェクトと、実装定義のサイズを持つ型を含む、言語全体を実装する必要があります。

この質問のために、外部ストレージはありません。実行できる唯一の入出力は、getchar(プログラム入力を読み取るため)およびputchar(プログラム出力を書き込むため)です。また、未定義の動作を呼び出すプログラムは無効です。有効なプログラムの動作は、C仕様に加えて、付録J(C99の場合)にリストされている実装定義動作の実装の説明で定義されている必要があります。標準で言及されていないライブラリ関数の呼び出しは未定義の動作であることに注意してください。

私の最初の反応は、Cの実装はアドレス指定可能なメモリの量に制限があるため、有限オートマトンに過ぎないということでした(sizeof(char*) * CHAR_BIT格納する際に個別のメモリアドレスは個別のビットパターンを持たなければならないため、ストレージのビット以上をアドレス指定することはできません)バイトポインター)。

しかし、実装ではこれ以上のことができると思います。私が知る限り、標準は再帰の深さに制限を課していません。したがって、必要なだけ再帰関数呼び出しを行うことができregisterます。アドレス指定不可能な()引数を使用する必要があるのは、有限数の呼び出しを除いてすべてです。したがって、任意の再帰を許可し、registerオブジェクトの数に制限のないC実装は、決定性プッシュダウンオートマトンをエンコードできます。

これは正しいです?より強力なC実装を見つけることができますか?チューリング完全なC実装は存在しますか?


4
@Dave:Gillesが説明したように、無制限のメモリを使用できますが、直接アドレスする方法はありません。
ユッカスオメラ

2
あなたの説明から、どのC実装も、文脈自由言語よりも弱い決定論的プッシュダウンオートマトンで受け入れられる言語を受け入れるようにしかプログラムできないように思えます。しかし、この観察は、漸近論の誤用であるため、私の意見にはほとんど関心がありません。
ウォーレンシューディ

3
留意すべき1つのポイントは、「実装定義の動作」(または「未定義の動作」)をトリガーする方法は多数あるということです。そして一般的に、実装は、たとえば、C標準で定義されていない機能を提供するライブラリ関数を提供できます。これらはすべて、たとえばチューリング完全なマシンにアクセスできる「抜け穴」を提供します。または、停止する問題を解決する神託のような、もっと強力なものですらあります。愚かな例:符号付き整数オーバーフローまたは整数ポインター変換の実装定義の動作により、このようなOracleにアクセスできます。
ユッカスオメラ

7
ところで、人々がこれをあまりにも真剣に受け取らないように、「レクリエーション」タグ(または面白いパズルに使用しているもの)を追加することをお勧めします。これは明らかに「間違った質問」ですが、それでも面白いと興味をそそられるものでした。:)
ユッカスオメラ

2
@Jukka:いいね。たとえば、Xによるオーバーフロー=テープにX / 3を書き込み、X%3の方向に移動します。アンダーフロー=テープ上のシンボルに対応する信号をトリガーします。それは少し虐待のように感じますが、それは間違いなく私の質問の精神です。答えとして書いていただけますか?(@others:!私はそのような他の巧妙な提案を阻止したくないということ)
ジル「SO-停止されて悪」

回答:


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を有限オートマトンからプッシュダウンオートマトンに変えます。

ただし、別の潜在的なしわがあります。異なるオブジェクトへの2つのポインタに関連付けられた文字値の基になる固定長シーケンスをプログラムで調べる場合、それらのシーケンスは一意である必要があります。U C H A R _ M A X s i z e o f u n s i g n e d c h a r しかないためUCHAR_MAXsizeof(unsigned char)可能性のある文字値のシーケンス、コードがこれらのポインタに関連付けられた文字のシーケンスを調べた場合、C標準に準拠できなかった、異なるオブジェクトへの多数のポインタを作成したプログラム。ただし、場合によっては、コンパイラーが、ポインターに関連付けられた文字のシーケンスを検査するコードがないことを判断することもできます。各「char」が実際に任意の有限整数を保持でき、マシンのメモリが数え切れないほどの整数のシーケンスである場合(無制限のテープチューリングマシンがあれば、実際には遅いがそのようなマシンをエミュレートできます)、 Cをチューリング完全言語にすることは確かに可能です。


このようなマシンでは、sizeof(char)は何を返しますか?
TLW

1
@TLW:他のマシンと同じ:1. CHAR_BITSおよびCHAR_MAXマクロはもう少し問題があります。標準では、境界のない型の概念は許可されません。
-supercat

おっと、あなたが言ったように申し訳ありませんが、CHAR_BITSを意味しました。
TLW

7

C11の(オプションの)スレッドライブラリを使用すると、再帰の深さが無制限であれば、Turingを完全に実装できます。

新しいスレッドを作成すると、2番目のスタックが生成されます。チューリングの完全性には2つのスタックで十分です。1つのスタックは、ヘッドの左側にあるものを表し、もう1つのスタックは、右側にあるものを表します。


しかし、テープが1方向にのみ無限に進行するチューリングマシンは、テープが2方向に無限に進行するチューリングマシンと同じくらい強力です。それとは別に、スケジューラによって複数のスレッドをシミュレートできます。とにかく、スレッドライブラリも必要ありません。
18:08のxamid

3

私はそれがチューリング完全だと思います:このトリックを使用してUTMをシミュレートするプログラムを書くことができます(私はすぐに手でコードを書いたので、おそらくいくつかの構文エラーがあります...しかし、私はロジックに(主要な)エラーがないことを望みます:-)

  • テープ表現の二重リンクリストとして使用できる構造を定義する
    typdef struct {
      cell_t * pred; //左側のセル
      cell_t * succ; //右側のセル
      int val; //セル値
    } cell_t 

headポインタになりますcell_t構造

  • 現在の状態とフラグを保存するために使用できる構造を定義する
    typedef struct {
      int状態;
      intフラグ。
    } info_t 
  • 次に、ヘッドが二重リンクリストの境界の間にあるときにユニバーサルTMをシミュレートする単一ループ関数を定義します。頭が境界にぶつかると、info_t構造体のフラグ(HIT_LEFT、HIT_RIGHT)を設定して戻ります。
void Simulate_UTM(cell_t * head、info_t * info){
  while(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');
       exit(0);
    }
    int move = UTM_nextmove [info-> state、head-> val];
    if(move == MOVE_LEFT){
      head = head-> pred; //左に移動
      if(head == NULL){info-> flag = HIT_LEFT; 戻り; }
    } else {
      head = head-> succ; // 右に動く
      if(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){
  simulate_UTM(head、info);
  cell_t newcell; //新しいセル
  newcell.pred = top; //新しいセルで二重リンクリストを更新します
  newcell.succ = NULL;
  top-> succ =&newcell;
  newcell.val = EMPTY_SYMBOL;

  switch(info-> hit){
    ケース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();
  if(c == END_OF_INPUT)stacker(&top、bottom、bottom、info); //記号はもう必要ありません、開始
  newcell.pred = top;
  if(top!= NULL)top.succ =&newcell; else bottom =&newcell;
  init_tape(&newcell、bottom、info);
}

編集:それについて少し考えた後、ポインターに問題があります...

再帰関数のstackerすべての呼び出しが、呼び出し側でローカルに定義された変数への有効なポインターを保持できる場合、すべてが正常です。そうしないと、私のアルゴリズムは、無制限の再帰で有効な二重リンクリストを保持できません(この場合、再帰を使用して無制限のランダムアクセスストレージをシミュレートする方法がわかりません)。


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@Gilles:そのとおりです(私の編集を参照)。あなたは再帰の深さを制限する場合は、有限オートマトンを取得
マルツィオ・デ・BIASI

@MarzioDeBiasiいいえ、標準が前提としていない具体的な実装を参照しているため、彼は間違っています。実際、Cの再帰の深さに理論的な制限はありません。限定スタックベースの実装を使用する選択は、言語の理論的な制限については何も言いません。しかし、チューリング完全性は理論的な限界です。
xamid

0

呼び出しスタックのサイズに制限がない限り、呼び出しスタックでテープをエンコードし、関数呼び出しから戻らずにスタックポインターを巻き戻すことでテープにランダムアクセスできます。

EdIT有限のRAMしか使用できない場合、この構造は機能しなくなります。以下を参照してください。

しかし、なぜスタックが無限になりうるのか、本質的なramはそうではないのかは非常に疑わしいです。したがって、実際には、状態の数に制限があるため、すべての通常言語を認識することさえできないと言います(無限スタックを活用するためのスタック巻き戻しトリックをカウントしない場合)。

認識できる言語の数は有限であると推測することさえできます(言語自体は無限である場合でも、たとえばa*大丈夫ですがb^k、有限数のksでのみ機能します)。

編集現在の状態を追加の関数でエンコードできるため、これは正しくありませんしたがって、すべての通常の言語を本当に認識することができます。

ほとんどの場合、同じ理由ですべてのType-2言語を取得できますが、状態スタックの両方をcall-stack に配置できるかどうかはわかりません。ただし、一般的な注意事項として、アルファベットがラムの容量を超えるようにオートマトンのサイズをいつでもスケーリングできるため、ラムについては事実上忘れることができます。スタックのみでTMをシミュレートできる場合、Type-2はType-0になりますよね?


5
「スタックポインタ」とは何ですか?(「スタック」という単語はC標準には表示されないことに注意してください。)私の質問は、コンピューター上のC実装(明らかに有限状態マシン)ではなく、Cを正式な言語のクラスとして使用することです。コールスタックにアクセスする場合は、言語で提供されている方法でアクセスする必要があります。たとえば、関数の引数のアドレスを取得しますが、どの実装でも有限数のアドレスしか持たないため、再帰の深さが制限されます。
ジル 'SO-悪であるのをやめる'

スタックポインターの使用を除外するように回答を変更しました。
ビットマスク

1
修正された回答でどこに向かっているのかわかりません(定式化を計算可能な関数から認識された言語に変更することは別として)。関数にもアドレスがあるため、任意の有限状態マシンを実装するのに十分な大きさの実装が必要です。問題は、どのようにCの実装がに頼らず(ユニバーサルチューリングマシンを実装する、と言う)より行うことができるかどうかである国連の挙動を定義しました。
ジル「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();
}

{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型ですvoid
ベンスタンデブン

そのようにコールスタックを旅すると、Cでの動作を定義するならば、私は非常に驚かれることでしょう
ラドゥグリゴール

ああ、これは機能しません。最初の「b」がread_a()の失敗を引き起こし、拒否をトリガーするためです。
ベンスタンデブン

しかし、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型の値のシーケンスで識別できるような定数が必要です。 char。

unsigned char*N{01}sizeof(unsigned char*){01}のサイズあなたはnsgned charNsizeof(unsigned char*)Nω

この時点で、C標準が実際にそれを許可していることを確認する必要があります。

sizeofZ


1
整数型の多くの演算は、結果が「結果の型で表現可能な最大値よりも1多い剰余」となるように定義されています。その最大値が非有限序数であれば、どのように機能しますか?
ジル「SO-悪であるのをやめる」

@Gillesこれは興味深い点です。実際、セマンティクスが何であるかは明らかではありませんuintptr_t p = (uintptr_t)sizeof(void*)(\ omegaを符号なし整数を保持するものに入れる)。私は知らない。結果を0(または他の任意の数)に定義することで済ますことができます。
アレクセイB.

1
uintptr_t無限である必要があります。気を付けてください、この型はオプションです—しかし、明確なポインタ値の数sizeof(void*)が無限である場合、無限でsize_tなければならないので、無限でなければなりません。ただし、リダクションモジュロに関する私の反論はそれほど明白ではありません。オーバーフローが発生した場合にのみ機能しますが、無限の型を許可すると、オーバーフローしない可能性があります。しかし、握り手では、各タイプには最小値と最大値があり、私が知る限り、UINT_MAX+1オーバーフローする必要があることを意味します。
ジル「SO-悪であるのをやめる」

良い点も。実際、ℕ、ℤ、またはそれらに基づいた何らかの構造(should should {ω}のような場合はsize_t)である必要のある型(ポインターとsize_t)を取得します。さて、これらの型のいくつかについて、標準が最大値(PTR_MAXまたはそのようなもの)を定義するマクロを必要とする場合、物事は難しくなります。しかし、これまでのところ、非ポインタータイプのMIN / MAXマクロの要件にのみ資金を提供することができました。
アレクセイB.

調査するもう1つの可能性は、両方size_tとポインタータイプをℕ∪{ω}と定義することです。これにより、最小/最大の問題が解消されます。オーバーフローセマンティクスの問題は依然として残っています。のセマンティクスはどうあるべきかはuint x = (uint)ω私には明らかではありません。繰り返しますが、偶然にも0をとることができますが、少しいように見えます。
アレクセイB.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.